@@ -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.
1515The generated topic + per-model index is [`docs/LEARNINGS-INDEX.md`](docs/LEARNINGS-INDEX.md).
1616Find the lesson there, then jump with `grep -n "<heading text>" LEARNINGS.md`.
1717Regenerate both with `python tools/gen-learnings-index.py` after adding a section.
@@ -18833,3 +18833,59 @@ and fail at zero.
1883318833Transferable: any decoder that resamples, upsamples, or expands attacker-
1883418834controlled input needs an amplification bound, and `|| true` on a `cp` of
1883518835required 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.
0 commit comments