Skip to content

[AMD][kimik3][ROCm][Perf] Fuse MLA q/kv RMSNorm in AMD Kimi-K3 MLA wrapper - #52494

Merged
dllehr-amd merged 3 commits into
vllm-project:mainfrom
rbrugaro-amd:rocm-kimi-k3-amd-mla-qk-rmsnorm
Sep 4, 2026
Merged

dllehr-amd merged 3 commits into
vllm-project:mainfrom
rbrugaro-amd:rocm-kimi-k3-amd-mla-qk-rmsnorm

Conversation

@rbrugaro-amd

@rbrugaro-amd rbrugaro-amd commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

Fuse the MLA layer's q_a_layernorm / kv_a_layernorm pair into a single
AITER fused_qk_rmsnorm launch for eager Kimi-K3 on ROCm, without putting
vendor checks in shared vllm/model_executor/layers/mla.py.

Kimi-K3 (no @support_torch_compile) never hits MLADualRMSNormFusionPass,
so the two RMSNorms stay as separate AITER launches. NVIDIA already owns a
vendor-local MLA under vllm/models/kimi_k3/nvidia/; this mirrors that layout
for AMD:

  • add vllm/models/kimi_k3/amd/mla.py with
    KimiK3MultiHeadLatentAttentionWrapper
  • wire it from vllm/models/kimi_k3/amd/linear.py
  • leave shared mla.py untouched

The fused path uses torch.ops.vllm.fused_mla_dual_rms_norm when AITER is
enabled and not torch.compiler.is_compiling(), so compiled MLA models keep
using the fusion pass.

Supersedes the shared-mla.py approach in #52215 and the compiler pass approach in #52190

Trace evidence

MI355X TP=8, MTP k=2, ISL 128k / OSL 1k, conc 8, decode-phase torch profile
(rank 0, 6 engine steps):

kernel count
aiter::fused_qk_rmsnorm_kernel<bf16,256,8,true,1> 144 (= 24 MLA × 6 steps)

Paired per-MLA add_rmsnorm launches for the q/kv latents are gone

image

Config used

Image: vllm/vllm-openai-rocm:nightly-cb8104839c141609d99f1254459ef3a4f1bd4263
(+ this PR mounted under vllm/models/kimi_k3/amd/)
Env:

export VLLM_ROCM_USE_AITER=1
export VLLM_ROCM_AITER_MLA_ASM_PADDING=asm
export VLLM_ROCM_USE_AITER_MOE_SITUV2_A8W4=1
export AITER_BF16_FP8_MOE_BOUND=0
export VLLM_USE_BREAKABLE_CUDAGRAPH=0
export SAFETENSORS_FAST_GPU=1
vllm serve <Kimi-K3> --served-model-name moonshotai/Kimi-K3 \
  --tensor-parallel-size 8 --moe-backend auto --trust-remote-code \
  --load-format dummy --gpu-memory-utilization 0.9 --language-model-only \
  --max-num-seqs 20 --max-model-len 1048576 \
  --kv-cache-dtype fp8 --enable-prefix-caching \
  --tool-call-parser kimi_k3 --reasoning-parser kimi_k3 \
  --compilation-config '{"mode":3,"cudagraph_mode":"FULL_AND_PIECEWISE","max_cudagraph_capture_size":60,"custom_ops":["+fused_rms_norm_gated"],"cudagraph_capture_sizes":[1,2,...,60]}' \
  --speculative-config '{"model":"Inferact/Kimi-K3-DSpark","num_speculative_tokens":2,"method":"dspark","attention_backend":"TRITON_MLA","kv_cache_dtype":"auto","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' \
  --profiler-config '{"profiler":"torch","torch_profiler_dir":"<out>/trace","torch_profiler_use_gzip":true,"ignore_frontend":true,"delay_iterations":0,"max_iterations":6}'

GSM8k accuracy:

2026-08-16:06:25:52 INFO     [_cli.run:375] Selected Tasks: [{'tag': ['math_word_problems'], 'task': 'gsm8k', 'dataset_path': 'openai/gsm8k', 'dataset_name': 'main', 'output_type': 'generate_until', 'training_split': 'train', 'fewshot_split': 'train', 'test_split': 'test', 'doc_to_text': 'Question: {{question}}\nEnd your response with the answer on the last line, formatted as: #### [number]\nAnswer:', 'doc_to_target': '{{answer}}', 'metric_list': [{'metric': 'exact_match', 'aggregation': 'mean', 'higher_is_better': True, 'ignore_case': True, 'ignore_punctuation': False, 'regexes_to_ignore': [',', '\\$', '(?s).*#### ', '\\.$']}], 'generation_kwargs': {'until': ['</s>', '<|im_end|>'], 'do_sample': False, 'temperature': 0.0}, 'repeats': 1, 'num_fewshot': 5, 'filter_list': [{'name': 'strict-match', 'filter': [{'function': 'regex', 'group_select': -1, 'regex_pattern': '#### (\\-?[0-9\\.\\,]+)'}, {'function': 'take_first'}]}, {'name': 'flexible-extract', 'filter': [{'function': 'regex', 'group_select': -1, 'regex_pattern': '(-?[$0-9.,]{2,})|(-?[0-9]+)'}, {'function': 'take_first'}]}], 'metadata': {'version': 3.0}}]
2026-08-16:06:37:46 INFO     [loggers.evaluation_tracker:209] Saving results aggregated
|gsm8k|      3|flexible-extract|     5|exact_match|↑  |0.9644|±  |0.0051|
|     |       |strict-match    |     5|exact_match|↑  |0.9644|±  |0.0051|

Performance

aiperf profile --model moonshotai/Kimi-K3 \
  --tokenizer /data/workloads-inference/models/Kimi-K3 \
  --tokenizer-trust-remote-code \
  --url http://127.0.0.1:8000/ --api-key EMPTY \
  --endpoint-type chat --streaming --use-server-token-count \
  --num-prefix-prompts 8 --prompt-prefix-length 63240 \
  --synthetic-input-tokens-mean 4760 --synthetic-input-tokens-stddev 0 \
  --output-tokens-mean 350 --output-tokens-stddev 0 \
  --extra-inputs ignore_eos:true \
  --extra-inputs min_tokens:350 --extra-inputs max_tokens:350 \
  --warmup-request-count 3 --sweep-type zip \
  --concurrency 1,2,8,16,32 \
  --request-count 5,10,40,80,160 \
  --random-seed 42 --ui simple

(--tokenizer is the local Kimi-K3 checkout; equivalent to moonshotai/Kimi-K3
with hub access. Idle-node baseline reruns used the same flags with
--concurrency / --request-count subset to the point under test.)

conc out tok/s base opt Δ% itl p50 (ms) base opt Δ% ttft p50 (ms) base opt Δ%
1 † 42.3 42.2 -0.2% 11.22 11.15 -0.6% 5056 5067 +0.2%
2 † 58.3 58.7 +0.7% 14.71 12.87 -12.5% 5593 5974 +6.8%
8 † 142.9 150.1 +5.0% 30.26 31.70 +4.8% 1452 1741 +19.9%
16 215.0 219.4 +2.1% 49.46 48.35 -2.2% 1817 1938 +6.7%
32 258.2 270.6 +4.8% 59.84 57.15 -4.5% 15367 15100 -1.7%

Keep the AITER fused_qk_rmsnorm path for eager Kimi-K3 out of shared
mla.py by subclassing MultiHeadLatentAttentionWrapper under
vllm/models/kimi_k3/amd, matching the vendor-local model layout.

Signed-off-by: Rita Brugarolas Brufau <rita.brugarolasbrufau@amd.com>
@mergify mergify Bot added kimi k3 rocm Related to AMD ROCm labels Aug 16, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Aug 16, 2026
@rbrugaro-amd
rbrugaro-amd marked this pull request as ready for review August 24, 2026 04:54

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

@jamesETsmith

Copy link
Copy Markdown
Contributor

@rbrugaro-amd this looks great! Just a small question: the ITL and TTFT for the conc 8 run in your results seem to move in the opposite direction as the rest of your results. Is that anomalous?

@rbrugaro-amd

Copy link
Copy Markdown
Contributor Author

Good catch @jamesETsmith — that’s an artifact of the table, not of C=8 fusion behavior.

The posted C=8 baseline was a standalone idle rerun (3 warmups, fresh server) while optimized C=8 came from the zip sweep after C1/C2, so prefix-cache mix differed. Same-protocol C=8 (both sides from the original zip sweep) is flat: 150.6 vs 150.1 tok/s; ITL mean 36.96 vs 36.75; identical TTFT histograms.

p50 TTFT/ITL at conc 8 is bimodal (cached prefix vs queued prefill) because that run used --num-prefix-prompts 8 with only 3 warmups. C=16 has the same split (TTFT p50 ~1.8s vs p90 ~29s). We’ll lean on mean / p90 / throughput there.

Updated table: all five concs from the original zip sweep (fusion off vs on). Idle-node C1/C2/C8 reruns dropped.

conc out tok/s base opt Δ% ITL p50 (ms) base opt Δ% ITL p90 (ms) base opt Δ% TTFT p50 (ms) base opt Δ% TTFT p90 (ms) base opt Δ%
1 42.3 42.2 -0.2% 11.22 11.15 -0.7% 11.27 11.28 +0.1% 5057 5067 +0.2% 5538 5553 +0.3%
2 58.7 58.7 +0.0% 13.90 12.87 -7.4% 23.74 22.53 -5.1% 5977 5974 -0.1% 10516 10453 -0.6%
8 150.6 150.1 -0.3% 32.07 31.70 -1.1% 60.47 60.68 +0.4% 1978 1741 -12.0% 16836 16846 +0.1%
16 215.0 219.4 +2.1% 49.46 48.35 -2.2% 80.15 73.31 -8.5% 1817 1938 +6.7% 28853 29053 +0.7%
32 258.2 270.6 +4.8% 59.84 57.15 -4.5% 74.19 65.82 -11.3% 15367 15100 -1.7% 47132 41822 -11.3%

The remaining cross-conc sign flips are not a fusion story. This opt removes one kernel launch with the same FLOPs, and only on MLA layers (~1:3 vs KDA), so E2E is a small decode-step slice. A 68k-prefix / 350-decode agentic sweep is dominated by prefill + prefix-cache queueing, so those deltas are noise (and the C32 tok/s bump is the opposite of “launch savings show up at low conc”).

To check the implementation under a tighter decode constraint I ran fixed 8k/1k, no prefix-cache sharing, C=1/4/8, fusion off vs on (ITL is the decode metric; TTFT still includes 8k prefill):

aiperf profile --model moonshotai/Kimi-K3 \
  --tokenizer /data/workloads-inference/models/Kimi-K3 \
  --tokenizer-trust-remote-code \
  --url http://127.0.0.1:8000 --api-key EMPTY \
  --endpoint-type chat --streaming --use-server-token-count \
  --synthetic-input-tokens-mean 8192 --synthetic-input-tokens-stddev 0 \
  --output-tokens-mean 1024 --output-tokens-stddev 0 \
  --extra-inputs ignore_eos:true \
  --extra-inputs min_tokens:1024 --extra-inputs max_tokens:1024 \
  --warmup-request-count 3 --sweep-type zip \
  --concurrency 1,4,8 --request-count 10,20,40 \
  --random-seed 42 --ui simple
conc out tok/s base opt Δ% ITL avg (ms) base opt Δ% ITL p50 base opt Δ% ITL p90 base opt Δ%
1 86.2 86.5 +0.3% 9.94 10.03 +0.9% 10.77 10.72 -0.4% 10.84 10.78 -0.6%
4 246.7 247.5 +0.3% 13.21 13.81 +4.5% 14.05 14.02 -0.2% 15.26 15.16 -0.7%
8 420.7 421.3 +0.2% 15.26 15.48 +1.4% 15.84 16.00 +1.0% 18.24 18.33 +0.5%

Decode ITL p50/p90 at C=1 is ~0.5% — consistent with a launch-only MLA fusion being smaller than serving noise. Correctness of the path is the decode kernel census (144× fused_qk_rmsnorm = 24 MLA × 6 steps).

@dllehr-amd

Copy link
Copy Markdown
Contributor

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #85999 for commit 8ddd333e0377.

@rbrugaro-amd rbrugaro-amd changed the title [kimik3][ROCm][Perf] Fuse MLA q/kv RMSNorm in AMD Kimi-K3 MLA wrapper [AMD][kimik3][ROCm][Perf] Fuse MLA q/kv RMSNorm in AMD Kimi-K3 MLA wrapper Aug 30, 2026
@rbrugaro-amd

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

❌ @rbrugaro-amd, A reviewer with write access must run /ci run, approve the PR, or add the ready label first.

@rbrugaro-amd

Copy link
Copy Markdown
Contributor Author

@dllehr-amd @tjtanaa could you please retry ci? the failed test is unrelated to PR changes

@tjtanaa tjtanaa added the verified Run pre-commit for new contributors without triggering other tests label Aug 30, 2026
@tjtanaa

tjtanaa commented Aug 30, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #86193 for commit 8ddd333e0377.

@AndreasKaratzas AndreasKaratzas added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ @rbrugaro-amd, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@rbrugaro-amd

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #86659 for commit 8ddd333e0377.

@dllehr-amd dllehr-amd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @rbrugaro-amd ! I think we can move forward with this!

@dllehr-amd
dllehr-amd enabled auto-merge (squash) September 3, 2026 20:53
@coderabbitai

coderabbitai Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: cdbe9106-d69c-4790-9938-3773592aa8bc

📥 Commits

Reviewing files that changed from the base of the PR and between cee0f92 and 3c0cf00.

📒 Files selected for processing (2)
  • vllm/models/kimi_k3/amd/linear.py
  • vllm/models/kimi_k3/amd/mla.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Summary

Summary by CodeRabbit

  • New Features
    • Added an AMD/ROCm-optimized attention implementation for Kimi-K3 models.
    • Enabled fused query/key-value normalization when supported, with automatic fallback to standard processing.
    • Preserved support for rotary embeddings, sparse indexing, scaling, distributed execution, and gated output projection.

Walkthrough

The change adds an AMD-specific Kimi-K3 MLA wrapper with optional fused RMSNorm and routes KimiMLAAttention through this wrapper.

Changes

AMD Kimi-K3 MLA

Layer / File(s) Summary
Implement AMD MLA execution
vllm/models/kimi_k3/amd/mla.py
Adds KimiK3MultiHeadLatentAttentionWrapper. The wrapper supports fused q/kv RMSNorm and implements projection, rotary embedding, sparse indexing, DCP replication, attention, gating, and output projection.
Wire wrapper into Kimi attention
vllm/models/kimi_k3/amd/linear.py
Replaces the generic MLA wrapper with KimiK3MultiHeadLatentAttentionWrapper in KimiMLAAttention.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 3c0cf

This change routes AMD Kimi-K3 MLA through a vendor-local wrapper that fuses eager RMSNorm launches while preserving the compiled execution path. No merge-blocking correctness or runtime risk remains identified.

Sequence Diagram(s)

sequenceDiagram
  participant KimiMLAAttention
  participant KimiK3MultiHeadLatentAttentionWrapper
  participant fused_qkv_a_proj
  participant mla_attn
  participant o_proj
  KimiMLAAttention->>KimiK3MultiHeadLatentAttentionWrapper: forward positions and hidden states
  KimiK3MultiHeadLatentAttentionWrapper->>fused_qkv_a_proj: project q and kv when q_lora_rank is set
  KimiK3MultiHeadLatentAttentionWrapper->>mla_attn: execute latent attention
  mla_attn-->>KimiK3MultiHeadLatentAttentionWrapper: return attention output
  KimiK3MultiHeadLatentAttentionWrapper->>o_proj: project gated output
  o_proj-->>KimiMLAAttention: return attention result
Loading

Suggested reviewers: kliuae

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the AMD Kimi-K3 ROCm performance change: fusing MLA q/kv RMSNorm operations in the AMD wrapper.
Description check ✅ Passed The description directly explains the AMD-specific wrapper, fused AITER RMSNorm path, wiring changes, compiler behavior, profiling evidence, and performance results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dllehr-amd

Copy link
Copy Markdown
Contributor

/ci run

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #87185 for commit 3c0cf009c566.

@dllehr-amd
dllehr-amd merged commit 3ff4f02 into vllm-project:main Sep 4, 2026
67 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in AMD Sep 4, 2026
ItsRoy69 pushed a commit to ItsRoy69/vllm that referenced this pull request Sep 10, 2026
…apper (vllm-project#52494)

Signed-off-by: Rita Brugarolas Brufau <rita.brugarolasbrufau@amd.com>
Signed-off-by: Jyotirmoy Roy <jyotirmoyroy649@gmail.com>
rbrugaro-amd added a commit to rbrugaro-amd/vllm that referenced this pull request Sep 22, 2026
…ITER

On a stock build, Kimi-K3 MLA decode runs two kernels per layer per step: the
fp8 KV-cache write (`vllm::concat_and_cache_mla_kernel`) and the
`[ql_nope | q_pe]` concat plus that query's static fp8 quant, which the
`mla_decode_concat_quant_fp8` custom op emits as a single
`triton_poi_fused__to_copy_cat_clamp_mul_reciprocal_view` kernel. Collapse both
into one AITER `fused_qk_rope_concat_and_cache_mla` launch.

Measured on MI355X at TP=8, plain `vllm serve` on the stock upstream nightly with
no speculative decoding and no compilation-config override: 288 -> 144 launches
and 1351.8 -> 682.8 us per rank over 6 decode steps, i.e. 9.39 -> 4.74 us per MLA
layer per decode step. End-to-end serving throughput is unchanged within
measurement noise and this change does not claim a throughput win.

K3 MLA is NoPE, while the kernel always applies RoPE and takes the cos/sin
caches as required arguments. Feed it an identity rotation (cos=1, sin=0) so the
rotation is a no-op -- the approach ROCm/ATOM uses for K3. The cache is a single
row: the `_opt` decode kernel clamps `pos` into `[0, cos_cache.size(0))`, so real
positions pass through untouched and K3's 1M max_position_embeddings does not
cost ~268 MB to express a no-op. AITER's general decode kernel does not clamp,
so the head counts that would select it are excluded.

Kept entirely in `vllm/models/kimi_k3/amd/mla.py`, mirroring the q/kv RMSNorm
fusion this wrapper already owns (vllm-project#52494); no shared MLA code is touched, so
this carries no dependency on vllm-project#47757 and cannot affect any other model or
backend. Everything that is fixed at load time -- platform, KV dtype, shapes,
DCP/PCP/HiSparse, head padding and the W_UK variant -- is resolved once in
`_fused_qk_prep_supported()`, so the only per-batch condition is that the batch
is decode-only; anything else falls through to the stock `MLAAttention` path.

Signed-off-by: Rita Brugarolas Brufau <rita.brugarolasbrufau@amd.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Rita Brugarolas Brufau <rita.brugarolasbrufau@amd.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

k3 kimi ready ONLY add when PR is ready to merge/full CI is needed rocm Related to AMD ROCm verified Run pre-commit for new contributors without triggering other tests

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants