Skip to content

Commit b033e13

Browse files
hans00claude
andcommitted
docs: capture S3G integration lessons in CLAUDE.md
- One-graph-per-model rule: unroll fixed-iteration loops (CFM ODE / CFG cond-uncond / n-step solvers) into the graph; fold feedforward stages that look like CPU loops (NSF source, forward STFT, OLA iSTFT) into ggml ops. - Recurring pitfalls (memory literal overflow, mul_mat contraction axis, convtr1d weight layout, embedding indexing, F.normalize vs rms_norm, HiFi-GAN vs Vocos iSTFT trim, periodic vs symmetric Hann, Espnet rel-pos interleaving, clangd diagnostic noise). - Parity testing strategy: stage parity is scaffolding, E2E smoke is the contract; delete per-stage binaries + parity scripts once chained. - Add Chatterbox-S3G status section + list updated reusable ops. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c28614f commit b033e13

1 file changed

Lines changed: 46 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# codec.cpp
22

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.).
44

55
The intended architecture is **llama.cpp-style**:
66
- Build model forward passes as **ggml graphs (ops)**.
@@ -115,6 +115,45 @@ If conversion scripts are involved, regenerate gguf after changes (stale gguf is
115115
- When touching graph execution / backend scheduler: be careful with allocation lifetimes (`eval_ctx`, scheduler reset semantics).
116116
- Prefer small, reviewable commits.
117117

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+
118157
---
119158

120159
## Useful entry points for Codex
@@ -137,3 +176,9 @@ If you need to add/replace an op:
137176
- The unified graph builder is the only Mimi encode graph path (`frontend -> transformer -> downsample -> unrolled RVQ`).
138177
- Split/legacy graph kinds for Mimi encode stages are removed from runtime graph enums.
139178
- 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

Comments
 (0)