Repository navigation
[Perf] [AMD] Qwen3.8: Serve QSA decode with AITER's fused FlyDSL sparse decode on gfx950 - #45196
Open
amd-nprotaso wants to merge 6 commits into
Open
amd-nprotaso wants to merge 6 commits into
amd-nprotaso wants to merge 6 commits into
Conversation
On gfx950, QwenSparseAttnBackend.forward_decode now hands sparse decode and speculative verify to aiter.ops.flydsl.qsa_sparse_decode, which reads each row's selected KV straight from the pool through req_to_token (no gather pass, no packed scratch) and splits each row across CTAs. The Triton fallback it replaces runs one program per (row, kv head). The workspace is sized once at the captured maximum batch with workspace_partials, since CUDA graphs keep raw pointers to it; a larger eager batch gets a per-call workspace. SGLANG_ENABLE_QSA_FLYDSL_DECODE (default on) turns the path off; other GPUs, operands the kernel does not support, or an AITER build without the module fall back to the existing path. Requires AITER with aiter.ops.flydsl.qsa_sparse_decode. Kernel correctness tests live in AITER (op_tests/test_flydsl_qsa_sparse_decode.py); here test_qsa_flydsl_decode.py checks the backend wiring against the Triton fallback, and bench_qsa_sparse_decode.py compares the two. Co-Authored-By: Claude <noreply@anthropic.com>
amd-nprotaso
requested review from
Fridge003,
HaiShaw,
Qiaolin-Yu,
hebiao064,
ispobock and
merrymercy
as code owners
October 19, 2026 15:16
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
1am9trash
reviewed
Oct 23, 2026
| @lru_cache(maxsize=1) | ||
| def _resolve_flydsl_sparse_decode(): | ||
| """The fused gfx950 FlyDSL decode module, or None when off or unavailable.""" | ||
| if not (is_hip() and envs.SGLANG_ENABLE_QSA_FLYDSL_DECODE.get()): |
Collaborator
There was a problem hiding this comment.
Hi, @amd-nprotaso
Should _resolve_flydsl_sparse_decode() also check et_bool_env_var("SGLANG_USE_AITER")?
Contributor
Author
There was a problem hiding this comment.
I removed SGLANG_ENABLE_QSA_FLYDSL_DECODE and use SGLANG_USE_AITER instead.
1am9trash
approved these changes
Oct 27, 2026
1am9trash
left a comment
Collaborator
There was a problem hiding this comment.
LGTM. The comments resolved (small nits).
Only impact on amd code path.
Collaborator
sogalin
approved these changes
Oct 28, 2026
Contributor
Author
Updated |
Contributor
Author
|
ROCm/aiter#6670 was merged |
6 of 11 tasks
5 of 11 tasks
This branch has not been deployed
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
On ROCm, Qwen3.8 QSA decode and speculative verify run the sparse attention as two passes:
At decode batch sizes that is an extra round trip through the selected KV, and most of the GPU sits idle. At 32k context with MTP on one MI355X, QSA attention takes about 9.4% of decode GPU time, roughly 120 µs per layer call.
AITER now has a fused FlyDSL kernel for gfx950:
aiter.ops.flydsl.qsa_sparse_decode(AITER PR). It reads the selected KV straight from the pool throughreq_to_tokenand splits each row's selection across CTAs. This PR serves QSA decode and verify with it.Modifications
QwenSparseAttnBackend.forward_decode→_forward_paged_attentiontakes the fused kernel first on HIP whencan_use_flydsl_qsa_sparse_decodeaccepts the operands: gfx950, bf16 q, bf16 or fp8 KV, head_dim 256, GQA group ≤ 16. That covers decode, target verify and draft extend, since each query row is its own length-one query. Everything else falls through to the existing path unchanged.recommended_num_splitssets the KV split count per batch size, so it is fixed for each captured CUDA graph.workspace_partials, because graphs keep raw pointers to it. A larger eager batch gets a per-call workspace.test/registered/kernels/ops/attention/qsa/test_qsa_flydsl_decode.pychecks that the backend gives the kernel the same rows, lengths and per-row request map as the Triton fallback, and that the outputs match.op_tests/test_flydsl_qsa_sparse_decode.py).test/registered/kernels/benchmark/attention/bench_qsa_sparse_decode.pycompares the two paths.Requires AITER with
aiter.ops.flydsl.qsa_sparse_decode. No recommended-deployment flags change, so the cookbook page is unchanged.Accuracy Tests
GSM8K through the chat API: greedy, thinking off, final number scored, 1024 max tokens, MTP on with real acceptance.
Accept length is total completion tokens ÷ total verify steps, from each response's
spec_tokens_details.Repeated baseline runs vary by about 1 point on short GSM8K, so the accuracy differences are within noise.
At the kernel level, against an fp64 reference at Qwen3.8 TP1 shapes (Hq=24, Hkv=2, top-k 2051), the fused kernel is more accurate than the Triton path it replaces:
Speed Tests and Profiling
Setup:
--attention-backend aiter --page-size 64, MTP NEXTN 3/1/4,--max-running-requests 16.SGLANG_SIMULATE_ACC_LEN=2.32), so both arms do identical verify work; accept length is 2.32 in both.End to end: ISL 32768 / OSL 32768, 24 prompts at concurrency 8, 8 warmup requests (
sglang.benchmark.serving, random dataset).TTFT is prefill, which this PR does not touch; the difference is run-to-run noise.
Decode profile: 40 decode steps at full batch, 8 requests at 32k context, MTP.
Triton per call:
_sparse_gqa_chunk_prefill71.8 µs +_compact_kv39.4 µs +_fa2_valid_counts4.4 µs +_fa2_prefix_sum4.4 µs. FlyDSL per call:sparse_decode_attn_kernel28.9 µs +sparse_decode_reduce_kernel4.4 µs.Kernel benchmark:
bench_qsa_sparse_decode.py, top-k 2051, cold cache, CUDA graph.CI States
Latest PR Test (Base): ❌ Run #40664232734
Latest PR Test (Extra): ❌ Run #40664232716
Latest PR Test (AMD ROCm 10): ❌ Run #40664232700