[bugfix]Indexer init skip and MTP TopK share for iteration (#45895)

Signed-off-by: JaredforReal <w13431838023@gmail.com>

Jared Wen committed Jun 19, 2026 at 09:57 UTC ab666069935c1f23e8ef56038b4659ac9e8f19f8
9 files changed +69 -30
vllm/model_executor/layers/attention/mla_attention.py
+6
@@ -349,6 +349,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
349 attn_backend: type[AttentionBackend] | None = None,
350 use_sparse: bool = False,
351 indexer: object | None = None,
352 + topk_indices_buffer: torch.Tensor | None = None,
353 **extra_impl_args,
354 ):
355 super().__init__()
@@ -437,6 +438,11 @@ class MLAAttention(nn.Module, AttentionLayerBase):
438 )
439 cache_config.enable_prefix_caching = False
440
441 + # Sparse MLA reads top-k indices from a shared buffer. Pass it
442 + # explicitly so backbone "skip" layers (indexer=None) still find it.
443 + if use_sparse:
444 + extra_impl_args["topk_indices_buffer"] = topk_indices_buffer
445 +
446 impl_cls = cast(type[MLAAttentionImpl], self.attn_backend.get_impl_cls())
447 self.impl = impl_cls( # type: ignore[assignment] # impl_cls always returns an MLAAttentionImpl subclass
448 num_heads=self.num_heads,
vllm/model_executor/layers/mla.py
+1
@@ -112,6 +112,7 @@ class MultiHeadLatentAttentionWrapper(PluggableLayer):
112 kv_b_proj=self.kv_b_proj,
113 use_sparse=self.is_sparse,
114 indexer=self.indexer,
115 + topk_indices_buffer=mla_modules.topk_indices_buffer,
116 )
117
118 self.prefix = prefix
vllm/model_executor/models/deepseek_mtp.py
+6 -2
@@ -119,8 +119,12 @@ class DeepSeekMultiTokenPredictorLayer(nn.Module):
119 hidden_states=hidden_states,
120 residual=None,
121 )
122 - hidden_states = residual + hidden_states
123 - return hidden_states
122 + hidden_states = residual + hidden_states # pre-final-norm (logits hidden)
123 + # Recycle the post-final-norm hidden into the next draft step.
124 + # compute_logits applies shared_head (== final norm) to the pre-norm
125 + # element, so logits and the recycle each get exactly one final-norm.
126 + # Matches SGLang's deepseek_nextn.
127 + return hidden_states, self.shared_head(hidden_states)
128
129
130 class DeepSeekMultiTokenPredictor(nn.Module):
vllm/model_executor/models/deepseek_v2.py
+22 -17
@@ -998,8 +998,29 @@ class DeepseekV2MLAAttention(nn.Module):
998
999 self.is_v32 = hasattr(config, "index_topk")
1000
1001 + # IndexCache config
1002 + # Refer: https://arxiv.org/abs/2603.12201 for more details.
1003 _skip_topk = False
1002 - if self.is_v32:
1004 + _index_topk_freq = getattr(config, "index_topk_freq", 1)
1005 + _index_topk_pattern = getattr(config, "index_topk_pattern", None)
1006 + _index_skip_topk_offset = getattr(config, "index_skip_topk_offset", 2)
1007 + layer_id = extract_layer_index(prefix)
1008 +
1009 + if _index_topk_pattern is None:
1010 + _skip_topk = (
1011 + max(layer_id - _index_skip_topk_offset + 1, 0) % _index_topk_freq != 0
1012 + )
1013 + elif 0 <= layer_id < len(_index_topk_pattern):
1014 + _skip_topk = _index_topk_pattern[layer_id] == "S"
1015 +
1016 + # The skip pattern only governs backbone layers. MTP/nextn layers
1017 + # (layer_id >= num_hidden_layers) always build a full indexer: they
1018 + # compute indices at draft step 0 and toggle at runtime via
1019 + # set_skip_topk (index_share_for_mtp_iteration).
1020 + _num_hidden_layers = getattr(config, "num_hidden_layers", None)
1021 + is_mtp_layer = _num_hidden_layers is not None and layer_id >= _num_hidden_layers
1022 +
1023 + if self.is_v32 and (not _skip_topk or is_mtp_layer):
1024 self.indexer_rope_emb = get_rope(
1025 qk_rope_head_dim,
1026 max_position=max_position_embeddings,
@@ -1017,22 +1038,6 @@ class DeepseekV2MLAAttention(nn.Module):
1038 f"{prefix}.indexer",
1039 is_inplace_rope=self.indexer_rope_emb.enabled(),
1040 )
1020 -
1021 - # IndexCache config
1022 - # Refer: https://arxiv.org/abs/2603.12201 for more details.
1023 - _index_topk_freq = getattr(config, "index_topk_freq", 1)
1024 - _index_topk_pattern = getattr(config, "index_topk_pattern", None)
1025 - _index_skip_topk_offset = getattr(config, "index_skip_topk_offset", 2)
1026 - layer_id = extract_layer_index(prefix)
1027 -
1028 - if _index_topk_pattern is None:
1029 - _skip_topk = (
1030 - max(layer_id - _index_skip_topk_offset + 1, 0) % _index_topk_freq
1031 - != 0
1032 - )
1033 - elif 0 <= layer_id < len(_index_topk_pattern):
1034 - _skip_topk = _index_topk_pattern[layer_id] == "S"
1035 -
1041 else:
1042 self.indexer_rope_emb = None
1043 self.indexer = None
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
+7 -3
@@ -271,7 +271,7 @@ class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata
271 attn_type: str,
272 kv_sharing_target_layer_name: str | None,
273 # MLA Specific Arguments
274 - topk_indice_buffer: torch.Tensor | None = None,
274 + topk_indices_buffer: torch.Tensor | None = None,
275 indexer: "Indexer | None" = None,
276 **mla_args,
277 ) -> None:
@@ -301,8 +301,12 @@ class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata
301 self.qk_nope_head_dim: int = mla_args["qk_nope_head_dim"]
302 self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
303
304 - assert indexer is not None, "Indexer required for sparse MLA"
305 - self.topk_indices_buffer: torch.Tensor | None = indexer.topk_indices_buffer
304 + # The indexer carries the shared buffer for normal layers and tests;
305 + # the explicitly-passed buffer covers backbone skip layers, whose
306 + # indexer is not constructed (see deepseek_v2.py).
307 + self.topk_indices_buffer: torch.Tensor | None = (
308 + indexer.topk_indices_buffer if indexer is not None else topk_indices_buffer
309 + )
310
311 self._workspace_buffer: torch.Tensor | None = None
312 self.bmm1_scale: float | None = None
vllm/v1/attention/backends/mla/flashmla_sparse.py
+6 -2
@@ -568,8 +568,12 @@ class FlashMLASparseImpl(SparseMLAAttentionImpl[FlashMLASparseMetadata]):
568 self.kv_cache_dtype = kv_cache_dtype
569 self.kv_lora_rank: int = mla_args["kv_lora_rank"]
570 self.softmax_scale = scale
571 - assert indexer is not None
572 - self.topk_indices_buffer: torch.Tensor | None = indexer.topk_indices_buffer
571 + # The indexer carries the shared buffer for normal layers and tests;
572 + # the explicitly-passed buffer covers backbone skip layers, whose
573 + # indexer is not constructed (see deepseek_v2.py).
574 + self.topk_indices_buffer: torch.Tensor | None = (
575 + indexer.topk_indices_buffer if indexer is not None else topk_indices_buffer
576 + )
577 # Prefill BF16 kernel requires 64 on Hopper, 128 on Blackwell
578 self.prefill_padding = (
579 128 if current_platform.is_device_capability_family(100) else 64
vllm/v1/attention/backends/mla/rocm_aiter_mla_sparse.py
+7 -3
@@ -629,7 +629,7 @@ class ROCMAiterMLASparseImpl(SparseMLAAttentionImpl[ROCMAiterMLASparseMetadata])
629 attn_type: str,
630 kv_sharing_target_layer_name: str | None,
631 # MLA Specific Arguments
632 - topk_indice_buffer: torch.Tensor | None = None,
632 + topk_indices_buffer: torch.Tensor | None = None,
633 indexer: "Indexer | None" = None,
634 **mla_args,
635 ) -> None:
@@ -642,8 +642,12 @@ class ROCMAiterMLASparseImpl(SparseMLAAttentionImpl[ROCMAiterMLASparseMetadata])
642 self.kv_cache_dtype = kv_cache_dtype
643 self.kv_lora_rank: int = mla_args["kv_lora_rank"]
644 self.softmax_scale = scale
645 - assert indexer is not None
646 - self.topk_indices_buffer: torch.Tensor | None = indexer.topk_indices_buffer
645 + # The indexer carries the shared buffer for normal layers and tests;
646 + # the explicitly-passed buffer covers backbone skip layers, whose
647 + # indexer is not constructed (see deepseek_v2.py).
648 + self.topk_indices_buffer: torch.Tensor | None = (
649 + indexer.topk_indices_buffer if indexer is not None else topk_indices_buffer
650 + )
651
652 vllm_config = get_current_vllm_config()
653 max_tokens = vllm_config.scheduler_config.max_num_batched_tokens
vllm/v1/attention/backends/mla/xpu_mla_sparse.py
+7 -3
@@ -184,7 +184,7 @@ class XPUMLASparseImpl(SparseMLAAttentionImpl[XPUMLASparseMetadata]):
184 attn_type: str,
185 kv_sharing_target_layer_name: str | None,
186 # MLA Specific Arguments
187 - topk_indice_buffer: torch.Tensor | None = None,
187 + topk_indices_buffer: torch.Tensor | None = None,
188 indexer: Optional["Indexer"] = None,
189 **mla_args,
190 ) -> None:
@@ -195,8 +195,12 @@ class XPUMLASparseImpl(SparseMLAAttentionImpl[XPUMLASparseMetadata]):
195 self.kv_cache_dtype = kv_cache_dtype
196 self.kv_lora_rank: int = mla_args["kv_lora_rank"]
197 self.softmax_scale = scale
198 - assert indexer is not None
199 - self.topk_indices_buffer: torch.Tensor | None = indexer.topk_indices_buffer
198 + # The indexer carries the shared buffer for normal layers and tests;
199 + # the explicitly-passed buffer covers backbone skip layers, whose
200 + # indexer is not constructed (see deepseek_v2.py).
201 + self.topk_indices_buffer: torch.Tensor | None = (
202 + indexer.topk_indices_buffer if indexer is not None else topk_indices_buffer
203 + )
204
205 def _forward_bf16_kv(
206 self,
vllm/v1/spec_decode/llm_base_proposer.py
+7
@@ -918,6 +918,13 @@ class SpecDecodeBaseProposer:
918 return per_group_attn_metadata, per_layer_attn_metadata
919
920 def model_returns_tuple(self) -> bool:
921 + if self.method == "mtp":
922 + # DeepSeek-family MTP (deepseek_mtp.py) recycles the post-final-
923 + # norm hidden, so its forward returns (logit_hidden,
924 + # recycle_hidden). Other MTP families return a single tensor.
925 + return "DeepSeekMTPModel" in (
926 + self.draft_model_config.hf_config.architectures or []
927 + )
928 return self.method not in ("mtp", "draft_model", "dflash")
929
930 def prepare_next_token_ids_cpu(