Last updated: 2026-05-12 (post-q8_0-warp1 session). Read this first.
| Metric | Value |
|---|---|
| Active model | fixed-imatrix-b0c3326/...-chat-v2-imatrix.gguf (via ./ds4flash.gguf symlink — still needs manual swap, see below) |
| Baseline gen tok/s @ ctx 7047 | 15.16 (no MTP, all session optimizations on) |
| Pre-session-1 baseline (all this-day's flags disabled) | 13.66 |
| Two-session cumulative delta | +1.50 tps, -6.93 ms/token total |
--logprob-vectors against local.vec |
OK |
--bench-mtp-spec-source --bench-exact-replay-source --cuda-indexed-decode-heads8-source --decode-profile-source |
OK |
| Source state | committed and pushed; q8_0 warp1 wins at 1315d31, F16 vec8 + output fusion at 7fed170 |
Investigative goal: pattern-match the F16 vec8 win onto remaining q8_0 GEMVs. Outcome: the F16 pattern was falsified for q8_0 (34-byte block layout blocks 16-byte vector loads), but a different lever — CTA-parallelism — paid off.
Two new templated kernels:
matmul_q8_0_pair_preq_warp1_quad_kernel<128>— for shared_gate_up and attn_q_a/kv pair. Splits each row to its own CTA with 8 warps cooperating (4 per output of the pair).matmul_q8_0_hc_expand_preq_warp1_kernel<BLOCKS, WARPS_PER_OUT>—<64, 2>for shared_down,<256, 8>for attn_output_b.
CTA count went from ~256-512 → 1024-4096 across affected shapes. Achieved bandwidth on shared_gate_up went from 131 → ~150 GB/s.
Bench: 15.00 → 15.16 tps (+0.16). Per-substage:
- attention: 33.85 → 32.62 ms (-1.23 ms)
- shared_ffn: 8.79 → 8.51 ms (-0.28 ms)
- total: 67.07 → 66.24 ms (-0.83 ms)
Disable: DS4_CUDA_Q8_PAIR_NO_WARP1_QUAD=1,
DS4_CUDA_Q8_HC_EXPAND_NO_WARP1=1.
- Naive uint4 vectorization on q8_0 weights — falsified. The 2-byte scale at offset 0 of each 34-byte block breaks 16-byte alignment; the dp4a inner loop already reads 4 bytes at a time.
- Template-unroll-only on the pair kernel — null result, reverted.
- Step 5 (compressor_update kernel review) — fragmented; ~2.2 ms of uninstrumented compressor stage is split across 86 small launches/token. No single lever ≥ 0.3 ms. Realistic path is fusion or CUDA Graphs.
Two evidence-backed kernel-level wins. Full breakdown in
docs/hardware/gb10-cuda-notes.md under
"F16 GEMV vectorization wins (2026-05-12, post-q_a/kv-pair-fusion)".
grouped_q8_0_a_preq_to_q8_kernel + ds4_gpu_attention_output_q8_fused_hc_tensor
write low directly as Q8_0 layout, skipping one
quantize_q8_0_f32_kernel launch and the float roundtrip between
attn_output_a and attn_output_b. First design (8 warps × 4 outputs/warp)
regressed because it cut CTA count to 1/4 and added intra-warp sequential
work; final design (1024 threads/CTA, 32 warps × 1 output each) preserves
the original per-warp parallelism. Win: +0.07 tps, -0.33 ms output_proj.
Disable: DS4_CUDA_DISABLE_OUT_AB_FUSE=1.
matmul_f16_pair_ordered_chunks_kernel and
matmul_f16_ordered_chunks_kernel were doing scalar __half2float(wr[i])
reads with a shared-memory + serial-32 reduction; both were at ~44% of
GB10's sequential-read ceiling. Added
matmul_f16_pair_warp_vec8_kernel and matmul_f16_warp_vec8_kernel:
uint4 (16-byte) weight loads = 8 halves per LSU op, __half22float2
unpack, warp_sum_f32 (warp-shuffle) reduction.
Bench delta: compressor 5.51 → 2.94 ms (~211 GB/s, 86% of peak); also
helps attn_hc_pre and shared_ffn because the ordered_router branch
covers every single-token F16 GEMV in the decode loop. Total:
+1.27 tps, -5.48 ms total_ms/token.
Disable: DS4_CUDA_NO_F16_PAIR_VEC8=1, DS4_CUDA_NO_F16_VEC8=1.
Prior handoff established a "3-5× optimism bias on kernel-side estimates" warning based on MoE and q8_0 fusion data. That was real for those kernels, but it was conditioned on specific evidence sets, not a universal multiplier. F16 GEMV vectorization went the other way: estimate "F16 already well-tuned, no notebook entry" — actual +1.3 tps. Each new probe should set its own theoretical floor before measuring; don't apply a session-old optimism multiplier to a different kernel family.
Scoping showed GPU is compute-bound (~67 ms/token GPU vs ~5 ms CPU enqueue,
fully overlapped on the spin-scheduled path). Estimated CUDA-Graph gain
0.1-0.3 tps for a real structural refactor. Deferred. Concrete entry
point for the next session is in gb10-cuda-notes.md "Step 3" — start
with a GPU-idle-window probe before committing to the graph capture.
Prior plan claimed Q4_K vectorization would unlock 16–19 tok/s.
Q4_K kernel was vectorized with dp4a (see ds4_cuda.cu::dev_dot_q4_32
and dev_dot_q4_K_q8_K_block). Correctness preserved, α=0.571 unchanged.
The plan's math was wrong. Profiling reveals MTP can't reach 20 tps
on this architecture — see below.
Measured MTP cycle (steady-state, 64 tokens, draft=2, strict, with vectorized Q4_K):
| Cycle step | Cost | Notes |
|---|---|---|
Vanilla decode of accepted token (in ds4_session_eval) |
~74 ms | unavoidable |
MTP draft inside ds4_session_eval line 17463 |
~21 ms | sets up next-cycle drafts[0] |
| MTP draft in spec for-loop line 17616 | ~21 ms | produces drafts[1] |
decode2_exact verify (2 positions) |
~165 ms | runs both positions SERIALLY per layer (line 13733-13770), no weight sharing |
| Snapshot / restore / misc | ~5 ms | KV-state copies |
| Per-cycle total | ~286 ms for max 2 tokens = ~7 tps theoretical best | |
| Measured | 3.11 tps (extra time from reject-fallback decodes) |
DS4_MTP_BATCH_VERIFY=1 (uses metal_graph_verify_suffix_tops via the
prefill batch-layer path) gave only 143 ms vs 165 ms — 13% improvement,
not the ~50% one would expect from true weight sharing. So even the
nominally-batched path isn't really weight-sharing across the 2 rows in
the dimensions where it could.
Theoretical ceiling, even with a true weight-sharing batch verifier and α=1.0: ~12 tps. MoE (23% of decode cost) doesn't batch-share — different experts per token. That hard limit makes MTP at draft=2 a poor fit for this model on this hardware regardless of further drafter/Q4_K work.
Decode breakdown after this session's wins
(DS4_CUDA_ATTENTION_EVENT_PROFILE=1 DS4_CUDA_DECODE_EVENT_PROFILE=1,
ctx 7047, 128 gen-tokens, fixed-imatrix model):
| Stage | ms/token (post-vec8) | Δ from prev | % of decode |
|---|---|---|---|
| Attention (q_b + out_a/b + main kernel + hc_pre) | 33.2 | -1.5 | 50% |
| MoE | 16.1 | -0.6 | 24% |
| Compressor + indexer | 6.5 | -4.5 | 10% |
| Shared FFN | 8.8 | -0.8 | 13% |
| Output logits | 2.6 | 0.0 | 4% |
| Total | 67.07 | -6.09 |
Compressor stage shrunk dramatically; q_proj and the attention kernel are now the dominant non-MoE substages.
Next session candidates, ordered by EV:
- q8_0 GEMV vectorization audit (1 session, est +0.0-0.3 tps) —
Check whether
matmul_q8_0_preq_warp8_kerneland the_hc_expand_family have the same scalar-load underutilization that F16 had. Most of the q8_0 path was hand-tuned for dp4a already, but some scalar loads survive. Target shapes:attn_q_b(q_rank=1024 → N_HEAD×HEAD_DIM=32768),attn_output_b(8192 → 4096),hc_ffn_fn(4096 → mix_hc). - Compressor_update kernel review (1 session, est +0.0-0.5 tps) — The compressor stage still has ~4.2 ms of uninstrumented work (APE + norm + EMA + FP8 quantize). Split it into substages first, then look for a kernel-level lever. Bias to "no win here" — these are compute-heavy not bandwidth-bound — but quick to confirm.
- GPU-idle-window probe → CUDA Graphs (1-2 sessions, est +0.1-0.3 tps) —
See
gb10-cuda-notes.md"Step 3" for the concrete first probe. Only commit to graphs if the host-side window measurement justifies it.
| Item | Effort | Expected tok/s | Notes |
|---|---|---|---|
| q8_0 GEMV vectorization audit | 1 session | +0.0–0.3 tps | Pattern-match the F16 win onto the remaining q8_0 GEMVs (attn_q_b, etc.) |
| Compressor_update substage split + review | 1 session | +0.0–0.5 tps | Uninstrumented ~4 ms in compressor stage |
| GPU-idle-window probe + (maybe) CUDA Graphs | 1-2 sessions | +0.1–0.3 tps | Verify gap before refactoring |
Drop redundant MTP draft inside ds4_session_eval |
4 hrs | +0.1 tps (only when MTP on) | Modest correctness risk; defer |
| Build a true weight-sharing batch verifier | weeks | unlocks MTP as a path | Strategic; not session-scale |
| Multi-fd cache (thorough fd-cache fix) | 2-4 hrs | ~0 in steady state | Code hygiene, ~3.8 GB host page-cache savings |
| Land MTP head bit-equivalence test | 2-3 hrs | None | Regression gate for future MTP work |
| File | Purpose |
|---|---|
docs/SESSION_HANDOFF.md |
This file. Start here. |
docs/mtp-fdcache-bug-report.md |
Shareable upstream bug report (root cause + fix + repro) |
docs/hardware/gb10-cuda-notes.md |
Durable notebook; all measurements + decisions across sessions |
tests/test-vectors/regen_local_vectors.py |
Regenerate local.vec for a new model (cloud-fixture-independent regression gate) |
tests/test-vectors/local.vec |
Local-model logprob fixture (replaces cloud official.vec when the imatrix changes) |
tests/test-vectors/README.md |
Updated with the local.vec workflow |
| File | Change | Purpose |
|---|---|---|
ds4_cuda.cu |
grouped_q8_0_a_preq_to_q8_kernel (1024-thread/CTA) |
Writes Q8_0 layout directly from grouped output_a; skips one prequant launch + the float low DRAM roundtrip |
ds4_cuda.cu |
ds4_gpu_attention_output_q8_fused_hc_tensor |
Host launcher that calls the fused kernel then the existing hc_expand_preq_warp8_unroll<256> directly |
ds4_cuda.cu |
matmul_f16_pair_warp_vec8_kernel + matmul_f16_warp_vec8_kernel |
uint4 8-half loads + __half22float2 + warp_sum_f32; replaces the scalar-load chunks kernels on in_dim % 8 == 0 |
ds4_cuda.cu |
Dispatch in ds4_gpu_matmul_f16_pair_tensor_impl + ds4_gpu_matmul_f16_tensor (ordered_router branch) |
Default-on selection of the vec8 variants |
ds4_gpu.h |
ds4_gpu_attention_output_q8_fused_hc_tensor prototype |
New extern entry |
ds4.c |
metal_graph_disable_out_ab_fuse() + call-site switch in metal_graph_encode_decode_layer |
Wires the fused output path into the production decode flow |
| File | Change | Purpose |
|---|---|---|
ds4_cuda.cu |
cudaSetDeviceFlags(cudaDeviceScheduleSpin) in ds4_gpu_init |
Match llama.cpp Blackwell decode tuning; flag stays even though it's neutral on GB10 today |
ds4_cuda.cu |
moe_gate_up_mid_decode_lut_qwarp32_unroll_kernel<XQ_BLOCKS> + dispatch |
+0.7% from compile-time unroll of the iq2 inner loop; default on for xq_blocks=16 |
ds4_cuda.cu |
float2 reads in attention_indexed_mixed_kernel + attention_decode_mixed_kernel phase-3 fast path |
Tiny win on attention; always on |
ds4_cuda.cu |
cuda_block_q4_K + dev_q4_K_get_scale_min + dev_dot_q4_K_q8_K_block + dev_dot_q4_32 |
Q4_K decode primitives; dp4a-vectorized (8x __dp4a per sub-block, unrolled outer loop) |
ds4_cuda.cu |
moe_gate_up_mid_decode_q4K_qwarp32_kernel + moe_down_q4K_sum6_qwarp32_kernel |
Q4_K MoE kernel variants for MTP draft head |
ds4_cuda.cu |
routed_moe_launch accepts (gate=12, down=12) Q4_K |
Unblocks MTP routed MoE |
ds4_cuda.cu |
fd-cache host-base check in cuda_model_range_ptr_from_fd + g_model_fd_host_base |
The MTP α=0 root cause fix. See mtp-fdcache-bug-report.md. |
ds4_cuda.cu |
ds4_gpu_memfloor_bench + ds4_check_nan_kernel + ds4_gpu_check_nan |
Bench infra. Memfloor is permanent; NaN check is env-gated diagnostic. |
ds4.c |
NaN probe calls in MTP draft chain (env-gated by DS4_MTP_NAN_PROBE) |
Diagnostic; zero cost when off |
ds4.c |
DS4_MTP_VERIFY_LOG, DS4_MTP_CHAIN_LOG instrumentation |
Diagnostic; zero cost when off |
tests/ds4_test.c |
Source-hook assertions for new env flags + memfloor bench test | CI gate |
# Build
make ds4-bench ds4_test ds4
# Baseline (no MTP)
./ds4-bench --cuda -m ./ds4flash.gguf \
--prompt-file bench/promessi_sposi.txt \
--ctx-start 7047 --ctx-max 7047 --gen-tokens 128
# Correctness gate against local fixture
DS4_TEST_MODEL=./ds4flash.gguf \
DS4_TEST_VECTOR_FILE=tests/test-vectors/local.vec \
./ds4_test --logprob-vectors
# MTP α measurement (the breakthrough run)
DS4_MTP_STRICT=1 DS4_MTP_RESTORE_DRAFT_FRONTIER=1 \
./ds4-bench --cuda -m ./ds4flash.gguf \
--mtp gguf/DeepSeek-V4-Flash-MTP-Q4K-Q8_0-F32.gguf \
--mtp-draft 2 \
--prompt-file bench/promessi_sposi.txt \
--ctx-start 7047 --ctx-max 7047 --gen-tokens 16
# Expect: draft_accept_rate=0.571, avg_accepted_per_cycle=2.000
# All source-hook tests
./ds4_test --bench-mtp-spec-source --bench-exact-replay-source \
--cuda-indexed-decode-heads8-source --decode-profile-source
# Regenerate local fixture (if the model changes)
./tests/test-vectors/regen_local_vectors.py \
-m ./ds4flash.gguf \
-o tests/test-vectors/local.vecAll commands above assume ./ds4flash.gguf is symlinked to the fixed-imatrix
model. If download_model.sh left it pointing at the older chat-v2.gguf,
repoint it once:
ln -sfn gguf/fixed-imatrix-b0c3326/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix.gguf \
./ds4flash.ggufUntil you do, every command above that uses ./ds4flash.gguf
points at the old May-9 model.
| Flag | Purpose |
|---|---|
DS4_CUDA_DISABLE_OUT_AB_FUSE=1 |
Disable the fused output_a/output_b/HC-expand path; restores the two-call sequence |
DS4_CUDA_NO_F16_PAIR_VEC8=1 |
Force the scalar matmul_f16_pair_ordered_chunks_kernel instead of the new _warp_vec8_kernel |
DS4_CUDA_NO_F16_VEC8=1 |
Same, but for the single-output F16 GEMV (ordered_router branch) |
| Flag | Purpose |
|---|---|
DS4_MTP_VERIFY_LOG=1 |
Per-cycle MTP gate/draft/verify state |
DS4_MTP_CHAIN_LOG=1 |
Per-step status inside metal_graph_eval_mtp_draft_from_hc, also logs routed_moe quant-type rejections |
DS4_MTP_NAN_PROBE=1 |
Host-side NaN scan of mtp_embed/mtp_enorm/mtp_eproj/etc; pinpoints the upstream NaN source if one returns |
DS4_MTP_FDCACHE_DBG=1 |
Trace fd-cache bypasses (verifies the host-base fix is firing for MTP weights) |
DS4_CUDA_DECODE_EVENT_PROFILE=1 |
CUDA event-based decode stage profile (low overhead) |
DS4_CUDA_MOE_EVENT_PROFILE=1 |
Aggregate MoE achieved GB/s per token |
DS4_CUDA_INDEXER_EVENT_PROFILE=1 |
Compressor + indexer sub-stage timing |
DS4_TEST_MEMFLOOR=1 |
Run the ./ds4_test --memfloor-bench microbench |
All zero-cost when off.
docs/hardware/gb10-cuda-notes.md is the durable log. Most relevant
sections:
- "Decode Bandwidth Floor (2026-05-11 investigation)" — first hardware bandwidth accounting
- "Decode baseline refresh" — current session's baseline numbers
- "CUDA event timing for decode stages" — Task 2 (host vs event profile)
- "MoE bandwidth event-profile microbench (Task 4)" — 115 GB/s achieved vs 245 GB/s sustained ceiling
- "Phase 1+++" — MoE iq2 unroll and attention float2 wins
- "MTP α=0 root cause (2026-05-11)" — the diagnostic trail that identified the fd-cache bug
- Don't redo the kernel BW analysis. Already in the notebook. The MoE iq2 and Q4_K kernels are the two with real headroom.
- Don't try heads8 attention at B=1. Already rejected; recorded in notebook with measurements.
- Don't try the H2 vectorization on compressor pair. Broke
--logprob-vectorsprecision; reverted. - Don't re-quantize the MTP head to escape Q4_K. The bug was not in Q4_K decode; it was in the fd-cache. Q4_K decode is correct.
- Don't chase further MTP speedups via Q4_K tile/batch tweaks. Q4_K is in the MTP head only (3.6 GB of 81 GB total weights). The MTP cycle bottleneck is the verifier doing 2 positions serially per layer, not Q4_K decode speed. See "MTP isn't the lever" above.
Predicted vs actual:
- "Output_a/b fusion ceiling 0.3-0.5 ms" — right (0.33 ms in output_proj substage). Calibration-note pessimism (3-5× optimism bias) did not apply here; the notebook's own number was on the nose.
- "F16 GEMV was already tuned" (implicit, no entry in notes) — wrong.
Scalar
__half2floatloads left both pair and single F16 kernels at 44% of peak bandwidth. uint4 vectorization recovered most of the gap. - "CUDA Graphs gives 1-3 ms" — untested but downgraded after scoping. Per-launch overhead is ~5 µs × ~1000 launches = ~5 ms CPU enqueue, but the GPU at 67 ms/token absorbs that easily. Realistic gain is whatever GPU-idle gaps exist between back-to-back short kernels; not yet measured.
- "Calibration note's 3-5× optimism bias is a universal warning" — partially wrong. It held for the kernels it was derived from (MoE, q8_0 fusion) but didn't apply to F16 GEMV. Per-probe theoretical floors beat carried-forward heuristics.
- "Q4_K decode bug" — wrong (turned out the math was correct; the upstream cache bug was the culprit)
- "MTP definitively closed" — wrong (it works at α=0.571 with the fd-cache fix)
- "+0.2-0.4 tok/s per kernel-only session" — was right then; this session blew through it (+1.34 tps)
- "EAGLE-class drafter required for speculation" — wrong premise; the MTP head drafter is healthy.
- "Q4_K vectorization unlocks 16-19 tok/s" — wrong (post-Q4_K-vectorize profiling shows the MTP cycle is verifier-bound, not draft-bound; effective_tps≈3.1, no improvement from Q4_K speed).
Net lesson: do the bandwidth+structure accounting per cycle before committing to a multi-day plan. Don't let one session's calibration note override per-probe theoretical floors in unrelated kernel families.