voice_audio_multi/tests/lfm3_audio/lfm3_audio_model_test.h
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

573 lines
26 KiB
C++

#pragma once
// Shared by the LFM3-Audio real-weight tests (test_lfm3_audio_{parity,asr,tts,s2s}).
//
// Arguments: --model <safetensors dir or GGUF> (default models/LFM3-Audio-2B), --fixtures <dir> (default
// models/LFM3-Audio-2B/fixtures, made by tools/community_models/lfm3_audio/make_fixtures.py),
// --backend cpu|best, --threads N, --weight-type f32|native (default f32). Skips with 125 without the model or
// the fixtures. Every expected value comes from the fixtures at run time; nothing derived from the
// checkpoint is compiled in.
//
// Tolerances (rel = ||got - want|| / ||want||, per stage):
// - F32 weights on the CPU: 1e-5, the codec waveform 1e-4; generated ids and codes identical. Encoder stages run on the
// host's features 3e-5 (test_lfm3_audio_parity), the 418 s clip 1e-4.
// - F32 weights on a GPU (TF32 / half-tile matmuls): 5e-3, the waveform 1e-2; a greedy sequence may only diverge
// at a near-tie.
// - Native bf16/f16: max(2e-2, 2x the stage's bf16-vs-fp32 noise floor from the fixture manifest).
// - q8_0: max(5e-2, 3x the floor). q4_k: numbers are reported, not checked.
#include "engine/community_models/lfm3_audio/debug.h"
#include "engine/community_models/lfm3_audio/session.h"
#include "engine/framework/audio/wav_reader.h"
#include "engine/framework/core/backend.h"
#include "engine/framework/io/filesystem.h"
#include "engine/framework/io/json.h"
#include "engine/framework/model_spec/package.h"
#include "engine/framework/runtime/model.h"
#include "engine/framework/runtime/session.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <limits>
#include <memory>
#include <optional>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
#ifndef ENGINE_REPO_ROOT
#define ENGINE_REPO_ROOT "."
#endif
namespace lfm3_model_test {
namespace lfm3 = engine::community_models::lfm3_audio;
namespace runtime = engine::runtime;
namespace json = engine::io::json;
constexpr int kExitPass = 0;
constexpr int kExitFail = 1;
constexpr int kExitSkip = 125;
constexpr int kCodebooks = 12;
// A greedy flip on a GPU counts as a near-tie when the reference's top-2 logit gap is below this; GPU F32 logits
// are off by up to ~1e-2 absolute (TF32 / half tiles).
constexpr double kNearTieGap = 1e-2;
constexpr double kReportOnly = std::numeric_limits<double>::infinity();
enum class Profile { F32Cpu, F32Gpu, Native, Q8_0, Q4_K };
inline const char * profile_name(Profile profile) {
switch (profile) {
case Profile::F32Cpu: return "f32-cpu";
case Profile::F32Gpu: return "f32-gpu";
case Profile::Native: return "native";
case Profile::Q8_0: return "q8_0";
case Profile::Q4_K: return "q4_k";
}
return "?";
}
inline std::filesystem::path repo_path(const std::string & relative) {
return std::filesystem::path(ENGINE_REPO_ROOT) / relative;
}
inline 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;
}
inline bool has_flag(int argc, char ** argv, const std::string & name) {
return std::find(argv + 1, argv + argc, name) != argv + argc;
}
struct Context {
std::filesystem::path model;
std::filesystem::path fixtures;
engine::core::BackendConfig backend;
std::string weight_type;
Profile profile = Profile::F32Cpu;
json::Value manifest;
// The tests call the loader and asset reader directly, which ignore request.model_spec_override.
std::unique_ptr<engine::model_spec::ScopedSpecOverride> specs;
std::unique_ptr<runtime::ILoadedVoiceModel> loaded;
[[nodiscard]] bool strict_text() const { return profile != Profile::Q8_0 && profile != Profile::Q4_K; }
[[nodiscard]] lfm3::Lfm3WeightOptions weight_options() const {
lfm3::Lfm3WeightOptions out;
out.type = weight_type == "f32" ? lfm3::Lfm3WeightType::F32 : lfm3::Lfm3WeightType::Native;
return out;
}
// The bf16-vs-fp32 rel-RMS the fixture run measured for a stage, 0 when it has none.
[[nodiscard]] double noise_floor(const std::string & key) const {
const auto * floors = manifest.find("noise_floor");
const auto * stage = floors != nullptr ? floors->find(key) : nullptr;
const auto * rel = stage != nullptr ? stage->find("rel_rms") : nullptr;
return rel != nullptr && rel->is_number() ? rel->as_number() : 0.0;
}
// `long_input`: the 418 s clip, where the fp32 reference itself is 1.15e-5 off an fp64 run, so 1e-5 cannot hold.
[[nodiscard]] double tolerance(const std::string & floor_key, bool wave = false, bool long_input = false) const {
switch (profile) {
case Profile::F32Cpu: return wave || long_input ? 1e-4 : 1e-5;
case Profile::F32Gpu: return wave ? 1e-2 : 5e-3;
case Profile::Native: return std::max(2e-2, 2.0 * noise_floor(floor_key));
case Profile::Q8_0: return std::max(5e-2, 3.0 * noise_floor(floor_key));
case Profile::Q4_K: return kReportOnly;
}
return kReportOnly;
}
[[nodiscard]] std::string result_text(const std::string & key) const {
return manifest.require("results").require(key).require("text").as_string();
}
[[nodiscard]] std::filesystem::path fixture(const std::string & name) const { return fixtures / name; }
std::vector<float> f32(const std::string & name, std::vector<int64_t> * shape = nullptr) const {
return lfm3::lfm3_read_npy_f32(fixtures / (name + ".npy"), shape);
}
std::vector<int32_t> i32(const std::string & name, std::vector<int64_t> * shape = nullptr) const {
return lfm3::lfm3_read_npy_i32(fixtures / (name + ".npy"), shape);
}
runtime::ILoadedVoiceModel & model_handle() {
if (loaded == nullptr) {
runtime::ModelLoadRequest request;
request.model_path = model;
request.family_hint = "lfm3_audio";
loaded = lfm3::make_lfm3_audio_loader()->load(request);
}
return *loaded;
}
template <typename Session>
std::unique_ptr<Session> open(runtime::VoiceTaskKind kind, runtime::RunMode mode, std::unordered_map<std::string, std::string> extra = {}) {
runtime::SessionOptions options;
options.backend = backend;
options.options = std::move(extra);
options.options["lfm3_audio.weight_type"] = weight_type;
if (kind == runtime::VoiceTaskKind::Asr) {
options.options["lfm3_audio.vad_model_path"] = repo_path("assets/framework/models/silero_vad").string();
}
auto session = model_handle().create_task_session({kind, mode}, options);
session->prepare({});
auto * typed = dynamic_cast<Session *>(session.get());
if (typed == nullptr) {
throw std::runtime_error("unexpected LFM3-Audio session type");
}
session.release();
return std::unique_ptr<Session>(typed);
}
};
inline std::optional<Context> parse_context(int argc, char ** argv, const char * test, int & exit_code) {
Context ctx;
ctx.model = arg_value(argc, argv, "--model", repo_path("models/LFM3-Audio-2B").string());
ctx.fixtures = arg_value(argc, argv, "--fixtures", repo_path("models/LFM3-Audio-2B/fixtures").string());
const auto backend = arg_value(argc, argv, "--backend", "cpu");
ctx.weight_type = arg_value(argc, argv, "--weight-type", "f32");
const int threads = std::atoi(arg_value(argc, argv, "--threads", "8").c_str());
if ((backend != "cpu" && backend != "best") || (ctx.weight_type != "f32" && ctx.weight_type != "native")) {
std::cerr << "FAIL: --backend must be cpu or best, --weight-type f32 or native\n";
exit_code = kExitFail;
return std::nullopt;
}
if (!std::filesystem::exists(ctx.model) || !engine::io::is_existing_file(ctx.fixtures / "manifest.json")) {
std::fprintf(stderr,
"SKIP: %s needs the LFM3-Audio model at '%s' and its fixtures at '%s' (--model, --fixtures).\n"
" Fixtures: tools/community_models/lfm3_audio/make_fixtures.py\n",
test, ctx.model.string().c_str(), ctx.fixtures.string().c_str());
exit_code = kExitSkip;
return std::nullopt;
}
ctx.backend.type = backend == "cpu" ? engine::core::BackendType::Cpu : engine::core::BackendType::BestAvailable;
ctx.backend.threads = threads > 0 ? threads : 1;
std::string file = ctx.model.filename().string();
std::transform(file.begin(), file.end(), file.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (ctx.weight_type == "f32") {
ctx.profile = backend == "cpu" ? Profile::F32Cpu : Profile::F32Gpu;
} else if (file.find("q8_0") != std::string::npos) {
ctx.profile = Profile::Q8_0;
} else if (file.find("q4_k") != std::string::npos || file.find("q4_0") != std::string::npos) { // any 4-bit: reported only
ctx.profile = Profile::Q4_K;
} else {
ctx.profile = Profile::Native;
}
ctx.manifest = json::parse_file(ctx.fixtures / "manifest.json");
ctx.specs = std::make_unique<engine::model_spec::ScopedSpecOverride>(repo_path("model_specs"), ctx.model);
std::cout << test << ": model " << ctx.model.string() << ", backend " << backend << ", weights " << ctx.weight_type << ", profile "
<< profile_name(ctx.profile) << "\n";
return ctx;
}
struct Diff {
double rel = 0.0;
double max_abs = 0.0;
};
inline Diff diff(const float * got, const float * want, size_t n) {
double num = 0.0, den = 0.0, max_abs = 0.0;
for (size_t i = 0; i < n; ++i) {
const double d = static_cast<double>(got[i]) - want[i];
num += d * d;
den += static_cast<double>(want[i]) * want[i];
max_abs = std::max(max_abs, std::fabs(d));
}
return {std::sqrt(num / std::max(den, 1e-30)), max_abs};
}
inline Diff diff(const std::vector<float> & got, const std::vector<float> & want) {
return diff(got.data(), want.data(), std::min(got.size(), want.size()));
}
inline std::string sci(double value) {
char out[32];
std::snprintf(out, sizeof(out), "%.3e", value);
return out;
}
class Checks {
public:
void expect(bool ok, const std::string & label, const std::string & detail = "") {
std::cout << (ok ? "PASS " : "FAIL ") << label << (detail.empty() ? "" : ": " + detail) << "\n";
failures_ += ok ? 0 : 1;
}
void info(const std::string & label, const std::string & detail = "") {
std::cout << "INFO " << label << (detail.empty() ? "" : ": " + detail) << "\n";
}
bool expect_rel(const Diff & d, double tolerance, const std::string & label, const std::string & extra = "") {
const bool ok = d.rel <= tolerance;
const std::string detail = "rel " + sci(d.rel) + " max_abs " + sci(d.max_abs) +
(std::isinf(tolerance) ? std::string(" (report only)") : " (tol " + sci(tolerance) + ")") + extra;
if (std::isinf(tolerance)) {
info(label, detail);
} else {
expect(ok, label, detail);
}
return ok;
}
void expect_or_info(bool strict, bool ok, const std::string & label, const std::string & detail = "") {
if (strict || ok) {
expect(ok, label, detail);
} else {
info(label + " (report only, differs)", detail);
}
}
[[nodiscard]] int failures() const { return failures_; }
int finish(const char * test) const {
std::cout << (failures_ == 0 ? "PASS" : "FAIL") << ": " << test << ", " << failures_ << " failed checks\n";
return failures_ == 0 ? kExitPass : kExitFail;
}
private:
int failures_ = 0;
};
inline void set_env(const char * name, const std::string & value) {
#ifdef _WIN32
_putenv_s(name, value.c_str());
#else
setenv(name, value.c_str(), 1);
#endif
}
inline void unset_env(const char * name) {
#ifdef _WIN32
_putenv_s(name, "");
#else
unsetenv(name);
#endif
}
// Depth logits are dumped for 400 frames: the fixtures keep 40, and later frames give our own top-2 gaps.
class ScopedDumps {
public:
explicit ScopedDumps(const std::string & name, const std::filesystem::path & orders = {})
: dir_(std::filesystem::temp_directory_path() / ("lfm3_audio_model_test_" + name + "_" + std::to_string(std::random_device{}()))) {
std::filesystem::remove_all(dir_);
std::filesystem::create_directories(dir_);
set_env("LFM3_AUDIO_DUMP_DIR", dir_.string());
set_env("LFM3_AUDIO_DUMP_FRAMES", "400");
if (!orders.empty()) {
set_env("LFM3_AUDIO_MASKGIT_ORDERS", orders.string());
}
}
~ScopedDumps() {
stop();
std::error_code ignored;
std::filesystem::remove_all(dir_, ignored);
}
ScopedDumps(const ScopedDumps &) = delete;
ScopedDumps & operator=(const ScopedDumps &) = delete;
void stop() {
unset_env("LFM3_AUDIO_DUMP_DIR");
unset_env("LFM3_AUDIO_DUMP_FRAMES");
unset_env("LFM3_AUDIO_MASKGIT_ORDERS");
}
[[nodiscard]] bool has(const std::string & name) const { return std::filesystem::exists(dir_ / (name + ".npy")); }
std::vector<float> f32(const std::string & name, std::vector<int64_t> * shape = nullptr) const {
return lfm3::lfm3_read_npy_f32(dir_ / (name + ".npy"), shape);
}
std::vector<int32_t> i32(const std::string & name, std::vector<int64_t> * shape = nullptr) const {
return lfm3::lfm3_read_npy_i32(dir_ / (name + ".npy"), shape);
}
private:
std::filesystem::path dir_;
};
inline runtime::AudioBuffer read_audio(const std::filesystem::path & path) {
auto wav = engine::audio::read_wav_f32(path);
return runtime::AudioBuffer{wav.sample_rate, wav.channels, std::move(wav.samples)};
}
inline runtime::TaskRequest audio_request(runtime::AudioBuffer audio, std::unordered_map<std::string, std::string> options = {}) {
runtime::TaskRequest out;
out.audio_input = std::move(audio);
out.options = std::move(options);
return out;
}
inline runtime::TaskRequest text_request(const std::string & text, std::unordered_map<std::string, std::string> options = {}) {
runtime::TaskRequest out;
out.text_input = runtime::Transcript{text, ""};
out.options = std::move(options);
return out;
}
inline std::string text_of(const runtime::TaskResult & result) {
return result.text_output.has_value() ? result.text_output->text : std::string("<no text>");
}
inline std::vector<std::string> wer_words(const std::string & text) {
std::vector<std::string> words;
std::string word;
for (const char ch : text) {
const auto c = static_cast<unsigned char>(ch);
if (c >= 0x80 || std::isalnum(c) || c == '\'') {
word.push_back(static_cast<char>(std::tolower(c)));
} else if (!word.empty()) {
words.push_back(word);
word.clear();
}
}
if (!word.empty()) {
words.push_back(word);
}
return words;
}
inline double wer(const std::string & reference, const std::string & hypothesis) {
const auto ref = wer_words(reference);
const auto hyp = wer_words(hypothesis);
std::vector<size_t> row(hyp.size() + 1);
for (size_t j = 0; j <= hyp.size(); ++j) row[j] = j;
for (size_t i = 1; i <= ref.size(); ++i) {
size_t diagonal = row[0];
row[0] = i;
for (size_t j = 1; j <= hyp.size(); ++j) {
const size_t up = row[j];
row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (ref[i - 1] == hyp[j - 1] ? 0 : 1)});
diagonal = up;
}
}
return static_cast<double>(row[hyp.size()]) / static_cast<double>(std::max<size_t>(ref.size(), 1));
}
// A text token, or a frame (token -1, 12 codes) as the generator's steps dump writes it.
struct Slot {
int32_t token = -1;
std::vector<int32_t> codes;
bool operator==(const Slot & other) const { return token == other.token && codes == other.codes; }
};
inline std::vector<Slot> dumped_slots(const ScopedDumps & dumps) {
std::vector<int64_t> shape;
const auto rows = dumps.i32("steps", &shape);
std::vector<Slot> out(static_cast<size_t>(shape.at(0)));
for (size_t s = 0; s < out.size(); ++s) {
const auto * row = rows.data() + s * (kCodebooks + 1);
out[s].token = row[0];
if (row[1] >= 0) out[s].codes.assign(row + 1, row + 1 + kCodebooks);
}
return out;
}
inline std::vector<Slot> fixture_slots(const Context & ctx, const std::string & tag) {
const auto kind = ctx.i32("gen/" + tag + "_slot_kind");
const auto token = ctx.i32("gen/" + tag + "_slot_token");
const auto codes = ctx.i32("gen/" + tag + "_slot_codes");
std::vector<Slot> out(kind.size());
for (size_t s = 0; s < out.size(); ++s) {
if (kind[s] == 1) {
out[s].codes.assign(codes.begin() + static_cast<std::ptrdiff_t>(s * kCodebooks), codes.begin() + static_cast<std::ptrdiff_t>((s + 1) * kCodebooks));
} else {
out[s].token = token[s];
}
}
return out;
}
// "T3 A5 ..." run-length pattern of the slot kinds.
inline std::string slot_pattern(const std::vector<Slot> & slots) {
std::string out;
for (size_t s = 0; s < slots.size();) {
const bool frame = !slots[s].codes.empty();
size_t e = s;
while (e < slots.size() && slots[e].codes.empty() != frame) ++e;
out += (out.empty() ? "" : " ") + std::string(frame ? "A" : "T") + std::to_string(e - s);
s = e;
}
return out;
}
inline double top2_gap(const float * logits, size_t n) {
float first = -std::numeric_limits<float>::infinity(), second = first;
for (size_t i = 0; i < n; ++i) {
if (logits[i] > first) {
second = first;
first = logits[i];
} else if (logits[i] > second) {
second = logits[i];
}
}
return static_cast<double>(first) - second;
}
// Compares a greedy run with the fixture run `tag`: slots, MaskGIT orders and depth logits before the first difference.
// `drop_last` leaves out the fixture's trailing <|im_end|>, which TTS does not generate. Returns whether the slots are identical.
inline bool check_greedy_run(Checks & checks, const Context & ctx, const ScopedDumps & dumps, const std::string & tag, bool drop_last) {
auto want = fixture_slots(ctx, tag);
if (drop_last) want.pop_back();
const auto got = dumped_slots(dumps);
size_t first = 0;
while (first < std::min(got.size(), want.size()) && got[first] == want[first]) ++first;
const bool same = first == got.size() && got.size() == want.size();
const std::string label = tag + " greedy slots";
if (same) {
checks.expect(true, label, "identical, " + std::to_string(got.size()) + " slots, " + slot_pattern(got));
} else {
std::string where = "first difference at slot " + std::to_string(first) + " of " + std::to_string(want.size()) + " (got " +
std::to_string(got.size()) + ", " + slot_pattern(got) + " vs " + slot_pattern(want) + ")";
double gap = kReportOnly;
if (first < want.size()) {
const auto before = static_cast<size_t>(std::count_if(want.begin(), want.begin() + static_cast<std::ptrdiff_t>(first), [&](const Slot & s) {
return s.codes.empty() == want[first].codes.empty();
}));
if (want[first].codes.empty()) {
std::vector<int64_t> shape;
const auto top = ctx.f32("gen/" + tag + "_text_top20_vals", &shape);
if (before < static_cast<size_t>(shape[0])) gap = static_cast<double>(top[before * 20]) - top[before * 20 + 1];
where += ", text step " + std::to_string(before);
} else if (first < got.size() && !got[first].codes.empty()) {
// The first codebook to differ in reveal order (later passes see it), on the raw picks (the reference's before EOS
// normalization, ours the argmax of each dumped pass): an EOS frame is all end-of-audio codes whichever pick ended it.
const auto order = ctx.i32("gen/" + tag + "_frame_order");
const auto * row = order.data() + before * kCodebooks;
std::vector<int64_t> shape;
const auto logits = ctx.f32("gen/" + tag + "_frame_pass_logits", &shape);
const size_t vocab = static_cast<size_t>(shape[3]);
const auto raw = ctx.i32("gen/" + tag + "_frame_codes_raw");
size_t at = 0;
for (; at < kCodebooks; ++at) {
const auto name = "depth_logits_f" + std::to_string(before) + "_p" + std::to_string(at / 4);
if (!dumps.has(name)) break; // MaskGIT stops after the pass that yields the EOS code
const auto mine = dumps.f32(name);
const float * r = mine.data() + static_cast<size_t>(row[at]) * vocab;
if (std::max_element(r, r + vocab) - r != raw[before * kCodebooks + static_cast<size_t>(row[at])]) break;
}
// Without the raw difference in the dumps, the normalized codes tell.
if (at == kCodebooks || !dumps.has("depth_logits_f" + std::to_string(before) + "_p" + std::to_string(at / 4))) {
at = 0;
while (at < kCodebooks && got[first].codes[row[at]] == want[first].codes[row[at]]) ++at;
at = std::min<size_t>(at, kCodebooks - 1);
}
const auto k = static_cast<size_t>(row[at]);
const size_t pass = at / 4;
where += ", frame " + std::to_string(before) + " codebook " + std::to_string(k) + " pass " + std::to_string(pass);
const bool has_ref = static_cast<int64_t>(before) < shape[0];
const float * ref = has_ref ? logits.data() + ((before * 3 + pass) * kCodebooks + k) * vocab : nullptr;
if (has_ref) gap = top2_gap(ref, vocab);
const auto name = "depth_logits_f" + std::to_string(before) + "_p" + std::to_string(pass);
if (dumps.has(name)) {
// The fixtures keep 40 frames of logits; past them only this run's own gap tells a near-tie.
const auto mine = dumps.f32(name);
const double ours = top2_gap(mine.data() + k * vocab, vocab);
where += ", our top-2 gap " + sci(ours) + (has_ref ? ", row max_abs vs reference " + sci(diff(mine.data() + k * vocab, ref, vocab).max_abs) : "");
if (!has_ref) gap = ours;
}
}
}
where += ", near-tie gap " + (std::isinf(gap) ? std::string("n/a") : sci(gap));
if (ctx.profile == Profile::F32Cpu) {
checks.expect(false, label, where);
} else if (ctx.profile == Profile::F32Gpu) {
checks.expect(gap < kNearTieGap, label + " diverge only at a near-tie (gap < " + sci(kNearTieGap) + ")", where);
} else {
checks.info(label + " (report only)", where);
}
}
// The orders are injected for the fixture's frames; a longer run draws the rest.
std::vector<int64_t> shape;
const auto orders = dumps.i32("maskgit_orders", &shape);
const auto reference = ctx.i32("gen/" + tag + "_frame_order");
const size_t compared = std::min(orders.size(), reference.size());
checks.expect(std::equal(orders.begin(), orders.begin() + static_cast<std::ptrdiff_t>(compared), reference.begin()),
tag + " MaskGIT orders are the injected ones", std::to_string(compared / kCodebooks) + " of " + std::to_string(shape.at(0)) + " frames");
// Depth logits of the frames both runs reached with the same inputs.
const auto frames_before = static_cast<size_t>(std::count_if(want.begin(), want.begin() + static_cast<std::ptrdiff_t>(first), [](const Slot & s) {
return !s.codes.empty();
}));
const auto logits = ctx.f32("gen/" + tag + "_frame_pass_logits", &shape);
const size_t vocab = static_cast<size_t>(shape[3]);
double num = 0.0, den = 0.0, max_abs = 0.0;
size_t passes = 0;
for (size_t f = 0; f < std::min<size_t>(frames_before, static_cast<size_t>(shape[0])); ++f) {
for (size_t p = 0; p < 3; ++p) {
const auto name = "depth_logits_f" + std::to_string(f) + "_p" + std::to_string(p);
if (!dumps.has(name)) continue; // MaskGIT stops after the pass that yields the EOS code
const auto mine = dumps.f32(name);
const float * ref = logits.data() + (f * 3 + p) * kCodebooks * vocab;
const auto d = diff(mine.data(), ref, kCodebooks * vocab);
double ref_sq = 0.0;
for (size_t i = 0; i < kCodebooks * vocab; ++i) ref_sq += static_cast<double>(ref[i]) * ref[i];
num += d.rel * d.rel * ref_sq;
den += ref_sq;
max_abs = std::max(max_abs, d.max_abs);
++passes;
}
}
if (passes > 0) {
// Generation feeds the depth decoder this run's own backbone output, so only F32 is held to the stage tolerance.
const bool f32 = ctx.profile == Profile::F32Cpu || ctx.profile == Profile::F32Gpu;
checks.expect_rel({std::sqrt(num / std::max(den, 1e-30)), max_abs}, f32 ? ctx.tolerance("depth.logits") : kReportOnly,
tag + " depth logits in generation", ", " + std::to_string(passes) + " passes of " + std::to_string(frames_before) + " frames");
} else if (frames_before > 0 && shape[0] > 0) {
checks.expect(false, tag + " depth logits in generation", "no depth_logits dumps for " + std::to_string(frames_before) + " frames");
}
return same;
}
} // namespace lfm3_model_test