Skip to content

[Perf] [AMD] mla: Fuse RoPE, concat and KV-cache write for MLA decode on gfx1250 - #44459

Open
jiaryang wants to merge 6 commits into
sgl-project:mainfrom
jiaryang:amd/gfx1250-mla-fused-rope-kv-cache
Open

jiaryang wants to merge 6 commits into
sgl-project:mainfrom
jiaryang:amd/gfx1250-mla-fused-rope-kv-cache

Conversation

@jiaryang

@jiaryang jiaryang commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

On gfx950, aiter MLA decode runs RoPE, the q/k concat and the KV-cache write in one fused_qk_rope_cat_and_cache_mla kernel. On gfx1250 these are still separate: native RoPE, two torch.cat and set_kv_buffer, more than a dozen elementwise kernels per layer. This PR extends the gfx950 fused branch to gfx1250.

Modifications

With SGLANG_USE_AITER=1 on gfx1250, MLA decode with the aiter or triton backend skips the standalone RoPE and takes the fused branch, as aiter MLA does on gfx950. gfx950 is unchanged.

  • As on gfx950, the gate is evaluated per call: decode, the aiter or triton backend, no DSA, one DCP rank, a rope module of exactly RotaryEmbedding or DeepseekScalingRotaryEmbedding, and a bf16 or FP8 KV pool. The pool check reads the pool's dtype, so an fp16 pool, which the kernel rejects, stays on the unfused path.
  • sglang's rope keeps cos and sin in one cos_sin_cache; the kernel gets the two halves as strided views.
  • Q keeps q_nope_out's dtype, which is bf16 on gfx1250: MLA decode there takes an unquantized Q even over an FP8 KV cache.
  • Prepare's rope-skip condition moves into _applies_rope_in_prepare, so the unit test can check that prepare skips RoPE exactly when core fuses it.
  • CPU unit tests are added to test/registered/unit/models/test_rocm_mla_absorb_fusion_gate.py.

Requires ROCm/aiter#5501, which both ROCm Docker pins in docker/rocm.Dockerfile already contain. On main, gfx1250 MLA decode runs the triton backend, which the gate covers, and the first set of numbers below is main with the triton decode backend. With #44458's aiter decode backend the same fused branch applies; the second table measures that.

Validated on gfx1250 with Kimi-K2-Thinking-MXFP4 at TP8, with the triton decode backend on main and the aiter decode backend from #44458.

Accuracy Tests

The fused kernel matches the unfused path (q and the written KV rows) for bf16 and FP8 KV, batch 1 to 128.

GSM8K, 8-shot, 1319 questions, triton decode backend on main:

KV cache Runs Accuracy
Before FP8 4 0.942 (0.950, 0.939, 0.939, 0.939)
After FP8 4 0.940 (0.942, 0.938, 0.934, 0.945)
Before bf16 1 0.947
After bf16 1 0.938

Unchanged main moves by 0.011 between server launches (0.950 and 0.939), so the single bf16 run is within that spread.

Speed Tests and Profiling

RoPE, concat and KV write per layer, Kimi-K2 TP8 shapes, CUDA graph replay:

batch unfused (us) fused (us)
1 37.6 1.9
4 40.8 1.6
32 55.6 1.7
128 81.6 1.9

bench_serving, 8k input / 1k output, FP8 KV, triton decode backend on main:

Concurrency Throughput per GPU (tok/s/GPU) Interactivity (tok/s/user) Median TTFT (ms) Median TPOT (ms)
4 Before 171.4 39.7 416.8 25.20
4 After 217.7 51.2 406.4 19.54
4 Change +27.0% +29.0% -2.5% -22.5%
16 Before 398.8 22.9 440.6 43.63
16 After 483.4 28.0 427.1 35.68
16 Change +21.2% +22.3% -3.1% -18.2%
64 Before 647.9 9.4 677.0 106.87
64 After 745.6 10.7 518.1 93.66
64 Change +15.1% +14.1% -23.5% -12.4%

In the decode profile (concurrency 4, 8k context), each layer's RoPE, concat and KV write go from about 14 PyTorch kernels taking about 82 us to the fused kernel taking about 3 us, and the decode step goes from 30.0 ms to 23.6 ms. The triton decode attention kernels are unchanged.

Concurrency 4 with #44458's aiter decode backend on both sides:

Throughput per GPU (tok/s/GPU) Interactivity (tok/s/user) Median TTFT (ms) Median TPOT (ms)
Before 304.7 71.4 271.3 14.00
After 443.1 104.9 262.6 9.53
Change +45.4% +46.9% -3.2% -31.9%

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): ✅ Run #41622351290
Latest PR Test (Extra): ❌ Run #41622351313
Latest PR Test (AMD ROCm 10): ✅ Run #41622351159

Tip

Base CI only covers the core cases, and its workload may not exercise your change. Use /rerun-test to run the tests your change touches, or copy this prompt for an agent:

Find the registered tests this PR affects and run them with /rerun-test, following https://github.com/sgl-project/sglang/blob/main/.agents/skills/rerun-test/SKILL.md

@github-actions github-actions Bot added the amd label Oct 15, 2026
@jiaryang
jiaryang force-pushed the amd/gfx1250-mla-fused-rope-kv-cache branch from 68175b3 to f142e83 Compare October 20, 2026 15:55
@jiaryang
jiaryang marked this pull request as ready for review October 20, 2026 15:58
@sogalin sogalin added the run-ci CI: run the baseline test suite on this PR label Oct 22, 2026
@jiaryang

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@jiaryang
jiaryang force-pushed the amd/gfx1250-mla-fused-rope-kv-cache branch from 217506b to b377194 Compare October 22, 2026 16:26
@jiaryang jiaryang changed the title [Perf] [AMD] mla: Fuse RoPE, concat and KV-cache write for triton MLA decode on gfx1250 [Perf] [AMD] mla: Fuse RoPE, concat and KV-cache write for MLA decode on gfx1250 Oct 22, 2026
@jiaryang

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

3 similar comments
@jiaryang

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@jiaryang

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@jiaryang

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@jiaryang

Copy link
Copy Markdown
Contributor Author

@amd-bot ci-status

@bingxche

Copy link
Copy Markdown
Collaborator

@jiaryang

CI Status for PR #44459

Warning

The Claude Code agent did not work, so the CI status check could not be produced.
Error: git worktree add failed: fatal: invalid reference: HEAD
No API fallback is used. Re-run the command, or check the amd-bot runners.


Generated by amd-bot

@jiaryang

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

jiaryang and others added 2 commits November 3, 2026 06:45
With SGLANG_USE_AITER=1 on gfx1250, aiter or triton MLA decode skips the
standalone RoPE and uses aiter fused_qk_rope_cat_and_cache_mla, the same
fused branch aiter MLA takes on gfx95, with a bf16 Q. sglang's
RotaryEmbedding keeps cos and sin in one cos_sin_cache, so the kernel gets
its two halves as strided views. Limited to plain rope modules whose table
covers the whole rope head, without DSA or DCP, and to bf16 or fp8 e4m3 KV
caches. Requires aiter with ROCm/aiter#5501.

The layer-static terms of the gate are computed once in __init__. A CPU
unit test pins down each term of the gate and the cos/sin order of the
tables.

Co-authored-by: Cursor <cursoragent@cursor.com>
Address review feedback.

- The gate is evaluated per call as on gfx95 instead of caching the
  layer-static terms in __init__, so init_mla_rocm_forward is removed.
- The rope check is the exact type RotaryEmbedding or
  DeepseekScalingRotaryEmbedding; subclasses such as Bailing MoE v3's video
  mrope compute their rope differently.
- The KV check reads the pool's logical dtype (bf16 or fp8) instead of the
  kv_cache_dtype server arg, so fp8-quantized checkpoints and draft runners
  whose server arg is "auto" are covered, and an fp16 pool stays unfused.
- Q keeps q_nope_out's dtype on gfx1250, which is bf16 there.
- Prepare's rope-skip condition moves into _applies_rope_in_prepare so the
  tests can check that prepare skips rope exactly when core fuses it.
- The CPU cases join test_rocm_mla_absorb_fusion_gate.py, and
  test_rocm_mla_gfx1250_fused_rope_gate.py is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jiaryang
jiaryang force-pushed the amd/gfx1250-mla-fused-rope-kv-cache branch from e99dec6 to 59901a8 Compare November 3, 2026 06:51
@github-actions

This comment has been minimized.

jiaryang and others added 4 commits November 3, 2026 07:15
main moved ForwardMode into sglang.srt.model_executor.forward_mode and its
check-indirect-imports hook rejects importing it through forward_batch_info.

Co-authored-by: Cursor <cursoragent@cursor.com>
On gfx1250 with SGLANG_USE_AITER=1, _fused_rope_cat_and_cache takes the
gfx1250 branch and reads rotary_emb.cos_sin_cache, which the test's stub
lacks. That branch excludes DSA, so the test's DSA pools pin
_use_gfx1250_fused_mla_rope to False.
@jiaryang

jiaryang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

@kkHuang-amd kkHuang-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jiaryang

jiaryang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

This branch has not been deployed

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

Labels

amd deepseek run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants