perf(cuda): head_dim-128 arm for the warp-split-KV decode kernel, gated OFF - #383
perf(cuda): head_dim-128 arm for the warp-split-KV decode kernel, gated OFF#383filipsajdak wants to merge 1 commit into
Conversation
…ed OFF
PagedAttentionDecodeOptKernel is reachable only at `d == 32 * kDecEpl` (256),
so head_dim 128 -- what Qwen3-dense, Llama and Mistral actually use -- falls to
the generic block kernel. This adds the EPL=4 instantiation behind
VT_ATTN_DECODE_D128, DEFAULT OFF.
The kernel is templated on elements-per-lane. EPL == 8 forwards through
LoadRowN<8> to the existing LoadRow8, so the d256 path keeps the same
transactions, the same order and the same accumulation -- the d256 launcher
still instantiates the default EPL and is byte-for-byte what it was. EPL == 4
adds one 64-bit load per lane for bf16 and one 128-bit for f32; 32 lanes * 4
elems == 128, so a warp still covers exactly one head-dim row.
No GQA carve-out: PagedAttentionDecodeGqaKernel sits inside the d == 256 branch,
so at head_dim 128 it can never run, and excluding qpk == kDecGqaQG would strand
exactly those models (e.g. Qwen3-32B) on the block kernel this arm replaces.
Gated OFF because it is correctness-complete but NOT byte-exact against the
block kernel it replaces: the two reduce the KV sequence in a different ORDER
(warp-strided online softmax vs the block kernel's per-tile loop), so a greedy
anchor can move at an exact bf16 tie. OFF keeps every existing golden
byte-identical. This mirrors how the FA2 decode GQA group-swap landed --
correctness-complete and gated OFF -- before it was flipped ON against the full
gate. The flip is a separate change and owes the near-tie razor, the
distributional gate, and regen under the ratified-tie rule.
The env var is also the same-binary A/B your perf protocol requires: one binary,
VT_ATTN_DECODE_D128=0/1, no rebuild between arms.
EVIDENCE (sm_110 / Jetson AGX Thor, Qwen3-1.7B-NVFP4A16, W4A16 Marlin build):
Kernel numerics, EPL=4 vs the generic block kernel, 8 context lengths
straddling the 32-token page boundary, 2048 elements each:
ctx max_abs max_rel bitwise%
1 0 0 100.0000
31 0 0 100.0000
32 0 0 100.0000
33 0 0 100.0000
64 0 0 100.0000
100 3.725e-09 0 99.9512
685 0 0 100.0000
1000 6.104e-05 5.076e-03 99.9512
Six of eight are bit-for-bit identical. The worst case is 0.5% relative on
small-magnitude outputs at ctx 1000 -- named plainly because that is the
magnitude at which a near-tie flips.
Comparator self-test: seeding 1-ULP corruption into 3 of the 2048 elements of
the ctx=31 case (offsets 5, 1024, 2047) moves that row from 100.0000% to
99.8535% bitwise with max_abs 4.883e-04, and no other row changes. So the
comparison is demonstrably able to fail and localizes correctly. This is a
COMPARATOR self-test, not a kernel negative control.
Serving A/B, same harness / model / config, c=1,2,4,8, 128 tokens, temp 0:
OFF 81.630 / 162.738 / 316.255 / 477.070 tok/s
ON 131.023 / 258.953 / 508.139 / 936.048 tok/s
ratio 1.61x / 1.59x / 1.61x / 1.96x
Build: the single TU compiles clean on the device (nvcc, sm_110, Release,
`ninja CMakeFiles/vllm.dir/src/vt/cuda/cuda_paged_attn.cu.o`, exit 0).
LIMITATIONS, stated because they bound what the numbers above prove:
- ONE model, ONE head_dim, ONE arch. Not measured anywhere but sm_110.
- The serving A/B is a SINGLE repetition per point (~1 s per point) and was
taken across two builds, not through the new flag.
- No model-level token-exact gate was run against this arm.
- LoadRowN<4, float> and the windowed (HasWindow=true) EPL=4 instantiation
are compiled but unexercised by the evidence above.
- The GQA-fused kernel remains d256-only; this change does not widen it.
Refs mudler#382.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [ClaudeCode]
|
Closing the gap this PR named in its own Limitations: the model-level token-exact comparison has now been run, as a same-binary A/B. One binary, sm_110 / Jetson AGX Thor,
5 of 6 prompts are byte-identical, including the two longest contexts ( The one divergence, On a separate 400-token run of the same prompt, reading token ids out of What this does NOT show, and whyThe near-tie razor could not be run. I can point at where it degrades, though not at why. All 400 returned tokens are the literal string That narrows it to: the engine supplied a logprobs container whose per-position entries were empty, or did not contain the sampled token id. I have not determined why, and I am not calling it a defect -- it may be a sampler path or configuration I have not found. If there is a supported way to get top-k logprobs out of this server, I would like to know it, because that is what would let a contributor run your near-tie methodology without needing the oracle. The consequence for this PR is concrete: the flip at token 316 is consistent with a reduction-order near-tie -- 5/6 identical, the divergence appearing only deep in a long generation, the kernel-level numerics 6/8 contexts bit-identical -- but it has not been demonstrated to be one, and no logprob gap can be quoted. Distinguishing "benign near-tie" from "real numerical error" still needs either top-k logprobs or your oracle. Limitations, updated from measurementThe body's bullet "No model-level token-exact gate was run against this arm" is now superseded by the above. Two of the other limitations are also narrowed:
Nothing here changes the recommendation that this ships gated OFF. If anything it sharpens the reason: a default flip would move the greedy anchor on at least some prompts, so it owes the near-tie razor and a golden regen under the ratified-tie rule -- on your hardware, under your rules. The flag exists so that comparison is a same-binary A/B when you come to make it. Raw artifacts (per-prompt SHA256 both arms, full generated text, token id sequences, the run report) were written to disk this time and can be attached in whatever form is useful. |
|
One correction to the provenance of the token-exact numbers above, which I would rather state than have you ask for. The tree those two arms were built from was not a pristine checkout of Why the comparison is nevertheless valid: both arms are the same binary, selected by What it does mean: the SHA256 values are not reproducible against a clean Separately, and mentioned here only because it was found on the same tree: that build also failed every |
|
Superseded by #425, which lands this work on current Neither CI failure here was yours. This branch sat 46 commits behind, and both reds were stale-base:
Not one is
One thing I added, in a separate commit that is mine, not folded into yours: Nothing else changed. The lever stays default-OFF, and flipping it ON remains the separate change your commit message describes — owing the near-tie razor and the distributional gate. If #425 does not land for any reason, I will reopen this rather than leave the work stranded. |
Unblocks the cuda-fat-build red, which was never this branch's defect. The gencode audit failed on seven files, every one of them Marlin: src/vt/cuda/cuda_marlin_dense.cu: gencode ['110','120a','121a'] != expected ['120a','121a'] src/vt/cuda/cuda_moe_marlin.cu ... and five more marlin sources This branch touches ROCm and gemma4_moe.cpp; it does not touch Marlin gencode at all. That failure is the drift mudler#407 fixed -- "Marlin gencode drifted from the feature table" -- and this branch predates it. Verified: `git merge-base --is-ancestor c7384fa <tip>` was false before this merge, true after. The identical seven-file failure was diagnosed on mudler#383 an hour earlier, from the same cause and with the same fix, so this is a stale-base class rather than two coincidences. Gates on the merged tree: check-agent-record, check-doc-checkpoint, check-now-current and check-public-doc-tables all green. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…ated OFF (#382) Lands Filip Sajdak's work from PR #383 via #425. Merged --no-ff rather than squashed ON PURPOSE: a squash would re-author the commit to whoever pressed the button, and this is his change. `git log` keeps `author=Filip Sajdak` on the kernel commit. PagedAttentionDecodeOptKernel was reachable only at d == 32 * kDecEpl (256), so head_dim 128 -- what Qwen3-dense, Llama and Mistral actually use -- never reached it and fell to the generic block kernel. This adds the EPL=4 instantiation behind VT_ATTN_DECODE_D128, DEFAULT OFF. EPL == 8 still forwards through LoadRowN<8> to the existing LoadRow8, so the d256 path is byte-for-byte what it was. Gated OFF for an honest reason rather than caution: it is correctness-complete but NOT byte-exact against the block kernel, because the two reduce the KV sequence in a different ORDER, so a greedy anchor can move at an exact bf16 tie. Flipping it ON is a separate change owing the near-tie razor and the distributional gate, exactly as the FA2 decode GQA group-swap did in #48/#49. #383's CI red was never its own: the gencode audit failed on seven files, every one of them Marlin, while this change touches only cuda_paged_attn.cu. That was the drift #407 fixed, and the branch predated it by 46 commits. Its agent-record red was audit-live-rows reading transient record state on main; reproduced at the tip as rc=0 both before and after merging main. Carried in: issue #382 was never in the roadmap intake table, so only two of the three required links agreed. Filed under KERNEL-ATTN-PAGED, the row that anchors src/vt/cuda/cuda_paged_attn.cu. That commit is mine; the kernel commit is his and is untouched. Gate: scripts/agent-preflight.sh rc=0 on the merged tree. CI is queue-blocked (#274) -- every recent run is queued or cancelled -- so the operator's own gate run is the authority here, as AGENTS.md prescribes. The lever is default-OFF, so no golden moves. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…dK-WMMA (#41) Lands bakon11's work from PR #317, split out of #228. Merged --no-ff so the contributor commits keep their authorship rather than being re-authored to whoever pressed the button. FP8 resident MoE experts and a SharedK-WMMA prefill path for Gemma-4 on RDNA4 ROCm, plus a CPU seam test that pins the portable vt::ExpertGeGLUFp8* symbols link and the recipe env knobs parse inertly. The concern this PR was originally held on is CLOSED BY MEASUREMENT, not by reading the diff. It was held because ~1,600 added lines sat in gemma4.cpp and gemma4_moe.cpp and Gemma-4 is a gate model. bakon11 rebuilt it to drop the CUDA forward restructure entirely, and both arms were then gated on dgx GB10 in one flock window: `gmain` (main) and `g317` (main + this PR) each returned gemma4-E4B STRICT 32/32 token-exact vs the vLLM 0.25.0 golden. gemma4.cpp is byte-identical to main. Its cuda-fat-build red was never its own: the gencode audit failed on seven files, every one of them Marlin, while this branch touches ROCm and gemma4_moe.cpp and no Marlin gencode at all. That was the drift #407 fixed and the branch predated it -- the same stale-base class that also explained #383 and #393 tonight. ACCEPTED WITH A NAMED RESIDUAL, recorded because it is a product decision and not a gate result. This flips four knobs DEFAULT-ON for ROCm: VT_GEMMA4_PREFILL_PEER_ACT, VT_ATTN_PREFILL_FLASH_SHAREDK, VT_ATTN_PREFILL_SHAREDK_WMMA and VT_ROCM_GRAPH. Non-ROCm builds are unaffected -- every new lever is env-gated off or behind moe.enabled / ROCm ExpertGeGLU dispatch -- and each knob is individually reversible by env. But there is NO AMD CI runner, so those defaults rest entirely on the contributor's lab evidence (2x AMD Radeon AI PRO R9700 gfx1201 / RDNA4, ROCm 7.x, gemma-4-26B-A4B-it-fp8; prefill ~2k @11k, short decode ~45-52 t/s). Feature correctness is unprovable on GB10 and honestly so: all nine cached Gemma checkpoints there are dense bf16 with enable_moe_block false, so RunGemma4Moe, EnsureGemma4Fp8* and ExpertGeGLU* are simply unreachable. The GB10 gate proves NON-REGRESSION, never the feature. Gate: scripts/agent-preflight.sh rc=0 on the merged tree; check-agent-record, check-doc-checkpoint, check-public-doc-tables, check-now-current, check-env-doc and check-commit-trailers all green. CI is queue-blocked (#274), so the operator's own gate run is the authority, as AGENTS.md prescribes. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
PagedAttentionDecodeOptKernelis reachable only atd == 32 * kDecEpl(256), so head_dim 128 -- the width Qwen3-dense, Llama and Mistral actually use -- never reaches it and falls to the generic block kernel. This adds theEPL=4instantiation behindVT_ATTN_DECODE_D128, default OFF.Refs #382.
What changes
The decode-opt kernel is templated on elements-per-lane.
EPL == 8forwards throughLoadRowN<8>to the existingLoadRow8, so the d256 path keeps the same transactions, the same order and the same accumulation -- the d256 launcher still instantiates the defaultEPLand is byte-for-byte what it was.EPL == 4adds one 64-bit load per lane for bf16 and one 128-bit for f32;32 lanes * 4 elems == 128, so a warp still covers exactly one head-dim row.No GQA carve-out.
PagedAttentionDecodeGqaKernelsits inside thed == 32 * kDecEpl(256) branch, so at head_dim 128 it can never run; excludingqpk == kDecGqaQGwould strand exactly those models (e.g. Qwen3-32B,hq/num_kv_heads == 8) on the block kernel this arm exists to replace.Why it is gated OFF
It is correctness-complete but not byte-exact against the block kernel it replaces: the two reduce the KV sequence in a different order (warp-strided online softmax vs the block kernel's per-tile loop), so a greedy anchor can move at an exact bf16 tie. Default-OFF keeps every existing golden byte-identical.
This mirrors how the FA2 decode GQA group-swap landed --
perf(fa2): decode GQA group-swap port, gated OFF - correctness-complete(#48), flipped ON in #49 against the full gate. The flip here is a separate change and owes the near-tie razor, the distributional gate, and regen under the ratified-tie rule (dense_nvfp4_gemm.h:119-120). That decision is yours -- we cannot run your SACRED gates and are not asking you to take a golden shift on this evidence.The flag is also the same-binary A/B your perf protocol requires: one binary,
VT_ATTN_DECODE_D128=0/1, no rebuild between arms. We cannot run that comparison ourselves under our own operational rules (below), but it exists so you can produce the flip-ON evidence on your rig under your rules.Motivation is sharper on sm_110 than the general case
cmake/CudaArchFeatures.cmake:349readsfa2|8.0,8.6,8.7,8.9,12.0a,12.1a--11.0is absent, soVLLM_CPP_FLASH_ATTNis not defined for sm_110 and the whole FA2 block, including the d128fa2_decode_qwen3arm, compiles tofalse. On Thor every d128 decode therefore lands on the generic block kernel; there is no FA2 fallback underneath it.docs/BUILD.md:56-58describes this as attention falling back to "the portable path, which is slower. Nothing fails and no test goes red."Whether that
fa2cell should gain11.0is a separate question from this PR and we have deliberately not bundled it. We raise it only because it is why this arm matters more on sm_110 than the head_dim gate alone suggests.Evidence
sm_110 / Jetson AGX Thor, Qwen3-1.7B-NVFP4A16, W4A16 Marlin build (the one #326 enabled).
Kernel numerics,
EPL=4vs the generic block kernel, 8 context lengths straddling the 32-token page boundary, 2048 elements each:Six of eight context lengths are bit-for-bit identical. The worst case is 0.5% relative error on small-magnitude outputs at ctx 1000 -- stated in those terms rather than as "6.1e-05", because 0.5% is the magnitude at which a near-tie flips, which is the whole reason your ratified-tie rule exists.
Comparator self-test. Seeding 1-ULP corruption into 3 of the 2048 elements of the ctx=31 row (offsets 5, 1024, 2047 -- interior, midpoint, last element) moves that row from
100.0000%bitwise /max_abs 0to99.8535%/max_abs 4.883e-04, and no other row changes. So the comparison is demonstrably able to fail, detects a single-ULP deviation, and localizes correctly. To be precise about what this is: it is a comparator self-test, not a kernel negative control -- it proves the measuring instrument works, not that the kernel comparison could have caught a bad kernel.Serving A/B, same harness / model / config, c=1,2,4,8, 128 tokens, temp 0, 3 warmups:
Build. The changed TU compiles clean on the device:
ninja CMakeFiles/vllm.dir/src/vt/cuda/cuda_paged_attn.cu.o, nvcc, sm_110, Release, exit 0, no warnings.Provenance of the numbers above
The recorded tensors and tok/s come from an on-device experiment, not from this branch's binary. The generator script that produced that experiment has since been found defective -- it emits a self-referential default template argument (
int EPL = EPL) -- so the script does not reproduce what was built. The source tree that was actually compiled on the device does not contain that defect; it carries the correctint EPL = kDecEpl. We diffed this branch against that compiled tree: the only functional difference is theVT_ATTN_DECODE_D128gate itself. With the flag on, this branch is behaviourally the code that produced the measurements.We are flagging this rather than leaving it for you to find, because "the numbers came from a slightly different tree" is the kind of thing that should come from the submitter.
Limitations
Stated because they bound what the numbers above prove:
LoadRowN<4, float>and the windowed (HasWindow=true)EPL=4instantiation are compiled but unexercised by the evidence above.Gates
Rebased onto
dbd0d51cimmediately before submitting. Run withupstream/mainas base:scripts/agent-preflight.shwas run on this branch and on a pristineupstream/maincheckout as a baseline, because the first run's failures pointed at commits that were not ours. On a fork clone the committed-range gates resolveorigin/main, which is not yourmain, so they judge the wrong commits. Baseline fails 7; this branch fails those same 7 plus exactly one --check-role-discipline, see below. The delta is one gate, and it is a branch-naming question rather than a defect.VT_ATTN_DECODE_D128was added toscripts/env-doc-allowlist.txtnext toVT_ATTN_DECODE_OPT/VT_ATTN_DECODE_GQA, matching how the sibling kernel switches are classified;check-env-doc.pyfails without it.Two open questions
check-role-discipline.pyfails because this branch is notrow/<ID>: "repository change reached main without arriving on a task branch." Same question as build(cuda): enable marlin-nvfp4 for sm_110 (Jetson Thor) - 8.0x-29.0x faster NVFP4 W4A16 decode #326, which landed without a board row. Tell us how you want an outside contribution to satisfy this and we will redo it -- we did not want to claim an internal roadmap row unasked.Not done
No test was added.
VT_ATTN_DECODE_OPTappears in no test today, so there is no decode-dispatch coverage to extend, and a RED-first test for this arm needs a device gate we cannot run. Happy to write one against whatever harness you would accept.