Checked as use_rocm_custom_paged_attention; custom kernel against Triton fallback, with speedups of 2 to 7 times
On gfx11, vLLM's ROCm custom paged-attention kernel is enabled only when the query-to-KV head ratio is at least 3. In the excluded range, the custom kernel is 1.70× to 7.28× faster than the Triton fallback it is passed over for. Inside what bf16 gives for a sum this long, it is numerically equivalent. Sixty cells, two vLLM versions, no exception.
The bound came in with
vllm#17004, whose own description says
the kernel "supports gqa_ratio up to 16, and shows performance gains over the existing kernel when
gqa_ratio is 3 or higher". The guard is therefore a performance heuristic, not a capability limit.
On CDNA, the same function runs this custom kernel starting from ratio 1. Both statements remain true on
main.
When tested, the heuristic is inverted. Across sixty measured cells spanning five shapes, six context lengths, and two vLLM generations, the custom kernel is faster in every one. The excluded band overlaps the admitted band on both versions. There is no region the bound separates.
The report that prompted this, vllm#50603, also blames the fallback for losing accuracy at long context. That hypothesis is not supported. Across 90 cells on two architectures, relative error stays between 2.12e-3 and 4.72e-3, and no two depths' pooled medians differ by more than 1.09×. A flat band only means something if the instrument can see a fault, so the same harness was given one: NaN in the unwritten tail of the final KV tile poisons both paths, at exactly the lengths that straddle it and no others.
End to end on a real model, widening the gate is worth 1.02× at 1K and 1.17× at 32K, reproduced in both run orders on the newer image. That change is proposed in vllm#54210, and this page is what it rests on.
Both paths are driven through vLLM's own chunked_prefill_paged_decode dispatch with only
use_rocm_custom_paged_attention forced. Argument marshalling is therefore identical between
the arms. Pure decode is measured: one query token per sequence over a fully cached context, which
matches the shape described in the report. The fp32 reference is computed from pre-paging KV tensors
rather than the cache, so a paging bug cannot cancel itself out. Scoring uses relative
L-infinity against the reference's own scale, making different context lengths comparable.
The shape set covers ratios 1, 2, 3 and 4 at head size 128 and block size 16, plus 32/16; that final
shape belongs to gemma-3-27b-it, satisfying every condition of the gfx11 gate except the
ratio. For the CUDA control, the kernel is lifted verbatim into a standalone module compiling the same
Triton text, and the launch mirrors the dispatch's argument list.
The bands are the argument, not the individual maxima. On 0.23.1, the excluded ratios span 1.84× to 7.28×, while the admitted ones span 2.35× to 7.40×. On 0.27, excluded ratios span 1.70× to 6.05× against 2.20× to 6.05× for admitted shapes. The two overlap on both versions, which is what "the bound does not separate anything" means in one sentence.
The originating report describes two symptoms. The first symptom—taking the fallback path at all—is confirmed and forms the subject of the findings above. The second claim is that the fallback degrades with context. That hypothesis is directly testable here, and it does not hold.
Kernel microbenchmarks alone do not justify a pull request. An end-to-end model test was run using
gemma-3-27b-it-w4a16 at TP=2 with CUDA graphs on. Decode latency was isolated by
64-against-8 differencing, launching one fresh container per cell so source edits could not leak
between arms:
Routing is recorded directly inside the TP workers rather than inferred from timing. Measurements
covered every cell on 0.23.1, and on 0.27 at 1K in both arms from a dedicated run after the in-place
recorder returned nothing. The full-attention layers arrive with head size 128, block size 16, native KV
layout, and a power-of-two block, so the ratio is the only gate condition they fail;
the sliding layers fail on sliding_window == 0 in both arms and never move.
Opening a gate lets in whatever lurks behind it. Upstream PR vllm#53856 fixes a real fault in the same custom kernel: it masks logits past the sequence end, but its vectorised V-cache load still consumes the whole final block, allowing an unwritten NaN in the tail to poison output. Its test plan notes that gfx11 received compile validation only, because the author had no such device.
The forced column bears directly on the proposal. Forced past the gate, the stock arm poisons eight cells rather than four: ratio 2 fails identically at the straddling length across both dtypes, under both V-only and K+V corruption. All eight are clean on the patched arm. So widening the gate admits ratio 2 to this kernel, and this is the fault it would admit with it — which is why the two threads belong together, and why the gfx11 runtime evidence went to that PR's author on request.
These tests eliminate only one hypothesis: that the Triton 2D kernel loses accuracy as context grows. The reporter observed degradation on a VLM not available here, running on different hardware; this probe evaluates the kernel rather than model output. The reported symptom still needs an explanation.
Sliding-window shapes, fp8 KV, prefill shapes, and kernel-level TP>1 were not tested. While the model benchmark uses TP=2, that is a model-level measurement rather than a kernel one.
Because allocate_kv_cache zero-fills memory, the defect is not uninitialised allocation
garbage; vllm#53856 attributes it to sleep mode and allocator behaviour. This harness injects NaN
deliberately to observe how the kernel responds.
All measurements ran on a single gfx1100 machine. A related question — whether Model Runner V2 reaches the same guard — was measured here: it does, on 36 of 37 rows identical to V1. Whether speculation makes decode carry multiple query rows under MRv2 was not measured, because no available checkpoint configures for it on that image. That half rests on a code read.
use_rocm_custom_paged_attention: one insertion, one deletion. Open as of 2026-09-09:
a maintainer asked on 2026-09-07 for kernel correctness and a full gsm8k run, both supplied the same
day; no formal review yet. The PR body carries the tables on this page; the repository carries the rows
they are computed from.
verify_doc_figures.py rather than transcribed.
verify_doc_figures.py pins both counts.
benchmarks/vllm-50603/README.md records — and routing there comes from
route_027.py, a run of its own at 1K in both arms. The sentence now says so.
Every cell. The probes, output JSONL, and container logs reside in
benchmarks/vllm-50603/,
along with the CUDA control's lifted kernel and the hash check keeping both sides on the same Triton
source. Every figure here is recomputed from those files by
verify_doc_figures.py.
Upstream: vllm#50603 is the report, vllm#17004 introduced the bound, vllm#54210 is the one-line change, and vllm#53856 is the fault Figure 4 is about.