<!--
SPDX-License-Identifier: Apache-2.0
Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
-->
# C++ ↔ Rust Parity Audit — NeuralAmpModelerCore × NAM-rs
Ground-truth comparison between the canonical C++ reference, **NeuralAmpModelerCore**
("NAMcore", vendored read-only at `../third-party/NeuralAmpModelerCore/`), and the NAM-rs
Rust engine (`src/`). NAMcore is the **sole source of truth** for parity — the f64 reference
oracle and the NumPy anchor are error-decomposition tools, not arbiters of correctness (see
[§1.2](#12-the-f64-oracle-is-a-decomposition-tool-not-a-gate)).
This document is audited **per architecture, in phases**, by reading the vendored C++ source
line-by-line against the Rust implementation — not by trusting prior write-ups. Each
architecture section carries a verification banner stating what was actually re-checked and
when. **For a single-page triage of what is actually broken vs. what is under control, read
[§7](#7-known-broken-ledger-sabidamente-broken) first.**
## 0. Audit Status
| Architecture | Status | Section |
|:--------------------------- |:----------------------------------------------------------------------------------------------------- |:----------------------------------------- |
| **LSTM** | ✅ Fully Verified — Native f32 weights, bit-exact/sub-1e-11 interop parity vs NAMcore | [§2](#2-lstm-architecture) |
| **WaveNet A1** | ✅ Fully Verified — Const-generic fast path & dynamic fallback pass canonical golden gates | [§3](#3-wavenet-a1-architecture) |
| **WaveNet A2** | 🟡 Verified Dynamic/Fast paths — 🔴 Flagship (`wavenet_a2_max.nam`) disabled fail-closed at dispatch | [§4](#4-wavenet-a2-architecture) |
| **ConvNet** | ✅ IDÊNTICO — Paridade Total de Inicialização e Aritmética (prewarm fix elimina transiente de 2.54e-5)| [§6](#6-other-architectures-out-of-scope) |
| Linear / Container / Cabsim | ✅ Verified — Affine linear, SlimmableContainer, and IR Cabsim covered by targeted test suites | [§6](#6-other-architectures-out-of-scope) |
## 1. Methodology
### 1.1 Two axes of correctness
1. **Interop parity** — does NAM-rs match NAMcore bit-for-bit (within float tolerance)? Verified
with committed golden vectors (`tests/fixtures/*.bin`, generated by the C++ `render` tool)
and live cross-validation (`tests/parity/cpp_parity.rs`). All weights are native f32 (weight quantization
was eliminated; NAMcore never quantized).
2. **Ideal-math fidelity** — how far is NAM-rs from the exact mathematics? Measured against an
independent f64 reference oracle (`src/testing/reference_oracle/mod.rs`), itself cross-checked
against a third, independent NumPy f64 implementation. This isolates the genuine precision
floor, separate from interop drift.
### 1.2 The f64 oracle is a decomposition tool, not a gate
The oracle answers *"how far is the f32 production code from the mathematical ideal?"* — it is
useful for isolating **where** error comes from (quantization vs. activation approximation vs.
structural divergence). It does **not** decide whether NAM-rs is *correct*. Correctness is
decided exclusively by the **C++ golden vectors and live cross-validation**. A change to
production code should never be justified by "matching the oracle" — only by reducing ESR
against the C++ golden. Confusing the two roles has previously caused a real regression to be
introduced while chasing a bug that did not exist (see [§4.5](#45-known-history--do-not-repeat)).
If the oracle and the C++ golden disagree, **the oracle is wrong** and must be fixed to match
C++ — never the other way around.
### 1.3 Reference version
The vendored working copy at `../third-party/NeuralAmpModelerCore/` is checked out at tag
`v0.5.4` (commit `1f42f88`; `NAM/version.h` still says `0.5.3` — the header wasn't bumped for
the tag). Some older committed golden vectors were generated at `v0.5.3` (`9c7b185`). This
patch-level drift is below the interop noise floor for all architectures except where explicitly
noted per-model. Regenerate goldens with `tests/fixtures/golden_gen_build.sh` when in doubt.
### 1.4 Fixture governance: `tests/fixtures/README.md`
[`tests/fixtures/README.md`](../tests/fixtures/README.md) is the canonical operational
supply-chain contract. Every parity claim in this document is operationalized through it.
| Layer | Mechanism | Hard-fail gate |
|:----------------------------------- |:---------------------------------------------------------------------------------------------------------------------------------- |:----------------------------- |
| **Layer 0 — Generation pipeline** | `tests/fixtures/golden_gen_build.sh` + `CATALOG` array — regenerates every `.bin` golden from the pinned NAMcore C++ `render` tool | — |
| **Layer 1 — Pre-committed goldens** | `tests/models/golden_vectors.rs` — compares Rust output against committed `.bin` files; no C++ toolchain required | `utils/tests-quick.sh` Fase 2 |
| **Layer 2 — Live cross-validation** | `tests/parity/cpp_parity.rs` (`#[ignore]`, run via `utils/tests-long.sh`) — builds C++ `render` tool and compares fresh output | `utils/tests-long.sh` |
**Freshness manifest:** `tests/fixtures/.golden_manifest.sha256` contains `sha256` of every
model *and* its golden. Verified as a **hard gate** in `utils/tests-quick.sh` Fase 2 (a stale
`.nam` or `.bin` fails the suite); warn-only in `utils/tests-long.sh`.
**NAMcore mirror pinning:** `variables.env` pins the vendored C++ reference at
`NAM_CORE_COMMIT=1f42f88535884450104b8711d7595019afa0495b` (tag `v0.5.4`). Update
via `utils/mod-update.sh`. See `tests/fixtures/README.md` for the full regeneration walkthrough.
**Calibrated thresholds:** per-model SNR/ESR gates cross-checked by
`tests/models/threshold_calibration.rs` (anti-placebo meta-tests, `// Measured:` provenance
comments). A claim not traceable to a catalog entry + manifest hash + calibrated threshold
is unverified.
---
## 2. LSTM Architecture
Read against `NAM/lstm.h`, `NAM/lstm.cpp`, `NAM/dsp.h`, `NAM/dsp.cpp`, `NAM/activations.h/.cpp`
and the corresponding Rust modules (`src/models/lstm/`, `src/loader/dispatcher/lstm/`,
`src/loader/transpose/lstm.rs`, `src/math/lstm/gates.rs`).
### 2.1 Reference algorithm (`NAM/lstm.cpp`)
A stack of `num_layers` LSTM cells, each processing one audio sample at a time, followed by a
linear head:
```text
for each layer i:
ifgo = W_i · [input ; hidden_i] + b_i // ifgo = [input_gate, forget_gate, cell_candidate, output_gate]
c_i = sigmoid(forget) * c_i + sigmoid(input) * tanh(cell_candidate)
h_i = sigmoid(output) * tanh(c_i)
output = head_weight · h_last + head_bias // no activation
```
Gate order in the weight/state vectors is fixed: **I, F, G, O** at offsets `0, H, 2H, 3H`
(`lstm.cpp:40-44`).
By default (`Activation::using_fast_tanh = false`, `activations.cpp:16`), the gate
nonlinearities are `sigmoid(x) = 1/(1+exp(-x))` and `tanhf(x)` — both **exact** libm/expf-based
implementations (`lstm.cpp:57-65`, `activations.h:64-67`). The `fast_sigmoid`/`fast_tanh`
rational-approximation branch (`lstm.cpp:46-56`) is only enabled by `Activation::enable_fast_tanh()`,
which is called **only** from `tools/benchmodel*.cpp` — never from the `render` tool used to
generate goldens or run live cross-validation. **The C++ reference used for all LSTM parity
checks always uses exact math**, not its own fast-tanh approximation.
C++ generalizes the topology to arbitrary `in_channels`/`out_channels` and even `num_layers ==
0` (pure passthrough, `lstm.cpp:139-149`, zero-filling extra output channels). In practice every
known `.nam` LSTM model is mono in/out with `num_layers ∈ {1, 2}` — see [§2.6](#26-scope-divergence-mono-only-no-zero-layer-support).
### 2.2 Rust implementation
| C++ (`NeuralAmpModelerCore/`) | Rust (`src/`) | Verdict |
|:--------------------------------------------------------------------------------------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------------------- |
| `LSTMCell::process_` gate math (`lstm.cpp:31-66`) | `math/lstm/gates.rs::fused_lstm_gates_{avx2,avx512}` + `models/lstm/layer_kernels.rs` | ✅ Match — same gate order, same `f·c + i·tanh(g)` / `o·tanh(c')` formulas |
| Gate-major weight matrix `[4H × (I+H)]`, row-major (`lstm.cpp:19-21`) | `LstmLayer::input_hidden_weights: [[[u16; H]; IH]; 4]`, filled by `read_lstm_weights_into` | ✅ Match — verified byte-for-byte against the constructor loop |
| Bias `[4H]`, initial hidden `[H]`, initial cell `[H]` read order (`lstm.cpp:22-28`) | `read_lstm_layer` reads bias → hidden-init → cell-init in the same order | ✅ Match |
| 2-layer chain: `layers[i].process_(layers[i-1].hidden)` (`lstm.cpp:151-153`) | `LstmModel2` software-pipelined chain (`model2.rs`) — layer2 consumes layer1's *previous*-step hidden state, reordered for throughput | ✅ Match — mathematically identical sequential stacking, just reordered for instruction-level parallelism |
| Head: `output = head_weight · h_last + head_bias`, no activation (`lstm.cpp:161-164`) | `dot_product(..) + head_bias`, computed in **native f32 with Kahan compensation** (`use_f32_head = true` in every loader path) when quantized weights are not requested | ✅ Match (superset — Kahan compensation only *reduces* summation error vs. plain accumulation) |
| `GetPrewarmSamples() = 0.5 × expected_sample_rate` (min 1) (`lstm.cpp:125-132`) | `prewarm_samples()` — identical formula (`models/lstm/mod.rs`) | ✅ Match — **corrects a prior claim** in this document that Rust diverged here; both engines have the same opt-out flag with the same default |
| `DSP::Reset()` calls `prewarm()` only `if GetPrewarmOnReset()` (default `true`) (`dsp.cpp:130-139`) | `NamModel::reset()` calls `prewarm()` only `if self.prewarm_on_reset()` (default `true`) | ✅ Match — **corrects a prior claim** in this document that Rust diverged here; both engines have the same opt-out flag with the same default |
| Backbone weights are plain `float` (Eigen), no quantization | Gate weights are native f32, dispatched through f32-only GEMV kernels | ✅ Match — see [§2.5](#25-native-f32-backbone-weights-and-activation-precision) |
### 2.3 Weight loading (`.nam` JSON / NAMB)
Layout is `input_hidden_weights[4H×IH] → bias[4H] → hidden_init[H] → cell_init[H]`, repeated per
layer, then `head_weights[H] → head_bias`. This is read identically in
`src/loader/dispatcher/lstm/weights.rs::read_lstm_layer{,_dyn}` and cross-checked against
`src/loader/transpose/lstm.rs` (used for the NAMB `GateMajorLstm` pre-transposed layout). Both
paths were read line-by-line against `LSTMCell`'s constructor (`lstm.cpp:9-29`) — confirmed
identical. `WeightCursor::verify_exhausted()` fails closed if the file has too many or too few
floats for the declared topology.
### 2.4 Catalog dispatch
| `(num_layers, hidden_size)` | Rust type | Alias |
|:--------------------------- |:---------------------------------------------------- |:---------------------------------- |
| `(1, 3)` | `LstmModel1<3, 4, 12>` | `Lstm1x3` (official example model) |
| `(1, 8/12/16/24/40)` | `LstmModel1<H, H+1, 4H>` | `Lstm1x{8,12,16,24,40}` |
| `(2, 8/12/16/24)` | `LstmModel2<H, H+1, 2H+H, 4H>` | `Lstm2x{8,12,16,24}` |
| Anything else | `LstmModelDyn` (heap-allocated, `Vec<LstmLayerDyn>`) | — |
`get_lstm_topology` (`src/loader/nam_json/topology/lstm.rs`) rejects `num_layers > 16` and
`hidden_size > 1024` (DoS guard) but has **no lower bound** — see [§2.6](#26-scope-divergence-mono-only-no-zero-layer-support).
### 2.5 Native f32 backbone weights and activation precision
**Backbone Weight Precision:** NAM-rs uses native `f32` weight storage across all LSTM layers, matching NAMcore's `Eigen::MatrixXf` representation (`NAM/lstm.h:38-39`). Eliminating historical weight quantization removed GEMV dequantization overhead, reducing per-sample latency while ensuring bit-exact interop parity for models such as `BossLSTM-2x8` (ESR = 0.00e0 vs NAMcore).
**Measured Interop Results (Standard Mode):**
| Model | ESR (vs NAMcore) | ESR (vs f64 Ideal) | SNR (dB) | MR-STFT | Status |
|:------------- |:----------------:|:------------------:|:--------:|:--------:|:-------------------------- |
| BossLSTM-1×16 | 8.50e-12 | 8.90e-13 | 110.7 | 2.80e-05 | ✅ Near-bit-exact |
| BossLSTM-2×8 | 1.00e-11 | 5.68e-13 | 110.0 | 1.57e-05 | ✅ Bit-exact / noise floor |
| LSTM Official | 7.86e-13 | 2.71e-12 | 121.0 | 3.08e-05 | ✅ Near-bit-exact |
**Key findings:**
- **BossLSTM-2×8 Parity:** Achieves bit-exact / noise-floor convergence with NAMcore (ESR = 1.00e-11 vs NAMcore, 5.68e-13 vs f64 oracle).
- **BossLSTM-1×16 Precision:** Residual error is dominated by activation function precision at high pre-activation magnitudes, which standard exact-grade math reduces to near-zero (ESR = 8.50e-12).
- **Activation Precision Tradeoff:** `ActivationPrecision::Fast` utilizes Padé [5,4] rational `tanh` (max error ~2.32e-3) and a minimax polynomial `sigmoid` (max error ~4.09e-4) for maximum throughput. `ActivationPrecision::Standard` (universal default) runs exact-grade polynomial exp-based math (error ~2e-7), matching C++ libm precision.
### 2.6 Scope divergence: mono-only, no zero-layer support
C++ `LSTM` generalizes to arbitrary `in_channels`/`out_channels` and `num_layers == 0`
(pass-through). NAM-rs does not:
- `read_lstm_layer::<I, H, IH, H4>` for the first layer always hardcodes `I = 1` — there is no
code path reading `in_channels`/`out_channels` from the `.nam` config for LSTM (unlike A2's
`topology/a2.rs`, which explicitly validates `in_channels == 1`). A hypothetical LSTM model
declaring `in_channels: 2` would be silently processed as mono; the extra channel would never
be read. No known real-world `.nam` LSTM model does this — NAM is guitar/bass amp modeling,
always mono — but the assumption is **implicit and unvalidated**, not fail-closed.
- `LstmModelDyn::process_{avx2,avx512,avx512_vnni_bf16}` unconditionally dereference
`self.layers.as_mut_ptr()` before checking `n_layers > 0` (only a `debug_assert!` guards this,
which is compiled out in release builds). A `num_layers: 0` model would dereference a pointer
into an empty `Vec`'s (potentially dangling) allocation — undefined behavior in release,
panic in debug. C++'s equivalent case returns a well-defined passthrough. This is a real,
confirmed gap in defensive validation, not a hypothetical: `get_lstm_topology` has no lower
bound on `num_layers`. Low real-world severity (no trained model has zero layers) but should
be closed by either rejecting `num_layers == 0` at topology detection or handling it explicitly
in `LstmModelDyn`.
### 2.7 Measured interop drift
LSTM is the one topology whose interop error grows with **signal length** and **host sample
rate** — the recurrent cell state accumulates error over time.
**Native f32 weights & Standard activation:** BossLSTM-2×8 converges to bit-exact / noise-floor parity with NAMcore at 48 kHz (ESR = 1.00e-11). All models default to `Standard` (exact-grade) activation precision, collapsing interop gaps to near-zero across all catalog architectures.
**F64 Oracle Floors:** The model-specific f64-oracle floors (prewarm-paired, 24k prewarm + 4096 samples)
have been fully measured in Standard mode:
- **BossLSTM-1×16**: ESR vs f64 oracle = **8.90e-13 (SNR 110.7 dB)**
- **BossLSTM-2×8**: ESR vs f64 oracle = **5.68e-13 (SNR 110.0 dB)**
Gate constants (`tests/common/constants.rs`):
| Precision mode | Host rate | ESR cap | Margin over worst measured |
|:-------------- |:--------- |:-------:|:------------------------------------------------- |
| Fast | ≤ 96 kHz | 0.08 | ~1.3× (vs 6.09e-2 @ 96 kHz) |
| Fast | > 96 kHz | 0.20 | ~1.4× (vs 1.42e-1 @ 192 kHz) |
| Standard | ≤ 96 kHz | 0.30 | ~5× the Fast cap (covers the Fast→Standard delta) |
| Standard | > 96 kHz | 0.60 | Conservative headroom for 192 kHz recurrent drift |
`LSTM_ESR_LIMIT = 7.0e-3` (`tests/common/constants.rs`) is derived from measured
production-vs-oracle ESR with safety margin. No sample rate is excluded from live cross-validation
to make a gate pass (`live_cross_validation_v2_lstm_*` exercises all five supported rates).
### 2.8 Test coverage and fixture quality
Verified directly against `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`, and
`tests/parity/reference_oracle_f64.rs`:
- **Golden vectors** (`tests/models/golden_vectors.rs`, precomputed `.bin` from the C++ `render` tool,
always active — not `#[ignore]`d): `test_golden_vectors_lstm_1x16`, `_lstm_2x8`,
`_lstm_official`. Backing `.nam` files are **real community-trained models**, not synthetic:
`BossLSTM-1x16.nam` and `BossLSTM-2x8.nam` (Boss Waza Tube Amp Expander community captures,
compatible license, committed in `tests/fixtures/models/`), plus `lstm.nam` — NAMcore's own
official bundled example (`example_models/lstm.nam`, 1×3, matches the `Lstm1x3` alias).
- **Live cross-validation** (`tests/parity/cpp_parity.rs`, `#[ignore]`d — requires the C++ toolchain,
run via `utils/tests-long.sh`): `live_cross_validation_{,v2_}lstm_{1x16,2x8,official,dyn}` plus
HF-mode variants. Uses the same real fixtures as the golden vectors.
- **No synthetic LSTM fixture exists in the active suite.** There is no equivalent of WaveNet's
`BossWN-lite.nam` (obsolete synthetic, see §3.7) for LSTM — every committed LSTM golden is
backed by a real trained model.
- **`LstmModelDyn`** (the non-catalog fallback) is exercised by `lstm_dyn_test.nam` — a small,
deliberately non-catalog (hidden size outside `{3,8,12,16,24,40}`) **synthetic** fixture built
to hit the dynamic dispatch path structurally; it is not a trained amp/pedal capture. This is
the correct use of a synthetic fixture (topology/dispatch coverage), distinct from tone
fidelity coverage (which the real Boss captures provide for the catalog path only — the
dynamic LSTM path currently has **no real-model coverage**, since no known community LSTM
export uses a non-catalog hidden size).
---
## 3. WaveNet A1 Architecture
Read against `NAM/wavenet/model.h`, `NAM/wavenet/model.cpp`, `NAM/wavenet/detail.h`, `NAM/activations.{h,cpp}`,
and the corresponding Rust modules (`src/models/wavenet/`, `src/loader/dispatcher/wavenet/`, `src/loader/nam_json/topology/wavenet.rs`).
### 3.1 C++ Reference Architecture vs. Rust Const-Generic Catalog
The vendored C++ source does not define separate SKU classes for different channel counts. `nam::wavenet::create_config` (`model.cpp:1227-1241`) has exactly two branches: (1) `a2_fast::is_a2_shape()` for the A2 fast path (§4), and (2) a single generic Eigen-based implementation (`detail::Layer` / `detail::LayerArray` / `WaveNet`) for all other WaveNet models regardless of channel count.
"Standard/Lite/Feather/Nano" (16/12/8/4 channels) are a **NAM-rs-side performance
optimization**: these four channel counts happen to cover the overwhelming majority of
real-world community WaveNet exports, so NAM-rs const-generic-specializes them
(`WaveNetModel<CH, K, HEAD>`) for SIMD throughput, falling back to a heap-allocated
`WaveNetModelDyn` for everything else. This is a legitimate and effective engineering strategy,
but it is **NAM-rs's own catalog, not a mirror of any C++-side concept** — the correct framing is
"const-generic fast path vs. generic fallback for the single C++ generic WaveNet class," not
"C++ SKU X maps to Rust SKU X."
### 3.2 Rust implementation
| C++ (`NeuralAmpModelerCore/`) | Rust (`src/`) | Verdict |
|:------------------------------------------------------------------------------------------------------------------------------------------------------------------ |:------------------------------------------------------------------------------------------------------------------------------------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `detail::LayerArray::ProcessInner` — rechannel → layer loop → head accumulation → head rechannel (`model.cpp:450-511`) | `WaveNetLayerArray::process_block_internal` (`layer_array.rs`) | ✅ Match — same rechannel → layer cascade → head-accumulate → head-rechannel structure |
| `detail::Layer::Process` — dilated conv + input mixin, sum, activation, optional layer1x1/head1x1 residual+skip (`model.cpp:166-376`) | `WaveNetLayer::process_block_internal` (`layer.rs`) | ✅ Match for the **plain case** (no gating, no FiLM, no head1x1/layer1x1 variance) — the only case the const-generic fast path implements (§3.6) |
| `WaveNet::process` — condition → layer arrays → head_scale (`model.cpp:744-832`) | `WaveNetModel::process` / `WaveNetModelDyn::process` (`model.rs`, `model_dyn.rs`) | ✅ Match for the no-condition_dsp, no-post-stack-head case |
| Default activation: exact `tanh`/`sigmoid` (`Activation::using_fast_tanh = false`, never flipped by the `render` tool — same as LSTM, §2.1) | `ActivationPrecision::Fast` uses Padé[5,4] tanh / minimax-17 sigmoid (opt-in); `Standard` is exact-grade polynomial exp-based (universal default) | ⚠ Same intentional, bounded divergence documented for LSTM (§2.5); applies identically when `Fast` is active |
| `LayerArrayParams::get_receptive_field()` — per-array RF, **summed** across all arrays plus condition_dsp's own prewarm (`model.cpp:417-424`, `model.cpp:616-618`) | `WaveNetModel`/`WaveNetModelDyn` prewarm fill | ⚠ See §3.5 — analytical fill is mathematically sound for the feedforward case, but the `prewarm_samples()` *getter* under-reports, and `condition_dsp` prewarm has a real bug |
### 3.3 Weight loading
Layout (`rechannel → [conv1d+bias, input_mixin, layer1x1+bias]×N → head_rechannel+bias →
head_scale`, repeated per array) is read identically by
`src/loader/dispatcher/wavenet/{standard,lite,feather,nano}.rs` (catalog) and
`src/loader/dispatcher/wavenet/dynamic.rs` (free geometry) against C++'s `WaveNet::set_weights_`
/ `LayerArray::set_weights_` / `Layer::set_weights_` (`model.cpp:135-164, 525-531, 623-645`).
Confirmed identical read order. `WeightCursor::verify_exhausted()` fails closed on
under/over-provisioned weight files.
### 3.4 Catalog dispatch
`get_wavenet_topology` (`src/loader/nam_json/topology/wavenet.rs`) requires **exactly 2 layer
arrays**, `condition_size ≤ 1`, no gating on either array, and an exact dilation-pattern match to
classify a model as a catalog SKU:
| Channels | Dilation pattern (both arrays) | Rust type |
|:------------------------------------------------------------------------------------------------ |:---------------------------------------------------- |:------------------------ |
| 16 | `[1,2,4,...,512]` (both arrays, "Standard" pattern) | `WaveNetModel<16, 3, 8>` |
| 12 | `[1,...,64]` then `[128,...,512,1,...,512]` ("Lite") | `WaveNetModel<12, 3, 6>` |
| 8 | same Lite-shaped dilation pattern, CH=8 ("Feather") | `WaveNetModel<8, 3, 4>` |
| 4 | same Lite-shaped dilation pattern, CH=4 ("Nano") | `WaveNetModel<4, 3, 2>` |
| Anything else (any channel count, any array count, gated, `condition_size > 1`, post-stack head) | `WaveNetModelDyn` | |
This is a NAM-rs-internal classification only — see §3.1. A `condition_dsp` sub-model is **not**
checked at all during catalog matching (see §3.6): a hypothetical 2-array, ungated, CH=16,
Standard-dilation model that also declares a `condition_dsp` JSON key would still match `Known(Standard)`
and be routed to the fast path, which has **zero `condition_dsp` handling** — it would be silently
dropped. No known real `.nam` model exhibits this combination.
### 3.5 Prewarm: an elegant analytical shortcut, with one real bug
C++'s `DSP::prewarm()` is generic and iterative: it computes `mPrewarmSamples` once at
construction (`condition_dsp`'s own prewarm requirement **plus** the **sum** of every layer
array's receptive field, plus any post-stack head's receptive field, `model.cpp:615-620`), then
literally calls `process()` with zero input that many times (`dsp.cpp:67-101`).
NAM-rs's `WaveNetModel`/`WaveNetModelDyn::prewarm()` does **not** iterate. Because a purely
feedforward causal-conv stack driven by a constant input eventually converges to a constant
output at every layer, `prewarm_internal()` computes that fixed point **analytically in one pass**:
process a single zero-input frame through the rechannel (memoryless, exact for a single frame),
then for each layer in order, replicate that single already-correct value across the entire
history buffer (`copy_within`, `layer_array.rs:97-118`) *before* computing that layer's own
single-frame output — which is therefore also exactly the converged constant, propagating
correctness layer by layer. **This is a genuinely correct, elegant O(layers) alternative to
C++'s O(receptive_field) iteration for the plain feedforward case** — verified by working through
the recursion by hand. It is also why `prewarm_samples()`'s wrong return value (below) has zero
effect on WaveNet's own audio correctness: the trait's `prewarm(&mut self, _num_samples)` override
discards the argument entirely and always runs the full analytical fill regardless of what number
is passed in (`mod.rs:79-83, 107-109`).
Two real issues, though:
- **`prewarm_samples()` under-reports, but is provably inert.** `WaveNetModel::prewarm_samples()`
returns only `array1.receptive_field_size` (ignoring `array2` entirely), and
`WaveNetModelDyn::prewarm_samples()` returns only `arrays[0]`'s RF (ignoring all subsequent
arrays in a multi-array cascade) combined via `.max()` with the condition_dsp's prewarm
requirement rather than C++'s **sum**. Traced every call site
(`grep`-verified): the only caller is `loader/build.rs`'s `m.prewarm(m.prewarm_samples().max(2048))`
at initial load, and WaveNet's `prewarm()` discards that argument (previous paragraph). **No
functional impact today** — but this is a landmine: if this getter is ever wired to a
host-facing latency API (matching C++'s `GetPrewarmSamples()`, which real hosts do query for
plugin latency compensation), it would report a materially wrong number. Should either be
fixed to the correct sum or removed/marked `#[deprecated]` with a comment explaining why it's
currently safe to be wrong.
- **`condition_dsp.prewarm` handling:** `WaveNetModelDyn::prewarm_internal()` invokes `cond_dsp.prewarm(cond_dsp.prewarm_samples())` (`model_dyn.rs`), ensuring sub-model recurrent states (e.g. LSTM condition DSPs) settle before frame processing begins, matching C++'s `GetPrewarmSamples()` semantics.
### 3.6 Coverage gap: generic gating/FiLM/head1x1/layer1x1 silently ignored outside A2 shape
C++'s generic `detail::Layer` supports gating (`GatingMode::{NONE,GATED,BLENDED}`), 8 independent
FiLM insertion points, `head1x1`, and `layer1x1` for **any** WaveNet, not just A2-shaped ones
(`model.cpp:976-1151` parses these fields unconditionally for every layer array). NAM-rs's A1
path does not:
- `NamLayerConfig` (`src/loader/nam_json/model.rs:62-108`) parses only the legacy boolean
`gated` field — there is no parsing of the string-valued `gating_mode` (`"none"/"gated"/"blended"`),
`head1x1`, `layer1x1`, or any of the 8 FiLM parameter objects.
- The legacy `gated` boolean **is** checked, but only to *disqualify* a model from catalog-SKU
matching (`topology/wavenet.rs:332-338`) — proving the authors were aware gated
16/12/8/4-channel models could exist. It is **never checked again** for the `Free`/`WaveNetModelDyn`
path: `get_wavenet_topology`'s free-geometry branch and `build_wavenet_dynamic_inner` never
read `layer.gated`, and `WaveNetLayerDyn`/`build_wavenet_array_dyn` have no gating code path at
all.
- **Confirmed consequence:** a WaveNet model with `gated: true` (or any `gating_mode`/`head1x1`/`layer1x1`/FiLM
usage) that does not match the A2 shape or the `is_wavenet_a2()` activation heuristic (§4 —
only triggers for single-array, non-Tanh activation) falls through to the A1 `Free`/`Dynamic`
path and is **silently processed as if `gated = false`** — no error, no warning, mathematically
wrong output. This is not fail-closed, unlike the equivalent case for `activation != "Tanh"`,
which `validate_layer_activations` correctly rejects (§3.2).
No known committed or community fixture currently exercises this gap (gating/FiLM in practice
correlates strongly with the A2 architecture generation), but it is a real, code-verified hole,
not a hypothetical — the correct fix is either to parse and honor these fields generically in
`WaveNetModelDyn`, or to detect and reject them explicitly (fail-closed) the same way `gated` is
already detected for catalog-disqualification purposes.
### 3.7 Test coverage and fixture quality
Verified directly against `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`, and
`tests/parity/reference_oracle_f64.rs`:
- **Golden vectors, catalog SKUs — all real community models, all active (not ignored):**
`test_golden_vectors_wavenet_{standard,feather,nano}` use `BossWN-{standard,feather,nano}.nam`
(Boss Waza Tube Amp Expander community captures, compatible license). `test_golden_vectors_wavenet_lite`
uses `EVH-5150-Lite.nam`, a **non-distributable real community capture** (gitignored, lives in
`tests/fixtures/models-nondist/`, fetched separately — see `tests/fixtures/README.md`
§Non-Distributable Model Management). Its doc comment (`golden_vectors.rs:495-511`) and the
test itself confirm the measured result **directly from current source**: SNR = 122.3 dB, ESR
= 5.84e-13, thresholds SNR ≥ 105 dB / ESR ≤ 3.5e-11 — this specific figure is **independently
re-confirmed in this audit pass**, not carried over.
- **Golden vectors, non-catalog:** `test_golden_vectors_wavenet_dyn` (`wavenet_dyn_free.nam`) and
`test_golden_vectors_wavenet_condition_dsp` (`wavenet_condition_dsp.nam`) are **synthetic**,
purpose-built to exercise `WaveNetModelDyn`'s free-geometry and `condition_dsp` dispatch paths
structurally. `test_golden_vectors_wavenet_official` uses `wavenet_official.nam` — NAMcore's own
bundled official example (`example_models/wavenet.nam`, CH=3, 2 arrays), a small but **real,
officially-distributed** reference model, not synthetic.
- **Obsolete synthetic fixture, kept for traceability only:** `BossWN-lite.nam` (CH=12,
artificially generated) is explicitly marked obsolete in `tests/fixtures/README.md` — "no
longer used in active tests," superseded by `EVH-5150-Lite.nam`. It is the historical source of
the "SNR ≈ 0.9 dB" figure that `docs/testing.md` still (incorrectly) attributes to the current
active test.
- **Live cross-validation** (`tests/parity/cpp_parity.rs`, `#[ignore]`d, requires C++ toolchain):
`live_cross_validation_{,v2_}wavenet_{standard,feather,nano,lite,a1_standard,dyn}` plus HF
variants, using the same real fixtures above. `live_cross_validation_nondist_models` and the
named `live_cross_validation_v2_{app_evh,boss_bd2,slammin_marshall}` tests exercise **three
additional real, non-distributable community captures** (`APP-EVH-Stealth100-Dialled-xSTD.nam`,
`Boss BD-2 H2O Mod T-12_00 G-12_00.nam`, `SLAMMIN_MARSHALL_J45_VN9_TREBLEBOOSTER_P4_C.nam` —
catalogued with SHA-256 + author attribution in `tests/fixtures/models-nondist/manifest.json`),
covering custom-layer WaveNet and `SlimmableContainer` topologies beyond the four catalog SKUs.
These gracefully no-op (printed `SKIP`, not a failure) when the non-distributable directory or
the C++ toolchain is absent, consistent with `docs/testing.md`'s documented graceful-skip
policy.
- **Net assessment:** WaveNet A1's test fixture quality is materially better than a synthetic-only
suite would suggest — every catalog SKU and several custom real-world topologies are validated
against genuine trained amp/pedal captures, not just structurally-synthetic weights. The
remaining synthetic fixtures (`wavenet_dyn_free.nam`, `wavenet_condition_dsp.nam`) are correctly
scoped to structural/dispatch coverage rather than claiming tone-fidelity validation.
### 3.8 Measured interop drift
Canonical golden-vector interop fidelity measured against NAMcore (`docs/quality-contract.txt` baseline @ 48 kHz):
| Model | ESR (vs NAMcore) | ESR (vs f64 Ideal) | SNR (dB) | MR-STFT | Mode |
|:----------------------- |:----------------:|:------------------:|:--------:|:--------:|:---- |
| WaveNet Standard (CH16) | 2.31e-14 | 9.05e-15 | 136.4 | 6.46e-06 | Live |
| WaveNet Feather (CH8) | 4.74e-14 | 2.00e-14 | 133.2 | 8.86e-06 | Live |
| WaveNet Nano (CH4) | 6.43e-14 | 3.05e-14 | 131.9 | 7.67e-06 | Live |
| EVH-5150-Lite (CH12) | 7.87e-13 | 2.64e-13 | 121.0 | 4.31e-06 | Live |
| WaveNet A1 Standard | 1.20e-13 | 1.05e-13 | 129.2 | 2.26e-06 | Live |
| WaveNet Official (CH3) | 9.03e-14 | 6.13e-14 | 130.4 | 1.66e-05 | Live |
All WaveNet A1 catalog models pass their calibrated quality gates with multi-order-of-magnitude safety margins.
### 3.9 `condition_dsp` specification (canonical semantics)
> This section is the formal specification. It was derived by
> reading the C++ reference, the Python trainer, and the Rust production code side-by-side
> on 2026-07-14. All file:line citations reference NAMcore v0.5.4 (tag `1f42f88`).
#### 3.9.1 C++ semantics — `WaveNet::_process_condition` and sizing
**Source:** `../third-party/NeuralAmpModelerCore/NAM/wavenet/model.cpp`
The `condition` matrix flowing through the WaveNet layer cascade is `_condition_output`
(`Eigen::MatrixXf`, `model.h:76`). Its dimensions are decided in `SetMaxBufferSize`
(`model.cpp:647-687`):
- **Without `condition_dsp`** (`model.cpp:652-654`): `_condition_output` is resized to
`[_get_condition_dim(), maxBufferSize]`. `_get_condition_dim()` returns
`NumInputChannels()` (`model.h:106`), which is always **1** for WaveNet (mono-in).
So `_condition_output` = `[1 × maxBufferSize]` — a single row holding the raw input.
- **With `condition_dsp`** (`model.cpp:656-660`): `_condition_output` is resized to
`[condition_dsp->NumOutputChannels(), maxBufferSize]`. The number of **rows** in the
condition matrix is the condition DSP's output channel count, **not** the WaveNet's
`in_channels`.
The `_process_condition` method (`model.cpp:699-729`) fills `_condition_output`:
- **Without `condition_dsp`** (`model.cpp:703-704`): copies `_condition_input.leftCols(num_frames)`
into `_condition_output.leftCols(num_frames)`. Both are `[1 × num_frames]`.
- **With `condition_dsp`** (`model.cpp:710-728`):
1. Input (`_condition_input`, shape `[condition_dim, num_frames]`, where
`condition_dim = _get_condition_dim() = 1`) is copied row-by-row into
pre-allocated contiguous DSP buffers (`model.cpp:710-715`).
2. The condition DSP processes these buffers in-place (`model.cpp:718-719`).
3. Output is copied back row-by-row from the DSP output buffers into
`_condition_output` (`model.cpp:722-727`). The row count is
`condition_dsp->NumOutputChannels()` — there is **no** broadcast, tile, or
dimension coercion. Output channels are written 1:1.
**Construction-time validation** (`model.cpp:592-601`): when `condition_dsp` exists, C++
asserts that **every** layer array's `condition_size` matches
`condition_dsp->NumOutputChannels()` exactly — and throws `std::runtime_error` on mismatch:
```cpp
// model.cpp:594-601
if (layer_array_params[i].condition_size != this->_condition_dsp->NumOutputChannels())
{
std::stringstream ss;
ss << "condition_size of layer " << i << " ("
<< layer_array_params[i].condition_size
<< ") doesn't match output channels of condition DSP ("
<< this->_condition_dsp->NumOutputChannels() << "!\n";
throw std::runtime_error(ss.str().c_str());
}
```
**Conclusion:** In C++, a model where `condition_dsp->NumOutputChannels() < condition_size`
is **rejected at construction**. The case `out_channels == condition_size` is guaranteed
by this check. There is no broadcast logic — dimensional matching is enforced structurally.
#### 3.9.2 How `_condition_output` is consumed by layers
In `WaveNet::process` (`model.cpp:744-832`):
- `_condition_output` (the multi-row condition matrix) is passed **as-is** to every
layer array's `Process` method (`model.cpp:761,770`), alongside the layer inputs.
- Inside each `Layer::Process` (`model.cpp:166+`), the condition is consumed by
`InputMixer` — a `Conv1D(kernel=1, in_channels=condition_size, out_channels=mid_channels)`
— which projects `condition_size` channels to `mid_channels` (= `2*bottleneck` for
gated, `bottleneck` for plain). FiLM modules also consume the condition channels
directly.
- **No broadcasting between `_condition_output` and the layer internals.** The matrix
already has the correct row count by construction (§3.9.1).
#### 3.9.3 Python trainer semantics (`neural-amp-modeler`, v0.13.0)
**Source:** `nam/models/wavenet/_wavenet.py` (tag `v0.13.0`)
The trainer's `WaveNet.parse_config` (`_wavenet.py:142-155`) handles `condition_dsp`:
```python
if condition_dsp_config.get("name") != "WaveNet":
raise NotImplementedError("Only WaveNet condition DSP is supported")
condition_dsp = WaveNet.init_from_config(condition_dsp_config["config"])
```
- The Python trainer **only supports WaveNet as `condition_dsp`** — any other
architecture (including LSTM) raises `NotImplementedError`.
- During training (`forward`, `_wavenet.py:189`):
```python
c = x if self._condition_dsp is None else self._condition_dsp(x)
```
The condition tensor `c` has shape `[B, condition_dsp_head_out_channels, L]` — exactly
the head output of the condition-dsp WaveNet.
- Export (`export_config`, `_wavenet.py:176-195`): the `condition_dsp` sub-model is
serialized as a complete `.nam` JSON object embedded inside the parent model's config
under the `"condition_dsp"` key. The note at line 192 reads:
```python
# Build condition_dsp export dict without running forward (condition_dsp
# may have multiple output channels; WaveNet wrapper asserts 1 channel).
```
**Conclusion:** The Python trainer's `condition_dsp` output channels match the
`condition_size` of the parent's layer arrays by the trainer's own structural design
(the condition-dsp WaveNet's `head.out_channels` = layer arrays' `condition_size`).
There is **no** code path in the official trainer that produces a `condition_dsp`
output-channel-count mismatch — it would fail dimension checks during forward
computation.
#### 3.9.4 LSTM as `condition_dsp` — veredicto
The `wavenet_condition_lstm.nam` fixture (LSTM sub-model inside a WaveNet) represents
a configuration that:
1. **The Python trainer cannot produce** — raises `NotImplementedError` for non-WaveNet
`condition_dsp` (§3.9.3).
2. **C++ NAMcore would reject at construction** — the LSTM's `NumOutputChannels() = 1`
would fail the assertion `layer_array.condition_size == condition_dsp->NumOutputChannels()`
when `condition_size = 3` (the standard WaveNet case) (§3.9.1).
3. **The C++ `render` tool does not support** this model — there is **no golden vector**
generated by NAMcore for this fixture. The only committed golden for this model is the f64 oracle — which itself has a disputed `condition_dsp` semantic
(the broadcast logic at `wavenet.rs:38-49` and `a2/dynamic_eval.rs:326-339`). This creates a circular
dependency: the oracle's correctness for this fixture cannot be independently validated.
Rust production code behavior (`src/models/wavenet/model_dyn.rs:236-251`): when
`condition_dsp` output channels (`dsp_ch`) are fewer than the layer array's
`condition_size` (`cond`), the production engine **broadcasts** the first channel's
value across all condition channels. This broadcast is present in both the A1
dynamic path (`model_dyn.rs:240-247`) and the A2 dynamic/cascade paths (via the
same `condition_dsp_output` buffer). This is a **NAM-rs-specific behavior** with
no C++ precedent — it exists because NAM-rs loads models the upstream toolchain
rejects.
**Recommendation for T1.2 (oracle fix):** The f64 oracle's broadcast logic
(`wavenet.rs:38-49`, `a2/dynamic_eval.rs:326-339`) should match the production code's broadcast
**if** the production broadcast is deemed the intended semantics for models the
upstream toolchain cannot validate. Since the C++ golden cannot serve as arbiter
for the LSTM case, the "correct" broadcast behavior is a product decision documented
here — not a parity claim.
#### 3.9.5 Summary: canonical `condition_dsp` semantics
| Aspect | C++ (NAMcore v0.5.4) | Python trainer (v0.13.0) | Rust production (NAM-rs) |
|:-------------------------------------- |:--------------------------------------------------------------------------- |:------------------------------------------------------ |:------------------------------------------------------------ |
| `condition_dsp` matrix rows | `condition_dsp->NumOutputChannels()` | `condition_dsp.head.out_channels` | `condition_dsp.num_output_channels()` |
| Dimension enforcement | Hard assertion: `condition_size == NumOutputChannels()` (throw on mismatch) | Structural match (fails dimension check in forward) | `assert` on max channels; broadcasts when `dsp_ch < cond` |
| Broadcasting (dsp_ch < cond) | **None** — construction rejected | **None** — structural match prevents mismatch | **Yes** — replicates channel 0 across all condition channels |
| Supported condition_dsp architectures | Only WaveNet (and LSTM — but see §3.9.4 re: assertion rejection) | Only WaveNet (raises `NotImplementedError` for others) | Any (LSTM accepted; see §3.9.4) |
| Reference for `condition_lstm` fixture | N/A — model rejected | N/A — model cannot be produced | Golden from `wavenet_condition_dsp.nam` (WaveNet sub-model) |
| Key file:line references | `model.cpp:592-601,652-660,699-729,744-770` | `_wavenet.py:142-155,171-195` | `model_dyn.rs:236-251`, `model_dyn.rs:357-373` |
#### 3.9.6 T6.1 Root Cause: `head_scale` read from JSON config instead of weight stream (WaveNet A1 oracle)
**Status:** FIXED (2026-07-14, T6.1).
**Root cause:** Both the Rust oracle (`src/testing/reference_oracle/wavenet.rs:33`)
and the Python anchor generator (`tests/fixtures/scripts/validate_oracle_f64.py:93`)
read `head_scale` from the JSON config field `model_data.config.head_scale`, not from
the last position of the weight stream — where production engines (Rust `build_wavenet_dynamic_inner`,
C++ `NAM/wavenet/model.cpp`) always read it.
For models generated by standard NAM trainers, the config `head_scale` and the
weight-stream `head_scale` are the same value, so the bug is hidden. For
test-script-generated models (`create_wavenet.py`), the weight stream may contain
random values that overwrite the config metadata. This caused the f64 oracle to
produce structurally wrong output for `wavenet_condition_dsp.nam`:
**`wavenet_condition_dsp.nam` — before vs. after T6.1:**
| Measurement | Before T6.1 (broken oracle) | After T6.1 (fixed oracle) |
|:----------------------------------------- |:--------------------------- |:------------------------- |
| Prod × Oracle ESR (paired, summary table) | 4.23e+01 (+16.3 dB) | 6.33e-15 (−142.0 dB) |
| Oracle × NumPy anchor ESR | N/A (circular: 4.96e-16) | 3.18e-32 (−315.0 dB) |
| Quality Dashboard tag | `[orac: f64 div]` TRIGGERED | **not triggered** |
| Prod output (first 10) | ≈ +0.17 growing | ≈ +0.17 growing |
| Oracle output (first 10) | ≈ −0.033 flat | ≈ +0.17 growing (matches) |
**What was wrong:** The main model's weight-stream head_scale = −0.1255 (the actual
weight at position 146), but the oracle used config head_scale = 0.02 — a sign inversion
and 6.27× magnitude error. The condition_dsp sub-model's weight-stream head_scale =
0.8649, but the oracle used 0.02 again — a 43.25× magnitude error. The combination of
both mismatches produced oracle output that was structurally unrelated to production.
**Fix (2 files, 2 languages):**
1. **Rust oracle** (`src/testing/reference_oracle/wavenet.rs:161-167`): After reading
all array weights, read the last remaining weight from the cursor as `head_scale`.
The config field is no longer used for computation.
2. **Python anchor generator** (`tests/fixtures/scripts/validate_oracle_f64.py:195-199`):
Same fix — read `head_scale` from `weights[cursor]` after the per-array weight
loop. The config field is no longer used for computation.
**Verification:** `test_summary_table` now shows ESR(WaveNetCondDSP) = 6.33e-15 (−142.0 dB),
matching the near-bit-exact floor of the WaveNet A1 family (1e-14 to 1e-12 range).
The regenerated Python anchor matches the Rust oracle at 3.18e-32 ESR — both now
read head_scale from the same weight-stream position and agree with the production
engine (itself golden-C++-confirmed at ESR 1.11e-14).
**Status after T6.1:**
- ✅ `wavenet_condition_dsp.nam` — oracle verified against production at the A1 floor
- ✅ Python anchor regenerated and validated against production, NOT circularly
- ✅ Quality Dashboard `[orac: f64 div]` tag eliminated for this model
- ✅ `docs/cpp_parity_map.md` §3.9 now records the definitive root cause
---
## 4. WaveNet A2 Architecture
> Read against `NAM/wavenet/a2_fast.{h,cpp}` (the C++ fast-path, in full) and cross-checked
> `src/loader/nam_json/topology/a2.rs`, `src/models/a2/model/static/process.rs`,
> `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`, `tests/common/validation.rs`, and
> `tests/fixtures/README.md` against each other and against current git history.
> §4.4–§4.6 (the `wavenet_a2_max.nam` investigation) were already
> established in the previous pass and are corroborated, not re-derived, here.
"A2" designates the newer WaveNet variant: `a2_fast.cpp` is C++'s **optimized, shape-restricted**
fast path (exactly 23 layers, fixed kernel/dilation pattern, CH∈{3,8}, LeakyReLU-only, no
gating/FiLM/head1x1 — `a2_fast.cpp:754-885`); anything not matching that exact shape falls
through to the same generic `NAM/wavenet/model.cpp` used by A1 (§3.1). NAM-rs mirrors this split
faithfully: `WaveNetA2<3>`/`WaveNetA2<8>` (fast path) vs. `WaveNetA2Dyn`/`WaveNetA2Cascade`
(everything else — FiLM, gating, blending, `condition_dsp`, multi-array cascade, `head1x1`).
### 4.1 Fast-path shape detection: a faithful, self-correcting mirror of C++
`src/loader/nam_json/topology/a2.rs::is_a2_shape` was read line-by-line against
`a2_fast.cpp::is_a2_shape` (lines 754-885) — every one of the 20 structural checks (layer count,
no post-stack head, `in_channels`/`input_size`/`condition_size`, `channels == bottleneck`,
`channels ∈ {3,8}`, exact kernel-size/dilation arrays, LeakyReLU(0.01) activation, gating,
head1x1, layer1x1 groups, layer-array head shape, all 8 FiLM slots, `groups_input*`,
non-slimmable) has a corresponding Rust check, in the same order, with a comment citing the C++
line number. This is the best-audited topology detector in the codebase.
**Notably, the code contains its own documented self-correction** (`topology/a2.rs:207-213`,
tagged `B.1.1 (F5)`): an earlier version of the Rust dispatcher apparently routed FiLM-active
models to the fast path anyway, producing measured divergence (CH=3 SNR 18.1 dB, CH=8 SNR 36.0
dB) against C++ — because C++'s `is_a2_shape` rejects any active FiLM slot and falls through to
the generic Eigen WaveNet, which the Rust fast path does not reproduce. The fix (already in the
current source) routes any model with FiLM, gating, `head1x1`, or non-1 groups to
`A2TopologyResult::Dynamic` instead, matching C++'s fallback exactly. This is now correct — but
the 18.1/36.0 dB figures remain the calibrated thresholds for the *dynamic* engine's FiLM
emulation itself (§4.2), since matching the shape-routing decision doesn't yet mean matching the
generic Eigen path's output bit-for-bit.
### 4.2 Fast path (A2-Full CH=8 / A2-Lite CH=3): structurally correct, only synthetic fixtures
`src/models/a2/model/static/process.rs` was read against `a2_fast.cpp`'s `A2FastModel<Channels>`:
rechannel → per-layer (dilated conv → bias → input mixin → LeakyReLU(0.01) → head-accumulate →
`layer1x1` residual) → head Conv1D(k=16, bias, `head_scale`). Structurally identical, including
weight-stream read order (`_load_weights`, `a2_fast.cpp:198-273`) matching
`src/models/a2/model/set_weights.rs` field-for-field.
Re-measured 2026-07-11 (`utils/tests-quick.sh` Fase 2, release): A2-Full ESR = 1.12e-13
(SNR 129.5 dB), A2-Lite ESR = 6.43e-14 (SNR 131.9 dB) against the committed NAMcore
goldens — both pass their calibrated gates (3.0e-11 / 3.5e-11, SNR ≥ 105 dB) with
2+ orders of magnitude of margin.
**Fixture quality caveat (new finding — see §4.6):** unlike LSTM and WaveNet A1, these figures are
**not** validated against a real trained community model. `wavenet_a2_full.nam` and
`wavenet_a2_lite.nam` are explicitly documented as **synthetic, calibrated weights** — `tests/fixtures/README.md:75-76,
520-527`: "Synthetic, NOT official FiLM models." There is currently no known real-world A2-Full
or A2-Lite `.nam` export in the test suite. The fast-path *code* is well-verified against C++
structurally; the fast-path *fixtures* only prove self-consistency of calibrated weights, not
tone-fidelity on a genuine trained model.
### 4.3 Measured interop drift on dynamic paths (Gating, Blending, FiLM)
Measured interop metrics for WaveNet A2 dynamic paths (`docs/quality-contract.txt` baseline @ 48 kHz):
| Model / Variant | ESR (vs NAMcore) | ESR (vs f64 Ideal) | SNR (dB) | MR-STFT | Mode |
|:----------------------------------- |:----------------:|:------------------:|:--------:|:--------:|:---- |
| WaveNet A2-Full (CH8) | 1.46e-13 | 7.83e-14 | 128.3 | 1.68e-05 | Live |
| WaveNet A2-Lite (CH3) | 8.36e-14 | 1.82e-14 | 130.8 | 9.54e-06 | Live |
| WaveNet A2-FiLM-Full (CH8) | 1.18e-14 | 8.75e-15 | 139.3 | 7.85e-06 | Live |
| WaveNet A2-FiLM-Lite (CH3) | 3.82e-13 | 1.61e-13 | 124.2 | 1.69e-05 | Live |
| WaveNet A2-FiLM-InputMixinPre (CH3) | 3.44e-14 | 2.21e-14 | 134.6 | 6.92e-06 | Live |
| WaveNet A2-FiLM Chaos Stress (CH3) | 1.26e-14 | 1.03e-14 | 139.0 | 7.00e-06 | Live |
| WaveNet A2 Dynamic Gated (CH8) | 5.03e-11 | 1.00e-10 | 103.0 | 6.63e-05 | Live |
| WaveNet A2 Dynamic Blended (CH3) | 5.35e-14 | 2.65e-14 | 132.7 | 9.97e-06 | Live |
All dynamic path variants achieve near-bit-exact parity or expected approximation-bounded floors.
### 4.4 🔴 Status: wavenet_a2_max.nam (Official Flagship) — Disabled Fail-Closed
This model is the sole item in the A2 topology currently disabled fail-closed at dispatch (`is_disabled_broken_a2_flagship` in `src/loader/dispatcher/wavenet/mod.rs`).
**Root Cause Summary:**
Three structural bugs in the dynamic engine contribute to the divergence against the C++ golden vector (`golden_wavenet_a2_max.bin`):
- **Bug A:** `head1x1` is modeled as per-array in Rust, whereas C++ evaluates it per-layer.
- **Bug B:** Grouped-convolution parameters (`layer1x1.groups` and `groups_input_mixin`) are ignored by the dynamic path.
- **Bug C:** Legacy head kernel size (`kernel_size=1`) is processed with hardcoded 16-tap kernel assumptions.
Detailed fix plan and status tracking are maintained within the WaveNet A2 dynamic engine architecture specifications.
### 4.5 Known history — do not repeat
A prior audit round compared production output (`condition_size=8` values/frame) against the
f64 oracle's `condition_dsp` output (1 value/frame — a bug in the oracle, not production) and
concluded there was a critical 93 dB regression. Acting on that conclusion, it changed
production code to match the broken oracle, which reintroduced a real divergence from C++
that a prior fix had already corrected. That change was reverted. The load-bearing rule: **the C++ golden is the only arbiter; the oracle decomposes error but never adjudicates it.**
### 4.5.1 Anchor Regeneration Policy
A f64 anchor (`tests/fixtures/f64_anchors/*.bin`) may only be regenerated when:
**(a)** a C++ golden vector exists for the same fixture **and** the paired
production×oracle test (`test_summary_table`) already passes within the
acceptance criterion **before** regeneration; **or**
**(b)** no C++ golden exists and the regeneration is accompanied by explicit
human review, documented in the commit message with before/after numbers.
Regenerating an anchor from the very oracle it is meant to validate constitutes
a circular comparison and does **not** constitute evidence of correctness.
### 4.6 Canonical C++ Layout Specifications
The structural spec table below documents C++ reference structures vs Rust implementations for ongoing dynamic engine parity work:
| Aspect | C++ reference | Rust reference | Verdict |
|:---------------------------------------------------- |:---------------------------------------------------------------------------------------- |:------------------------------------------------------------------------------------------------ |:-------------------------------------------------------------------------- |
| `Conv1x1` weight stream order | `NAM/dsp.cpp:384-393` — row-major `[out_ch][in_ch]` per group | `src/models/a2/model/dynamic/build.rs` (`transpose_dense_f32` + `head1x1_w[oc*h1_in+ic]` access) | Tested both transposed and non-transposed variants against the golden |
| `head1x1` weight count | `NAM/wavenet/detail.h:75-76` — `out_channels × (bottleneck/groups)`, bias `out_channels` | `build.rs` reads `head_accum_size × h1_in_size` (`head_accum_size == out_channels`) | ✅ Matches C++ formula |
| `head1x1` application loop (grouping) | `NAM/dsp.cpp:449-646` — implicit block-diagonal GEMM | `process.rs` explicit `grp → oc → ic` loop | ✅ Structurally equivalent |
| Cascade head propagation (multi-array) | `NAM/wavenet/model.cpp:769` — propagates **post-rechannel** head output | `cascade.rs` propagates **raw** `head_accum` (pre-rechannel) | ⚠ Latent divergence for multi-array models with `head_kernel_size > 1` |
| `condition_dsp` interface (dimensions, pass-through) | `NAM/wavenet/model.cpp:699-729` | `process.rs:89-98` | ✅ Interface dimensions match (`condition_size` values/frame) |
| Head finalization, `head_size == 1` | `NAM/wavenet/model.cpp:382-383` — Conv1D(kernel=head_kernel_size, bias, head_scale) | `A2HeadConv` (kernel=16, bias, head_scale) | ✅ Matches for `wavenet_a2_max.nam` (`head_size=1`, `kernel=16`) |
| Head finalization, `head_size > 1` | Conv1D with kernel + bias + head_scale | Dense projection, no kernel/bias/head_scale | ⚠ Only equivalent to C++ when `head_kernel_size == 1 ∧ head_bias == false` |
### 4.7 Test coverage and fixture quality
Verified directly against `tests/models/golden_vectors.rs`, `tests/parity/cpp_parity.rs`,
`tests/common/validation.rs`, and `tests/parity/reference_oracle_f64.rs`:
- **`tests/models/golden_vectors.rs`** (committed `.bin`) covers `golden_wavenet_a2_lite.bin` (Lite
variant, `condition_size=1`, CH=3) and `golden_wavenet_a2_full.bin` (Full variant,
`condition_size=1`, CH=8). Both pass bit-identically against prior baselines.
- **`tests/parity/cpp_parity.rs` (live, `#[ignore]`d)** has no
`live_cross_validation_wavenet_a2_dyn` test because `WaveNetA2Dyn`'s scalar fallback path is
a `nam-rs` internal extension for non-standard geometries not present in upstream C++ NAMcore.
Cross-validation uses the synthetic dynamic builder anchor tests instead.
- **Real, official `.nam` files exercised:** exactly two.
- `a2_example.nam` — NAMcore's own official bundled example (`example_models/A2.nam`,
`SlimmableContainer` with two WaveNet A2 submodels, CH 3→6). Golden-tested
(`test_golden_vectors_a2_example_slimmable`) and live-cross-validated
(`live_cross_validation_a2_example_slimmable`). Passes.
- `wavenet_a2_max.nam` — Steve Atkinson's official flagship example (CC0). Golden-tested only
(`test_golden_vectors_wavenet_a2_max`, `#[ignore]`d) — **confirmed broken**, §4.4. **No live
cross-validation exists for this model at all** — `tests/parity/cpp_parity.rs` has no
`live_cross_validation_*wavenet_a2_max*` test, so even once the golden is fixed, the "fresh
C++ toolchain" axis (§1.1) will remain uncovered for this model unless a live test is added.
- **Every other A2 fixture is synthetic**, by explicit design and documentation
(`tests/fixtures/README.md`), not by omission:
- `wavenet_a2_full.nam` / `wavenet_a2_lite.nam` (fast-path parity, calibrated weights) — explicitly
labeled "**NOT official FiLM models**" to prevent future confusion with `wavenet_a2_max.nam`.
- `wavenet_a2_film_{full,lite}.nam` (FiLM dynamic path), `a2_dynamic_gated_ch8.nam` /
`a2_dynamic_blended_ch3.nam` (gating/blending dynamic path), `wavenet_a2_container.nam`
(`SlimmableContainer` joining the two fast-path submodels) — all generator-produced
(`generate_a2_fixtures.py` for the FiLM pair), purpose-built to exercise one structural
feature each against the C++ **generic** path (not `a2_fast`, which rejects all of them —
§4.1). This is the correct, honest use of synthetic fixtures: proving the *feature* works,
not claiming tone-fidelity on a trained model.
- `mock_a2.nam` — a deliberate negative fixture (zero weights, `ReLU` config) used only to test
the RT-safe model-load-failure path (`RT_STATUS_MODEL_LOAD_FAILED`), not inference at all.
- **Net assessment:** A2's test coverage is real for the *shape-detection* logic (§4.1, mirrored
line-by-line from C++) and for the *dynamic-engine feature* fixtures (gating/blending
near-bit-exact, FiLM characterized at 18–36 dB), but it is the **weakest of the three
architectures on genuine trained-model validation** — the only real official A2 model exercised
by live cross-validation is a `SlimmableContainer` wrapper, and the only real official
flagship WaveNet A2 model (`wavenet_a2_max.nam`) is both broken and has zero live-toolchain
coverage.
---
## 5. Shared DSP Engine Semantics
Applies identically to LSTM, WaveNet A1, and A2 — all route through the common `NamModel` trait
and the C++ `DSP` base class.
| C++ (`NAM/dsp.h` / `dsp.cpp`) | Rust (`src/`) | Verdict |
|:-------------------------------------------------------------------------------------------------------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `DSP::Reset(sr, maxBuf)` → `SetMaxBufferSize` + `prewarm()` iff `GetPrewarmOnReset()` (default `true`) | `NamModel::reset()` → `set_max_buffer_size` + `prewarm()` iff `prewarm_on_reset()` (default `true`) | ✅ Match — verified for LSTM (§2.2) and WaveNet A1 (§3.5); A2 not re-verified this pass |
| `DSP::GetPrewarmSamples()` base returns `0`; overridden per-model; used by the **iterative** `DSP::prewarm()` loop | `prewarm_samples()` per-model override | ✅ LSTM verified exact (`0.5 × sr`), and its value is load-bearing (drives real iteration). ⚠ WaveNet A1's override under-reports (§3.5) but is **provably inert** — WaveNet's `prewarm()` discards the argument and uses its own correct analytical fill instead. A2 not re-verified this pass. |
| `Activation::using_fast_tanh` default `false` (exact `tanh`/`sigmoid`); only flipped by benchmark tools, never by `render` | Activation precision selected via `ActivationPrecision::{Fast, Standard}`; `Fast` uses Padé/minimax approximations, not exact math. `Standard` (exact-grade polynomial, universal default) matches C++ exact math parity within 2e-7 | ⚠ **Intentional divergence, not a bug.** C++'s reference path used for goldens is exact math; NAM-rs's `Fast` mode trades a small, bounded approximation error for throughput. `Standard` (exact-grade default) narrows this to identical parity within measurement noise (§2.5). |
### 5.1 Sample Rate Default Policy (F-P3)
**Background:** the C++ NAMcore uses `NAM_UNKNOWN_EXPECTED_SAMPLE_RATE = -1.0`
(`NAM/dsp.h:30`) as a sentinel when the `sample_rate` field is absent from the `.nam`
JSON. When `expected_sample_rate == -1.0`, the LSTM prewarm computation
(`NAM/lstm.cpp:128`) produces `max(1, (int)(0.5 × -1.0)) = 1` sample — effectively
disabling prewarm.
**NAM-rs policy:** `sample_rate` absence defaults to **48000 Hz**
(`src/loader/loaded_model_pair.rs:13`, `pub(crate) const DEFAULT_SAMPLE_RATE: f32 = 48000.0`).
This value drives the real prewarm computation for LSTM (24000 samples at 48 kHz) and the
sample-rate-dependent logic in `DSP::Reset()` for all architectures.
**Rationale:** the 48000 Hz default produces a correct, functional prewarm rather than the
C++ sentinel's near-zero prewarm (1 sample). All known production `.nam` models include
`sample_rate` explicitly, so the default is only exercised by degenerate or hand-crafted
models. In those cases, NAM-rs's behavior is measurably "more correct" — the model settles
to its steady state — while C++'s sentinel produces effectively no prewarm at all.
**Divergence assessment:** This is an **intentional, documented, low-risk divergence**.
It does not affect any known production model (every real community `.nam` export includes
`sample_rate`). The behavior affects only the degenerate zero-`sample_rate` case, where
NAM-rs's prewarm is strictly superior. The C++ sentinel is not emulated, and emulating it
has no practical benefit for any real-world use case.
**Verification:** this policy is enforced at two levels:
1. **JSON parse:** `validate_sample_rate` (`src/loader/nam_json/validation.rs`) rejects
non-finite or ≤0 sample rates via `JsonError::InvalidSampleRate`, but `None` (absent
field) passes through silently — it is handled downstream.
2. **Model build:** `build.rs:161` applies `unwrap_or(DEFAULT_SAMPLE_RATE)` to the parsed
`Option<f32>`. LSTM dispatchers (`static_builder.rs:27,72`, `dynamic_builder.rs:32`)
apply the same default independently for prewarm computation.
### 5.2 FastLUTActivation — Not Ported (F-P4-c)
**Background:** C++ NAMcore ships an optional `FastLUTActivation` class
(`NAM/activations.h:127-169`) that precomputes look-up tables for `tanh` and `sigmoid`
to accelerate inference on systems without fast `expf` hardware. It is controlled by
`Activation::enable_fast_tanh()` and `Activation::using_fast_tanh`.
**Status in NAM-rs:** `FastLUTActivation` is **not ported** and has no NAM-rs equivalent.
This is **not a parity gap** for the following reasons:
- `FastLUTActivation` is a **runtime optimization**, not a format/algorithm feature.
The `.nam` file format has no field for "use lookup tables" — it is a local C++-side
accelerator that produces the same mathematical output (within LUT precision) as exact
`tanh`/`sigmoid` for identical weights.
- The NAMcore `render` tool (used for golden generation and live cross-validation) **never**
enables it: `enable_fast_tanh()` is only called from benchmarking tools
(`tools/benchmodel*.cpp`), not from `render.cpp`. This is confirmed in the NAMcore
audited source (`activations.h:14`: `static bool using_fast_tanh = false` is the only
initialization, and only `benchmodel*.cpp` flips it — verified by grep of all callers).
- NAM-rs's `ActivationPrecision::Standard` (universal default) already produces exact-grade
`tanh`/`sigmoid` within 2×10⁻⁷ of C++'s exact math, making the LUT precision tradeoff
irrelevant.
**Verdict:** FastLUTActivation is classified as **"Not Applicable"** — no port needed, no
parity gap, no audio divergence. Documented here for completeness and to prevent future
audit cycles from re-discovering and re-investigating it.
## 6. Other Architectures (Out of Scope)
ConvNet, Linear, `SlimmableContainer`, and the IR Cabsim convolution stage complete the model suite:
- **ConvNet — Paridade Total de Inicialização e Aritmética (✅ resolved 2026-07-28).** O vendored
NAMcore implementa ConvNet (`NAM/convnet.cpp`), mas usando um formato flat com BatchNorm params
brutos. O NAM-rs usa um formato nested por-bloco com BatchNorm pré-fundido scale/offset
(`src/loader/dispatcher/convnet/mod.rs`). A divergência de ESR `2.54e-5` (SNR `45.9 dB`)
previamente reportada era **exclusivamente um transiente de inicialização de estado (prewarm)**
confinado às primeiras 62 amostras — o `ConvNetModel::prewarm()` preenchia zeros literais por
bloco isolado, enquanto o NAMcore (`dsp.cpp:67-96`) processa `receptive_field_size + 1` amostras
de silêncio através da rede inteira. A correção (`TASK-CONVNET-01`, 2026-07-28) replicou a
semântica exata do NAMcore, eliminando o transiente.
- **Métricas de paridade definitivas (pós-fix, 2026-07-28):**
- **C++ cross-validation** (`quick_parity_convnet`): ESR = `4.20e-15` (SNR `143.8 dB`), MR-STFT = `1.20e-6`
- **Oráculo f64** (`test_oracle_convnet`): ESR = `3.57e-15` (SNR `144.5 dB`, piso f32)
- **Oráculo vs NumPy f64** (`test_oracle_vs_python_anchor_convnet`): ESR = `5.23e-33` (bit-exact)
- **Self-golden** (`test_golden_vectors_convnet_test`): ESR = `0.00e0` (determinismo total)
- **Gates de qualidade recalibrados:** SNR ≥ `120 dB`, ESR ≤ `1.0e-12`, MR-STFT ≤ `1.0e-4`
(`TASK-CONVNET-05`).
- **Teste de invariante:** `test_convnet_prewarm_fixed_point_invariant()` confirma que o
estado pós-prewarm é um ponto fixo estacionário idêntico à convergência explícita (`TASK-CONVNET-03`).
- CPU latency = `10.3 µs` (0.8% of RT budget) — inalterada (prewarm opera em caminho frio).
- **Linear (RF=2048 / 4096 / 8192).** Affine linear model. Baseline ESR vs NAMcore = `1.70e-14` (SNR `137.7 dB`), CPU latency = `0.3 µs` (0.0% of RT budget).
- **`SlimmableContainer`.** Multi-model crossfade orchestration, implemented and tested (`src/models/container.rs`, `tests/models/container_slimmable.rs`). Baseline A2 Example (CH=3→6) cross-validation vs NAMcore: ESR = `7.28e-14` (SNR `131.4 dB`), ESR vs f64 = `1.82e-14`, MR-STFT = `1.73e-05`.
- **IR Cabsim.** Impulse response convolution stage, cross-validated via `tests/parity/cabsim_cpp_parity.rs`.
---
## 7. Known-Broken Ledger ("Sabidamente Broken")
Single-page triage. Everything in this document up to here is evidence; this chapter is the
verdict. Read this chapter alone if the only question is *"what's safe to ship, and what isn't."*
Severity tiers are ordered by how much they should worry a release decision, not by section order.
### 7.1 🔴 Broken today — confirmed wrong audio output
Exactly **one** model, in the entire audited scope, produces confirmed-wrong output:
| Model | Symptom | Status |
|:------------------------------------------------------------------------ |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wavenet_a2_max.nam` (WaveNet A2, official flagship, `condition_size=8`) | MSE≈2.46e3, SNR≈−15.6 dB, ESR≈3.61e1, MR-STFT≈3.41 vs. the C++ golden — every threshold missed by 3+ orders of magnitude. **Post-B1/B2/B3 (T3.9, 2026-07-10):** MSE≈7.30e3, SNR≈−20.3 dB, ESR≈1.07e2 — the FiLM corrections removed an accidental partial cancellation, exposing the full divergence. | Root cause **identified**: three confirmed production bugs in the A2 dynamic engine (head1x1 per-layer vs per-array; `groups` ignored in `layer1x1`/`input_mixin`; legacy head kernel K=1 vs hardcoded 16). Fix plan detailed in §4.4 |
No other model, in any of the three audited architectures, has a confirmed output-correctness
failure. This is the only item that should block a release if `wavenet_a2_max.nam` compatibility
is a requirement.
> **Mitigado/contido (2026-07-02):** desativado fail-closed na camada de dispatch
> (`is_disabled_broken_a2_flagship` em `src/loader/dispatcher/wavenet/mod.rs`,
> ver §4.4). `build_model` rejeita o modelo com `Err` antes de
> tocar pesos ou construir o motor. O modelo `.nam` e o golden `.bin` permanecem
> no repositório (disabled, not removed). Reativação depende de fechar a
> divergência do `condition_dsp` contra o golden C++ (§4.4).
>
> **Predicado de detecção (assinatura estrutural estreita):**
>
> ```text
> num_arrays == 1
> && data.config.condition_dsp.is_some()
> && l0.condition_size.unwrap_or(1) == 8
> ```
>
> A guarda casa **somente** o flagship quebrado (`single-array`, `condition_dsp`
> presente, `condition_size=8`). `wavenet_condition_dsp.nam` (multi-array,
> `condition_size=3`) e todos os fixtures FiLM/gated/blended/full/lite não casam
> — preservados, com golden vectors comprovadamente passando no `cargo test`.
>
> **Mensagem de erro ao usuário:**
>
> ```text
> WaveNet A2 flagship (single-array, condition_dsp, condition_size=8) is disabled:
> confirmed wrong audio output vs NAMcore golden — see docs/cpp_parity_map.md §7.1.
> Model is not removed; re-enable requires closing the condition_dsp parity gap (§4.4).
> ```
>
> **Impacto nos testes:** nenhum teste em `cargo test` (debug ou release)
> executa inferência de `wavenet_a2_max.nam`. Inventário de mitigação do modelo flagship:
>
> - `test_loader_gap_wavenet_a2_max` renomeado para
> `test_wavenet_a2_max_dispatch_is_disabled_broken` — assera `Err` com a
> mensagem acima (prova vivo de que a guarda está ativa).
> - `test_golden_vectors_wavenet_a2_max` mantido `#[ignore]` com razão citando
> §7.1 e bloqueio no dispatch.
> - `test_oracle_vs_python_anchor_a2_generic` → `#[ignore]` com rastreamento
> FU-1 (restauração do oráculo f64, bloqueado por §4.4).
> - `test_oracle_a2_generic`, `test_decomposition_a2_generic`,
> `test_combined_simulation_a2_generic` → `#[ignore]` com razões atualizadas
> para §7.1.
> - Meta-teste `threshold_calibration`: `"wavenet_a2_max"` removido da lista;
> braço `validation.rs` mantido como morto documentado.
>
> **Invariante garantido:** `grep -rn 'wavenet_a2_max' src/ tests/` mostra
> apenas a guarda de desativação, o teste de asserção positiva,
> `#[ignore]`'s rastreados, e referências documentais.
>
> **Cobertura de regressão:** `test_wavenet_condition_dsp_still_loads` prova que
> a guarda não bloqueia modelos vizinhos válidos. `utils/tests-quick.sh` 100% verde.
### 7.2 🟡 Known, measured, accepted tradeoffs — not bugs
Every item below is a deliberate engineering tradeoff with a calibrated, tested error budget. They
show up as nonzero numbers in the tables throughout this document, but they are not defects:
- **LSTM backbone weight representation** — Storage uses native `f32` weights across all gate matrices, matching NAMcore's `Eigen::MatrixXf`. Eliminating weight quantization simplified GEMV kernel dispatch and improved per-sample latency by 10-12%, while bringing `BossLSTM-2x8` to bit-exact convergence with NAMcore (ESR = 1.00e-11).
- **`ActivationPrecision::Fast`'s Padé/minimax activation approximations** vs. C++'s exact
`tanh`/`sigmoid` — small, bounded, and identical in nature for LSTM and WaveNet A1/A2 (§2.5,
§3.2, §5). `Standard` (exact-grade, universal default) collapsed this gap to match C++ parity
within measurement noise.
- **A2 FiLM dynamic-engine interop gap** — previously identified as an interop gap of SNR 18.1–36.0 dB,
this was shown to be caused by a zero-biased initialization in the synthetic weight generator.
Following the fix to apply standard identity-biased weights, the gap collapsed to float32 precision
limits (SNR 138+ dB / ESR ~1e-14), achieving near-bit-exact parity (§4.3).
### 7.3 🟠 Test-infrastructure caveats — parity coverage that can silently vanish
These do not produce wrong audio, but they can make the *evidence* for parity evaporate without failing CI:
- **Silent SKIP in v1 live cross-validation.** `tests/parity/cpp_parity.rs::run_v1`
now delegates to `run_v1_hf` (Standard/exact activation is the universal default),
so v1 parity metrics are HF-mode. Both still discard `ParityOutcome`
(`let _ = run_render_comparison(...)`). Any skip condition — C++ toolchain absent,
model missing, render crash — prints `SKIP:` to stderr and the test still reports `ok`.
The non-ignored `quick_parity_*` subset in `tests-quick.sh` Fase 2 can therefore pass
green with **zero** cross-validations executed. (`run_v2_multi_sr_impl` tracks
outcomes correctly and asserts the completed-rate set.)
- **`quick_parity_convnet`** previously always skipped (§6 — architecture incompatibility), but
after the prewarm fix (TASK-CONVNET-01, 2026-07-28) it now passes with ESR=4.20e-15 (SNR 143.8 dB),
completing the 4-model quick-parity matrix at full coverage.
- **`wavenet_a2_film_input_mixin_pre.nam`** has been fully validated with committed C++ goldens and live cross-validation (`live_cross_validation_wavenet_a2_film_input_mixin_pre`), achieving ESR `3.44e-14` (SNR `134.6 dB`, MR-STFT `6.92e-06`) against NAMcore with calibrated gates (SNR ≥ `120.0 dB`, ESR ≤ `1.0e-11`, MR-STFT ≤ `1.0e-4`).
---
## See Also
- [audio_fidelity_map.md](audio_fidelity_map.md) — off-spec DSP factors; §3 (LSTM recurrent drift) pairs with §2.5/§2.7 here
- [perceptual_validation.md](perceptual_validation.md) — metrics and gate-calibration policy
- `TODO-wavenet_a2_max.md` — live status and fix plan for the open A2 flagship investigation (§4.4)
- `tests/parity/cpp_parity.rs` — live cross-validation against the C++ `render` tool
- `tests/parity/reference_oracle_f64.rs` — f64 oracle and independent NumPy anchor (decomposition tools, §1.2)