[AMD] [GLM5] Fuse the DSA indexer Q/K rope + quant + K-cache write in… - #4
Open
namle-213 wants to merge 1 commit into
Open
[AMD] [GLM5] Fuse the DSA indexer Q/K rope + quant + K-cache write in…#4namle-213 wants to merge 1 commit into
namle-213 wants to merge 1 commit into
Conversation
…to one aiter kernel The DSA indexer Q/K fusion has been CUDA-only. On ROCm the indexer still runs the unfused path: rope on q, LayerNorm + rope on k, an fp8 quant of each, the head-gate scale, and the index-K cache write, as separate launches per layer per step. All of it is launch-bound. aiter's indexer_qk_rope_quant_and_cache does the whole thing in one launch, so extend the existing fusion switch to ROCm rather than build a second one: the fused wk_weights_proj GEMM, the no-Hadamard invariant, the weight loader and the graph split-op plumbing are all platform-agnostic already. - Probe aiter for the kernel at import and fall back with a warning; aiter is pinned per image and an older one would only fail at the first forward. - Build k_norm in fp32 whenever fusion is on. The kernel requires fp32 norm params, as the CUDA kernels already do; bf16 stays for the unfused ROCm path, where matching x.dtype is what selects aiter's CK layernorm. - Read cos/sin off aiter's rope module, which keeps them apart as [max_position, 1, 1, rope_dim/2] instead of one cos_sin_cache. - Give _fused_k_prepare_and_store a ROCm branch. Its CUDA fallback is a JIT kernel, and the k-only decode fast path reaches it whenever fusion is on. - Gate the fused-store branch on _is_cuda so non-CUDA stops paying for a JIT compile attempt that can only fail. Two adjacent fixes: - lora_manager imported _use_dsa_indexer_fusion, which sgl-project#30111 deleted, so the indexer-LoRA guard raised ImportError instead of its intended error. Restored as dsa_indexer_fusion_supported(). - The ROCm branch of _store_index_k_cache read forward_batch.out_cache_loc, ignoring the sliced out_cache_loc its caller passes under the graph split-op contract. The two K-cache writers stay live at once -- the k-only decode path writes unfused -- and they are not byte-identical: the unfused path rounds to bf16 before quantizing while the fused kernel goes fp32 -> fp8 directly, so a value near an fp8 midpoint can land on either neighbour. Measured on 41x128: scales identical, 15/5248 elements differ, each by one fp8 code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The DSA indexer Q/K fusion (sgl-project#27705) is CUDA-only. On ROCm the indexer still runs
the unfused path every layer, every step: rope on q and k, LayerNorm on k, an fp8
quant of each, the head-gate scale, and the index-K cache write. On GLM-5.2 decode
that work is launch-bound, not arithmetic-bound.
aiter ships
indexer_qk_rope_quant_and_cache, which does all of it in one launch.This wires it up by extending the existing fusion switch to ROCm rather than adding
a second one -- the fused
wk_weights_projGEMM, the no-Hadamard invariant, theweight loader and the graph split-op plumbing are already platform-agnostic. No
kernel changes; the kernel is already in the aiter that ships in the ROCm image.
Modifications
use_dsa_indexer_fusionto ROCm, gated on probing aiter for the kernel atimport (aiter is pinned per image; an older one imports fine and would only fail
at the first forward, so it falls back with a warning).
_aiter_fused_qk_prepare_and_store, the single-kernel Q/K path. It returnsthe same
(q_fp8, weights)contract as the CUDA path, including thenum_tokensslicing the graph split-op relies on, so the call site is unchanged.
k_normin fp32 whenever fusion is on. The kernel requires fp32 norm params,exactly as the CUDA kernels already do. bf16 stays for the unfused ROCm path, where
matching
x.dtypeis what selects aiter's CK layernorm over the native upcast.[max_position, 1, 1, rope_dim/2]rather than in onecos_sin_cache._fused_k_prepare_and_storea ROCm branch. Its CUDA fallback is a JIT kernel,and the k-only decode fast path ([AMD] [GLM5] Skip DSA decode indexer when kv_len <= index_topk (dense k-only fast path) sgl-project/sglang#31324) reaches it whenever fusion is on.
_is_cudaso non-CUDA stops paying for a compileattempt that can only fail.
Two adjacent fixes, easy to split out if preferred:
lora_managerimports_use_dsa_indexer_fusion, which [Fix] Fix DSA indexer fusion for NeoX RoPE sgl-project/sglang#30111 deleted when it movedthe flag into
Indexer.__init__. The indexer-LoRA guard therefore raisesImportErrorinstead of its intended error, on CUDA today. Restored asdsa_indexer_fusion_supported()._store_index_k_cachereadforward_batch.out_cache_loc,ignoring the sliced
out_cache_locits caller passes under the graph split-opcontract.
Accuracy
GLM-5.2-FP8, TP8, 8x MI355X (gfx950).
Two independent runs per side, measured on two different bases (this PR's base and
upstream main, whose
dsa_indexer.pyis byte-identical):sigma is ~0.0093 for n=500, and the per-run difference changes sign between the two
pairs, so this is no measurable change in either direction -- not a regression and not
an improvement.
GPQA-Diamond
Fused build only; there is no on-box control run.
All 1584 requests returned 200, zero dropped or retried, and zero
retract_decode: aborted last request(ordinary retraction preservesoutput_ids,so it costs throughput, not correctness).
The 8 repeats reuse the identical 198 questions with identical answer orderings
(
random.Random(0)insimple_eval_gpqa.py), so they are correlated and estimatetemperature-1.0 sampling variance only -- not 1584 independent trials. Observed stdev
across repeats is 0.0080 against a 0.0202 single-run binomial SE, which is consistent
with that. Absent a control run this is an absolute quality check, not evidence about
the fusion; the paired evidence for the kernel is the gsm8k table above.
RULER / NIAH long context
lm_evalRULERniah_single_2, thinking on, 500 samples per context length. Theserver is served at
--context-length 262144because the 131072 tier needs room forthe prompt plus an 8192-token generation budget.
1000 records, 1 empty generation, 64 min wall clock.
--model local-chat-completionsrather than lm_eval's in-process
vllmbackend, so the eval runs against this PR'sSGLang server instead of loading a second engine.
Cross-checked against NVIDIA/RULER driven through its native
sglangclient at n=100with thinking off and RULER's own 128-token budget: 100.0 at both lengths. Two
harnesses, two prompting regimes and a 5x sample difference agree, so the result is a
property of the model and kernel rather than of either harness.
The two K-cache writers stay live at once, since the k-only decode path keeps writing
unfused, and they are not byte-identical: the unfused path rounds to bf16 before
quantizing while the fused kernel goes fp32 -> fp8 in one step, so a value near an fp8
midpoint can land on either neighbour. Measured over 41x128 elements: ue8m0 scales
identical, 15/5248 elements differ, each by exactly one fp8 code, and the fused result
is the closer of the two to an fp32 reference.
test_dsa_indexer_qk_fuse_rocm.pypinsthat bound.
Benchmark
BS=1 at 16K context, where
kv_len > index_topkso the full indexer runs and decodeis launch-bound. Two runs per side, both stable to two decimals:
The same comparison against upstream main as the base gives 70.74/70.76 -> 75.32/75.33
(+6.5%), so the win does not depend on which of the two bases it is measured from.
TTFT is flat because prefill runs the same indexer but is compute-bound; the whole
gain lands in decode, which is the signature a launch-count fix should have.
At
--random-input-len 8192 --random-output-len 1024 --max-concurrency 64,1278.73 -> 1301.31 output tok/s (+1.8%). Launch cost amortizes across a large batch,
so that point reads as "no throughput regression" rather than as the fusion's
contribution.
A short-context BS=1 point is not a measurement of this change: with
index_topk=2048, a 32-token prompt never exceedskv_len2048, so every decode steptakes the k-only dense path and never calls the fused kernel.
Profile
Torch profiler, GPU activities, decode at 16K context, measured on this PR's base and
this PR's head. Both captures cover exactly 2.0 decode steps, normalising by
_fused_qk_rope_cat_and_cache_mla_kernel, which fires once per layer per step acrossall 78 layers. GLM-5.2 runs a full indexer on 21 of those 78 (
indexer_types: 21full, 57shared).Launches per full-indexer layer per decode step (raw counts in parentheses):
kn_entry_2c_sbhd_cached_indirect_inplace(rope q+k)Layernorm2dFwd(aiter CK)act_quant_kernel__kernelindexer_k_quant_and_cache_kernelindexer_qk_rope_quant_and_cache_kernel84 launches per decode step across the 21 full-indexer layers become 21.
Checklist
test/registered/kernels/ops/attention/test_dsa_indexer_qk_fuse_rocm.py, 6 cases,all passing on MI355X: K-cache agreement with the unfused writer in both paged
layouts, the one-fp8-step numeric bound, q/head-gate agreement against
rope +
act_quant+_scale_head_gates, strided-vs-contiguous byte equality, andthe cos/sin 2-D view tracking a replaced rope cache.
jit-kernel-unit-test-amdsuite.