Skip to content

[AMD] [GLM5] Fuse the ROCm DSA indexer q/k prep into aiter's single kernel - #34394

Open
Jacob0226 wants to merge 9 commits into
sgl-project:mainfrom
Jacob0226:jacob/glm5-dsa-indexer-aiter-fuse
Open

Jacob0226 wants to merge 9 commits into
sgl-project:mainfrom
Jacob0226:jacob/glm5-dsa-indexer-aiter-fuse

Conversation

@Jacob0226

@Jacob0226 Jacob0226 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

On ROCm the DSA indexer spends 11 kernels per decode layer on q/k prep. This PR collapses most of that into one aiter kernel.

  • indexer_qk_rope_quant_and_cache does k LayerNorm + RoPE + fp8 quant + paged index-k store, q RoPE + fp8 quant, and the head-gate scale in a single launch — six kernels become one. Like the CUDA fused path it drops the Hadamard rotation, which is logit-preserving and only shifts fp8 quantisation error. Prefill drops it too, since prefill writes the index-K cache decode reads back and the two must agree on basis.
  • wk and weights_proj are folded into one wk_weights_proj GEMM. This needs only the concatenated weight, no fused kernel, but was gated on CUDA; it now runs on CUDA or HIP and is numerically identical to the split projections.
  • The aiter kernel is tuned for decode's one-token-per-sequence shape and costs about twice what it replaces at prefill widths, so it is gated to decode. It also requires gfx95x, SGLANG_USE_AITER, and an aiter build exporting the symbol — a capability probe, not a preference: anything outside the envelope falls back to the existing path. CUDA and NPU are unchanged.
image

Decode indexer on GLM-5.2 / MI355X: 11 kernels / 70.8 us per layer → 5 kernels / 41.4 us (−41.5%), which closes the MI355X-vs-B200 indexer gap from 2.55x to 1.49x.

Drive-by fix: define the module-level _use_dsa_indexer_fusion that lora_manager.py already imports but nothing defined, so an indexer-targeted LoRA adapter raised ImportError instead of the intended ValueError.

Test plan

MI355X TP4, GLM-5.2-MXFP4, fp8 KV, chunked_prefill=16384, DSA backend triton, i8192 / o1024, one run per point. Baseline is the same AMD GLM-5.2 stack without this PR (#30519, #30575, #30715, #30808, #31323, #31324, #32888, plus ROCm/aiter#4453).

conc Output tok/s Median ITL (ms) Median TTFT (ms)
4 323.3 → 335.9 (+3.9%) 10.61 → 10.16 (−4.2%) 366.7 → 367.6 (+0.2%)
8 465.3 → 482.3 (+3.6%) 14.02 → 13.43 (−4.2%) 379.1 → 379.8 (+0.2%)
16 731.5 → 762.9 (+4.3%) 15.73 → 14.86 (−5.5%) 378.0 → 383.3 (+1.4%)
32 1044.3 → 1081.1 (+3.5%) 19.39 → 18.37 (−5.3%) 393.7 → 389.5 (−1.1%)
64 1342.4 → 1382.2 (+3.0%) 25.33 → 24.03 (−5.1%) 631.6 → 615.6 (−2.5%)

ITL improves −4.2% to −5.5% at every concurrency, matching the 29.4 us/layer the kernel table predicts over the 21 layers that run the indexer (0.62 ms per decode forward). TTFT is flat, which is the check that matters given the kernel is decode-only.

GSM8K (1200 questions): 0.928 baseline, 0.934 this PR. The index-K cache is bit-exact against the unfused path across both preshuffle and scale_fmt; the q fp8 payload differs by 1 ULP on 0.10% of elements, because aiter multiplies by the reciprocal rather than dividing.

Per-layer decode indexer kernels, and why the aiter kernel is decode-only
baseline us this PR us
hgemm_16x64x128x3 (wk) 6.59 hgemm_16x64x128x3 (merged wk_weights_proj) 6.50
hgemm_16x64x64x6 (weights_proj) 5.30
Layernorm2dFwd 4.19 indexer_qk_rope_quant_and_cache 5.60
kn_entry_2c_sbhd_cached_indirect (RoPE) 8.78
fast_hadamard_transform_kernel 4.43
_hadamard_quant_kernel 4.55
indexer_k_quant_and_cache_kernel 4.19
triton_poi_fused_mul_unsqueeze_0 4.18
wv_splitk_small (q proj) 5.70 wv_splitk_small 5.95
fp8_paged_mqa_logits_preshuffle 4.34 fp8_paged_mqa_logits_preshuffle 4.46
topk_transform_decode_kernel 18.60 topk_transform_decode_kernel 18.93
total 70.8 total 41.4

Of the 29.4 us/layer saved, 5.4 is the merged projection and 21.3 is six kernels becoming one; downstream topk and logits move by 0.1–0.3 us, which is noise.

With the aiter kernel enabled for prefill too, the prefill indexer went from 1614.5 up to 1780.5 us/layer: the fused kernel alone costs 300.4 us/layer against the 162.8 of the six it replaces, and drags downstream topk and logits ~8% each. Gating it to decode gives prefill 1519.6 us/layer (−5.9%), since prefill still benefits from the merged projection and from dropping the Hadamard.


CI States

Latest PR Test (Base): ✅ Run #34937420770
Latest PR Test (Extra): ❌ Run #34937420593
Latest PR Test (AMD ROCm 10): ➖ No AMD PR run found for this commit.

@Raiden-Makoto

Copy link
Copy Markdown
Contributor

/tag-and-rerun-ci

…ernel

ROCm ran the DSA indexer q/k preparation as a chain of separate kernels:
k LayerNorm, RoPE, fp8 quant into the paged index-k cache, then q Hadamard
and quant, then the head-gate scale. aiter's indexer_qk_rope_quant_and_cache
covers all of it in one launch, and fusing lets the Hadamard rotation go
entirely -- the indexer logits are invariant to it, so dropping it is safe as
long as the write and the read back agree on the basis.

ROCm now shares upstream's use_dsa_indexer_fusion flag rather than carrying a
separate opt-in. On gfx95 the flag additionally requires a LayerNorm k_norm,
head_dim 128, rope_head_dim 64 and block_size == head_dim, which is what the
kernel accepts. The flag also folds wk and weights_proj into one bf16
wk_weights_proj parameter, so the weight loader fills it from either shard and
indexer LoRA, which wraps modules that no longer exist, now raises instead of
being silently dropped.

Two places needed the platform test that the flag used to imply: the k-only
prefill path, which has no aiter counterpart, and the graph head-gate scale,
whose custom op is CUDA-only.

Measured on MI355X, GLM-5.2-MXFP4 TP4, 8k prefill, per indexer layer per
forward: 189.3 -> 55.3 us, a 3.4x cut worth ~2.8 ms per forward across the 21
indexer layers. Needs an aiter carrying the vectorized form of the kernel; the
earlier scalar one runs at 228 us and is slower than the unfused path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Jacob0226
Jacob0226 force-pushed the jacob/glm5-dsa-indexer-aiter-fuse branch from 249b762 to 82bd89f Compare August 20, 2026 00:07
@Jacob0226

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@bingxche

Copy link
Copy Markdown
Collaborator

@Jacob0226

CI Status for PR #34394

Merge verdict:Do not merge on green — PR CI is incomplete and does not exercise this PR's changed code. Every executed failure is unrelated to this PR (0 related failures out of 6 executed), but the AMD pipeline fast-failed at stage-a on an unrelated test_umbp_store.py bug, which skipped all downstream AMD stages — including the MI35X (gfx95x) stages that are the only jobs that run this PR's new fused ROCm DSA-indexer kernel. The changed code was never executed by CI, so the green jobs prove nothing about it.

Caution

PR CI is incomplete AND this PR's core code path is unexercised. The AMD run (PR Test ROCm 7.2 (AMD)) fast-failed at wait-for-stage-a-amd because an unrelated stage-a unit test crashed, so every stage-b/c AMD job was skipped — including stage-b-test-1-gpu-small-amd-mi35x, stage-c-dsv4-flash-fp4-fp8-amd-mi35x, stage-c-dsv4-pro-fp4-amd-mi35x, stage-c-test-large-8-gpu-amd-mi35x, stage-b-test-large-8-gpu-mi35x-disaggregation-amd. The new fused kernel is gated on gfx95x + SGLANG_USE_AITER + an aiter symbol (MI35X only); the one AMD job that did run is a gfx942/MI300 stage-a unit suite that touches none of the changed files. Before merge: fix/ignore the unrelated stage-a test_umbp_store failure and re-run AMD (or apply bypass-fastfail) so the MI35X DSA stages actually execute; also confirm those stages cover the GLM-5.2 DSA-indexer path the author benchmarked manually.

Changed files: dsa_indexer.py (+185/-26), dsa_prefill_cuda_graph.py (+2/-1), lora_manager.py (+2/-2), deepseek_weight_loader.py (+5/-5) — all AMD/DSA/LoRA; the bulk of new logic is the gfx95x-gated fused path in dsa_indexer.py.

Executed CI failure attribution: AMD: 1 executed failure (0 related) · Others: 5 executed failures (0 related). NVIDIA base run overall cancelled but 52/65 jobs passed — see notes.

AMD Executed Failures

Job Test File Test Function Error Related? Why
stage-a-test-1-gpu-small-amd (rocm10, mi300) test/registered/unit/mem_cache/test_umbp_store.py TestUMBPStore.test_basic_set_get TypeError: '>' not supported between 'MagicMock' and 'int' (umbp_store.py:1066) 🟢 PR touches none of the mem_cache/umbp code; a MagicMock mem-pool in the test hits tensor.numel() > 0. This is the fast-fail root that skipped all AMD stage-b/c.

Other Executed Failures

Job Test File Test Function Error Related? Why
stage-a-unit-test-mlx test/registered/unit/hardware_backend/mlx/test_scheduler_mixin.py TestOverlapLoopGracefulExit.test_loop_exits_after_shutdown_req _StopLoop in mlx/scheduler_mixin.py:213 🟢 MLX (Apple-silicon) scheduler backend; PR touches no MLX code.
multimodal-gen-test-4-npu-a3 (0) sglang/multimodal_gen/test/server/ascend/test_server_2_npu.py TestDiffusionServerTwoNpu::test_diffusion_generation[wan2_2_t2v_14b_w8a8_2npu] [consistency] GT image not found (404 in ci-data-diffusion repo) 🟢 Missing ground-truth reference asset (test-data/infra); NPU diffusion path, unrelated to PR. Root cause of the two NPU perf cascades below.
base-c-test-perf-2-npu-a3 · base-c-test-perf-16-npu-a3 N/A N/A Fast-fail: root cause job multimodal-gen-test-4-npu-a3 🟢 Fast-fail cascade of the NPU diffusion failure above, not independent.

Not real failures / not tested (excluded from the count):

  • PR Test Extra & PR Test Extra (AMD) call-gate / pr-gate: gate exit 1 = missing run-ci-extra label (opt-in). Extra suites did not run — add the label / /tag-and-rerun-ci extra if you want that signal.
  • PR Test (NVIDIA base): run overall cancelled but 52/65 jobs passed; only base-c-test-4-gpu-b200 (4) was cancelled after sitting ~8h in the runner queue (never ran tests). B200 shards 0–3 + base-b passed.
  • PR Test (XPU) stage-a-test-1-gpu-xpu: cancelled (not tested). MUSA / Arm64 / Xeon / sgl-router passed.

Details / what to do before merge

  • Close the coverage gap (highest priority). Re-run AMD after the unrelated test_umbp_store.py stage-a failure is resolved on the branch, or apply the bypass-fastfail label + rerun so stage-b/stage-c … -mi35x execute. Confirm those MI35X stages actually cover the GLM-5.2 DSA-indexer decode path (the author's own test plan is a manual MI355X TP4 GLM-5.2 benchmark, which suggests automated CI coverage of the fused path is thin).
  • CUDA side: deepseek_weight_loader.py and dsa_prefill_cuda_graph.py changes are mostly comment updates; the wk+weights_proj fold already ran on CUDA, so NVIDIA base (mostly green) is a reasonable but not conclusive signal — verify a DeepSeek/DSA-indexer weight-load test ran among the passing base-c jobs.
  • LoRA drive-by fix (lora_manager.py, _use_dsa_indexer_fusionindexer_merges_weights_proj(...)) only triggers when a LoRA targets DSA indexer modules — a niche path unlikely hit by generic PR CI; consider a targeted unit test.
  • The 3 unrelated red pipelines (AMD stage-a umbp, MLX, NPU) are pre-existing/infra and not blockers for this PR's correctness, but they currently mask the AMD signal you actually need.

Generated by amd-bot using Claude Code CLI

@Jacob0226

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@Jacob0226

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

1 similar comment
@Jacob0226

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@Jacob0226

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants