main
py 417 lines 15.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """Inference-only MiMo-V2 MTP (Multi-Token Prediction) draft model.
5
6 Supports both MiMo-V2-Pro and MiMo-V2-Flash checkpoints.
7
8 Checkpoint weight layout (model.mtp.layers.{idx}.*):
9 enorm - RMSNorm for token embeddings
10 hnorm - RMSNorm for previous hidden states
11 eh_proj - ReplicatedLinear(hidden*2 -> hidden)
12 input_layernorm - pre-attention RMSNorm
13 self_attn.* - attention weights; format differs by variant:
14 Pro: fused qkv_proj [Q;K;V] concatenated
15 Flash: separate q_proj, k_proj, v_proj
16 pre_mlp_layernorm - post-attention / pre-MLP RMSNorm
17 mlp.* - dense MLP (gate_proj / up_proj / down_proj)
18 final_layernorm - norm applied before logit computation
19 """
20
21 from collections.abc import Iterable
22
23 import torch
24 import torch.nn as nn
25 from transformers import PreTrainedConfig
26
27 from vllm.config import VllmConfig
28 from vllm.distributed import (
29 get_tensor_model_parallel_rank,
30 get_tensor_model_parallel_world_size,
31 )
32 from vllm.model_executor.layers.layernorm import RMSNorm
33 from vllm.model_executor.layers.linear import ReplicatedLinear
34 from vllm.model_executor.layers.logits_processor import LogitsProcessor
35 from vllm.model_executor.layers.quantization import QuantizationConfig
36 from vllm.model_executor.layers.vocab_parallel_embedding import (
37 ParallelLMHead,
38 VocabParallelEmbedding,
39 )
40 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
41 from vllm.sequence import IntermediateTensors
42
43 from .interfaces import (
44 MultiModalEmbeddings,
45 SupportsMultiModal,
46 _require_is_multimodal,
47 )
48 from .mimo_v2 import MiMoV2Attention, MiMoV2MLP, _shard_fp8_qkv_proj
49 from .utils import _merge_multimodal_embeddings, maybe_prefix
50
51 # MiMo-V2 checkpoints contain multiple MTP layers, but vLLM currently supports
52 # only the first layer
53 _MIMO_V2_PRO_NUM_MTP_LAYERS = 1
54 _MIMO_V2_FLASH_NUM_MTP_LAYERS = 1
55
56
57 class MiMoV2MTPLayer(nn.Module):
58 """Single MTP predictor layer for MiMo-V2 (Pro and Flash).
59
60 Mirrors the single-layer MiMo-V2 nextn reference implementation.
61 """
62
63 def __init__(
64 self,
65 config: PreTrainedConfig,
66 prefix: str,
67 quant_config: QuantizationConfig | None = None,
68 ) -> None:
69 super().__init__()
70
71 # Predictor head components
72 self.enorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
73 self.hnorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
74 self.eh_proj = ReplicatedLinear(
75 config.hidden_size * 2, config.hidden_size, bias=False
76 )
77
78 # MTP uses the SWA attention configuration
79 # implementation.
80 swa_rope_theta = getattr(
81 config,
82 "swa_rope_theta",
83 getattr(config, "rope_theta", 1000000),
84 )
85 sliding_window_size = getattr(config, "sliding_window_size", -1)
86
87 self.input_layernorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
88 self.self_attn = MiMoV2Attention(
89 hidden_size=config.hidden_size,
90 num_heads=config.swa_num_attention_heads,
91 num_kv_heads=config.swa_num_key_value_heads,
92 head_dim=config.swa_head_dim,
93 v_head_dim=getattr(config, "swa_v_head_dim", None),
94 v_scale=getattr(config, "attention_value_scale", None),
95 sliding_window_size=sliding_window_size,
96 attention_bias=config.attention_bias,
97 add_swa_attention_sink_bias=getattr(
98 config, "add_swa_attention_sink_bias", False
99 ),
100 layer_id=0,
101 rope_theta=swa_rope_theta,
102 max_position_embeddings=getattr(config, "max_position_embeddings", 32768),
103 quant_config=quant_config,
104 partial_rotary_factor=getattr(config, "partial_rotary_factor", 1.0),
105 prefix=f"{prefix}.self_attn",
106 )
107 self.pre_mlp_layernorm = RMSNorm(
108 config.hidden_size, eps=config.layernorm_epsilon
109 )
110 self.mlp = MiMoV2MLP(
111 hidden_size=config.hidden_size,
112 intermediate_size=config.intermediate_size,
113 hidden_act=config.hidden_act,
114 quant_config=quant_config,
115 prefix=f"{prefix}.mlp",
116 )
117 self.final_layernorm = RMSNorm(config.hidden_size, eps=config.layernorm_epsilon)
118
119 def forward(
120 self,
121 inputs_embeds: torch.Tensor,
122 positions: torch.Tensor,
123 previous_hidden_states: torch.Tensor,
124 ) -> torch.Tensor:
125 # Combine token embedding and previous hidden state
126 h, _ = self.eh_proj(
127 torch.cat(
128 [self.enorm(inputs_embeds), self.hnorm(previous_hidden_states)], dim=-1
129 )
130 )
131
132 # Transformer block with fused residual norms
133 residual = h
134 h = self.input_layernorm(h)
135 h = self.self_attn(positions=positions, hidden_states=h)
136 h, residual = self.pre_mlp_layernorm(h, residual)
137 h = self.mlp(h)
138 h = h + residual
139
140 return self.final_layernorm(h)
141
142
143 class _MiMoV2MTPLayers(nn.Module):
144 """Thin wrapper so parameter paths match checkpoint: model.mtp.layers.*."""
145
146 def __init__(
147 self,
148 config: PreTrainedConfig,
149 num_mtp_layers: int,
150 quant_config: QuantizationConfig | None,
151 prefix: str,
152 ) -> None:
153 super().__init__()
154 self.layers = nn.ModuleDict(
155 {
156 str(i): MiMoV2MTPLayer(
157 config=config,
158 prefix=f"{prefix}.{i}",
159 quant_config=quant_config,
160 )
161 for i in range(num_mtp_layers)
162 }
163 )
164
165
166 class MiMoV2MultiTokenPredictor(nn.Module):
167 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
168 super().__init__()
169
170 config = vllm_config.model_config.hf_config
171 spec_cfg = vllm_config.speculative_config
172 assert spec_cfg is not None
173 num_mtp_layers = 1
174
175 self.num_mtp_layers = num_mtp_layers
176
177 self.embed_tokens = VocabParallelEmbedding(
178 config.vocab_size,
179 config.hidden_size,
180 )
181
182 self.mtp = _MiMoV2MTPLayers(
183 config=config,
184 num_mtp_layers=num_mtp_layers,
185 quant_config=vllm_config.quant_config,
186 prefix=maybe_prefix(prefix, "mtp.layers"),
187 )
188
189 self.logits_processor = LogitsProcessor(config.vocab_size)
190
191 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
192 return self.embed_tokens(input_ids)
193
194 def forward(
195 self,
196 input_ids: torch.Tensor,
197 positions: torch.Tensor,
198 previous_hidden_states: torch.Tensor,
199 inputs_embeds: torch.Tensor | None = None,
200 spec_step_idx: int = 0,
201 ) -> torch.Tensor:
202 if inputs_embeds is None:
203 inputs_embeds = self.embed_input_ids(input_ids)
204 current_step_idx = spec_step_idx % self.num_mtp_layers
205 return self.mtp.layers[str(current_step_idx)](
206 inputs_embeds, positions, previous_hidden_states
207 )
208
209 def compute_logits(
210 self,
211 hidden_states: torch.Tensor,
212 lm_head: ParallelLMHead,
213 spec_step_idx: int = 0,
214 ) -> torch.Tensor:
215 return self.logits_processor(lm_head, hidden_states)
216
217
218 class MiMoV2MTP(nn.Module):
219 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
220 super().__init__()
221 self.config = vllm_config.model_config.hf_config
222 self.model = MiMoV2MultiTokenPredictor(
223 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
224 )
225 self.lm_head = ParallelLMHead(
226 self.config.vocab_size,
227 self.config.hidden_size,
228 prefix=maybe_prefix(prefix, "lm_head"),
229 )
230
231 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
232 return self.model.embed_input_ids(input_ids)
233
234 def forward(
235 self,
236 input_ids: torch.Tensor | None,
237 positions: torch.Tensor,
238 hidden_states: torch.Tensor,
239 intermediate_tensors: IntermediateTensors | None = None,
240 inputs_embeds: torch.Tensor | None = None,
241 spec_step_idx: int = 0,
242 ) -> torch.Tensor:
243 return self.model(
244 input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
245 )
246
247 def compute_logits(
248 self,
249 hidden_states: torch.Tensor,
250 spec_step_idx: int = 0,
251 ) -> torch.Tensor | None:
252 return self.model.compute_logits(hidden_states, self.lm_head, spec_step_idx)
253
254 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
255 tp_rank = get_tensor_model_parallel_rank()
256 tp_size = get_tensor_model_parallel_world_size()
257
258 stacked_params_mapping: list[tuple[str, str, str | int]] = [
259 ("gate_up_proj", "gate_proj", 0),
260 ("gate_up_proj", "up_proj", 1),
261 # Flash format: separate projections → fused qkv_proj
262 ("qkv_proj", "q_proj", "q"),
263 ("qkv_proj", "k_proj", "k"),
264 ("qkv_proj", "v_proj", "v"),
265 ]
266
267 params_dict = dict(self.named_parameters())
268 loaded_params: set[str] = set()
269 # The pairing state must outlive this call: AutoWeightsLoader delegates
270 # per contiguous group of names, so a pair can straddle two calls and
271 # would otherwise be dropped silently.
272 pending_qkv_proj = getattr(self, "_pending_qkv_proj", None)
273 if pending_qkv_proj is None:
274 self._pending_qkv_proj = pending_qkv_proj = {}
275
276 for name, loaded_weight in weights:
277 if "rotary_emb.inv_freq" in name:
278 continue
279
280 # Only load MTP-related weights, shared embeddings, and lm_head
281 if (
282 "model.mtp" not in name
283 and "model.embed_tokens" not in name
284 and not name.startswith("lm_head")
285 ):
286 continue
287
288 # Fused qkv_proj: the fp8 weight and its block scales arrive as
289 # separate tensors and are pre-sharded at `config.num_key_value_heads`
290 # chunks of [Q_c | K_c | V_c] (see _shard_fp8_qkv_proj for the
291 # layout). Shard them together: a plain chunk(tp_size) only happens
292 # to be right when tp_size is that chunk count *and* the scales are
293 # tiled per chunk (they are 4 x 29 = 116 rows for MiMo-V2.5, not
294 # tp_size slices of an even split).
295 if "qkv_proj" in name:
296 is_fp8_weight = (
297 name.endswith("qkv_proj.weight")
298 and loaded_weight.dtype == torch.float8_e4m3fn
299 )
300 is_fp8_scale = name.endswith("qkv_proj.weight_scale_inv")
301 if is_fp8_weight or is_fp8_scale:
302 prefix, kind = name.rsplit(".", 1)
303 if (
304 f"{prefix}.weight" not in params_dict
305 or f"{prefix}.weight_scale_inv" not in params_dict
306 ):
307 continue
308 entry = pending_qkv_proj.setdefault(prefix, {})
309 entry[kind] = loaded_weight
310 if "weight" not in entry or "weight_scale_inv" not in entry:
311 # Waiting for the other half of the fused projection.
312 continue
313 del pending_qkv_proj[prefix]
314
315 # Geometry comes from the module the weights belong to.
316 attn = self.get_submodule(prefix.rsplit(".", 1)[0])
317 w_rank, s_rank = _shard_fp8_qkv_proj(
318 entry["weight"],
319 entry["weight_scale_inv"],
320 num_heads=attn.total_num_heads,
321 num_kv_heads=attn.total_num_kv_heads,
322 head_dim=attn.head_dim,
323 v_head_dim=attn.v_head_dim,
324 tp_rank=tp_rank,
325 tp_size=tp_size,
326 ckpt_tp=self.config.num_key_value_heads,
327 )
328 for k, tensor in (("weight", w_rank), ("weight_scale_inv", s_rank)):
329 param = params_dict[f"{prefix}.{k}"]
330 if tensor.shape[0] > param.shape[0]:
331 tensor = tensor[: param.shape[0]]
332 default_weight_loader(param, tensor)
333 loaded_params.add(f"{prefix}.{k}")
334 elif name in params_dict:
335 # Non-fp8 fused checkpoint: unchanged behaviour.
336 param = params_dict[name]
337 loaded_weight = loaded_weight.chunk(tp_size, dim=0)[tp_rank]
338 default_weight_loader(param, loaded_weight)
339 loaded_params.add(name)
340 continue
341
342 # gate_proj/up_proj → gate_up_proj stacking (both formats);
343 # Flash: q_proj/k_proj/v_proj → qkv_proj merging.
344 stacked_matched = False
345 for param_name, weight_name, shard_id in stacked_params_mapping:
346 if weight_name not in name:
347 continue
348 name_rewritten = name.replace(weight_name, param_name)
349 if (
350 name_rewritten.endswith(".bias")
351 and name_rewritten not in params_dict
352 ):
353 continue
354 if name_rewritten not in params_dict:
355 continue
356 param = params_dict[name_rewritten]
357 weight_loader = getattr(param, "weight_loader", None)
358 if weight_loader is None:
359 default_weight_loader(param, loaded_weight)
360 else:
361 weight_loader(param, loaded_weight, shard_id)
362 loaded_params.add(name_rewritten)
363 stacked_matched = True
364 break
365
366 if stacked_matched:
367 continue
368
369 if name.endswith(".bias") and name not in params_dict:
370 continue
371 if name not in params_dict:
372 continue
373
374 param = params_dict[name]
375 # attention_sink_bias is head-parallel; slice by tp
376 if "attention_sink_bias" in name:
377 total_heads = loaded_weight.shape[0]
378 heads_per_rank = total_heads // tp_size
379 loaded_weight = loaded_weight.narrow(
380 0, tp_rank * heads_per_rank, heads_per_rank
381 )
382
383 direct_weight_loader = getattr(
384 param, "weight_loader", default_weight_loader
385 )
386 direct_weight_loader(param, loaded_weight)
387 loaded_params.add(name)
388
389 return loaded_params
390
391
392 class MiMoV2OmniMTP(MiMoV2MTP, SupportsMultiModal):
393 def embed_input_ids(
394 self,
395 input_ids: torch.Tensor,
396 multimodal_embeddings: MultiModalEmbeddings | None = None,
397 *,
398 is_multimodal: torch.Tensor | None = None,
399 ) -> torch.Tensor:
400 inputs_embeds = self._embed_text_input_ids(
401 input_ids,
402 self.model.embed_input_ids,
403 is_multimodal=is_multimodal,
404 )
405
406 if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
407 return inputs_embeds
408
409 is_multimodal = _require_is_multimodal(is_multimodal)
410
411 inputs_embeds = _merge_multimodal_embeddings(
412 inputs_embeds=inputs_embeds,
413 multimodal_embeddings=multimodal_embeddings,
414 is_multimodal=is_multimodal,
415 )
416
417 return inputs_embeds