main
py 347 lines 13.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """MiniMax M3 MTP (multi-token prediction) draft model -- ROCm/AMD variant.
4
5 Byte-identical to ``nvidia/mtp.py`` except this file lives under ``amd/`` so its
6 ``from .model import ...`` resolves to the self-contained AMD model (native Gemma
7 RMSNorm, native MXFP8 MoE, Triton sparse attention). The MTP logic is
8 platform-agnostic. (Mirrors ``vllm.models.deepseek_v4.amd.mtp``.)
9
10 TODO(future, separate diff): since this is byte-identical to ``nvidia/mtp.py``,
11 both copies could be consolidated into a single ``common/mtp.py`` that dispatches
12 its model import (``..amd.model`` vs ``..nvidia.model``) via
13 ``current_platform.is_rocm()`` -- the same dispatch ``minimax_m3/__init__.py``
14 uses. This was prototyped and VERIFIED working (``MiniMaxM3MTP`` resolves through
15 ``common.mtp`` to the AMD decoder layer / RMSNorm on ROCm), but it deletes the
16 upstream ``nvidia/mtp.py`` and touches the NVIDIA load path, so it is deferred to
17 a dedicated refactor diff to keep this AMD-enablement change NVIDIA-untouched.
18 """
19
20 from collections.abc import Iterable
21
22 import regex as re
23 import torch
24 import torch.nn as nn
25
26 from vllm.config import VllmConfig
27 from vllm.distributed import get_pp_group
28 from vllm.model_executor.layers.fused_moe import (
29 fused_moe_make_expert_params_mapping,
30 )
31 from vllm.model_executor.layers.linear import (
32 ReplicatedLinear,
33 )
34 from vllm.model_executor.layers.logits_processor import LogitsProcessor
35 from vllm.model_executor.layers.vocab_parallel_embedding import (
36 ParallelLMHead,
37 VocabParallelEmbedding,
38 )
39 from vllm.model_executor.model_loader.mtp_validation import (
40 is_mtp_completeness_check_enabled,
41 )
42 from vllm.model_executor.model_loader.weight_utils import (
43 default_weight_loader,
44 maybe_remap_kv_scale_name,
45 )
46 from vllm.model_executor.models.utils import (
47 maybe_prefix,
48 )
49 from vllm.sequence import IntermediateTensors
50
51 from .model import (
52 MiniMAXGemmaRMSNorm,
53 MiniMaxM3DecoderLayer,
54 )
55
56
57 class MiniMaxM3MultiTokenPredictorLayer(nn.Module):
58 def __init__(self, vllm_config: VllmConfig, prefix: str) -> None:
59 super().__init__()
60
61 assert vllm_config.speculative_config is not None
62 config = vllm_config.speculative_config.draft_model_config.hf_config
63 cache_config = vllm_config.cache_config
64 quant_config = vllm_config.quant_config
65
66 self.enorm = MiniMAXGemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
67 self.hnorm = MiniMAXGemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
68 self.eh_proj = ReplicatedLinear(
69 config.hidden_size * 2,
70 config.hidden_size,
71 bias=False,
72 quant_config=quant_config,
73 prefix=f"{prefix}.eh_proj",
74 )
75 self.transformer_layer = MiniMaxM3DecoderLayer(
76 config=config,
77 prefix=prefix,
78 cache_config=cache_config,
79 quant_config=quant_config,
80 force_sparse_attn=True,
81 force_moe=True,
82 )
83 self.final_layernorm = MiniMAXGemmaRMSNorm(
84 config.hidden_size, eps=config.rms_norm_eps
85 )
86
87 def forward(
88 self,
89 input_ids: torch.Tensor,
90 positions: torch.Tensor,
91 previous_hidden_states: torch.Tensor,
92 inputs_embeds: torch.Tensor | None = None,
93 spec_step_index: int = 0,
94 ) -> torch.Tensor:
95 assert inputs_embeds is not None
96 # Mask out inputs at position 0, as not needed by MTP.
97 inputs_embeds = torch.where(positions.unsqueeze(-1) == 0, 0, inputs_embeds)
98
99 # Combine the normalized token embeddings with the normalized
100 # previous hidden states.
101 inputs_embeds = self.enorm(inputs_embeds)
102 previous_hidden_states = self.hnorm(previous_hidden_states)
103 hidden_states, _ = self.eh_proj(
104 torch.cat([inputs_embeds, previous_hidden_states], dim=-1)
105 )
106
107 # Apply transformer layer.
108 hidden_states, residual = self.transformer_layer(
109 positions=positions,
110 hidden_states=hidden_states,
111 residual=None,
112 )
113
114 hidden_states += residual
115 return hidden_states
116
117
118 class MiniMaxM3MultiTokenPredictor(nn.Module):
119 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
120 super().__init__()
121
122 assert vllm_config.speculative_config is not None
123 # Use the draft (MTP) config, not the target model's. This is flat for a
124 # standalone checkpoint, and the promoted text_config for a bundled one.
125 config = vllm_config.speculative_config.draft_model_config.hf_config
126 self.num_mtp_layers = config.num_mtp_modules
127 self.layers = torch.nn.ModuleDict(
128 {
129 str(idx): MiniMaxM3MultiTokenPredictorLayer(
130 vllm_config, f"{prefix}.layers.{idx}"
131 )
132 for idx in range(self.num_mtp_layers)
133 }
134 )
135 self.embed_tokens = VocabParallelEmbedding(
136 config.vocab_size,
137 config.hidden_size,
138 prefix=maybe_prefix(prefix, "embed_tokens"),
139 )
140
141 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
142 return self.embed_tokens(input_ids)
143
144 def forward(
145 self,
146 input_ids: torch.Tensor,
147 positions: torch.Tensor,
148 previous_hidden_states: torch.Tensor,
149 inputs_embeds: torch.Tensor | None = None,
150 spec_step_idx: int = 0,
151 ) -> torch.Tensor:
152 if inputs_embeds is None:
153 inputs_embeds = self.embed_tokens(input_ids)
154 current_step_idx = spec_step_idx % self.num_mtp_layers
155 return self.layers[str(current_step_idx)](
156 input_ids,
157 positions,
158 previous_hidden_states,
159 inputs_embeds,
160 current_step_idx,
161 )
162
163
164 class MiniMaxM3MTP(nn.Module):
165 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
166 super().__init__()
167
168 if vllm_config.use_v2_model_runner and get_pp_group().world_size > 1:
169 # Preserve embeddings from loaders that bypass load_weights.
170 self.has_own_embed_tokens = vllm_config.load_config.load_format != "dummy"
171
172 assert vllm_config.speculative_config is not None
173 self.config = vllm_config.speculative_config.draft_model_config.hf_config
174 self.quant_config = vllm_config.quant_config
175 self.model = MiniMaxM3MultiTokenPredictor(
176 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
177 )
178 self.lm_head = ParallelLMHead(
179 self.config.vocab_size,
180 self.config.hidden_size,
181 quant_config=self.quant_config,
182 prefix=maybe_prefix(prefix, "lm_head"),
183 )
184 self.logits_processor = LogitsProcessor(self.config.vocab_size)
185
186 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
187 return self.model.embed_input_ids(input_ids)
188
189 def forward(
190 self,
191 input_ids: torch.Tensor | None,
192 positions: torch.Tensor,
193 hidden_states: torch.Tensor,
194 intermediate_tensors: IntermediateTensors | None = None,
195 inputs_embeds: torch.Tensor | None = None,
196 spec_step_idx: int = 0,
197 ) -> torch.Tensor:
198 return self.model(
199 input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
200 )
201
202 def compute_logits(
203 self,
204 hidden_states: torch.Tensor,
205 spec_step_idx: int = 0,
206 ) -> torch.Tensor | None:
207 current_step_idx = spec_step_idx % self.model.num_mtp_layers
208 mtp_layer = self.model.layers[str(current_step_idx)]
209 return self.logits_processor(
210 self.lm_head, mtp_layer.final_layernorm(hidden_states)
211 )
212
213 def _get_mtp_layer_idx_from_weight_name(self, name: str) -> int | None:
214 """Return the MTP layer index in *.mtp.layers.{idx}.*, else None."""
215 match = re.search(r"\.mtp\.layers\.(\d+)\.", name)
216 return int(match.group(1)) if match else None
217
218 def _map_checkpoint_name(self, name: str) -> str | None:
219 """Map a full checkpoint key to this MTP module's parameter name.
220
221 The MTP module only owns the *.mtp.layers.* weights plus the token
222 embedding and LM head, which the checkpoint shares with the main model.
223 Everything else belongs to other modules and is ignored here by returning
224 None.
225 """
226 # In the bundled checkpoint, the MTP weights are prefixed with
227 # "language_model". The standalone MTP checkpoint has no such prefix.
228 # Strip it if present.
229 name = name.removeprefix("language_model.")
230
231 if name == "model.embed_tokens.weight":
232 return "model.embed_tokens.weight"
233 if name == "lm_head.weight":
234 return "lm_head.weight"
235 if "model.mtp.layers" in name:
236 if "weight_scale_inv" in name:
237 # The checkpoint stores block scales as "weight_scale_inv".
238 # The ModelOpt MXFP8 layers expose them as "weight_scale".
239 name = name.replace("weight_scale_inv", "weight_scale")
240 # Strip "mtp" from prefix.
241 return name.replace(".mtp.", ".")
242 return None
243
244 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
245 # Map q/k/v projections to qkv_proj, and gate/up projections to gate_up_proj.
246 stacked_params_mapping: list[tuple[str, str, int | str]] = [
247 # (param_name, shard_name, shard_id)
248 (".qkv_proj", ".q_proj", "q"),
249 (".qkv_proj", ".k_proj", "k"),
250 (".qkv_proj", ".v_proj", "v"),
251 (".qkv_proj", ".index_q_proj", "index_q"),
252 (".qkv_proj", ".index_k_proj", "index_k"),
253 (".gate_up_proj", ".gate_proj", 0),
254 (".gate_up_proj", ".up_proj", 1),
255 ]
256
257 # Map expert weights w1/w2/w3 to gate/down/up.
258 # (param_name, weight_name, expert_id, shard_id)
259 expert_params_mapping = fused_moe_make_expert_params_mapping(
260 self,
261 ckpt_gate_proj_name="w1",
262 ckpt_down_proj_name="w2",
263 ckpt_up_proj_name="w3",
264 num_experts=self.config.num_local_experts,
265 )
266
267 params_dict = dict(self.named_parameters())
268 loaded_params: set[str] = set()
269 loaded_mtp_layers: set[int] = set()
270 for name, loaded_weight in weights:
271 mtp_layer = self._get_mtp_layer_idx_from_weight_name(name)
272 mapped_name = self._map_checkpoint_name(name)
273 if mapped_name is None:
274 # This weight does not belong to the MTP module, so skip it.
275 continue
276 name = mapped_name
277
278 for param_name, weight_name, shard_id in stacked_params_mapping:
279 if weight_name not in name:
280 continue
281
282 # Routed experts (w1/w2/w3) are handled below. Don't let the
283 # stacked mapping rewrite them.
284 if ("block_sparse_moe.experts." in name) and name not in params_dict:
285 continue
286 name = name.replace(weight_name, param_name)
287 if name not in params_dict:
288 continue
289 param = params_dict[name]
290 weight_loader = param.weight_loader
291 weight_loader(param, loaded_weight, shard_id)
292 break
293 else:
294 for (
295 param_name,
296 weight_name,
297 expert_id,
298 expert_shard_id,
299 ) in expert_params_mapping:
300 if weight_name not in name:
301 continue
302
303 name = name.replace(weight_name, param_name)
304 if name not in params_dict:
305 continue
306 param = params_dict[name]
307 weight_loader = param.weight_loader
308 weight_loader(
309 param,
310 loaded_weight,
311 name,
312 shard_id=expert_shard_id,
313 expert_id=expert_id,
314 )
315 break
316 else:
317 remapped_name = maybe_remap_kv_scale_name(name, params_dict)
318 if remapped_name is None or remapped_name not in params_dict:
319 continue
320 name = remapped_name
321 param = params_dict[name]
322 weight_loader = getattr(
323 param, "weight_loader", default_weight_loader
324 )
325 weight_loader(param, loaded_weight)
326
327 loaded_params.add(name)
328 if mtp_layer is not None:
329 loaded_mtp_layers.add(mtp_layer)
330
331 # Validate that weights were loaded for each MTP layer.
332 for layer_idx in range(self.model.num_mtp_layers):
333 if (
334 layer_idx not in loaded_mtp_layers
335 and is_mtp_completeness_check_enabled()
336 ):
337 raise ValueError(
338 f"Failed to load MTP layer {layer_idx} weights from checkpoint."
339 )
340
341 if hasattr(self, "has_own_embed_tokens"):
342 if "model.embed_tokens.weight" in loaded_params:
343 self.has_own_embed_tokens = True
344 elif is_mtp_completeness_check_enabled():
345 # A complete checkpoint without embeddings borrows the target's.
346 self.has_own_embed_tokens = False
347 return loaded_params