Skip to content

[Perf][ROCm][Attention] Fuse Kimi-K3 MLA chunked-context gather on AITER - #53166

Open
nsusanto wants to merge 7 commits into
vllm-project:mainfrom
nsusanto:rocm-mla-fused-context-gather
Open

nsusanto wants to merge 7 commits into
vllm-project:mainfrom
nsusanto:rocm-mla-fused-context-gather

Conversation

@nsusanto

@nsusanto nsusanto commented Aug 20, 2026 •

Copy link
Copy Markdown

Purpose

The dense-MLA prefill spends four kernels per context chunk in
_compute_prefill_context:

  • vllm::gather_and_maybe_dequant_cache<bf16,bf16,(0),576,64>
  • Cijk_…MT192x160x64 (kv_b_proj expansion)
  • at::native direct_copy (k[..., :128] = k_nope)
  • at::native direct_copy (k[..., 128:] = k_pe)

AITER ships gather_kv_b_proj, which does all four in one launch. AiterMLAImpl._compute_prefill_context
now calls it per context chunk, with the per-token KV indices built once per step in the metadata builder.
The fused gather runs only without decode context parallelism (dcp_world_size == 1); DCP > 1 keeps
main's DCP prefill path.

AiterMLAImpl._concat_k_nope_k_pe now calls the already-merged fused_kimi_k3_mla_kv_concat (#51772)
instead of two slice-assign copies, gated on the MLA dims that kernel asserts (STD_TORCH_CHECK on
128-wide NoPE / 64-wide rope).

Test Plan

Unit tests: tests/kernels/attention/test_rocm_aiter_mla_prefill_fusions.py (11 cases: fused vs unfused
prefill context, KV indices vs the block table, declined inputs, k concat), all pass on MI355X.

Accuracy:

lm_eval \
  --model local-completions \
  --model_args "model=/data/Kimi-K3/,base_url=http://localhost:8001/v1/completions,num_concurrent=64,max_retries=3,tokenized_requests=False,tokenizer_backend=None,trust_remote_code=True" \
  --tasks gsm8k \
  --num_fewshot 5 \
  --seed 42 \
  --output_path /app/lm_eval_out --log_samples

Performance on the InferenceX Kimi-K3 MXFP4 agentic recipe (8x MI355X, image
vllm/vllm-openai-rocm:nightly-rocm100-36768d1bf, vLLM 0.30.1rc1.dev312+g36768d1bf, AITER v0.1.23).
Before = the image; After = the image with this PR's rocm_aiter_mla.py. One run per point after a warm-up,
10 x concurrency prompts.

Server command (conc 14: DCP 1, DSpark k=3)

export VLLM_ROCM_AITER_MLA_ASM_PADDING=asm VLLM_ROCM_USE_AITER=1 SAFETENSORS_FAST_GPU=1
export VLLM_ROCM_USE_AITER_MOE_SITUV2=1 AITER_BF16_FP8_MOE_BOUND=0 VLLM_ROCM_QUICK_REDUCE_QUANTIZATION=INT4
export HSA_NO_SCRATCH_RECLAIM=1 VLLM_ENGINE_READY_TIMEOUT_S=7200 PYTHONNOUSERSITE=1 VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=1200
export VLLM_USE_DIRECT_DCP_A2A=0 VLLM_USE_DIRECT_DCP_Q_GATHER=0 VLLM_USE_DIRECT_DCP_KV_GATHER=0
export VLLM_USE_BREAKABLE_CUDAGRAPH=0 PYTHONHASHSEED=42
vllm serve /data/Kimi-K3 \
  --served-model-name moonshotai/Kimi-K3 \
  --host 0.0.0.0 \
  --port 8002 \
  --trust-remote-code \
  --moe-backend auto \
  --tensor-parallel-size 8 \
  --load-format fastsafetensors \
  --gpu-memory-utilization 0.9 \
  --language-model-only \
  --max-num-seqs 28 \
  --enable-auto-tool-choice \
  --tool-call-parser kimi_k3 \
  --reasoning-parser kimi_k3 \
  --max-model-len 1048576 \
  --enable-prefix-caching \
  --kv-cache-dtype fp8 \
  --max-num-batched-tokens 8192 \
  --attention-config '{"mla_prefill_backend":"ROCM_AITER_FA"}' \
  --compilation-config '{"mode":3,"cudagraph_mode":"FULL","max_cudagraph_capture_size":112,"custom_ops":["+fused_rms_norm_gated"],"cudagraph_capture_sizes":[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112]}' \
  --speculative-config '{"model":"Inferact/Kimi-K3-DSpark","num_speculative_tokens":3,"method":"dspark","attention_backend":"ROCM_AITER_MLA","kv_cache_dtype":"fp8","draft_sample_method":"probabilistic","rejection_sample_method":"block"}' \
  --kv-transfer-config '{"kv_connector":"SimpleCPUOffloadConnector","kv_role":"kv_both","kv_connector_extra_config":{"cpu_bytes_to_use_per_rank":224875000000,"lazy_offload":false}}'

Server command (conc 70: DCP 8, no draft model)

export VLLM_ROCM_AITER_MLA_ASM_PADDING=asm VLLM_ROCM_USE_AITER=1 SAFETENSORS_FAST_GPU=1
export VLLM_ROCM_USE_AITER_MOE_SITUV2=1 AITER_BF16_FP8_MOE_BOUND=0 VLLM_ROCM_QUICK_REDUCE_QUANTIZATION=INT4
export HSA_NO_SCRATCH_RECLAIM=1 VLLM_ENGINE_READY_TIMEOUT_S=7200 PYTHONNOUSERSITE=1 VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=1200
export VLLM_USE_DIRECT_DCP_A2A=0 VLLM_USE_DIRECT_DCP_Q_GATHER=0 VLLM_USE_DIRECT_DCP_KV_GATHER=0
export VLLM_USE_BREAKABLE_CUDAGRAPH=0 PYTHONHASHSEED=42
vllm serve /data/Kimi-K3 \
  --served-model-name moonshotai/Kimi-K3 \
  --host 0.0.0.0 \
  --port 8002 \
  --trust-remote-code \
  --moe-backend auto \
  --tensor-parallel-size 8 \
  --load-format fastsafetensors \
  --gpu-memory-utilization 0.9 \
  --language-model-only \
  --max-num-seqs 140 \
  --enable-auto-tool-choice \
  --tool-call-parser kimi_k3 \
  --reasoning-parser kimi_k3 \
  --max-model-len 1048576 \
  --enable-prefix-caching \
  --kv-cache-dtype fp8 \
  --max-num-batched-tokens 8192 \
  --attention-config '{"mla_prefill_backend":"ROCM_AITER_FA"}' \
  --compilation-config '{"mode":3,"cudagraph_mode":"FULL_AND_PIECEWISE","custom_ops":["+fused_rms_norm_gated"],"cudagraph_capture_sizes":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,256,512,1024,2048,4096,8192]}' \
  --kv-transfer-config '{"kv_connector":"SimpleCPUOffloadConnector","kv_role":"kv_both","kv_connector_extra_config":{"cpu_bytes_to_use_per_rank":224875000000,"lazy_offload":false}}' \
  --decode-context-parallel-size 8 \
  --dcp-comm-backend a2a

The conc-70 DCP 1 rows below use the same command without --decode-context-parallel-size 8 --dcp-comm-backend a2a.

Bench command

vllm bench serve --backend openai-chat --model moonshotai/Kimi-K3 --tokenizer /data/Kimi-K3 --trust-remote-code \
  --host localhost --port 8002 --endpoint /v1/chat/completions \
  --dataset-name random --random-input-len <ISL> --random-output-len <OSL> --random-range-ratio 0 \
  --request-rate=inf --random-prefix-len 0 --num-prompts <10 x CONC> --max-concurrency <CONC> \
  --ignore-eos --seed 43 --percentile-metrics ttft,tpot,itl,e2el

Test Results

gsm8k:

|Tasks|Version|     Filter     |n-shot|  Metric   |   |Value |   |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k|      3|flexible-extract|     5|exact_match|↑  |0.9591|±  |0.0055|
|     |       |strict-match    |     5|exact_match|↑  |0.9591|±  |0.0055|

Conc 14, (DCP 1, gather_kv_b_proj on)

Used "rejection_sample_method":"synthetic","synthetic_acceptance_length":1.9 to keep speculative decode static

ISL/OSL Metric Before After Delta
1k/1k Output throughput (tok/s) 681.60 692.60 +1.6%
1k/1k Mean TTFT (ms) 342.00 339.00 -0.9%
1k/1k Mean TPOT (ms) 19.46 19.23 -1.2%
8k/1k Output throughput (tok/s) 292.10 299.04 +2.4%
8k/1k Mean TTFT (ms) 3083.97 3069.45 -0.5%
8k/1k Mean TPOT (ms) 44.94 43.81 -2.5%

Conc 70, DCP 8 (production recipe; gather_kv_b_proj is gated off)

Minor change since gather_kv_b_proj is disabled through DCP 8

ISL/OSL Metric Before After Delta
1k/1k Output throughput (tok/s) 1730.18 1728.03 -0.1%
1k/1k Mean TTFT (ms) 1111.72 1116.27 +0.4%
1k/1k Mean TPOT (ms) 39.40 39.44 +0.1%
8k/1k Output throughput (tok/s) 1085.92 1085.19 -0.1%
8k/1k Mean TTFT (ms) 3064.91 3068.07 +0.1%
8k/1k Mean TPOT (ms) 61.42 61.46 +0.1%

Conc 70, DCP 1 (gather_kv_b_proj on)

ISL/OSL Metric Before After Delta
1k/1k Output throughput (tok/s) 1843.78 1848.36 +0.2%
1k/1k Mean TTFT (ms) 1111.11 1113.53 +0.2%
1k/1k Mean TPOT (ms) 36.90 36.81 -0.2%
8k/1k Output throughput (tok/s) 1103.27 1118.97 +1.4%
8k/1k Mean TTFT (ms) 3167.12 3103.83 -2.0%
8k/1k Mean TPOT (ms) 60.31 59.48 -1.4%

@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 for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream 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 the rocm Related to AMD ROCm label Aug 20, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Aug 20, 2026
@nsusanto
nsusanto force-pushed the rocm-mla-fused-context-gather branch from 47d8a4a to 973367a Compare August 20, 2026 20:45
@nsusanto nsusanto changed the title [ROCm][Perf] Fuse the MLA chunked-context gather on AITER [kimik3][ROCm][Perf] Fuse the MLA chunked-context gather on AITER Aug 20, 2026
@nsusanto nsusanto changed the title [kimik3][ROCm][Perf] Fuse the MLA chunked-context gather on AITER [kimik3][ROCm][Perf] Fuse MLA chunked-context gather on AITER Aug 20, 2026

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

Nice speedup — the index math checks out, and gather_kv_b_proj is already in AITER v0.1.19 (our pin) with a matching signature. Three things though:

  1. The AITER import at rocm_aiter_mla.py:9 is at module scope. Every other AITER import in this file is deferred, even flash_attn_varlen_func inside AiterMLAImpl.__init__. As written, anyone on an AITER without this op gets a hard ImportError at backend selection instead of just falling back. Can you make it lazy (or go through rocm_aiter_ops, already imported at line 11) and fold the availability check into _can_fuse_context_gather?

  2. _can_fuse_context_gather looks at q.dtype, but the base path branches on prefill_metadata.q_data_type and casts q/kv_nope/k_pe to fp8 afterwards. So a bf16 q with q_data_type == fp8 passes your guard, takes the fused path, and comes back with bf16 k/v and an unconverted q. It's masked today because AITER's fp8 prefill metadata is only built when chunked_context is None, but determine_prefill_query_data_type is inherited and can still return fp8 — worth gating on q_data_type directly.

  3. _use_fused_mla_kv_concat uses is_cuda_alike(), so it fires for any qk_nope=128 / qk_rope=64 MLA model, not just K3. On NVIDIA the flashinfer branch only wins at num_heads == 128, which means at TP8 this quietly takes over DeepSeek's prefill. Probably fine since the kernel is shape-generic and already used for K3 on NVIDIA, but for a [kimik3][ROCm] PR I'd either narrow it to is_rocm() or call it out with one DeepSeek datapoint.

@nsusanto
nsusanto marked this pull request as ready for review August 25, 2026 16:57

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

@nsusanto
nsusanto marked this pull request as draft August 25, 2026 17:10
@mergify

mergify Bot commented Aug 25, 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, @nsusanto.

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

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

  1. Is it possible to create unit tests to verify the functionality?
  2. AITER's _triton_gather_kv_b_proj_flat starts with assert KV_CDim == 4 * ScaleKGranularity, and ScaleKGranularity is 128. So kv_lora_rank must be 512. Any other value raises a Triton CompilationError. Can you add this check to ensure kv_lora_rank is 512 is we take the triton kernel path - I think you can add it to _can_fuse_context_gather.

@mergify

mergify Bot commented Sep 24, 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, @nsusanto.

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 Sep 24, 2026
@nsusanto
nsusanto force-pushed the rocm-mla-fused-context-gather branch from 1b86a93 to 4731351 Compare September 29, 2026 16:32
@nsusanto nsusanto changed the title [kimik3][ROCm][Perf] Fuse MLA chunked-context gather on AITER [Perf][ROCm][Attention] Fuse Kimi-K3 MLA chunked-context gather on AITER Sep 29, 2026
@mergify mergify Bot removed the needs-rebase label Sep 29, 2026
@nsusanto
nsusanto force-pushed the rocm-mla-fused-context-gather branch from 4731351 to ebf246f Compare September 29, 2026 19:01
@mergify

mergify Bot commented Oct 1, 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, @nsusanto.

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 Oct 1, 2026
@nsusanto
nsusanto marked this pull request as ready for review October 13, 2026 19:32

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

nsusanto and others added 7 commits October 13, 2026 20:42
The dense-MLA prefill spends four kernels per context chunk: a paged gather
into a full-width workspace, the kv_b_proj expansion, and two copies that
assemble [k_nope | k_pe]. AITER ships gather_kv_b_proj, which does all four in
one launch, feeding the expansion from the gathered rows in registers instead
of staging a 576-wide latent through HBM. On a Kimi-K3 gated-MLA layer that
region measures 35.2 us; the ATOM engine's equivalent single kernel is ~2.4x
cheaper per token.

AiterMLAImpl overrides _compute_prefill_context to use it, falling back to the
generic path whenever the kernel does not apply: a quantized kv_b_proj, an fp8
query, fp8_ds_mla, or a non-contiguous cache.

gather_kv_b_proj addresses its KV buffer one token per entry, so it needs flat
per-token indices rather than block ids. The AITER MLA decode path already
builds exactly those with _expand_page_indices_kernel, so that kernel gains an
optional per-request start offset -- which is all a chunked-context prefill
adds -- rather than growing a second implementation. The decode call site keeps
its behaviour via HAS_START_OFFSETS=False.

Separately, MLACommonBaseImpl._concat_k_nope_k_pe now uses the already-merged
fused_kimi_k3_mla_kv_concat instead of two slice-assign copies, gated on the
DeepSeek-shaped MLA dims that kernel asserts. That halves the launches for the
new-token key assemble on every backend that lacks the flashinfer fast path,
measured 15.95 -> 6.77 us at 3867 tokens.

Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
- Defer the AITER import
- gate the fused path on q_data_type instead of q.dtype
- narrow the fused KV concat to ROCm.

Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
…build

Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
Signed-off-by: Nicholas Susanto <nicholas.susanto@amd.com>
@nsusanto
nsusanto force-pushed the rocm-mla-fused-context-gather branch from e206847 to b511478 Compare October 13, 2026 20:45
@mergify mergify Bot removed the needs-rebase label Oct 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

k3 kimi rocm Related to AMD ROCm

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants