/* * Generation parity for MOSS-VoiceGenerator: runs the backbone, the 1 + n_vq heads and the * delay-pattern state machine greedily, and compares the emitted rows against a dump from * the reference PyTorch generate(). Greedy with the repetition penalty off, so a mismatch * is a real divergence rather than an RNG difference. * * moss_voicegen_generation_parity --model --prompt \ * --generation [--weight-type bf16] [--threads N] */ #include "engine/community_models/moss_voicegen/assets.h" #include "engine/framework/decoders/moss_tts_delay/backbone.h" #include "engine/framework/decoders/moss_tts_delay/delay_decoder.h" #include "engine/framework/decoders/moss_tts_delay/heads.h" #include "engine/community_models/moss_voicegen/tokenizer_text.h" #include "engine/framework/core/backend.h" #include "engine/framework/core/execution_context.h" #include "engine/framework/io/json.h" #include "engine/framework/modules/multi_codebook_embedding.h" #include #include #include #include #include #include namespace { namespace json = engine::io::json; 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; } engine::assets::TensorStorageType parse_weight_type(const std::string & value) { if (value == "f32") { return engine::assets::TensorStorageType::F32; } if (value == "bf16") { return engine::assets::TensorStorageType::BF16; } if (value == "q8_0") { return engine::assets::TensorStorageType::Q8_0; } if (value == "native") { return engine::assets::TensorStorageType::Native; } throw std::runtime_error("unsupported --weight-type: " + value); } std::vector> require_rows(const json::Value & object, const std::string & key) { std::vector> rows; for (const auto & row : object.require(key).as_array()) { std::vector values; for (const auto & value : row.as_array()) { values.push_back(value.as_i64()); } rows.push_back(std::move(values)); } return rows; } } // namespace int main(int argc, char ** argv) { try { const std::string model_dir = arg_value(argc, argv, "--model", ""); const std::string prompt_path = arg_value(argc, argv, "--prompt", ""); const std::string generation_path = arg_value(argc, argv, "--generation", ""); const auto weight_type = parse_weight_type(arg_value(argc, argv, "--weight-type", "bf16")); if (model_dir.empty() || prompt_path.empty() || generation_path.empty()) { std::cerr << "usage: moss_voicegen_generation_parity --model --prompt " " --generation \n"; return 2; } const auto prompt_reference = json::parse_file(prompt_path); const auto generation_reference = json::parse_file(generation_path); const auto expected = require_rows(generation_reference, "generated_rows"); const auto assets = engine::models::moss_voicegen::load_moss_voicegen_assets(model_dir); const auto & config = assets->config; const int64_t n_vq = config.num_codebooks; const int64_t hidden_size = config.backbone.hidden_size; const engine::models::moss_voicegen::MossVoiceGenTextProcessor processor(assets); const auto prompt = processor.build_generation_prefix( json::require_string(prompt_reference, "text"), json::require_string(prompt_reference, "instruction"), json::require_string(prompt_reference, "language")); const int64_t prompt_rows = static_cast(prompt.text_tokens.size()); const auto steps = static_cast(expected.size()); engine::modules::MultiCodebookEmbeddingSpec codebook_spec; codebook_spec.hidden_size = hidden_size; codebook_spec.num_codebooks = n_vq; codebook_spec.vocab_size = config.audio_vocab_size + 1; codebook_spec.pad_token_id = config.audio_pad_code; codebook_spec.tensor_prefix = "emb_ext"; const engine::modules::MultiCodebookEmbedding codebooks(*assets->model_weights, codebook_spec); std::vector prompt_bias(static_cast(prompt_rows * hidden_size), 0.0F); for (int64_t row = 0; row < prompt_rows; ++row) { codebooks.add_bias( prompt.audio_codes.data() + static_cast(row * n_vq), prompt_bias.data() + static_cast(row * hidden_size)); } engine::core::BackendConfig backend_config; backend_config.type = engine::core::BackendType::Cpu; backend_config.device = 0; backend_config.threads = std::stoi(arg_value(argc, argv, "--threads", "8")); engine::core::ExecutionContext execution_context(backend_config); const engine::decoders::MossTtsDelayBackboneRuntime backbone( assets->config, assets->model_weights, execution_context, 512ull * 1024ull * 1024ull, 8192ull * 1024ull * 1024ull, weight_type); const engine::decoders::MossTtsDelayHeadsRuntime heads( assets->config, assets->model_weights, execution_context, 256ull * 1024ull * 1024ull, 4096ull * 1024ull * 1024ull, weight_type); engine::decoders::MossTtsDelaySamplingOptions sampling; sampling.do_sample = false; sampling.text_temperature = 1.0F; sampling.audio_temperature = 1.0F; sampling.audio_repetition_penalty = 1.0F; engine::decoders::MossTtsDelayDecoder decoder(config, sampling, 0); backbone.begin_generation(prompt_rows + steps + 8); auto hidden = backbone.prefill(prompt.text_tokens, prompt_bias); engine::decoders::MossTtsDelayStepLogits logits; std::vector row_bias(static_cast(hidden_size), 0.0F); int64_t mismatching_rows = 0; int64_t produced = 0; int64_t first_divergent_row = -1; // Keeping the pre-decode logits lets a mismatch report *how close* the call was. // Greedy decoding is a step function: a rounding difference of a thousandth flips // the argmax and the trajectories part company for good. std::vector> logits_before; for (int64_t step = 0; step < steps; ++step) { heads.evaluate(hidden, logits); logits_before = logits.audio; const auto row = decoder.step(logits); ++produced; const auto & expected_row = expected[static_cast(step)]; bool row_matches = static_cast(expected_row.size()) == n_vq + 1 && expected_row[0] == static_cast(row.text_token); if (row_matches) { for (int64_t codebook = 0; codebook < n_vq; ++codebook) { if (expected_row[static_cast(codebook + 1)] != static_cast(row.codes[static_cast(codebook)])) { row_matches = false; break; } } } if (!row_matches) { if (first_divergent_row < 0) { first_divergent_row = step; for (int64_t codebook = 0; codebook < n_vq; ++codebook) { const auto expected_code = expected_row[static_cast(codebook + 1)]; const auto actual_code = static_cast(row.codes[static_cast(codebook)]); if (expected_code == actual_code) { continue; } const auto & before = logits_before[static_cast(codebook)]; if (expected_code < static_cast(before.size()) && actual_code < static_cast(before.size())) { std::cout << "first divergence at row " << step << " codebook " << codebook << ": logit(expected " << expected_code << ")=" << before[static_cast(expected_code)] << " vs logit(chosen " << actual_code << ")=" << before[static_cast(actual_code)] << " gap=" << (before[static_cast(actual_code)] - before[static_cast(expected_code)]) << "\n"; } break; } } if (++mismatching_rows <= 5) { std::cerr << "row " << step << " mismatch\n expected text=" << expected_row[0] << " got text=" << row.text_token << "\n"; std::cerr << " expected codes:"; for (int64_t codebook = 0; codebook < n_vq; ++codebook) { std::cerr << " " << expected_row[static_cast(codebook + 1)]; } std::cerr << "\n actual codes:"; for (int64_t codebook = 0; codebook < n_vq; ++codebook) { std::cerr << " " << row.codes[static_cast(codebook)]; } std::cerr << "\n"; } } if (decoder.stopped()) { break; } std::fill(row_bias.begin(), row_bias.end(), 0.0F); codebooks.add_bias(row.codes.data(), row_bias.data()); hidden = backbone.step(row.text_token, row_bias); } int64_t codebooks_out = 0; int64_t frames_out = 0; const auto codes = decoder.extract_audio_codes(codebooks_out, frames_out); const int64_t matching_prefix = first_divergent_row < 0 ? produced : first_divergent_row; std::cout << "generated rows=" << produced << "/" << steps << " mismatching=" << mismatching_rows << " matching_prefix=" << matching_prefix << "\n"; std::cout << "de-delayed codes: " << codebooks_out << " codebooks x " << frames_out << " frames (" << codes.size() << " values)\n"; const int64_t required_prefix = std::stoll(arg_value(argc, argv, "--min-matching-prefix", std::to_string(steps))); const bool passed = matching_prefix >= required_prefix; std::cout << (passed ? "PASS" : "FAIL") << ": required matching prefix " << required_prefix << "\n"; return passed ? 0 : 1; } catch (const std::exception & error) { std::cerr << "error: " << error.what() << "\n"; return 1; } }