Skip to content

Commit 7cee5d7

Browse files
authored
Merge pull request #430 from CrispStrobe/perf/81-round2
build: update ggml to v0.23 and record Q4 audit
2 parents 839f6af + 4d3834b commit 7cee5d7

32 files changed

Lines changed: 870 additions & 225 deletions

LEARNINGS.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ If a lesson is still "live" (affects current work), it's linked from
1111
---
1212

1313
<!-- BEGIN INDEX POINTER — generated by tools/gen-learnings-index.py -->
14-
**Do not read this file top to bottom** — it is ~1 MB across 299 sections.
14+
**Do not read this file top to bottom** — it is ~1 MB across 301 sections.
1515
The generated topic + per-model index is [`docs/LEARNINGS-INDEX.md`](docs/LEARNINGS-INDEX.md).
1616
Find the lesson there, then jump with `grep -n "<heading text>" LEARNINGS.md`.
1717
Regenerate both with `python tools/gen-learnings-index.py` after adding a section.
@@ -18833,3 +18833,59 @@ and fail at zero.
1883318833
Transferable: any decoder that resamples, upsamples, or expands attacker-
1883418834
controlled input needs an amplification bound, and `|| true` on a `cp` of
1883518835
required inputs is a silent pass wearing a gate's clothes.
18836+
18837+
## Device-side argmax can cost more than the readback it removes, and an unused graph output still runs (#81, 2026-09-07)
18838+
18839+
The TDT decoder read a full 8k-token joint-logit row back to the CPU at every
18840+
greedy step, so replacing that transfer with two in-graph argmax results looked
18841+
obviously favorable. A second arm precomputed 4 or 8 encoder frames against the
18842+
same predictor state and scanned duration-skipping blanks on the host.
18843+
18844+
The P100 Q4 A/B rejected both ideas. All arms were stable and transcript-exact,
18845+
but scalar device selection changed the 134 s clip from 1.2062 s to 1.2042 s
18846+
(0.17%, noise-sized). Batch 4 took 1.2369 s and batch 8 took 1.2700 s; combining
18847+
them with device selection took 1.2284 s and 1.3044 s. Decode-stage medians were
18848+
303.1, 298.8, 326.7, 364.7, 321.7 and 365.8 ms respectively. Duration skipping
18849+
makes speculative batches do work for frames the scalar decoder never visits,
18850+
and contiguous materialization of vocabulary/duration slices adds kernels before
18851+
CUDA argmax can consume them.
18852+
18853+
There was also a measurement trap: the supposed baseline graph contained the
18854+
new argmax tensors as outputs even when the environment switch was off. ggml
18855+
executes every graph output; ignoring the returned integers does not remove their
18856+
kernels. An opt-in experiment must gate graph construction itself, or use a
18857+
separate graph, if its baseline is meant to represent shipping code. We removed
18858+
the experimental graph and direct-convolution arms after measurement rather than
18859+
leaving dormant nodes in every decoder session.
18860+
18861+
18862+
The encoder matrix rejected three similarly plausible shortcuts. ggml's direct
18863+
standard and depthwise CUDA convolutions were 3.3% and 25.8% slower than the
18864+
existing lowering, while the fork's per-head flash-attention path was 57.2%
18865+
slower on sm_60. Selectively preserving FFN tensors at Q8 matched speed but
18866+
changed the transcript; F16 changed it and lost 3.8%. Backend support is not a
18867+
performance result, and a higher-precision intermediate is not automatically a
18868+
safe quantization exception. Keep each lever isolated until both timing and
18869+
end-to-end text parity pass.
18870+
18871+
## An upstream-sync PR must target the branch consumers pin, and cache export belongs after the build (#81, 2026-09-07)
18872+
18873+
The ggml v0.23 consolidation initially targeted an older sync branch. Its tree
18874+
contained current upstream, but merging that PR would not have updated the
18875+
`crispstrobe-ops` default branch that CrispASR pins, so GitHub correctly kept
18876+
reporting the fork as 15 commits ahead and 747 behind. Retargeting exposed four
18877+
default-only commits. Each was a backport of a fix already present in the sync
18878+
line; after verifying those original commits were ancestors of the candidate,
18879+
we merged the default history and resolved only the duplicate hunks. The merged
18880+
default branch is 43 commits ahead and 0 behind upstream. Validate ancestry
18881+
against the actual consumer branch, not merely the candidate's upstream merge
18882+
base.
18883+
18884+
The first model-level A/B then spent 48 minutes successfully compiling both
18885+
trees and failed because a raw Python string passed literal `\\n` characters
18886+
to an embedded child. Worse, the cache export was after the benchmark, so the
18887+
successful build could not refresh the seed. Compile embedded programs locally
18888+
before submission, and export the build cache immediately after a successful
18889+
build as well as after final validation. The corrected P100 run was
18890+
transcript-exact and made v0.23 0.53% faster on the 134 s clip; its 7,927-file
18891+
cache became the next account-matched dataset version.

PERFORMANCE.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,56 @@ needs a per-stage look).
882882
| onnx-asr parakeet-ctc CUDA fp32, 134 s varied speech, in-process | **214×** (tdt 121×) |
883883
| onnx-asr parakeet-ctc CPU int8, 134 s varied | 5.8× (tdt 5.7×) |
884884

885+
**Q4 round-2 rejected arms (P100, 2026-09-07; Kaggle
886+
`chr1str/crispasr-issue-81-q4-round-2`, encoder matrix v1 at `b82b7baf`,
887+
TDT matrix v3 at `c0527382`).** Device-side
888+
selection preserved exact transcripts but improved the experimental baseline by
889+
only 0.17% on the 134 s clip. Speculative joint batches lost 2.5–8.1%. The
890+
selection graph also made its argmax outputs part of the graph when the runtime
891+
switch was off, so that arm was removed rather than retained as a dormant option.
892+
893+
| arm | 134 s median | x-RT | vs same-run baseline |
894+
|---|---:|---:|---:|
895+
| selection-graph baseline | 1.2062 s | 111.15× ||
896+
| device argmax | 1.2042 s | 111.34× | +0.17% |
897+
| batch 4 | 1.2369 s | 108.39× | −2.48% |
898+
| batch 8 | 1.2700 s | 105.57× | −5.02% |
899+
| batch 4 + device argmax | 1.2284 s | 109.14× | −1.81% |
900+
| batch 8 + device argmax | 1.3044 s | 102.79× | −7.52% |
901+
902+
The preceding CTC matrix rejected the other proposed Q4 levers on the same
903+
134 s varied clip. Direct standard/depthwise convolution reduced throughput,
904+
and enabling the fork's per-head flash-attention path on sm_60 was more than
905+
2× slower. Keeping selected FFN tensors at Q8 or F16 either failed transcript
906+
parity or lost speed.
907+
908+
| CTC arm | x-RT | result |
909+
|---|---:|---|
910+
| baseline | **163.09×** | exact/stable |
911+
| direct initial conv | 157.78× | −3.3% |
912+
| direct depthwise conv | 120.94× | −25.8% |
913+
| both direct convs | 117.17× | −28.2% |
914+
| per-head flash attention | 69.87× | −57.2% |
915+
| flash + both direct convs | 60.11× | −63.1% |
916+
| selected FFN Q8 | 163.06× | transcript changed |
917+
| selected FFN F16 | 156.95× | transcript changed and −3.8% |
918+
919+
**ggml v0.23 fork consolidation (P100 sm_60, 2026-09-07; Kaggle
920+
`chr1s4/crispasr-ggml-v0-23-q4-a-b` v3).** The old fork pin and merged
921+
v0.23 runtime used separate source-compatible CrispASR checkouts, the same Q4
922+
Parakeet TDT model and audio, and an account-matched warm ccache dataset.
923+
Transcripts were stable and byte-identical. v0.23 was neutral-to-faster:
924+
925+
| clip | old fork | ggml v0.23 | change |
926+
|---|---:|---:|---:|
927+
| JFK, 11.00 s | 85.44× | **87.44×** | +2.34% |
928+
| varied speech, 134.07 s | 115.38× | **115.99×** | +0.53% |
929+
930+
The benchmark exercised ggml runtime commit `069a517d`; the merged default
931+
commit `2dd13edd` has the same runtime source tree (later commits only add
932+
patch guards, CI, and merge ancestry). Kaggle validation passed and the
933+
`chr1s4/crispasr-ccache` seed was refreshed to version 13.
934+
885935
CUDA rows resolved (2026-07-12, kernel `issue81-onnx-bench` v16, real
886936
134 s varied LibriSpeech, load-excluded, 301-word proof-of-work,
887937
same-run onnx head-to-head): the honest-methodology re-run with the

PLAN.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# CrispASR — Pending work
22

3+
## DONE 2026-09-07 — #81 Q4 CUDA optimization round 2
4+
5+
Worktree `.claude/worktrees/perf-81-round2`, branch `perf/81-round2`. Audit the
6+
CrispStrobe ggml fork against current upstream for convolution, per-head flash
7+
attention, CUDA graph and reduction improvements. Benchmark direct subsampler
8+
convolution, adaptive GPU TDT batching, device-side selection, per-head flash
9+
attention and selective hot-tensor precision as isolated Q4 arms. Preserve
10+
exact transcript parity, measure P100 through the Kaggle harness, and keep
11+
Ada/RTX 4070 conclusions explicitly separate from sm_60 evidence.
12+
13+
All isolated runtime arms were rejected; see `PERFORMANCE.md`. The ggml fork
14+
was consolidated onto v0.23.0 through CrispStrobe/ggml#3, finished 0 commits
15+
behind upstream, passed hosted CPU/Metal/Vulkan/CUDA validation, and improved
16+
the same-workload P100 Q4 result by 0.53% on the 134 s clip. CrispASR now pins
17+
the merged default-branch commit.
18+
319
## DONE 2026-09-07 — #81 Q4 FastConformer profiling
420

521
The P100 profile found TDT's encoder-to-joint projection consuming about

docs/LEARNINGS-INDEX.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ per-backend porting detail (Per-model port notes + the family cross-reference).
1717
Reach for the topic groups when you are stuck on a *technique*, and the
1818
cross-reference when you already know which model you are touching.
1919

20-
## Index by topic (299 lessons)
20+
## Index by topic (301 lessons)
2121

2222
**Security & untrusted input** (2)
2323

@@ -34,7 +34,7 @@ cross-reference when you already know which model you are touching.
3434
- L16962 — consent record — BIND to the audio, don't chain the log
3535
- L18257 — watermark detector — swap the STATISTIC, not the threshold
3636

37-
**Build, CI, release & packaging** (21)
37+
**Build, CI, release & packaging** (22)
3838

3939
- L637 — A packaging step that destroys the state a later step reads — and scripts that only run in a release job
4040
- L941 — A guard job that runs ONE compiler family guards one compiler family
@@ -57,6 +57,7 @@ cross-reference when you already know which model you are touching.
5757
- L11429 — Kaggle as a batch-rebake target: seven fragilities the script has to work around
5858
- L14873 — CI has no NVIDIA GPU — a CUDA-only graph path (mimo RVQ) is guarded by an on-Kaggle exact-parity smoke, not CI (#309)
5959
- L15700 — #313 Rust crates on crates.io via git dep
60+
- L18871 — An upstream-sync PR must target the branch consumers pin, and cache export belongs after the build
6061

6162
**Multi-surface wiring — CLI / C ABI / bindings / server** (29)
6263

@@ -90,7 +91,7 @@ cross-reference when you already know which model you are touching.
9091
- L18525 — A streaming callback over a growing buffer is buffered inference unless the public session owns every cache
9192
- L18550 — A managed assembly and its native library cannot share a name, and a live test that skips itself is not a test
9293

93-
**Parity, the diff harness & ground truth** (41)
94+
**Parity, the diff harness & ground truth** (42)
9495

9596
- L39 — The long-form recipe is part of the blueprint — and a comment citing the blueprint is not the blueprint
9697
- L169 — A model family's per-size behaviour lives in the reference RUNTIME, not the modelling code
@@ -133,6 +134,7 @@ cross-reference when you already know which model you are touching.
133134
- L15986 — #333 madlad400 quants + T5 parity
134135
- L16858 — chatterbox hift_pcm(ref_mel) cos≈0.879 was a diff-harness layout bug (source_stft fed transposed), not a…
135136
- L18488 — Chatterbox Multilingual V3: checkpoint names, quant hashes, and a non-silent clone are not parity
137+
- L18837 — Device-side argmax can cost more than the readback it removes, and an unused graph output still runs
136138

137139
**ggml graphs, allocation & caching** (44)
138140

examples/crispasr-quantize/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
set(TARGET crispasr-quantize)
1616
add_executable(${TARGET} main.cpp)
17+
target_compile_features(${TARGET} PRIVATE cxx_std_17)
1718

1819
include(DefaultTargetOptions)
1920

examples/talk-llama/llama-model-loader.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,7 @@ static bool weight_buft_supported(const llama_hparams& hparams, ggml_tensor* w,
10421042
ggml_tensor* B = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, d_state, n_group, n_seq_tokens, n_seqs);
10431043
ggml_tensor* C = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, d_state, n_group, n_seq_tokens, n_seqs);
10441044
ggml_tensor* ids = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, n_seqs);
1045-
op_tensor = ggml_ssm_scan(ctx, s, x, dt, w, B, C, ids);
1045+
op_tensor = ggml_ssm_scan(ctx, s, x, dt, w, B, C, ids, /*K=*/1);
10461046
} break;
10471047
case GGML_OP_RWKV_WKV6: {
10481048
// FIXME

examples/talk-llama/models/mamba-base.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ ggml_tensor* llm_build_mamba_base::build_mamba_layer(llm_graph_input_rs* inp, gg
113113
// Custom operator to optimize the parallel associative scan
114114
// as described in the Annex D of the Mamba paper.
115115
// => {d_inner, n_seq_tokens, n_seqs} and {d_state, d_inner, n_seqs}
116-
return ggml_ssm_scan(ctx, ssm, x, dt, A, B, C, ids);
116+
return ggml_ssm_scan(ctx, ssm, x, dt, A, B, C, ids, /*K=*/1);
117117
};
118118

119119
ggml_tensor* y_ssm = build_rs(inp, ssm_states_all, hparams.n_embd_s(), ubatch.n_seqs, get_ssm_rows);
@@ -242,7 +242,7 @@ ggml_tensor* llm_build_mamba_base::build_mamba2_layer(llm_graph_input_rs* inp, g
242242

243243
// TODO: use semistructured matrices to implement state-space duality
244244
// => {d_inner, n_seq_tokens, n_seqs} and {d_state, d_inner, n_seqs}
245-
return ggml_ssm_scan(ctx, ssm, x, dt, A, B, C, ids);
245+
return ggml_ssm_scan(ctx, ssm, x, dt, A, B, C, ids, /*K=*/1);
246246
};
247247

248248
ggml_tensor* y_ssm = build_rs(inp, ssm_states_all, hparams.n_embd_s(), ubatch.n_seqs, get_ssm_rows);

examples/talk-llama/models/plamo2.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ ggml_tensor* llm_build_plamo2::build_plamo2_mamba_layer(llm_graph_input_rs* inp,
266266
// Custom operator to optimize the parallel associative scan
267267
// as described in the Annex D of the Mamba paper.
268268
// => {d_inner, n_seq_tokens, n_seqs} and {d_state, d_inner, n_seqs}
269-
return ggml_ssm_scan(ctx, ssm, x, dt, A, B, C, ids);
269+
return ggml_ssm_scan(ctx, ssm, x, dt, A, B, C, ids, /*K=*/1);
270270
};
271271

272272
ggml_tensor* y_ssm = build_rs(inp, ssm_states_all, hparams.n_embd_s(), ubatch.n_seqs, get_ssm_rows);

ggml

Submodule ggml updated 426 files

src/silero_lid.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ extern "C" struct silero_lid_context* silero_lid_init(const char* gguf_path, int
852852
}
853853

854854
if (!lid_load(ctx->model, gguf_path, ctx->backend, ctx->backend_cpu)) {
855-
delete ctx;
855+
silero_lid_free(ctx);
856856
return nullptr;
857857
}
858858

0 commit comments

Comments
 (0)