voice_audio_multi/tests/perf/model_perf.cpp
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

598 lines
22 KiB
C++

#include "app/cli/request.h"
#include "engine/framework/audio/output.h"
#include "engine/framework/io/filesystem.h"
#include "engine/framework/io/json.h"
#include "engine/framework/runtime/registry.h"
#include <algorithm>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#ifdef _OPENMP
#include <omp.h>
#endif
namespace {
using JsonValue = engine::io::json::Value;
using JsonObject = engine::io::json::Value::Object;
struct PerfCaseSpec {
std::string id;
std::string family;
std::string case_id;
std::string request_id;
std::optional<std::string> warmup_request_id;
};
struct MeasurementSummary {
std::vector<double> wall_ms;
double average_wall_ms = 0.0;
double rtf = 0.0;
double duration_sec = 0.0;
};
struct CaseSummary {
std::string id;
std::string family;
std::string case_id;
std::string request_id;
std::string task;
std::string mode;
MeasurementSummary cold;
MeasurementSummary warm;
};
struct RunResult {
engine::runtime::TaskResult result;
double wall_ms = 0.0;
};
std::string optional_arg(int argc, char ** argv, const std::string & name, const std::string & fallback) {
for (int i = 1; i + 1 < argc; ++i) {
if (std::string_view(argv[i]) == name) {
return argv[i + 1];
}
}
return fallback;
}
std::optional<std::string> find_arg(int argc, char ** argv, const std::string & name) {
for (int i = 1; i + 1 < argc; ++i) {
if (std::string_view(argv[i]) == name) {
return std::string(argv[i + 1]);
}
}
return std::nullopt;
}
int parse_int_arg(int argc, char ** argv, const std::string & name, int fallback) {
if (const auto value = find_arg(argc, argv, name)) {
return std::stoi(*value);
}
return fallback;
}
std::string quote_json(const std::string & value) {
return engine::io::json::stringify_string(value);
}
std::string lower_ascii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
std::string make_timestamp() {
const auto now = std::chrono::system_clock::now();
const auto time = std::chrono::system_clock::to_time_t(now);
std::tm tm = {};
#if defined(_WIN32)
localtime_s(&tm, &time);
#else
localtime_r(&time, &tm);
#endif
std::ostringstream out;
out << std::put_time(&tm, "%Y%m%d-%H%M%S");
return out.str();
}
engine::core::BackendType parse_backend(const std::string & value) {
const std::string lowered = lower_ascii(value);
if (lowered == "cpu") {
return engine::core::BackendType::Cpu;
}
if (lowered == "cuda") {
return engine::core::BackendType::Cuda;
}
if (lowered == "vulkan") {
return engine::core::BackendType::Vulkan;
}
if (lowered == "metal") {
return engine::core::BackendType::Metal;
}
if (lowered == "best") {
return engine::core::BackendType::BestAvailable;
}
throw std::runtime_error("unsupported backend: " + value);
}
std::string shorten_text(std::string text, size_t limit) {
if (text.size() <= limit) {
return text;
}
size_t cut = text.rfind(' ', limit);
if (cut == std::string::npos || cut < (limit / 2)) {
cut = limit;
}
text.resize(cut);
while (!text.empty() && std::isspace(static_cast<unsigned char>(text.back())) != 0) {
text.pop_back();
}
return text;
}
std::filesystem::path write_clipped_wav(
const std::filesystem::path & source_path,
const std::filesystem::path & temp_dir,
double seconds) {
const auto audio = minitts::cli::read_audio_buffer(source_path);
const int64_t frame_limit = static_cast<int64_t>(seconds * static_cast<double>(audio.sample_rate));
const int64_t sample_limit = std::max<int64_t>(
1,
std::min<int64_t>(
static_cast<int64_t>(audio.samples.size()),
frame_limit * static_cast<int64_t>(audio.channels)));
std::vector<float> clipped(
audio.samples.begin(),
audio.samples.begin() + static_cast<std::ptrdiff_t>(sample_limit));
const auto target = temp_dir / source_path.filename();
engine::audio::WavPcm16Sink().write(
target,
engine::audio::AudioBuffer{
audio.sample_rate,
audio.channels,
std::move(clipped),
});
return target;
}
JsonValue clip_audio_path_field(
const JsonValue & value,
const std::filesystem::path & repo_root,
const std::filesystem::path & temp_dir,
double seconds) {
const auto absolute = std::filesystem::path(value.as_string()).is_absolute()
? std::filesystem::path(value.as_string())
: repo_root / value.as_string();
return JsonValue::make_string(write_clipped_wav(absolute, temp_dir, seconds).string());
}
JsonValue derive_warmup_request(
const JsonValue & request,
const std::filesystem::path & repo_root,
const std::filesystem::path & temp_dir) {
JsonObject out = request.as_object();
const auto shorten_field = [&](const char * key, size_t limit) {
const auto it = out.find(key);
if (it != out.end() && it->second.is_string()) {
it->second = JsonValue::make_string(shorten_text(it->second.as_string(), limit));
}
};
shorten_field("text", 80);
shorten_field("target_text", 80);
shorten_field("style_ref_text", 80);
shorten_field("reference_text", 80);
shorten_field("lyrics", 80);
shorten_field("instruct", 64);
const auto clip_field = [&](const char * key) {
const auto it = out.find(key);
if (it != out.end() && it->second.is_string()) {
it->second = clip_audio_path_field(it->second, repo_root, temp_dir, 2.0);
}
};
clip_field("audio");
clip_field("voice_ref");
clip_field("source_audio");
clip_field("target_voice");
clip_field("prosody_ref");
clip_field("style_ref");
const auto it_duration = out.find("duration_seconds");
if (it_duration != out.end() && it_duration->second.is_number()) {
it_duration->second = JsonValue::make_number(std::min(4.0, it_duration->second.as_number()));
}
const auto it_max_tokens = out.find("max_tokens");
if (it_max_tokens != out.end() && it_max_tokens->second.is_number()) {
it_max_tokens->second = JsonValue::make_number(std::min<int64_t>(64, it_max_tokens->second.as_i64()));
}
if (const auto options_it = out.find("options"); options_it != out.end() && options_it->second.is_object()) {
JsonObject options = options_it->second.as_object();
const auto reduce_numeric_option = [&](const char * key, double cap) {
const auto it = options.find(key);
if (it != options.end() && it->second.is_number()) {
it->second = JsonValue::make_number(std::min(cap, it->second.as_number()));
}
};
reduce_numeric_option("duration_seconds", 4.0);
reduce_numeric_option("target_duration_seconds", 4.0);
options_it->second = JsonValue::make_object(std::move(options));
}
return JsonValue::make_object(std::move(out));
}
const JsonValue & require_case(const JsonValue & root, const std::string & case_id) {
for (const auto & item : root.require("cases").as_array()) {
if (engine::io::json::require_string(item, "id") == case_id) {
return item;
}
}
throw std::runtime_error("perf harness could not find audiocpp_cli case: " + case_id);
}
const JsonValue & require_request(const JsonValue & case_value, const std::string & request_id) {
for (const auto & request : case_value.require("requests").as_array()) {
if (engine::io::json::require_string(request, "id") == request_id) {
return request;
}
}
throw std::runtime_error(
"perf harness could not find request " + request_id + " in case " +
engine::io::json::require_string(case_value, "id"));
}
std::unordered_map<std::string, std::string> json_string_map(const JsonValue * value) {
std::unordered_map<std::string, std::string> out;
if (value == nullptr || value->is_null()) {
return out;
}
for (const auto & [key, child] : value->as_object()) {
out.emplace(key, minitts::cli::json_option_string(child));
}
return out;
}
PerfCaseSpec parse_perf_case(const JsonValue & value) {
PerfCaseSpec out;
out.id = engine::io::json::require_string(value, "id");
out.family = engine::io::json::require_string(value, "family");
out.case_id = engine::io::json::require_string(value, "case_id");
out.request_id = engine::io::json::require_string(value, "request_id");
if (const auto * warmup_request_id = value.find("warmup_request_id"); warmup_request_id != nullptr && warmup_request_id->is_string()) {
out.warmup_request_id = warmup_request_id->as_string();
}
return out;
}
engine::runtime::ModelLoadRequest build_load_request(
const JsonValue & case_value,
const std::filesystem::path & repo_root) {
engine::runtime::ModelLoadRequest request;
request.model_path = repo_root / engine::io::json::require_string(case_value, "model");
request.family_hint = engine::io::json::require_string(case_value, "family");
request.options = json_string_map(case_value.find("load_options"));
return request;
}
engine::runtime::SessionOptions build_session_options(
const JsonValue & case_value,
engine::core::BackendType backend_type,
int device,
int threads) {
engine::runtime::SessionOptions options;
options.backend.type = backend_type;
options.backend.device = device;
options.backend.threads = threads;
options.options = json_string_map(case_value.find("session_options"));
return options;
}
engine::runtime::TaskSpec build_task_spec(const JsonValue & case_value) {
return engine::runtime::TaskSpec{
engine::runtime::parse_voice_task_kind(engine::io::json::require_string(case_value, "task")),
engine::runtime::parse_run_mode(engine::io::json::require_string(case_value, "mode")),
};
}
double audio_duration_sec(const engine::runtime::AudioBuffer & audio) {
if (audio.sample_rate <= 0 || audio.channels <= 0) {
return 0.0;
}
return static_cast<double>(audio.samples.size()) /
static_cast<double>(audio.sample_rate * audio.channels);
}
double result_duration_sec(
const engine::runtime::TaskRequest & request,
const engine::runtime::TaskResult & result) {
if (result.audio_output.has_value()) {
return audio_duration_sec(*result.audio_output);
}
if (!result.named_audio_outputs.empty()) {
return audio_duration_sec(result.named_audio_outputs.front().audio);
}
if (request.audio_input.has_value()) {
return audio_duration_sec(*request.audio_input);
}
return 0.0;
}
RunResult run_impl(
engine::runtime::IVoiceTaskSession & session,
const engine::runtime::TaskSpec & task_spec,
const engine::runtime::TaskRequest & request,
bool prepare_first) {
const auto started = std::chrono::steady_clock::now();
if (prepare_first) {
session.prepare(engine::runtime::build_preparation_request(request));
}
if (task_spec.mode == engine::runtime::RunMode::Offline) {
auto * offline = dynamic_cast<engine::runtime::IOfflineVoiceTaskSession *>(&session);
if (offline == nullptr) {
throw std::runtime_error("selected model does not support offline mode");
}
auto result = offline->run(request);
const auto ended = std::chrono::steady_clock::now();
return {
std::move(result),
std::chrono::duration<double, std::milli>(ended - started).count(),
};
}
auto * streaming = dynamic_cast<engine::runtime::IStreamingVoiceTaskSession *>(&session);
if (streaming == nullptr) {
throw std::runtime_error("selected model does not support streaming mode");
}
if (!request.audio_input.has_value()) {
throw std::runtime_error("streaming perf request requires request.audio_input");
}
streaming->reset();
const auto & audio = *request.audio_input;
constexpr int kChunkSamples = 512;
for (size_t offset = 0; offset < audio.samples.size(); offset += static_cast<size_t>(kChunkSamples)) {
const size_t available = std::min(static_cast<size_t>(kChunkSamples), audio.samples.size() - offset);
std::vector<float> chunk(static_cast<size_t>(kChunkSamples), 0.0f);
std::copy(
audio.samples.begin() + static_cast<std::ptrdiff_t>(offset),
audio.samples.begin() + static_cast<std::ptrdiff_t>(offset + available),
chunk.begin());
streaming->process_audio_chunk(engine::runtime::AudioChunk{
audio.sample_rate,
audio.channels,
static_cast<int64_t>(offset / static_cast<size_t>(std::max(1, audio.channels))),
std::move(chunk),
});
}
auto result = streaming->finalize();
const auto ended = std::chrono::steady_clock::now();
return {
std::move(result),
std::chrono::duration<double, std::milli>(ended - started).count(),
};
}
RunResult run_once(
engine::runtime::IVoiceTaskSession & session,
const engine::runtime::TaskSpec & task_spec,
const engine::runtime::TaskRequest & request) {
return run_impl(session, task_spec, request, true);
}
RunResult run_prepared_once(
engine::runtime::IVoiceTaskSession & session,
const engine::runtime::TaskSpec & task_spec,
const engine::runtime::TaskRequest & request) {
return run_impl(session, task_spec, request, false);
}
MeasurementSummary measure_cold(
const engine::runtime::ModelRegistry & registry,
const engine::runtime::ModelLoadRequest & load_request,
const engine::runtime::TaskSpec & task_spec,
const engine::runtime::SessionOptions & session_options,
const engine::runtime::TaskRequest & request) {
MeasurementSummary summary;
for (int rep = 0; rep < 3; ++rep) {
const auto started = std::chrono::steady_clock::now();
auto model = registry.load(load_request);
auto session = model->create_task_session(task_spec, session_options);
auto run = run_once(*session, task_spec, request);
const auto ended = std::chrono::steady_clock::now();
summary.wall_ms.push_back(std::chrono::duration<double, std::milli>(ended - started).count());
summary.duration_sec = result_duration_sec(request, run.result);
}
summary.average_wall_ms =
std::accumulate(summary.wall_ms.begin(), summary.wall_ms.end(), 0.0) /
static_cast<double>(summary.wall_ms.size());
if (summary.duration_sec > 0.0) {
summary.rtf = (summary.average_wall_ms / 1000.0) / summary.duration_sec;
}
return summary;
}
MeasurementSummary measure_warm(
const engine::runtime::ModelRegistry & registry,
const engine::runtime::ModelLoadRequest & load_request,
const engine::runtime::TaskSpec & task_spec,
const engine::runtime::SessionOptions & session_options,
const engine::runtime::TaskRequest & warmup_request,
const engine::runtime::TaskRequest & request) {
auto model = registry.load(load_request);
auto session = model->create_task_session(task_spec, session_options);
(void) run_once(*session, task_spec, warmup_request);
session->prepare(engine::runtime::build_preparation_request(request));
MeasurementSummary summary;
for (int rep = 0; rep < 3; ++rep) {
auto run = run_prepared_once(*session, task_spec, request);
summary.wall_ms.push_back(run.wall_ms);
summary.duration_sec = result_duration_sec(request, run.result);
}
summary.average_wall_ms =
std::accumulate(summary.wall_ms.begin(), summary.wall_ms.end(), 0.0) /
static_cast<double>(summary.wall_ms.size());
if (summary.duration_sec > 0.0) {
summary.rtf = (summary.average_wall_ms / 1000.0) / summary.duration_sec;
}
return summary;
}
std::string measurement_json(const MeasurementSummary & measurement) {
std::ostringstream out;
out << "{"
<< "\"wall_ms\":[";
for (size_t i = 0; i < measurement.wall_ms.size(); ++i) {
if (i != 0) {
out << ",";
}
out << measurement.wall_ms[i];
}
out << "],\"average_wall_ms\":" << measurement.average_wall_ms
<< ",\"duration_sec\":" << measurement.duration_sec
<< ",\"rtf\":" << measurement.rtf
<< "}";
return out.str();
}
std::string case_summary_json(const CaseSummary & summary) {
std::ostringstream out;
out << "{"
<< "\"id\":" << quote_json(summary.id)
<< ",\"family\":" << quote_json(summary.family)
<< ",\"case_id\":" << quote_json(summary.case_id)
<< ",\"request_id\":" << quote_json(summary.request_id)
<< ",\"task\":" << quote_json(summary.task)
<< ",\"mode\":" << quote_json(summary.mode)
<< ",\"cold\":" << measurement_json(summary.cold)
<< ",\"warm\":" << measurement_json(summary.warm)
<< "}";
return out.str();
}
} // namespace
int main(int argc, char ** argv) {
try {
std::cout.setf(std::ios::unitbuf);
const std::filesystem::path repo_root = std::filesystem::current_path();
const std::filesystem::path perf_cases_path = optional_arg(
argc,
argv,
"--perf-cases",
"tests/perf/model_perf_cases.json");
const std::filesystem::path path_cases_path = optional_arg(
argc,
argv,
"--request-cases",
"tests/perf/model_perf_request_cases.json");
const std::string backend_name = optional_arg(argc, argv, "--backend", "cuda");
const int device = parse_int_arg(argc, argv, "--device", 0);
const int threads = parse_int_arg(argc, argv, "--threads", 1);
if (threads <= 0) {
throw std::runtime_error("--threads must be positive");
}
#ifdef _OPENMP
omp_set_num_threads(threads);
#endif
const auto selected_id = find_arg(argc, argv, "--id");
const auto summary_out_arg = find_arg(argc, argv, "--summary-out");
const auto output_root = summary_out_arg.has_value()
? std::filesystem::path(*summary_out_arg).parent_path()
: repo_root / "build" / "logs" / "perf" /
("cpp_" + lower_ascii(backend_name) + "_" + make_timestamp());
std::filesystem::create_directories(output_root);
const auto perf_root = engine::io::json::parse_file(perf_cases_path);
const auto path_root = engine::io::json::parse_file(path_cases_path);
auto registry = engine::runtime::make_default_registry();
const auto backend_type = parse_backend(backend_name);
std::vector<CaseSummary> summaries;
for (const auto & perf_case_value : perf_root.require("cases").as_array()) {
const auto perf_case = parse_perf_case(perf_case_value);
if (selected_id.has_value() && *selected_id != perf_case.id) {
continue;
}
const auto & case_value = require_case(path_root, perf_case.case_id);
const auto & request_value = require_request(case_value, perf_case.request_id);
const auto load_request = build_load_request(case_value, repo_root);
const auto task_spec = build_task_spec(case_value);
const auto session_options = build_session_options(case_value, backend_type, device, threads);
const auto request = minitts::cli::build_request_from_json(request_value, repo_root);
const auto temp_dir = output_root / (perf_case.id + "_warmup");
std::filesystem::create_directories(temp_dir);
engine::runtime::TaskRequest warmup_request;
if (perf_case.warmup_request_id.has_value()) {
warmup_request = minitts::cli::build_request_from_json(
require_request(case_value, *perf_case.warmup_request_id),
repo_root);
} else {
warmup_request = minitts::cli::build_request_from_json(
derive_warmup_request(request_value, repo_root, temp_dir),
repo_root);
}
CaseSummary summary;
summary.id = perf_case.id;
summary.family = perf_case.family;
summary.case_id = perf_case.case_id;
summary.request_id = perf_case.request_id;
summary.task = engine::runtime::to_string(task_spec.task);
summary.mode = engine::runtime::to_string(task_spec.mode);
summary.cold = measure_cold(registry, load_request, task_spec, session_options, request);
summary.warm = measure_warm(registry, load_request, task_spec, session_options, warmup_request, request);
summaries.push_back(summary);
std::cout << perf_case.id
<< " cold_avg_ms=" << summary.cold.average_wall_ms
<< " cold_rtf=" << summary.cold.rtf
<< " warm_avg_ms=" << summary.warm.average_wall_ms
<< " warm_rtf=" << summary.warm.rtf
<< std::endl;
}
std::ostringstream summary_json;
summary_json << "{\"backend\":" << quote_json(backend_name)
<< ",\"device\":" << device
<< ",\"threads\":" << threads
<< ",\"cases\":[";
for (size_t i = 0; i < summaries.size(); ++i) {
if (i != 0) {
summary_json << ",";
}
summary_json << case_summary_json(summaries[i]);
}
summary_json << "]}";
const auto summary_path = summary_out_arg.has_value()
? std::filesystem::path(*summary_out_arg)
: output_root / "summary.json";
std::ofstream(summary_path) << summary_json.str() << "\n";
std::cout << "summary_json=" << summary_path.string() << std::endl;
return 0;
} catch (const std::exception & ex) {
std::cerr << "model_perf failed: " << ex.what() << "\n";
return 1;
}
}