{
  "_comment": [
    "funasr_nano correctness regime:",
    "  - GGUF dtype: BF16 (per-tensor: BF16 matrices/embeddings, F32 norms/biases).",
    "  - KV cache dtype: F16 (TRANSCRIBE_KV_TYPE_AUTO falls back to F16; ggml does",
    "    not yet support BF16 KV cache, which would be the natural match for the",
    "    LM's BF16 weights).",
    "  - Frontend: C++ kaldi-fbank+LFR (no CMVN), production path; the reference",
    "    runs FunASR's WavFrontend on CPU with dither=0 (after the baseline-",
    "    snapshot fix in the dumper, see below).",
    "  - Backend / threads: --backend cpu --threads 1 (deterministic).",
    "  - Reference framework: FunASR 1.3.1 author repo, F32-upcast inference",
    "    (encoder + adaptor) + BF16 LM (HF transformers Qwen3 in bf16).",
    "  - C++ compute: F32 throughout the encoder + adaptor; LM uses BF16 weights",
    "    with F32 mul_mat compute. KV cache reads/writes go through F16.",
    "",
    "Dominant drift sources:",
    "  - 70-block SAN-M residual stack (encoders0+encoders+tp_encoders) accumulates",
    "    fp32 reduction-order error and BF16 weight-storage error linearly through",
    "    each residual add. After ~50 blocks the post-residual magnitude carries",
    "    O(80) drift on a position with strong signal; the trailing LayerNorm",
    "    (after_norm, tp_norm) collapses it back to O(0.02).",
    "  - 28-block Qwen3 LM with BF16 weights and F16 KV. Prefill drift on logits",
    "    is small (~5e-2 max) because the F32 mul_mat dominates. Step-graph (gen8)",
    "    drift is ~50x larger because the F16 KV cache rounds K/V at every write",
    "    and step attention amplifies the per-write error across the cached",
    "    positions.",
    "  - Adaptor: 2 LayerNorm-residual blocks. Block-0 / block-1 outputs each",
    "    grow to O(10-30) before the next LayerNorm absorbs the magnitude.",
    "",
    "Tolerances are set to ~2x observed C++-vs-reference drift on jfk @ Stage 4",
    "(see reports/porting/funasr_nano/forward-map.md for the per-stage map).",
    "",
    "Sibling-variant widening (fun-asr-mlt-nano-2512, Stage 4 sibling shortcut):",
    "  - enc.encoders.48.out: nano drift ~155, MLT drift ~169. Tolerance widened",
    "    160 -> 260 (1.5 x max-of-variants drift) to cover both checkpoints. The",
    "    drift is spread across positions, mechanism is BLAS reduction-order +",
    "    BF16 LM weight quantization; not localized.",
    "  - enc.tp_encoders.0.out: nano drift ~3.5, MLT drift ~7.3. Tolerance widened",
    "    5.0 -> 12.0 (1.5 x max-of-variants drift). Same mechanism.",
    "  - dec.logits_raw.gen8: a real off-by-one in CPP's gen-step dump (capturing",
    "    the 10th lm_head call instead of the 9th to match REF) was masked on nano",
    "    because nano's adjacent-step logits are similar in magnitude. MLT's tied-",
    "    lm_head distribution is heavily shifted (REF mean ~13.4 vs nano's ~0.27),",
    "    so the off-by-one produced 12.9 mean drift on MLT. Fixed in",
    "    src/arch/funasr_nano/model.cpp (gen_dump_step 8 -> 7); both variants now",
    "    drift well under 1e-1 max on this tensor.",
    "",
    "Reference-dumper note: FunASR's AutoModel snapshots `self.kwargs` at __init__",
    "time and resets it with copy.deepcopy on every auto.generate() call. Setting",
    "auto.kwargs[\"frontend\"].dither=0 in the dumper was wiped by that reset, so",
    "auto.generate ran with the default dither=1.0, adding non-deterministic",
    "noise to the kaldi fbank feeding the encoder. The dumper now calls",
    "auto._store_base_configs() AFTER the dither tweak so the baseline carries",
    "the deterministic frontend. Without that fix every encoder dump diverged",
    "wildly at silence positions (drift up to ~565 on enc.embed.out) for reasons",
    "unrelated to the C++ port.",
    "",
    "Ship gate: transcript exact-match on jfk + dec.logits_raw.prefill drift small",
    "enough that argmax decisions agree with the reference. Both hold at ref dtype."
  ],
  "frontend.fbank.lfr.cmvn.out": { "max_abs": 0.030,  "mean_abs": 0.006   },
  "enc.embed.out":               { "max_abs": 0.7,    "mean_abs": 0.13    },
  "enc.encoders0.0.out":         { "max_abs": 0.40,   "mean_abs": 0.032   },
  "enc.encoders.0.out":          { "max_abs": 0.60,   "mean_abs": 0.036   },
  "enc.encoders.24.out":         { "max_abs": 3.0,    "mean_abs": 0.16    },
  "enc.encoders.48.out":         { "max_abs": 260.0,  "mean_abs": 0.70    },
  "enc.after_norm.out":          { "max_abs": 0.22,   "mean_abs": 0.0062  },
  "enc.tp_encoders.0.out":       { "max_abs": 12.0,   "mean_abs": 0.012   },
  "enc.tp_encoders.10.out":      { "max_abs": 22.0,   "mean_abs": 0.020   },
  "enc.tp_encoders.19.out":      { "max_abs": 20.0,   "mean_abs": 0.040   },
  "enc.tp_norm.out":             { "max_abs": 0.05,   "mean_abs": 0.0016  },
  "adaptor.linear1.out":         { "max_abs": 0.32,   "mean_abs": 0.006   },
  "adaptor.linear2.out":         { "max_abs": 3.5,    "mean_abs": 0.022   },
  "adaptor.blocks.0.out":        { "max_abs": 25.0,   "mean_abs": 0.36    },
  "adaptor.out":                 { "max_abs": 75.0,   "mean_abs": 0.26    },
  "dec.logits_raw.prefill":      { "max_abs": 0.12,   "mean_abs": 0.022   },
  "dec.logits_raw.gen8":         { "max_abs": 25.0,   "mean_abs": 2.5     }
}
