opencoti-llamafile / docs /evaluations /decode-levers.md
ManniX-ITA's picture
Upload folder using huggingface_hub
5ef4cc5 verified
|
Raw
History Blame Contribute Delete
16.4 kB
# Decode-throughput lever map β€” turbo / turbo_tcq / scalar (A4B, RTX 3090)
**Source:** nsys per-kernel decode profiles (`.opencoti/nsys/lever_{scalar,turbo3,tcq}.nsys-rep`),
A4B-98e-v6-coder Q4_K_M, ctx 4096, n_predict 96, DSO `60b17cb1` (post-#535 vectorized K-read).
Cross-config absolute totals are NOT comparable (different #decode-steps captured); the comparable
views are **per-phase % of that config's own total** and **per-call avg Β΅s**.
> **SCOPE (WS2 / #629).** Every "turbo/TCQ costs decode vs scalar" number in this document is
> **noDCA, short-ctx (ctx 4096), f16-lift** β€” the regime where the centroid-unpack / codebook lookup
> is pure overhead. **With DCA-on at long ctx the picture inverts:** the WS2 in-register turbo-V kernel
> (DEFAULT-ON) makes turbo/TCQ-V a decode **WIN of +68–95% @256k** over the f16-lift ref, because it
> kills the context-growing whole-cache `cpy_turboN_f16` materialize that dominates the long-ctx step.
> The gaps below are the short-ctx floor, not the serving verdict β€” see
> [context.md Β§5x](context.md).
## Measured baseline (decode tps, A4B)
scalar q8_0 **68.7** Β· turbo3 **58.2** Β· turbo3_tcq **55.6**. Gap turbo3β†’scalar = βˆ’15% β€” noDCA /
short-ctx only; DCA-on long-ctx inverts (Β§5x, WS2 in-register turbo-V is +68–95%).
## Per-call FA-VEC K-read (config-independent)
| D | scalar q8_0 | turbo3 | turbo3_tcq |
|---|---|---|---|
| 256 (local layers) | 21.1 Β΅s | 63.0 Β΅s (3.0Γ—) | 49.1 Β΅s (2.3Γ—) |
| 512 (global layers) | β€” | 270.4 Β΅s | 210.6 Β΅s |
`mul_mat_vec_q` experts = 17.8 Β΅s/call for ALL THREE (KV-type-independent β€” confirms FFN is shared).
## Phase fractions (% of each config's decode kernel time)
| Phase | scalar | turbo3 | tcq |
|---|---|---|---|
| FFN/MoE experts (mul_mat_vec_q) | 54.3 | 44.4 | 41.6 |
| NORM/softmax/act/elementwise | 15.9 | 13.0 | 12.2 |
| Turbo/TCQ K-read (FA-VEC) | (4.5 scalar) | 20.5 | 15.0 |
| TCQ KV-write (Viterbi enc/set_rows) | 2.5 | 2.3 | **13.6** |
| KVwrite cpy/quantize | 4.6 | 3.7 | 3.5 |
| WHT rotate (turbo) | 2.0 | 2.9 | 2.7 |
| embed/get_rows/argsort | 3.7 | 3.0 | 2.8 |
| ROPE | 1.1 | 0.9 | 0.9 |
| stream-k fixup/combine | 0.9 | 0.6 | 0.5 |
| FFN other matmul | 3.2 | 3.9 | 3.0 |
| OTHER (launch slack etc.) | 7.2 | 4.8 | 4.2 |
## LEVERS by phase β€” ranked by (impact Γ— tractability)
### L1 β€” SPECULATIVE DECODE (cross-cutting MULTIPLIER) ⭐ biggest
MTP draft head already ships 1.46Γ— on f16. If turbo/tcq COMPOSE with MTP, that ~1.4Γ— multiplies the
WHOLE decode β€” bigger than any single-kernel tweak. STATUS: unverified for turbo/tcq KV. Effort: gate-only
(no kernel) if it already composes; medium if KV-write-on-accept needs work. **Verify first.**
### L2 β€” FFN/MoE experts (mul_mat_vec_q) β€” 42–54%, SHARED, biggest absolute
17.8 Β΅s/call, dominates ALL configs. Improving it raises turbo AND scalar (doesn't close the gap, but the
user's goal is "improve turbo" in absolute terms). Levers: (a) fused up+gate op (GGML_OP_MOE_FUSED_UP_GATE
exists β€” verify it's ON the A4B decode path, not just prefill); (b) mmvq nrows batching across the 2 active
experts; (c) ik_llama fused-MoE (#292 OOAE) decode path. Effort: medium-high. Risk: numerics (cosine gate).
**(a) MEASURED 2026-06-23 β€” INAPPLICABLE TO A4B (op never engages); do NOT default-flip.** `--fused-moe-up-gate`
is a structural **no-op on Gemma A4B**: the boot `-v` log shows its experts load as a single combined
**`blk.N.ffn_gate_up_exps.weight`** tensor (up and gate projections already share one matmul). The graph hook
fires the fused op only when `fused_moe_up_gate && gate_exps && !gate_up_exps && n_tokens==1 && (GELU|SILU)`;
A4B *has* `gate_up_exps`, so `!gate_up_exps` is false β†’ the op is never inserted, flipping the flag leaves the
graph identical (bit-identical output). **2Γ—2 proof {graphs on/off}Γ—{fmug on/off}, A4B Q4_K_M decode tok/s:**
| | fmug=OFF | fmug=ON | fused-gain |
|---|---|---|---|
| graphs ON | 92.70 | 92.45 | βˆ’0.3% |
| graphs OFF | 78.25 | 78.33 | +0.1% |
Fused-gain β‰ˆ 0 in **both** rows β‡’ this is NOT subsumption-by-graphs (that would leave the graphs-OFF gain
visible); it's **the op never firing** because the experts are pre-fused. (Graphs reconfirmed orthogonally:
+18.5% at fmug-off.) So L2(a) is only meaningful for GGUFs with **separate** `ffn_gate_exps`+`ffn_up_exps`
experts β€” check the target model's tensor names first. For pre-fused-expert models like A4B, the up/gate fusion
is already baked into the weights. What remains in L2 is the *matmul itself* (b/c) β€” a genuine compute-bound
win, but medium-high effort with numeric risk; deferred unless the FFN matmul becomes the proven bottleneck
post-L1/L6. NOTE: on a model with separate experts AND graphs on, expect fused-MoE to be subsumed (graphs
already kill its launch saving) β€” the residual would only be the saved intermediate-buffer traffic.
**(a) CONTRAST β€” Qwen DOES have separate experts, op fires.** Qwen3.6-35B-A3B (qwen35moe, 256 experts) loads
**separate** `ffn_gate_exps`+`ffn_up_exps` (vs A4B's combined `ffn_gate_up_exps`), so the fused op engages.
2Γ—2 {graphs}Γ—{fmug}, f16 decode tok/s:
| | fmug=OFF | fmug=ON | fused-gain |
|---|---|---|---|
| graphs ON | 89.17 | 91.31 | **+2.4%** |
| graphs OFF | 76.97 | 76.26 | βˆ’0.9% (noise) |
So fused-MoE is **model-dependent**: inert on A4B (pre-fused weights), a small real win on Qwen-style separate-
expert MoEs β€” and, interestingly, the gain shows up *with graphs ON* (+2.4%), not off (the fewer graph nodes
help replay; off-graph it's neutral/noise). **CLOSED 2026-06-24 (greedy-identity gate):** on Qwen3.6-35B-A3B
(`qwen35moe`, 256 experts, separate `ffn_gate/up_exps`) fused-MoE off-vs-on is **greedy BIT-IDENTICAL** at
temp=0 for BOTH f16 and turbo3_tcq KV (764B==764B, 661B==661B) β€” numerically safe. But under the SHIPPING
config (graphs ON) the re-measured gain is only **+0.7–0.9%**, not +2.4%: the +2.4% was a graphs-OFF baseline
artifact (graphs already amortize the launch overhead fused-MoE targets, leaving a sub-1% residual once graphs
are on). **Verdict: safe but sub-1% under the shipping config β€” NOT worth a default-flip + the validated-model-
surface expansion; the flag stays opt-in (default OFF).** fused-MoE is a minor, situational lever, NOT a
headline like L6/L1.
**(b) "mmvq nrows-batch across active experts" β€” ALREADY IMPLEMENTED (nothing to build), 2026-06-23.**
`MMVQ_MAX_BATCH_SIZE = 8` (`mmvq.cuh:3`) and both targets use `n_expert_used = 8`, so `ne2=8 ≀ 8` takes the
batched MUL_MAT_ID path at `ggml-cuda.cu:2677-2681` β†’ `ggml_cuda_mul_mat_vec_q(src0, src1, ids, dst)`: one
launch with all 8 active experts mapped onto the grid's `nchannels_dst` axis (`mmvq.cu:673`, grid
`(nblocks, nchannels_dst, ntokens)`). There is no per-expert-launch loop to collapse. (Only `>8`-expert-used
models fall to the slower MMQ path β€” not our targets.) Reduces no bytes-read; moot.
**(c) "ik_llama OOAE + fused-MoE" β€” fused-MoE shipped (0042), OOAE irrelevant to the matmul, 2026-06-23.**
The fused up+gate op is already the `0042` path (`ggml-cuda.cu:3030`). ik_llama's other piece is `topk-moe`
(fused softmax+top-k expert *selection*); our tree uses standard `argsort_top_k`. topk-moe speeds *selection*,
not the expert matmul, and reads the same expert weights β€” orthogonal to the 42–54% FFN cost. (#292 was logged
"OOAE + fused-MoE pair" but only the fused-MoE half landed; the topk-moe half is absent and would not move
decode tps.)
**ROOT REASON b AND c ARE DEAD ENDS (verified):** the expert matmul is a GEMV over quantized weights β€”
arithmetic intensity β‰ˆ3.5 FLOP/byte β‰ͺ the RTX 3090 roofline ridge (~37) β‡’ hard **bandwidth-bound** on
*reading the expert weights*. Neither b nor c reduces those bytes. The only levers that attack a bandwidth-
bound weight read: **L1/MTP** (amortizes each weight read across K draft tokens per verify step β€” the proven
+55%) or **lower-bit experts** (quant choice). A faster GEMV *kernel* would help only if mmvq leaves bandwidth
on the table β€” a separate #397-class effort to be justified by an `ncu` DRAM-throughput measurement first, NOT
b/c. **Conclusion: L2(b)/(c) require no work; closed.**
**#397 GEMV-kernel verdict β€” MEASURED (ncu, 2026-06-23, A4B Q4_K_M decode):** the time-dominant expert
`mul_mat_vec_q<12,…>` launches run at **77–81 % of the 3090's peak DRAM bandwidth** (time-weighted 61.6 %
across all decode mmvq launches; SM ~41 %). The heavy kernels are already near the memory-bound practical
ceiling (~85–90 %), so a faster GEMV buys at most ~1.1–1.25Γ— on the dominant slice and less overall.
**#397 (int8-dp4a turbo KQ / faster-GEMV) is NOT worth pursuing for decode tps** β€” the real levers remain
L1/MTP and lower-bit experts, which cut *bytes read*, not read *speed*. Method (reusable): the cosmopolitan
APE binary can't be `execve`'d by ncu/nsys directly ("Unknown executable format") β€” copy it and
`./copy --assimilate` to a native ELF (cosmo runtime intercepts the flag pre-main), then
`ncu --kernel-name regex:mul_mat_vec --launch-skip N --launch-count M --metrics
gpu__dram_throughput.avg.pct_of_peak_sustained_elapsed,…` (set `GGML_CUDA_DISABLE_GRAPHS=1` so each launch is
individually filterable; `mul_mat_vec_q` is decode-only β€” prefill uses the batched `mul_mat` path).
### L3 β€” TCQ decode-time Viterbi encode/set_rows β€” 13.6% of tcq, TCQ-SPECIFIC ⭐ β€” (a) DONE (#541); (b) REJECTED (#545); (c) UNATTEMPTED
The trellis encode runs on EVERY decode KV-write. 13.6% of tcq decode = the single biggest tcq-vs-turbo3
penalty. Levers: (a) **codebook-hoist in the ENCODE kernel β€” DONE (#541, DSO b4ccda34)**: hoisted
`d_turbo3_tcq_codebook[16*lane+i]` out of the 128-step Viterbi DP into a per-lane `cb[16]` register array
(turbo2: `cb[8]`). Byte-identical CSE (encode REG:60/STACK:0, occupancy 3/SM held). Result **turbo3_tcq decode
59.7 β†’ 60.4 (+1.2%)**; scalar 87.6 / turbo3 76.2 unchanged. 27B KLD healthy on both edited kernels
(turbo3_tcq 0.034606 sametop 95.5%, turbo2_tcq 0.069940 PASS sametop 92.6%). VERDICT: the codebook reads were
NOT the encode bottleneck β€” the 128-step serial DP is β€” so the hoist only buys ~1%. The big tcq win is (b).
(b) cheaper 1-token decode-time encode (the Viterbi DP is NOT amortizable to O(8): the 128-step trellis spans
the 128 HEAD-DIM elements of the single new token's vector, all new each step; the candidate lever was a
greedy walk over the 128 element-steps, O(128Β·8) vs O(128Β·512)). **REJECTED 2026-06-24 (#545, P1 spike,
flag-gated greedy encode on DSO 9403e25f, then reverted to L3 byte-exact).** Same-DSO 27B fidelity-KLD:
**greedy-tcq 1.0575 (sametop 61.6%)** vs Viterbi-tcq 0.0346 (95.5%) vs plain turbo3 0.0470 (94.7%). Greedy
does not merely regress toward turbo3 β€” it **collapses ~30Γ— past Viterbi and ~23Γ— past plain turbo3**: the
512-entry codebook is jointly trained for the Viterbi-optimal paths, so a greedy walk lands on mismatched
centroids and each locally-best symbol paints the next step into a corner. **The trellis's MSE-optimal
noise-shaping is irreducible** β€” the ~8% decode-time encode cost is the *price* of tcq's quality edge, not a
removable inefficiency. Since plain turbo3 is both faster AND (at 0.047) far better than greedy-tcq (1.057),
the answer where greedy was the goal is simply "ship turbo3". (c) skip re-encode via an f16 ring + lazy
batched TCQ-encode β€” UNATTEMPTED; the only remaining idea, but it changes write semantics (a token's KV is
written f16 then re-encoded in a later batch), so it is a structural rework gated on the same KLD bar, not a
quick win. Effort: high. Risk: quality (KLD gate) + correctness (deferred-encode state IO).
**L3 round verdict (closes the decode-tps investigation):** the only banked win is L3(a) +1.2% on tcq. The
floor is real and now evidence-backed on both axes β€” turbo3 vs scalar q8_0 β‰ˆ βˆ’10% (centroid unpack is a
Shiftβ†’Maskβ†’LUTβ†’Scale dependency chain vs `int8*scale`), turbo3_tcq vs turbo3 β‰ˆ βˆ’8% to βˆ’13% (512-entry LUT +
9-bit unaligned extract). No free lever remains; further tcq speedup requires either format change (smaller
LUT β†’ retrain, P2, quality-gated) or the (c) deferred-encode rework. **β€” noDCA / short-ctx only; with DCA-on
at long ctx the WS2 in-register turbo-V path is a decode WIN (+68–95%), see [context.md Β§5x](context.md).**
### L4 β€” NORM/softmax/elementwise β€” 12–16%, SHARED, many small kernels
rms_norm + soft_max + scale/add/residual = lots of tiny launch-bound kernels. Lever: fuse (norm+scale,
residual-add into the matmul epilogue, RoPE+norm). Also a CUDA-graph candidate. Effort: medium. Risk: low
(byte-identical-ish).
### L5 β€” Turbo K-read FA-VEC β€” 20.5% turbo3 / 15% tcq, TURBO-SPECIFIC β€” βœ… DONE (#535b, 2026-06-23)
Vectorized loads (#535) took D256 70β†’63 Β΅s (+1.6%). Then **#535b smem-codebook port** (DSO 47b0bca5): the
plain turbo K-read/V-dequant indexed FATTN_TURBO{2,3,4}_CENTROIDS in `__constant__`, which serializes the
data-dependent per-element lookups up to 32-way/warp; the TCQ tiers already staged to per-block smem (#534).
Porting that staging to turbo2/3/4 is BYTE-IDENTICAL and gave:
- **D256 turbo3 K-read 63.0 β†’ 48.4 Β΅s (βˆ’23%)** β€” now *beats* turbo3_tcq's 49.1 (8-entry LUT < 512-entry codebook).
- **D512 turbo3 K-read 270.4 β†’ 135.2 Β΅s (βˆ’50%)**.
- **decode tps: turbo3 58.2 β†’ 62.08 (+6.7%)**, turbo4 β†’ 63.5, turbo2 β†’ 66.5; scalar 68.96 unchanged.
- KLD turbo3β€–f16 = 0.349646 == the #535 value (byte-identical); turbo4 0.109 sane.
- turbo2 quality stays bug-583 K-side degenerate (in-register 2-bit; pre-existing, reproduced byte-identically:
turbo2K/f16V=7.39, f16K/turbo2V=0.043) β€” use turbo2 via DCA dequant-on-lift, never this in-register reader.
LESSON β†’ cerebrum 2026-06-23: small data-dependent LUTs in hot decode loops belong in smem, not __constant__.
### L6 β€” CUDA graphs β€” βœ… DONE (#533/#539, 2026-06-23, ships as 0084-cuda-graphs)
Decode launches ~hundreds of tiny kernels/token; at 58 tps each token is ~17 ms but GPU SM throughput is
~4.5% (latency/launch-bound). **Root cause found: CUDA graph capture/replay was COMPILED OUT of the
llamafile build** β€” `USE_CUDA_GRAPH` is gated `#if defined(GGML_CUDA_USE_GRAPHS)` (common.cuh:1218) and the
llamafile `cuda.sh` never defined it (upstream CMake does, by default). So `#ifdef USE_CUDA_GRAPH`
(ggml-cuda.cu:3318/4478/4515/4593/4698) was absent and every token paid full per-launch CPU overhead.
**Fix = one build flag** `-DGGML_CUDA_USE_GRAPHS` (0084; `OPENCOTI_NO_CUDA_GRAPHS=1` to omit). MoE does NOT
disable it (the MUL_MAT_ID quantized-expert path stays graph-compatible; NEO head-split force-disables to
eager). **Measured (A4B Q4_K_M, airtight same-DSO `GGML_CUDA_DISABLE_GRAPHS` toggle):**
- **f16 decode 77.4 β†’ 92.4 (+19.4%)**, **turbo3_tcq 53.6 β†’ 60.6 (+13.1%)** β€” shared, lifts every KV tier.
- **Stacks with MTP** (L1): turbo3_tcq graphs+MTP β‰ˆ **89 tok/s** (+69% over the 52.9 base; exceeds plain f16
77.7, ~75% of f16-with-same-levers 119.7).
- **Numerically inert** β€” greedy bit-identical graphs-on vs -off across f16/turbo3/turbo3_tcq/q8_0/q4_0 + 8K
ctx (graphs replay the identical kernel sequence; not a compute change).
- **GENERALIZES to Qwen (model-agnostic).** Qwen3.6-35B-A3B (qwen35moe, 256 experts) decode graphs on vs off:
**f16 76.97β†’89.17 (+15.9%)**, **turbo3_tcq 68.52β†’78.49 (+14.6%)** β€” same magnitude as Gemma. The lever is
not arch-specific; it lifts every model's latency-bound decode. (turbo3_tcq also confirmed working on Qwen
head_dim 128.)
LESSON β†’ cerebrum 2026-06-23: opencoti inherited llamafile's `cuda.sh`, which omits upstream-default build
defines β€” audit `cuda.sh` vs upstream CMake `GGML_CUDA_*` options for other free wins (graphs were one).
### L7 β€” WHT rotate (turbo) β€” 2–3%, turbo-specific, small
k_turbo_wht runs per KV-write. Minor. Fuse into set_rows. Low priority.
## Recommended sequence (phase-by-phase)
1. **L1 verify** turbo/tcq Γ— MTP composition (cheap, possibly huge).
2. **L3** TCQ decode-encode smem/cheap-encode (closes tcq's 13.6% β€” the clearest tcq win).
3. **L5b** V-read vectorization (cheap byte-identical, finishes the turbo K/V read).
4. **L2** FFN/MoE fused-up-gate verify + decode-path (biggest absolute, shared).
5. **L4 / L6** norm fusion + CUDA graphs (shared launch-bound reduction).