Repository navigation
[ROCm][Spec Decode] Add Aiter MLA decode support non-causal draft block - #53001
Closed
JohnQinAMD wants to merge 1 commit into
Closed
JohnQinAMD wants to merge 1 commit into
JohnQinAMD wants to merge 1 commit into
Conversation
JohnQinAMD
requested review from
AndreasKaratzas,
LucasWilkinson,
MatthewBonanni,
pavanimajety and
tjtanaa
as code owners
August 19, 2026 20:35
JohnQinAMD
force-pushed
the
aiter-noncausal-decode
branch
5 times, most recently
from
August 19, 2026 22:31
2af1934 to
2bd685b
Compare
JohnQinAMD
force-pushed
the
aiter-noncausal-decode
branch
7 times, most recently
from
August 20, 2026 02:50
8977b7a to
ee8377f
Compare
… block A Kimi-K3 DSpark draft has to be pinned to TritonMLA on ROCm, because that is the only ROCm MLA backend that declares it can serve a non-causal query block. TritonMLA and FlashInferMLA get there by flattening the block into one row per query token, which is a repeat_interleave for a kernel that takes a block table. AiterMLA's takes a flat page-size-1 index list, so flattening would rebuild that list every forward. aiter's decode takes the mask instead: mla_decode_fwd grew a causal argument and get_mla_metadata_v1 always had is_causal, which vLLM hardcodes to True. Passing the real value through both runs the other mask on the same one-row-per-request call. The schedule is mask-dependent, so _build_decode needs it while it builds. Gluon masks the block causally with no way to turn it off, so a non-causal block cannot go there -- and does not need to, since padding the head count up gives it an asm kernel exactly as the fp8 path already does. use_gluon_verify now turns a non-causal block away at any head count or cache dtype, which is what lets a bf16 cache serve one at all. What is left for _build_decode is a block that missed the persistent schedule, and on fp8 a 2-token block, the one length aiter's dispatch has no fold for. vLLM pins aiter v0.1.19, which has no causal argument, so the default image is unchanged. On 8x MI355X, dropping the pin moves output throughput from 68.44 to 76.32 tok/s at concurrency 1 and from 294.57 to 324.44 at 8 with an fp8 cache; a bf16 cache, which had no non-causal path before, reaches 84.16 and 331.66. Full GSM8K is 0.9613 on fp8 and 0.9644 on bf16, against 0.9644 pinned. Co-authored-by: Claude <noreply@anthropic.com>
JohnQinAMD
force-pushed
the
aiter-noncausal-decode
branch
from
August 20, 2026 02:53
ee8377f to
02568e3
Compare
3 tasks done
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
1 of 2 tasks
Contributor
|
This branch is still conflicting with Rebased copy (current |
ppalanga
pushed a commit
to ppalanga/vllm
that referenced
this pull request
Sep 9, 2026
… blocks. Rebase vllm-project#53001 onto current main: pass the real mask into aiter MLA decode instead of flattening the draft query block, and keep Gluon/DCP routing causal-only. Co-authored-by: John Qin <zhengsheng1997@gmail.com> Signed-off-by: Poovaiah Palangappa <poovaiah.palangappa@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Poovaiah Palangappa <poovaiah.palangappa@amd.com>
Contributor
Author
|
close and replaced by #55966 |
2 of 7 tasks
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.
Purpose
On ROCm a Kimi-K3 DSpark draft has to be pinned to TritonMLA:
The draft's query block is non-causal. On ROCm only TritonMLA sets
supports_non_causal_multi_token_decode, so the pin.TritonMLA and FlashInferMLA serve such a block by flattening it: one decode row per query token, every row reading the same committed KV. That is cheap for them because their kernels take a block table, so flattening is a
repeat_interleave. AiterMLA's kernel takes a flat page-size-1 index list instead. Flattening there means rebuilding that list every forward -- roughly half a million entries at batch 8, block 8 and an 8k context.Fix: don't flatten. Pass the mask, which the kernel already accepts.
get_mla_metadata_v1has always takenis_causal. aiter'smla_decode_fwdtakescausalas of v0.1.20. vLLM hardcoded the first and never passed the second:Both now get the real value. Same one row per request, same index list.
Three pieces make that safe.
Selection. AiterMLA offers a non-causal block only if the installed
mla_decode_fwdtakescausal. Probed by signature, the same way this file already probes for fp8 scale support.Routing. Gluon's mask is causal and cannot be turned off, so
use_gluon_verifynow returns False for any non-causal block. Such a block takes the padded asm decode instead. Padding the head count up is what gives it a kernel there -- the fp8 path already relies on that. This is also what lets a bf16 cache serve a non-causal block at all. Builder and impl call the same predicate, so they cannot disagree.Build. One shape still reaches
_build_decodeand it raises: an fp8 2-token block. aiter's dispatch has no fold for that length, so the lookup lands on a causal-only kernel, and a missing kernel aborts the process rather than raising. ROCm/aiter#4872 adds the missing entry; once the pinned aiter carries it, this guard and its test can go. The bf16 fold has no such hole.Outside ROCm,
_build_decodegains acausalkwarg inMLACommonMetadataBuilder. The FlashAttn, FlashInfer, FlashMLA and Dots3Note overrides are signature-only updates. DCP is untouched;supports_non_causal_multi_token_dcpstays False.vLLM pins aiter v0.1.19 (
docker/Dockerfile.rocm_base). That build has nocausalargument, so on the default image this PR is a no-op:supports_non_causal()returns False, selection keeps handing the draft group to TritonMLA, and the recipe keeps its pin. It starts working once the pin moves.Test Plan
pytest tests/v1/attention/test_rocm_aiter_mla_non_causal.py # 15, CPU onlyThey stub
aiter.mla, so both aiter generations can be exercised on one machine. The two_build_decodeguards are covered intest_rocm_aiter_mla_mtp_split.py. That file already drives the method directly.End to end on 8x MI355X,
/model= Kimi-K3 and/draftits DSpark draft. Same server twice; the only difference is whether the draft is pinned:Drop
--kv-cache-dtype fp8and run again for the bf16 numbers. That run also needs #50619, for an unrelated main-branch cudagraph defect in the target's causal verify.Both were served against a v0.1.19 install with v0.1.20's MLA files copied in. That install reproduces a full v0.1.20 tree's kernel numbers bit for bit.
Test Result
Unit tests: 15 + 46 passed.
Both runs land on
FULL_AND_PIECEWISEwith no cudagraph downgrade. Unpinning drops the second attention backend. So the gap below is the draft's backend and nothing else.That is 11.5% at concurrency 1 and 10.1% at 8.
A bf16 cache reaches 84.16 tok/s at concurrency 1 and 331.66 at 8, TPOT 10.45 ms and 20.76 ms.
Accuracy, full GSM8K test set (1319 questions), 5-shot, greedy, fp8 KV:
At the kernel, checked with ad-hoc scripts rather than in-tree tests:
With an fp8 cache and 16 heads,
causal=Falselands 7.1e-03 from a non-causal reference. Against a causal one it sits 6.3e-02 away at block 4 and 9.1e-02 at block 8. So it runs the mask that was asked for. Four identical query rows come back identical, spread 0.0.With a bf16 cache the padded asm decode holds 3.5e-04 to 5.6e-04. That is across 8, 12, 16, 24 and 32 heads, and block lengths 1 through 16. fp8 covers 1 and 3 through 16.
Written with AI assistance (Claude Code); the commit carries
Co-authored-by: Claude. I reviewed every changed line and ran the tests above myself.