voice_audio_multi/tests/yue2/yue2_nar_parity_probe.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

245 lines
11 KiB
C++

// Standalone YuE2 NAR acoustic-flow parity probe.
//
// The probe drives Yue2ARRuntime/Yue2NARRuntime directly instead of adding
// reference-only inputs to the request surface: everything the acoustic flow
// consumes is read from a directory produced by
// tests/yue2/yue2_nar_reference_dump.py, and the latents it produces are
// compared against the reference dump. Feeding the exact prefix, codec tokens
// and noise the Python reference used isolates the port from sampling RNG.
//
// Reference directory contract (little-endian raw tensors):
// prefix.i32 token prefix: EOD, tags/lyrics text, ABC block, MUSIC_START
// codec.i32 codec tokens, one per acoustic frame
// noise.f32 [frames, 64] acoustic noise the reference integrated
// latents_ref.f32 [frames, 64] reference latents
// metadata.json {"seed": .., "ode_steps": .., "context": .., "latent_dim": ..}
//
// Example:
// yue2_nar_parity_probe --model models/Yue2-3B-GGUF --reference /tmp/yue2nar --backend cuda --threads 8
#include "engine/framework/assets/tensor_source.h"
#include "engine/framework/core/backend.h"
#include "engine/framework/core/execution_context.h"
#include "engine/framework/io/json.h"
#include "engine/framework/runtime/options.h"
#include "engine/models/yue2/assets.h"
#include "engine/models/yue2/ar_runtime.h"
#include "engine/models/yue2/nar_runtime.h"
#include "engine/models/yue2/types.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
namespace {
std::string arg_value(int argc, char ** argv, const std::string & name, const std::string & fallback = {}) {
for (int i = 1; i + 1 < argc; ++i) {
if (argv[i] == name) {
return argv[i + 1];
}
}
return fallback;
}
int64_t int_arg(int argc, char ** argv, const std::string & name, int64_t fallback) {
return std::stoll(arg_value(argc, argv, name, std::to_string(fallback)));
}
double float_arg(int argc, char ** argv, const std::string & name, double fallback) {
return std::stod(arg_value(argc, argv, name, std::to_string(fallback)));
}
engine::core::BackendType parse_backend(const std::string & value) {
if (value == "cpu") return engine::core::BackendType::Cpu;
if (value == "cuda") return engine::core::BackendType::Cuda;
if (value == "vulkan") return engine::core::BackendType::Vulkan;
throw std::runtime_error("unsupported backend: " + value);
}
std::vector<char> read_bytes(const std::filesystem::path & path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
throw std::runtime_error("failed to open " + path.string());
}
in.seekg(0, std::ios::end);
const auto bytes = in.tellg();
if (bytes < 0) {
throw std::runtime_error("invalid file size: " + path.string());
}
in.seekg(0, std::ios::beg);
std::vector<char> data(static_cast<size_t>(bytes));
in.read(data.data(), bytes);
return data;
}
std::vector<int32_t> read_i32_file(const std::filesystem::path & path) {
const auto bytes = read_bytes(path);
if (bytes.size() % sizeof(int32_t) != 0) {
throw std::runtime_error("invalid i32 byte size: " + path.string());
}
std::vector<int32_t> values(bytes.size() / sizeof(int32_t));
std::memcpy(values.data(), bytes.data(), bytes.size());
return values;
}
std::vector<float> read_f32_file(const std::filesystem::path & path) {
const auto bytes = read_bytes(path);
if (bytes.size() % sizeof(float) != 0) {
throw std::runtime_error("invalid f32 byte size: " + path.string());
}
std::vector<float> values(bytes.size() / sizeof(float));
std::memcpy(values.data(), bytes.data(), bytes.size());
return values;
}
void write_f32_file(const std::filesystem::path & path, const std::vector<float> & values) {
std::filesystem::create_directories(path.parent_path());
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) {
throw std::runtime_error("failed to write " + path.string());
}
out.write(reinterpret_cast<const char *>(values.data()), static_cast<std::streamsize>(values.size() * sizeof(float)));
}
struct CompareMetrics {
double max_abs = 0.0;
double rmse = 0.0;
double cosine = 0.0;
};
CompareMetrics compare(const std::vector<float> & got, const std::vector<float> & ref) {
if (got.size() != ref.size()) {
throw std::runtime_error("output/reference size mismatch");
}
double sum_sq = 0.0;
double dot = 0.0;
double got_sq = 0.0;
double ref_sq = 0.0;
double max_abs = 0.0;
for (size_t i = 0; i < got.size(); ++i) {
const double g = got[i];
const double r = ref[i];
const double d = g - r;
max_abs = std::max(max_abs, std::abs(d));
sum_sq += d * d;
dot += g * r;
got_sq += g * g;
ref_sq += r * r;
}
return {
max_abs,
std::sqrt(sum_sq / static_cast<double>(got.size())),
dot / std::sqrt(std::max(got_sq * ref_sq, std::numeric_limits<double>::min())),
};
}
engine::assets::TensorStorageType parse_weight_type(const std::string & value) {
const std::unordered_map<std::string, std::string> options{{"weight_type", value}};
return engine::runtime::parse_tensor_storage_option(
options, "weight_type", engine::assets::TensorStorageType::Native,
{engine::assets::TensorStorageType::Native,
engine::assets::TensorStorageType::F32,
engine::assets::TensorStorageType::F16,
engine::assets::TensorStorageType::BF16,
engine::assets::TensorStorageType::Q8_0,
engine::assets::TensorStorageType::Q4_0,
engine::assets::TensorStorageType::Q4_K});
}
constexpr size_t kMib = 1024ull * 1024ull;
} // namespace
int main(int argc, char ** argv) {
try {
const auto model = std::filesystem::path(arg_value(argc, argv, "--model"));
const auto reference = std::filesystem::path(arg_value(argc, argv, "--reference"));
if (model.empty() || reference.empty()) {
std::cerr << "usage: yue2_nar_parity_probe --model <dir> --reference <dir>"
" [--backend cpu|cuda|vulkan] [--threads N] [--model-gguf name]"
" [--weight-type native|f32|f16|bf16|q8_0|q4_0|q4_k]"
" [--min-cosine X] [--max-rmse X] [--dump-latents file]\n";
return 2;
}
const int threads = static_cast<int>(int_arg(argc, argv, "--threads", 8));
const auto model_gguf = arg_value(argc, argv, "--model-gguf", "yue2-3b-q8_0.gguf");
const auto min_cosine = float_arg(argc, argv, "--min-cosine", 0.995);
const auto max_rmse = float_arg(argc, argv, "--max-rmse", 0.1);
const auto metadata_path = reference / "metadata.json";
const auto metadata = std::filesystem::is_regular_file(metadata_path)
? engine::io::json::parse_file(metadata_path)
: engine::io::json::Value{};
const int64_t seed = engine::io::json::optional_i64(metadata, "seed", 1234);
const int64_t ode_steps = engine::io::json::optional_i64(metadata, "ode_steps", 8);
const int64_t context = engine::io::json::optional_i64(metadata, "context", engine::models::yue2::kContextTokens);
engine::core::BackendConfig backend_config;
backend_config.type = parse_backend(arg_value(argc, argv, "--backend", "cpu"));
backend_config.threads = threads;
engine::core::ExecutionContext execution(backend_config);
auto base = engine::models::yue2::load_yue2_assets(model);
auto assets = std::make_shared<engine::models::yue2::Yue2Assets>(*base);
assets->model_weights = engine::assets::open_tensor_source(model / model_gguf, "model_weights");
const auto prefix = read_i32_file(reference / "prefix.i32");
const auto codec = read_i32_file(reference / "codec.i32");
const auto noise = read_f32_file(reference / "noise.f32");
const auto latents_ref = read_f32_file(reference / "latents_ref.f32");
const int64_t latent_dim = engine::io::json::optional_i64(metadata, "latent_dim", 64);
if (latent_dim <= 0 ||
codec.empty() ||
noise.size() != static_cast<size_t>(codec.size()) * static_cast<size_t>(latent_dim) ||
latents_ref.size() != noise.size()) {
throw std::runtime_error("reference tensors do not agree with each other");
}
const auto weight_type = parse_weight_type(arg_value(argc, argv, "--weight-type", "native"));
const size_t weight_context_bytes = static_cast<size_t>(int_arg(argc, argv, "--weight-context-mb", 6144)) * kMib;
const size_t ar_prefill_arena_bytes = static_cast<size_t>(int_arg(argc, argv, "--ar-prefill-arena-mb", 4096)) * kMib;
const size_t ar_decode_arena_bytes = static_cast<size_t>(int_arg(argc, argv, "--ar-decode-arena-mb", 1536)) * kMib;
const size_t nar_arena_bytes = static_cast<size_t>(int_arg(argc, argv, "--nar-arena-mb", 6144)) * kMib;
engine::models::yue2::Yue2ARRuntime ar(
execution, assets, weight_type, weight_context_bytes, ar_prefill_arena_bytes, ar_decode_arena_bytes);
engine::models::yue2::Yue2NARRuntime nar(
execution, assets, weight_type, weight_context_bytes, nar_arena_bytes);
std::cout << "prefix_tokens=" << prefix.size() << " codec_frames=" << codec.size()
<< " ode_steps=" << ode_steps << " context=" << context << " seed=" << seed << "\n";
const auto latents = nar.synthesize(
prefix, codec,
[&ar](const std::vector<int32_t> & tokens) { return ar.prefill_device_state(tokens); },
noise, static_cast<uint64_t>(seed), ode_steps, context);
if (latents.size() != latents_ref.size()) {
throw std::runtime_error("latent count does not match the reference");
}
if (const auto dump_latents = arg_value(argc, argv, "--dump-latents"); !dump_latents.empty()) {
write_f32_file(dump_latents, latents);
}
const auto metrics = compare(latents, latents_ref);
std::cout << "latents=" << latents.size() << "\n";
std::cout << "max_abs=" << metrics.max_abs << "\n";
std::cout << "rmse=" << metrics.rmse << "\n";
std::cout << "cosine=" << metrics.cosine << "\n";
if (metrics.cosine < min_cosine || metrics.rmse > max_rmse) {
throw std::runtime_error("YuE2 NAR parity check failed");
}
return 0;
} catch (const std::exception & error) {
std::cerr << "yue2_nar_parity_probe failed: " << error.what() << "\n";
return 1;
}
}