Skip to content

[ROCm] ROCm binding on common Kimi-K3 Dspark MLA module - #60857

Draft
gronsti-amd wants to merge 11 commits into
vllm-project:mainfrom
gronsti-amd:sgronroo/amd-dspark-mla-module-reimplement
Draft

gronsti-amd wants to merge 11 commits into
vllm-project:mainfrom
gronsti-amd:sgronroo/amd-dspark-mla-module-reimplement

Conversation

@gronsti-amd

@gronsti-amd gronsti-amd commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

On ROCm, Kimi-K3 DSpark currently instantiates the NVIDIA draft module. This PR registers K3DSparkModel as ("vllm.models.kimi_k3", "K3DSparkForCausalLM") and, on ROCm, imports amd.dspark_mla. CUDA still imports nvidia.dspark_mla.
Builds on top of #60054
Replaces #57942, which was the duplicate-and-modify approach (not touching NVIDIA).

What changes

amd/dspark_mla.py is a binding over the common classes: a factory, build_self_attn, build_mlp, and kv_cache_layer returning attn.mla_attn.
Draft attention is KimiK3MultiHeadLatentAttentionWrapper around generic MLAAttention, with RoPE and non_causal_multi_token_decode=True.
KimiMLAAttention is not used: it is NoPE-only, and would add a second wrapper.
Context-KV insert, the weight mapper, the Markov head, and the confidence head stay in the common module, so the #58814 refresh and adaptive verification come along with the dispatch.

The factory duplicates the NVIDIA projection and YaRN setup.
The docstring records the settings that have to stay aligned.

HIP YaRN returns fp32. The query slice assignment casts Q back to bf16,
and rebinding k_pe used to leave it fp32. concat_and_cache_mla then stored those bits as the KV dtype.
After RoPE, the wrapper casts k_pe back with k_pe.to(kv_c_normed.dtype).
On MI355 that cast removed the repetitive garbage in the target output.

Decode-only FP8 with a real embedding must not enter _fused_decode.
That path applies a one-row identity RoPE after the wrapper has already rotated.
The call site requires rotary_emb is None.
Target K3 still has rotary_emb is None, so its launch stays. DeepSeek does not use this wrapper.

Trace

The baseline draft gap still launches fusedKimiK3MLADecodeQConcatKVCacheKernel five times per step, 21 µs total.
The trace for this PR does not use that kernel. Each draft layer instead runs native DeepseekScalingRotaryEmbedding.forward_hip, which calls forward_native, and then an ungrouped concat_and_cache_mla.
On the steady decode shape the eager gap grows from 3.41 ms to 3.85 ms (+0.44 ms, about 13%).

This PR is slower than the NVIDIA-module baseline on ROCm. The kernels that replaced the fused decode epilogue are the native YaRN elementwise chain plus the ungrouped cache insert.

Potential follow-up

If the fusion in the follow-up (#60864) is not merged, then the following alternative should be considered:
DeepseekScalingRotaryEmbedding.forward_hip calls forward_native.
Pointing it at AITER Triton RoPE would remove the elementwise chain for this draft and for every other HIP model
that uses the same embedding.

Validation

  • pytest tests/models/test_dspark_mla.py on ROCm, including test_k3_dspark_decoder_uses_mla_wrapper (ROCm-only) and test_hip_yarn_k_pe_is_cast_back_to_kv_dtype (CPU; a fake fp32 RoPE, asserting the seen key is bf16).
  • test_amd_kv_cache_layer_returns_inner_mla_attn: draft KV names end in .attn.
  • test_fused_decode_not_taken_when_rotary_emb_is_set.

Out of scope


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

gronsti-amd and others added 11 commits October 9, 2026 15:06
Keep the NVIDIA module as a re-export so the registry path is unchanged.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
NVIDIA keeps MultiHeadLatentAttention as the cache owner. A platform
override can point context-KV insert and draft layer names at another module.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The defaults keep today's concat and cache write. A subclass can replace
the decode query and the KV rows it writes without a new custom-op argument.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The common module is a new file, so pre-commit lints it in full. The
(list, tuple) form was already on main and fails UP038 once the file moves.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The file was not listed in Buildkite, so the refactor would not be collected on either platform.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
DeepseekV32Attention already assigns a concrete RotaryEmbedding to that name. Typing the base attribute as optional made mypy reject its cache reads.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Replay reads the captured positions and slot-mapping allocations, so a replacement tensor freezes the pointer. Lock the NVIDIA binding class and the default hook returns.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ROCm keeps the wrapper factory and points the KV cache at mla_attn. The copied decoder, context-KV insert, and weight mapper stay in the common module.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
HIP YaRN RoPE returns fp32. The query slice assignment casts q back to bf16, but k_pe stays fp32 and concat_and_cache_mla stores those bits as the KV dtype.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The fused decode launch applies a one-row identity table, so it is only valid when the wrapper has not already rotated. Document the duplicated YaRN setup and bind the construction test to the wrapper signature.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
KimiLinearConfig types the latent sizes as optional. Assert them the same way the NVIDIA K3 MLA constructor does, so the factory arithmetic typechecks.

Signed-off-by: Stig-Arne Grönroos <stig-arne.gronroos@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@mergify mergify Bot added the ci/build label Oct 9, 2026
@mergify mergify Bot added kimi k3 rocm Related to AMD ROCm dflash labels Oct 9, 2026
@gronsti-amd gronsti-amd mentioned this pull request Oct 9, 2026
2 of 7 tasks
@gronsti-amd gronsti-amd changed the title [ROCm] ROCm binding on the common Dspark MLA module [ROCm] ROCm binding on common Kimi-K3 Dspark MLA module Oct 9, 2026
@mergify

mergify Bot commented Oct 10, 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, @gronsti-amd.

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 10, 2026
@@ -0,0 +1,225 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""ROCm binding for the Kimi-K3 DSpark MLA draft.

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.

Overall provided solution is slower than the code it replaces.

The PR reports +0.44 ms per step in eager mode. Two things in this PR's scope cause most of it:

  1. The new self.rotary_emb is None fusion check (amd/mla.py:282-284) isn't needed for correctness.
  • The fused decode applies a one-row cos=1, sin=0 table. On already-rotated q_pe/k_pe that does nothing.
  • On MI355X I called fused_qk_rope_concat_and_cache_mla exactly as _fused_decode does. Run on pre-rotated inputs, the KV rows and the fp8 q_out are byte-identical to the unfused path.
  • With VLLM_ROCM_USE_AITER=1 and fp8 KV, this check is the only thing turning off the existing fused decode for the draft. In each of 5 layers, one fused launch becomes a separate cache write plus a q concat and quant.
  • Please drop the check (or move it to [ROCm] Fuse Kimi-K3 DSpark QK RoPE with the MLA cache write #60864, where the wrapper skips its own RoPE). Also drop the "double RoPE" wording at :106-108 and :279-281.
  1. Native YaRN costs 15 kernel launches per layer (75 per step): the forward_native elementwise chain, the slice copy-back, and the cast.
  • An in-place ops.rotary_embedding(positions, q[..., nope:], k_pe, ...) with the fp32 table does it in 1. The common module already uses this op for context-KV RoPE.
  • It also makes the k_pe cast unnecessary. k_pe comes out byte-identical, and q differs by at most 1 bf16 ulp on a tiny fraction of elements.
  • Please re-measure in the mode production uses. The draft replays inside full CUDA graphs there, so the eager 3.41 → 3.85 ms overstates the per-step cost.

# rotated q and k above, and this launch would apply cos=1, sin=0 on
# top of that.
fuse = (
self._fused_qk_prep and self.rotary_emb is None and q_dcp_replicated is None

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.

This check isn't needed for correctness. _fused_decode passes a one-row cos=1, sin=0 table, so on q_pe/k_pe that are already rotated it does nothing. On MI355X I called fused_qk_rope_concat_and_cache_mla exactly as _fused_decode does, on pre-rotated inputs. The KV rows and the fp8 q_out came out byte-identical to the unfused concat_and_cache_mla plus static quant. With VLLM_ROCM_USE_AITER=1 and fp8 KV, this check is the only thing turning off the existing fused decode for the draft. In each of the 5 draft layers, one launch becomes a cache write plus a q concat and quant, which is part of the reported slowdown. Could you drop it here?

# decode-only batches; anything else falls through to MLAAttention.
attn_metadata = layer = kv_cache = slot_mapping = None
fuse = self._fused_qk_prep and q_dcp_replicated is None
# Identity-RoPE fusion is the NoPE path. A real embedding has already

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.

This comment and the docstring at :106-108 describe a double RoPE that doesn't happen: cos=1, sin=0 on rotated inputs is an exact identity. The docstring also mentions a "DSpark epilogue" that only exists in #60864.

is_sparse=False,
topk_indices_buffer=None,
)
return KimiK3MultiHeadLatentAttentionWrapper(

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.

Moving the draft onto generic MLAAttention changes the precision of its absorb matmuls under AITER:

  • With VLLM_ROCM_USE_AITER=1 on gfx950, MLAAttention (mla_attention.py:717-724, :1336-1349) quantizes the bf16 draft kv_b_proj's W_UK/W_UV to MXFP4.
  • Decode then runs batched_gemm_a16wfp4(prequant=True), which quantizes the activations too.
  • On gfx942, or with FP4BMM off, the FP8 path runs instead.
  • The NVIDIA module keeps a bf16 torch.bmm.

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.

Separately, this wrapper's init now runs _fused_qk_prep_supported() for the draft. That function reads attn.W_UK_T (amd/mla.py:54), which only exists after process_weights_after_loading. So construction raises AttributeError with fp8 KV + AITER MLA + FP8BMM=0 + FP4BMM=0, which is the natural workaround for the precision change. The code is older (#57640), but the draft now reaches it. getattr(attn, "W_UK_T", None) or deciding after weights load would fix it. As written, the bf16 W_UK_T branch of _fused_decode can never run.

num_local_heads = num_heads // tp_size
scaling = qk_head_dim**-0.5

rope_parameters = dict(config.rope_parameters)

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.

Lines 81-103 copy the NVIDIA rope-type remap, get_rope(dtype=fp32) and the mscale^2 factor on the softmax scale, and only the docstring keeps them in sync. Deleting scaling *= mscale * mscale (:103) still passes all 36 tests. Could this be a shared helper in common/ (using yarn_get_mscale) that both bindings call? Please also add a CPU test that builds both bindings from the real YaRN rope_parameters and compares the scale, rope class, is_neox_style and cache dtype. They all match today.

quant_config=quant_config,
prefix=f"{prefix}.q_b_proj",
)
else:

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.

This branch can't be reached: K3DSparkConfig rejects q_lora_rank=None, and the common context-KV metadata asserts it is set. Replace it with an assert?


def _make_dspark_mla_attention(
*,
config: KimiLinearConfig,

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.

nit: the draft config is K3DSparkConfig, not KimiLinearConfig. Same at :195, and the import at :35.

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

Projects

Status: Todo
Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants