perf(rocm): head_dim=128 decode arm -- the ROCm half of #382, default OFF - #767
perf(rocm): head_dim=128 decode arm -- the ROCm half of #382, default OFF#767joral wants to merge 2 commits into
Conversation
…efault OFF FOLLOWING_AGENTS_PROTOCOL Implements .agents/specs/rocm-decode-attn-d128.md, committed ahead of this change. The ROCm mirror of PR mudler#425's CUDA arm for issue mudler#382. Every fast decode kernel in rocm_paged_attn.hip was gated to d == 256 || d == 512, so head_dim 128 -- Qwen3-dense, Llama and Mistral -- fell straight through to PagedAttnOnline. LoadRowEplBf16/StoreRowEplBf16 gain an EPL=4 (uint2) case, and both decode dispatch switches gain a d == 128 branch, so that geometry reaches PagedAttnDecodeGqaBf16 (qg=2, fused) or PagedAttnDecodeOptBf16T (per-head) instead. Gated VT_ATTN_DECODE_D128, DEFAULT OFF -- the same env var, default and reason as the merged CUDA arm, because the warp-strided online softmax reduces the KV sequence in a different ORDER than PagedAttnOnline, so a greedy anchor can move at an exact bf16 tie and OFF keeps every golden byte-identical. No allowlist entry is owed: the CUDA arm already added VT_ATTN_DECODE_D128. Testing: a new bf16 Qwen3-geometry (GQA 2, head_dim 128) cross-device case, where the suite previously had no bf16 coverage of any EPL-templated kernel. Because the arm ships OFF and its flag is read into a static const bool -- once per process -- the default registration can only ever gate the fallback, so tests/CMakeLists.txt registers a second invocation of the same binary with the flag set. Verified non-vacuous against the issue mudler#463 trap: 1 case, 6 assertions, not zero. Scope note: this commit is the SCALAR arm only. The rocWMMA d=128 decode spike that was originally developed alongside it is split into its own spec and its own PR -- it is a spike with its own (unfiled) issue, and bundling the two put 474 lines of two unrelated kernels in one review. The `|| decode_wmma` disjunct in the bf16_decode_opt gate belongs to that change, not this one: it only means anything once a second opt-in kernel for this head size exists. Row: BACKEND-ROCM Issue: mudler#382 Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-sonnet-5 [Claude Code] Assisted-by: Claude:claude-opus-5 [Claude Code]
Follow-up: the oracle comparison this PR listed as blocked is no longer blocked, and here is the numberThe PR body's "Not claimed" section says the residual gap to vLLM was not re-measured because the same-tool comparison was blocked on a Nix-glibc-vs-container ABI mismatch. That diagnosis was wrong and I'm retracting it. Our binary runs inside the pinned oracle container fine; the earlier failures were self-inflicted (exporting Sanity check first, since running our binary against the container's ROCm rather than the host's is a substitution worth proving inert — in-container matches native:
Both sides in the same container, matched workload (Qwen3-0.6B, 1024 in / 128 out, concurrency 1, 8 prompts, warmup discarded, 3 reps), oracle = vLLM
So this arm closes the decode gap from 6.35x to 1.75x against the oracle on this shape. What this is not
One measurement note worth recording: at One stale line on
|
FOLLOWING_AGENTS_PROTOCOL CI on mudler#767 failed `check-commit-trailers` with "range base must be an ancestor of range head". Not a trailer defect: main had advanced 39 commits past this branch's base (2784dd7), so main's tip was no longer an ancestor of the head and the checker could not form a range at all. Merged rather than rebased -- this repo's convention (`merge: origin/main into row/<ID>` appears throughout the history), and it keeps the pushed SHA a fast-forward instead of needing a force-push. No conflicts. Both auto-merged test files verified by hand rather than trusted: the head_dim-128 cross-device case and the VT_ATTN_DECODE_D128 ctest registration are both present and intact, and main touched neither rocm_paged_attn.hip nor the d128 dispatch. Re-gated on the merge result, gfx1200, $GPU_LOCK held: build 599/599, `ctest -R 'rocm|cross_device'` 5/5 including the flag-on registration. Row: BACKEND-ROCM Issue: mudler#382 Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
Implements
.agents/specs/rocm-decode-attn-d128.md, which landed ahead of this change as #564.The ROCm half of #382. The CUDA half merged as #425 (
66399617); this mirrors it, adopting that arm's flag, default and stated reason rather than inventing new ones.What changes
d == 128— the Qwen3-dense / Llama / Mistral head size — reaches the fast decode kernels instead of falling through to the genericPagedAttnOnline.LoadRowEplBf16/StoreRowEplBf16gain anEPL=4(uint2) case beside the existingEPL=8/16; the dispatch gates and the two launch switches gain ad == 128arm. No new kernel and no new algorithm — the kernel bodies were already generic overEPL.Default OFF, opt in with
VT_ATTN_DECODE_D128=1— the same env var, default and reason as the merged CUDA arm. The arm is correctness-complete but not byte-exact against the kernel it replaces: warp-strided online softmax reduces the KV sequence in a different order thanPagedAttnOnline's per-tile loop, so a greedy anchor can move at an exact bf16 tie. Shipping OFF keeps every existing golden byte-identical. The flip owes the near-tie razor, a distributional gate and regen under the ratified-tie rule, and per the spec must be argued per backend — see the reversal below. That is what keeps #382 open.Reviewer note
Spec §4 item 3 writes the gate as
(d == 128 && (decode_d128 || decode_wmma)). This commit implements it without thedecode_wmmadisjunct, which is what the same item's "Forward reference" paragraph instructs:VT_ATTN_DECODE_WMMAdoes not exist in the tree, and the flag lands with the rocWMMA arm on its own branch. The difference is intentional; it is visible in the diff before the note explaining it is.Evidence
gfx1200 (RX 9060 XT, RDNA4, 32 CU), ROCm 7.2.3,
$GPU_LOCKheld. All figures are a same-binary flag A/B — no rebuild between arms — at 1024-token synthetic prompt, 128 generated, greedy, seed 0, 2 reps per cell agreeing within ~1%.qg=2fusedqg=2fusedqg=4per-headQwen3-4B has no GQA fusion at any head_dim, so its 2.09x isolates the
EPLwidening from the fusion.Qwen3.5-0.8B is the negative control and it earned its keep. Its
head_dimis 256, so thed == 128gate provably cannot reach it. Its first OFF rep came in a 33% outlier at 31.14 ms, which a blind 2-rep average would have reported as a ~1.2x "win" for a model the flag cannot affect. Re-run three times: 23.86 / 23.75 / 23.68 against ON's 23.52 / 23.57.End-to-end output throughput rises less than TPOT on the same runs (0.6B 2.48x, 1.7B 2.05x, 4B 2.02x) because they carry a 1024-token prefill the flag does not touch. TPOT isolates decode; throughput dilutes it.
Concurrency — the advantage grows, it does not compress
Qwen3-1.7B,
--num-prompts= 2x concurrency:This refuted the prediction made before the run, which reasoned that a tiny grid at concurrency 1 flatters the fast kernel. The dominant effect is the reverse: from c8 to c16 the fallback scales only 1.33x against the arm's 1.54x, and scaling efficiency at c16 relative to perfect-linear-from-c1 is 38% OFF against 58% ON.
PagedAttnOnlineis the batch-scaling bottleneck, not merely slow per call, so the win is largest in the regime a server actually runs in.The c1 row reproduces an independently-run four-model sweep to within ~1% (52.85/21.93 there vs 53.40/22.26 here).
Correctness
ctest -R 'rocm|cross_device'5/5, chained directly to the exact-SHA push.RegisteredDevices(), NMSE <= 5e-4 vs the CPU oracle plusOpProviderStats::declines == 0. Genuinely new coverage — the existing generic cross-device test runs atd=8, f32and never reached any bf16EPL-templated kernel, so none of them had bf16 coverage in this suite. (The merged CUDA arm shipped with no test at all.)static const bool— once per process — the default registration can only ever gate the fallback.tests/CMakeLists.txtadds a second invocation with the flag set, same shape as the existingtest_dense_gateup_fused_marlin_off_*pair. Verified non-vacuous against the test_voxtral_e2e reports SUCCESS with zero assertions when its weights env var is unset (and ~40 more gates share the shape) #463 trap: 1 case, 6 assertions, not zero.ctest448/455. The 7 failures are proven pre-existing, not asserted: a cleanmain2784dd7bworktree built from source with none of this code fails the identical set (onlytest_op_parity's index shifts 403 -> 404, from the added registration). They are a missingshellcheck, an mmap-RSS assertion, a JSON type error, and theSharedExpertGateROCm registration gap owed to unmerged feat(rocm): MoE combine/gate ops — SharedExpertGate, MoeCombine, MoeCombineGate (issue #41) #509.agent-preflightfails 9, a strict subset of that same baseline's 10 (differing only byrole-undeclared).check-commit-trailersandcheck-doc-checkpointboth pass against this base.Carried finding
#382 measured this same
EPL=4arm 1.6x slower on sm_110 / Jetson AGX Thor, where gfx1200 measures it 2-3.6x faster. Recorded, not reconciled — different kernels, different fallbacks, different memory systems. It is why the default-ON flip must be argued per backend rather than once, and it is preserved in the spec rather than averaged away.Not claimed
This does not close #488. That issue reports an 8.1x per-call gap against vLLM and explicitly asserts no cause; this removes one cause without re-measuring the residual against the oracle (blocked on a Nix-glibc vs container-glibc ABI mismatch). Also out of scope and named in the spec: the dtype gap (ROCm's decode-opt is bf16-only at every head_dim, so 4 of 5 dtype combinations still fall to
PagedAttnOnlineatd=128— pre-existing, inherited, not introduced),qg=4/qg=8fusion,d=128prefill, and the rocWMMA arm.Issue: #382
Spec: #564