Skip to content

[AMD] Qwen3.5 ASM FMHA chunked-prefill context attention - #36758

Merged
HaiShaw merged 3 commits into
sgl-project:mainfrom
zijiecode:perf/aiter-asm-context-prefill
Aug 28, 2026
Merged

HaiShaw merged 3 commits into
sgl-project:mainfrom
zijiecode:perf/aiter-asm-context-prefill

Conversation

@zijiecode

@zijiecode zijiecode commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

On MI355X, long-input Qwen3.5 serving (70k prompt, chunked prefill) spends most of its prefill attention time in aiter's ck_tile paged mha_batch_prefill. Measured at production shapes (head_dim 256, fp8 KV, 16k-token chunks), that kernel runs at roughly 15% of the GPU's FP8 peak — and paging is not the reason: a fully contiguous KV layout is just as slow, and fp8 inputs are only ~1.2x faster than bf16, so the pipeline never reaches the fp8 rate.

The gfx950 ASM kernel fmha_fwd_hd256_fp8_causal_group (already used by SGLang for the no-prefix first chunk) runs the same shapes 3.5x faster. The only reason context chunks cannot use it today is that it wants contiguous varlen K/V, while the KV cache is paged.

It turns out the gather is nearly free: copying 70k tokens of fp8 KV (~72 MB) into a contiguous buffer takes ~18 µs, against several milliseconds for the attention itself. So we gather and call the ASM kernel.

Modifications

python/sglang/srt/layers/attention/aiter_backend.py, in forward_extend:

  • New branch for extend batches with a prefix (the no-prefix case already had an ASM path). It expands the page-granular kv_indptr / kv_indices metadata into per-token pool indices, gathers K/V into a contiguous varlen buffer (via a uint8 view, since index_select is not implemented for fp8), and calls flash_attn_varlen_fp8_pertensor_func with bottom-right causal masking.
  • Guard rails:
    • The branch checks that the page metadata actually covers seq_lens before gathering (mixed / speculative batches can disagree); on any mismatch it logs once and falls back to the original paged path. Nothing crashes, worst case is the old speed.
    • Gated on gfx950, bf16 Q, fp8 KV cache, head_dim 256, no sliding window, no sink, no softcap.
    • Disabled when the KV pool uses the vectorized_5d layout (different physical layout).
  • Draft-model catch-up chunks (EAGLE/MTP) hit the same branch and get the same speedup — they are ordinary extend batches with a prefix.

Accuracy Tests

GSM8K, 5-shot, all 1319 questions, temperature 0, max new tokens 2048; Qwen3.5-397B-A17B-MXFP4, TP2, MI355X.

Backend Accuracy Correct Invalid
AITER 92.8% 1224/1319 1/1319

Benchmarking and Profiling

Benchmarks were run on a tree with #36330 applied (the current MI355X perf baseline for this model); the change itself is independent of it and this PR is based on main.

Kernel microbenchmark

Single MI355X GPU, causal, head_dim 256, fp8 KV, 16384 new tokens attending to a 32k-token paged context (the production context-chunk shape):

kernel TP2 shape (16 q-heads / 2 kv-heads) TFLOP/s TP4 shape (8 / 1)
ck_tile paged batch_prefill, fp8 KV (current path) 14.24 ms 772 (~15% fp8 MFU) 7.21 ms / 762
ck_tile contiguous (page_size 1), fp8 KV 14.45 ms 761 —
ck_tile paged, bf16 KV 17.48 ms 629 —
ASM fmha_fwd_hd256_fp8_causal_group + gather 4.04 ms 2,725 (~55% fp8 MFU) 2.00 ms / 2,753

Paged and contiguous ck_tile run at the same speed, and fp8 is only ~1.2x faster than bf16 there — so paging is not the bottleneck, kernel efficiency is. Tail chunks show the same ratio (4464 new tokens over a 65k context: 2.44 ms vs 8.99 ms). The KV gather this PR adds costs 17.6 µs at 65k context (~7.6 TB/s effective), negligible against the attention itself.

End-to-end serving

End-to-end serving on MI355X, Qwen3.5-397B-A17B-MXFP4, random-ids 70k input / 300 output, EAGLE (3 steps, top-k 1, 4 draft tokens), warmup + measured requests, otherwise the pinned recipe (--attention-backend aiter --page-size 16 --kv-cache-dtype fp8_e4m3 --chunked-prefill-size 16384):

config metric before after delta
TP2 c4 mean E2E 13,393 ms 10,057 ms -24.9%
TP2 c4 mean TTFT 8,274 ms 6,382 ms -22.9%
TP2 c4 mean TPOT 17.12 ms 12.29 ms -28.2%
TP4 c4 mean E2E 8,512 ms 6,415 ms -24.6%
TP4 c4 mean TTFT 4,942 ms 3,638 ms -26.4%
TP4 c8 mean E2E 16,098 ms 11,724 ms -27.2%
TP4 c12 mean E2E 23,690 ms 16,933 ms -28.5%
TP4 c16 mean E2E 31,154 ms 22,585 ms -27.5%

TPOT improves as a side effect: other requests' prefill chunks block decode for less time. No crashes and zero guard fallbacks across all runs above.

Checklist


CI States

Latest PR Test (Base): ❌ Run #33142874228
Latest PR Test (Extra): ❌ Run #33142885073
Latest PR Test (AMD ROCm 7.2): ❌ Run #33142874151

…SM FMHA

On MI355X the ck_tile paged batch_prefill runs at ~15% FP8 MFU for
head_dim-256 fp8-KV context chunks, while the gfx950 ASM varlen fmha runs
the same shapes ~3.5x faster. Gathering the paged fp8 KV into a contiguous
varlen buffer costs ~20 us per layer at 70k context, so extend batches
with a prefix now gather and call the ASM kernel, with a metadata bounds
check that falls back to the paged path on any inconsistency.

70k/300 serving on Qwen3.5-397B-A17B-MXFP4 with EAGLE: mean E2E -25% (TP2
c4) and -25..-29% (TP4 c4-c16); TTFT -23..-26%, TPOT -28%.
Comment thread python/sglang/srt/layers/attention/aiter_backend.py
Comment thread python/sglang/srt/layers/attention/aiter_backend.py
@yichiche yichiche added the run-ci CI: run the baseline test suite on this PR label Aug 28, 2026

@yichiche yichiche 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.

@HaiShaw
HaiShaw merged commit acc918b into sgl-project:main Aug 28, 2026
203 of 242 checks passed
omirosh added a commit to omirosh/sglang that referenced this pull request Aug 28, 2026
The ASM context-prefill gather added in sgl-project#36758 returns before the paged
branch at the end of forward_extend, so on a build that has both the
page-level KV view is dead code: page-64 fp8 hd256 extends with a prefix
gather into a contiguous buffer and never reach the paged-varlen kernel.

Compute paged_asm_available once and read it from both sites - negated by
the gather, affirmed by the paged branch. Two copies of the same condition
would drift; one flag cannot. Where the paged kernel cannot serve the batch
or the layer the flag is False and the gather runs exactly as before, so
this is a handoff rather than a disable.

Co-authored-by: Cursor <cursoragent@cursor.com>
omirosh added a commit to omirosh/sglang that referenced this pull request Sep 14, 2026
Dropped in 8cfd17b once ROCm/aiter#4971 landed, on the reasoning that the
arch and layout tests were enough to gate the path. Sweeps on two MI355X
nodes since then show why that was premature: the paged path wins 1-2%
throughput and 3-7% TTFT at 60k and 128k, but is flat to marginally
negative at 8k, so which path is better depends on the workload rather
than on the shape alone.

Bring the flag back so an operator can pick, defaulting on since the
long-context win is the larger effect and the shape tests still decide
where the path is legal at all. Off restores the sgl-project#36758 gather
exactly, because paged_asm_available is derived from paged_kv_view and
both call sites read the one flag.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants