You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+46-1Lines changed: 46 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# codec.cpp
2
2
3
-
This repository is a C/C++ library + CLI that runs several neural audio codecs (currently WavTokenizer-Large, DAC, Mimi) using **ggml** graphs so execution can be offloaded via **ggml backends** (CPU/CUDA/Vulkan/Metal/etc.).
3
+
This repository is a C/C++ library + CLI that runs several neural audio codecs (currently WavTokenizer-Large, DAC, Mimi, Soprano, NeuCodec, NeMo Nano, Qwen3-TTS-Tokenizer, Chatterbox-S3T, Chatterbox-S3G) using **ggml** graphs so execution can be offloaded via **ggml backends** (CPU/CUDA/Vulkan/Metal/etc.).
4
4
5
5
The intended architecture is **llama.cpp-style**:
6
6
- Build model forward passes as **ggml graphs (ops)**.
@@ -115,6 +115,45 @@ If conversion scripts are involved, regenerate gguf after changes (stale gguf is
115
115
- When touching graph execution / backend scheduler: be careful with allocation lifetimes (`eval_ctx`, scheduler reset semantics).
116
116
- Prefer small, reviewable commits.
117
117
118
+
### One model = one graph
119
+
120
+
Every model in this repo decodes through a **single cached ggml graph** per public call. CPU-side orchestration is allowed only for: (a) RNG sampling, (b) deterministic LUT precompute that can't be expressed as ggml ops, (c) trivial output marshalling (allocating the output buffer, applying short trim/fade tails).
121
+
122
+
Whenever you find yourself running graph A → CPU loop → graph B, that is a refactor signal. Loops over fixed iteration counts (CFM ODE Euler steps, CFG cond/uncond passes, n-step diffusion solvers) **must be unrolled into the graph** — they are not "loops" semantically, just fixed-shape compute. The Chatterbox-S3G decode unrolls 10 Euler steps × 2 CFG passes (20 estimator subgraphs) inside a single graph; eval-arena memory grows to multi-GB but graph cache absorbs the build cost.
123
+
124
+
CPU computation that *looks* like a loop but is really feedforward (NSF source generation, forward STFT, OLA-based iSTFT) belongs in the graph too. The recipes:
125
+
-**STFT** → `ggml_conv_1d` against a `[k=n_fft, in=1, out=n_bins]` cos/-sin basis kernel (window pre-baked).
126
+
-**iSTFT OLA** → matmul against `[n_bins, n_fft]` synthesis basis, then `ggml_conv_transpose_1d` with an identity `[k, out=1, in=n_fft]` kernel and `stride=hop` to scatter-accumulate frames; divide by an envelope reconstructed by the same convtranspose on a constant `window^2` matrix.
127
+
-**Cumsum + nearest-upsample + step-mask** are all ggml primitives; `ggml_arange + sin/cos + concat` builds sinusoidal/rel-pos PE in-graph.
128
+
129
+
### Lessons from prior model integrations
130
+
131
+
These recur often enough that they're worth checking *before* you debug a parity failure:
132
+
133
+
-**Memory literals overflow**. `8u * 1024u * 1024u * 1024u` is `unsigned int` arithmetic and wraps to 0; the graph cache then fails with "ggml_new_object: not enough space". Always cast: `(size_t) 8 * 1024 * 1024 * 1024`.
134
+
-**`ggml_mul_mat(a, b)` contracts on `ne[0]`** — both operands' inner dim must agree. For attention `attn @ v` you must permute `v_dth (d, t, h)` → `(t, d, h)` first; see `codec_op_lm_attn_ctx_dth` for the canonical pattern.
135
+
-**`ggml_conv_transpose_1d` weight shape is `(k, out, in)`**, not `(k, in, out)`. PyTorch's `(in, out, k)` maps to ggml `ne[0]=k, ne[1]=out, ne[2]=in`; an "identity" OLA kernel needs `weight[k=i, 0, in=i] = 1`.
136
+
-**Embedding-table layout**. PyTorch `nn.Embedding.weight` saved as `(V, hidden)` row-major lands in ggml as `ne[0]=hidden, ne[1]=V`. A token's embedding row is at flat offset `ci + token_id * hidden` — easy to invert; use `ggml_get_rows` rather than indexing manually.
137
+
-**`F.normalize(x)` ≈ `ggml_rms_norm(x) / sqrt(N)`**. ggml_rms_norm divides by `sqrt(mean(x²))`; PyTorch divides by `sqrt(sum(x²))`. The two differ by `sqrt(N)`.
138
+
-**Snake activation expects `[t, c]`**. `codec_op_snake` reshapes alpha as `(1, ne[1])` and broadcasts; if you pass it `[c, t]` it'll silently misalign or assert.
139
+
-**PyTorch parametrized weights**. Newer checkpoints (`.parametrizations.weight.original0/1` style weight_norm) won't deserialize via the legacy `weight_g/weight_v` API. Bake `g * v / ||v||` at converter time so the runtime never sees the parametrization.
140
+
-**HiFi-GAN iSTFT trim ≠ Vocos iSTFT trim**. `codec_runtime_istft_from_head`'s default trim is `(n_fft − hop)/2` (Vocos/Wavtokenizer). HiFi-GAN's `torch.istft` with `center=True` removes `n_fft/2` per side; pass `trim_pad_override = n_fft/2`.
141
+
-**periodic vs symmetric Hann**. `scipy.get_window("hann", n, fftbins=True)` is `0.5 − 0.5·cos(2πn/N)` (periodic). The default Hann in `codec_runtime_istft_from_head` is symmetric (`/(N-1)`); pass an explicit window if the reference uses periodic.
142
+
-**Espnet rel-pos PE is interleaved**. `pe[r, 2k] = sin`, `pe[r, 2k+1] = cos` (not `concat([sin, cos])`). To build it in-graph, stack into `[half, 2, n_rows]` then `permute(1, 0, 2, 3) → cont` so the contiguous flatten gives the interleaved layout.
143
+
-**clangd noise is NOT real**. The codebase shows constant `Adding 'string' to a string does not append` and `lambda has no matching call` diagnostics from clangd because there's no compile_commands.json wired up. **Trust `cmake --build`** — if cmake compiles, the code is fine.
144
+
145
+
### Parity testing strategy
146
+
147
+
When matching PyTorch numerically:
148
+
149
+
- Stage parity: validate each subgraph against PyTorch *before* chaining them. Bit-perfect deterministic stages (encoder, single CFM step) catch shape/permute bugs early; once chained, only RNG-dependent stages will diverge.
150
+
- For RNG-dependent paths (CFM noise init, NSF random phase + Gaussian noise), expose a path that takes a precomputed noise tensor instead of sampling — then PyTorch-reference parity becomes deterministic.
151
+
- Once the full pipeline matches end-to-end, **delete the standalone parity test binaries and tests**. This project standardises on E2E smoke tests (`tests/e2e/<model>_decode_smoke.py`) that drive the public C API. Per-stage parity scripts are scaffolding, not artefacts.
152
+
153
+
### Build flow
154
+
155
+
`cmake --build build -j` is the source of truth. Run it after every non-trivial edit; the linter diagnostics surfaced inline are unreliable. After build, run `tests/e2e/<model>_*_smoke.py` from the repo root via `.venv/bin/python` (PyTorch deps live in that venv).
156
+
118
157
---
119
158
120
159
## Useful entry points for Codex
@@ -137,3 +176,9 @@ If you need to add/replace an op:
137
176
- The unified graph builder is the only Mimi encode graph path (`frontend -> transformer -> downsample -> unrolled RVQ`).
138
177
- Split/legacy graph kinds for Mimi encode stages are removed from runtime graph enums.
139
178
- Mimi encode weight writing now targets only the canonical encode graph path.
179
+
180
+
## Chatterbox-S3G Status
181
+
182
+
- Decode path is a single graph: tokens → encoder → unrolled CFM ODE (10 steps × CFG cond/uncond) → mel → f0_predictor → in-graph NSF source → STFT → main HiFT → in-graph iSTFT → PCM. Builtin-conds path only (no ref_wav embedding yet).
183
+
- Reusable building blocks landed in `src/ops/ggml_ops` (`codec_op_basic_transformer_block_tc`, `codec_op_cfm_causal_resnet_block_tc`, `codec_op_causal_block1d_tc`, `codec_op_hifigan_resblock_branch_ct`, `codec_op_sinusoidal_time_emb`, `codec_op_espnet_rel_pos_emb`) and `src/ops/lm_attn` (`codec_op_lm_attn_rel_pos_dth`, `codec_op_rel_shift_espnet`).
184
+
- meanflow checkpoints convert and load metadata but `init` rejects them — the meanflow ODE schedule + `time_embed_mixer` aren't wired into the unrolled graph.
0 commit comments