# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Inference-only MiMo-V2 MTP (Multi-Token Prediction) draft model. Supports both MiMo-V2-Pro and MiMo-V2-Flash checkpoints. Checkpoint weight layout (model.mtp.layers.{idx}.*): enorm - RMSNorm for token embeddings hnorm - RMSNorm for previous hidden states eh_proj - ReplicatedLinear(hidden*2 -> hidden) input_layernorm - pre-attention RMSNorm self_attn.* - attention weights; format differs by variant: Pro: fused qkv_proj [Q;K;V] concatenated Flash: separate q_proj, k_proj, v_proj pre_mlp_layernorm - post-attention / pre-MLP RMSNorm mlp.* - dense MLP (gate_proj / up_proj / down_proj) final_layernorm - norm applied before logit computation """ from collections.abc import Iterable import torch import torch.nn as nn from transformers import PretrainedConfig from vllm.config import VllmConfig from vllm.distributed import ( get_tensor_model_parallel_rank, get_tensor_model_parallel_world_size, ) from vllm.model_executor.layers.layernorm import RMSNorm from vllm.model_executor.layers.linear import ReplicatedLinear from vllm.model_executor.layers.logits_processor import LogitsProcessor from vllm.model_executor.layers.quantization import QuantizationConfig from vllm.model_executor.layers.vocab_parallel_embedding import ( ParallelLMHead, VocabParallelEmbedding, ) from vllm.model_executor.model_loader.weight_utils import default_weight_loader from vllm.sequence import IntermediateTensors from .interfaces import ( MultiModalEmbeddings, SupportsMultiModal, _require_is_multimodal, ) from .mimo_v2 import MiMoV2Attention, MiMoV2MLP from .utils import _merge_multimodal_embeddings, maybe_prefix # MiMo-V2 checkpoints contain multiple MTP layers, but vLLM currently supports # only the first layer _MIMO_V2_PRO_NUM_MTP_LAYERS = 1 _MIMO_V2_FLASH_NUM_MTP_LAYERS = 1 class MiMoV2MTPLayer(nn.Module): """Single MTP predictor layer for MiMo-V2 (Pro and Flash). Mirrors the single-layer MiMo-V2 nextn reference implementation. """ def __init__( self, config: PretrainedConfig, prefix: str, quant_config: QuantizationConfig | None = None, ) -> None: super().__init__() # Predictor head components self.enorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon) self.hnorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon) self.eh_proj = ReplicatedLinear( config.hidden_size * 2, config.hidden_size, bias=False ) # MTP uses the SWA attention configuration # implementation. swa_rope_theta = getattr( config, "swa_rope_theta", getattr(config, "rope_theta", 1000000), ) sliding_window_size = getattr(config, "sliding_window_size", -1) self.input_layernorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon) self.self_attn = MiMoV2Attention( hidden_size=config.hidden_size, num_heads=config.swa_num_attention_heads, num_kv_heads=config.swa_num_key_value_heads, head_dim=config.swa_head_dim, v_head_dim=getattr(config, "swa_v_head_dim", None), v_scale=getattr(config, "attention_value_scale", None), sliding_window_size=sliding_window_size, attention_bias=config.attention_bias, add_swa_attention_sink_bias=getattr( config, "add_swa_attention_sink_bias", False ), layer_id=0, rope_theta=swa_rope_theta, max_position_embeddings=getattr(config, "max_position_embeddings", 32768), quant_config=quant_config, partial_rotary_factor=getattr(config, "partial_rotary_factor", 1.0), prefix=f"{prefix}.self_attn", ) self.pre_mlp_layernorm = RMSNorm( config.hidden_size, eps=config.layernorm_epsilon ) self.mlp = MiMoV2MLP( hidden_size=config.hidden_size, intermediate_size=config.intermediate_size, hidden_act=config.hidden_act, quant_config=quant_config, prefix=f"{prefix}.mlp", ) self.final_layernorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon) def forward( self, inputs_embeds: torch.Tensor, positions: torch.Tensor, previous_hidden_states: torch.Tensor, ) -> torch.Tensor: # Combine token embedding and previous hidden state h, _ = self.eh_proj( torch.cat( [self.enorm(inputs_embeds), self.hnorm(previous_hidden_states)], dim=-1 ) ) # Transformer block with fused residual norms residual = h h = self.input_layernorm(h) h = self.self_attn(positions=positions, hidden_states=h) h, residual = self.pre_mlp_layernorm(h, residual) h = self.mlp(h) h = h + residual return self.final_layernorm(h) class _MiMoV2MTPLayers(nn.Module): """Thin wrapper so parameter paths match checkpoint: model.mtp.layers.*""" def __init__( self, config: PretrainedConfig, num_mtp_layers: int, quant_config: QuantizationConfig | None, prefix: str, ) -> None: super().__init__() self.layers = nn.ModuleDict( { str(i): MiMoV2MTPLayer( config=config, prefix=f"{prefix}.{i}", quant_config=quant_config, ) for i in range(num_mtp_layers) } ) class MiMoV2MultiTokenPredictor(nn.Module): def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None: super().__init__() config = vllm_config.model_config.hf_config spec_cfg = vllm_config.speculative_config assert spec_cfg is not None num_mtp_layers = 1 self.num_mtp_layers = num_mtp_layers self.embed_tokens = VocabParallelEmbedding( config.vocab_size, config.hidden_size, ) self.mtp = _MiMoV2MTPLayers( config=config, num_mtp_layers=num_mtp_layers, quant_config=vllm_config.quant_config, prefix=maybe_prefix(prefix, "mtp.layers"), ) self.logits_processor = LogitsProcessor(config.vocab_size) def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: return self.embed_tokens(input_ids) def forward( self, input_ids: torch.Tensor, positions: torch.Tensor, previous_hidden_states: torch.Tensor, inputs_embeds: torch.Tensor | None = None, spec_step_idx: int = 0, ) -> torch.Tensor: if inputs_embeds is None: inputs_embeds = self.embed_input_ids(input_ids) current_step_idx = spec_step_idx % self.num_mtp_layers return self.mtp.layers[str(current_step_idx)]( inputs_embeds, positions, previous_hidden_states ) def compute_logits( self, hidden_states: torch.Tensor, lm_head: ParallelLMHead, spec_step_idx: int = 0, ) -> torch.Tensor: return self.logits_processor(lm_head, hidden_states) class MiMoV2MTP(nn.Module): def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None: super().__init__() self.config = vllm_config.model_config.hf_config self.model = MiMoV2MultiTokenPredictor( vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model") ) self.lm_head = ParallelLMHead( self.config.vocab_size, self.config.hidden_size, prefix=maybe_prefix(prefix, "lm_head"), ) def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: return self.model.embed_input_ids(input_ids) def forward( self, input_ids: torch.Tensor | None, positions: torch.Tensor, hidden_states: torch.Tensor, intermediate_tensors: IntermediateTensors | None = None, inputs_embeds: torch.Tensor | None = None, spec_step_idx: int = 0, ) -> torch.Tensor: return self.model( input_ids, positions, hidden_states, inputs_embeds, spec_step_idx ) def compute_logits( self, hidden_states: torch.Tensor, spec_step_idx: int = 0, ) -> torch.Tensor | None: return self.model.compute_logits(hidden_states, self.lm_head, spec_step_idx) def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: tp_rank = get_tensor_model_parallel_rank() tp_size = get_tensor_model_parallel_world_size() stacked_params_mapping = [ ("gate_up_proj", "gate_proj", 0), ("gate_up_proj", "up_proj", 1), # Flash format: separate projections → fused qkv_proj ("qkv_proj", "q_proj", "q"), ("qkv_proj", "k_proj", "k"), ("qkv_proj", "v_proj", "v"), ] params_dict = dict(self.named_parameters()) loaded_params: set[str] = set() for name, loaded_weight in weights: if "rotary_emb.inv_freq" in name: continue # Only load MTP-related weights, shared embeddings, and lm_head if ( "model.mtp" not in name and "model.embed_tokens" not in name and not name.startswith("lm_head") ): continue # Support fused qkv_proj checkpoint (Pro format). # The checkpoint is stored pre-sharded for TP=8 as # [Q_rank0, K_rank0, V_rank0, Q_rank1, ...], so splitting along # dim 0 with chunk(tp_size) gives each rank its Q+K+V slice for # both the FP8 weight and the block weight_scale_inv. This matches # how the main model loads the same layout. if "qkv_proj" in name: if name in params_dict: param = params_dict[name] loaded_weight = loaded_weight.chunk(tp_size, dim=0)[tp_rank] default_weight_loader(param, loaded_weight) loaded_params.add(name) continue # gate_proj/up_proj → gate_up_proj stacking (both formats); # Flash: q_proj/k_proj/v_proj → qkv_proj merging. stacked_matched = False for param_name, weight_name, shard_id in stacked_params_mapping: if weight_name not in name: continue name_rewritten = name.replace(weight_name, param_name) if ( name_rewritten.endswith(".bias") and name_rewritten not in params_dict ): continue if name_rewritten not in params_dict: continue param = params_dict[name_rewritten] weight_loader = getattr(param, "weight_loader", default_weight_loader) weight_loader(param, loaded_weight, shard_id) loaded_params.add(name_rewritten) stacked_matched = True break if stacked_matched: continue if name.endswith(".bias") and name not in params_dict: continue if name not in params_dict: continue param = params_dict[name] # attention_sink_bias is head-parallel; slice by tp if "attention_sink_bias" in name: total_heads = loaded_weight.shape[0] heads_per_rank = total_heads // tp_size loaded_weight = loaded_weight.narrow( 0, tp_rank * heads_per_rank, heads_per_rank ) weight_loader = getattr(param, "weight_loader", default_weight_loader) weight_loader(param, loaded_weight) loaded_params.add(name) return loaded_params class MiMoV2OmniMTP(MiMoV2MTP, SupportsMultiModal): def embed_input_ids( self, input_ids: torch.Tensor, multimodal_embeddings: MultiModalEmbeddings | None = None, *, is_multimodal: torch.Tensor | None = None, ) -> torch.Tensor: inputs_embeds = self._embed_text_input_ids( input_ids, self.model.embed_input_ids, is_multimodal=is_multimodal, ) if multimodal_embeddings is None or len(multimodal_embeddings) == 0: return inputs_embeds is_multimodal = _require_is_multimodal(is_multimodal) inputs_embeds = _merge_multimodal_embeddings( inputs_embeds=inputs_embeds, multimodal_embeddings=multimodal_embeddings, is_multimodal=is_multimodal, ) return inputs_embeds