main
py 565 lines 22.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import typing
4 from collections.abc import Callable, Iterable
5
6 import torch
7 import torch.nn as nn
8 from transformers import PreTrainedConfig
9
10 from vllm.compilation.decorators import support_torch_compile
11 from vllm.config import VllmConfig
12 from vllm.distributed import tensor_model_parallel_all_gather
13 from vllm.model_executor.layers.fused_moe import (
14 fused_moe_make_expert_params_mapping,
15 )
16 from vllm.model_executor.layers.fused_moe.utils import (
17 is_model_fused_shared_expert_compatible,
18 )
19 from vllm.model_executor.layers.layernorm import RMSNorm
20 from vllm.model_executor.layers.logits_processor import LogitsProcessor
21 from vllm.model_executor.layers.quantization import QuantizationConfig
22 from vllm.model_executor.layers.vocab_parallel_embedding import (
23 ParallelLMHead,
24 VocabParallelEmbedding,
25 )
26 from vllm.model_executor.model_loader.mtp_validation import (
27 is_mtp_completeness_check_enabled,
28 )
29 from vllm.model_executor.model_loader.weight_utils import (
30 default_weight_loader,
31 maybe_remap_kv_scale_name,
32 )
33 from vllm.platforms import current_platform
34 from vllm.sequence import IntermediateTensors
35
36 from .deepseek_v2 import (
37 DeepseekV2DecoderLayer,
38 DeepseekV2MixtureOfExperts,
39 DeepseekV2MoE,
40 _try_load_fp8_indexer_wk,
41 )
42 from .interfaces import SupportsPP
43 from .utils import (
44 get_pp_missing_layer_names,
45 get_spec_layer_idx_from_weight_name,
46 make_empty_intermediate_tensors_factory,
47 maybe_prefix,
48 )
49
50
51 class SharedHead(nn.Module):
52 def __init__(
53 self,
54 config: PreTrainedConfig,
55 prefix: str,
56 quant_config: QuantizationConfig | None = None,
57 *,
58 defer_lm_head: bool = False,
59 ) -> None:
60 super().__init__()
61 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
62 if defer_lm_head:
63 self.head = None
64 else:
65 self.head = ParallelLMHead(
66 config.vocab_size,
67 config.hidden_size,
68 quant_config=quant_config,
69 prefix=maybe_prefix(prefix, "head"),
70 )
71
72 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
73 return self.norm(hidden_states)
74
75
76 class DeepSeekMultiTokenPredictorLayer(nn.Module):
77 def __init__(self, vllm_config: VllmConfig, prefix: str) -> None:
78 super().__init__()
79
80 assert vllm_config.speculative_config is not None
81 config = vllm_config.speculative_config.draft_model_config.hf_config
82 self.config = config
83 quant_config = vllm_config.quant_config
84
85 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
86 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
87 self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
88
89 self.device = current_platform.device_type
90
91 self.is_v32 = hasattr(config, "index_topk")
92 if self.is_v32:
93 topk_tokens = config.index_topk
94 topk_indices_buffer = torch.empty(
95 vllm_config.scheduler_config.max_num_batched_tokens,
96 topk_tokens,
97 dtype=torch.int32,
98 device=self.device,
99 )
100 else:
101 topk_indices_buffer = None
102
103 self.shared_head = SharedHead(
104 config=config, prefix=prefix, quant_config=quant_config
105 )
106 self.mtp_block = DeepseekV2DecoderLayer(
107 vllm_config,
108 prefix,
109 config=self.config,
110 topk_indices_buffer=topk_indices_buffer,
111 )
112
113 def forward(
114 self,
115 input_ids: torch.Tensor,
116 positions: torch.Tensor,
117 previous_hidden_states: torch.Tensor,
118 inputs_embeds: torch.Tensor | None = None,
119 spec_step_index: int = 0,
120 ) -> torch.Tensor:
121 assert inputs_embeds is not None
122 # masking inputs at position 0, as not needed by MTP
123 inputs_embeds = torch.where(positions.unsqueeze(-1) == 0, 0, inputs_embeds)
124 inputs_embeds = self.enorm(inputs_embeds)
125 previous_hidden_states = self.hnorm(previous_hidden_states)
126
127 hidden_states = self.eh_proj(
128 torch.cat([inputs_embeds, previous_hidden_states], dim=-1)
129 )
130
131 hidden_states, residual = self.mtp_block(
132 positions=positions,
133 hidden_states=hidden_states,
134 residual=None,
135 )
136 hidden_states = residual + hidden_states # pre-final-norm (logits hidden)
137 if self.mtp_block.use_sequence_parallel_moe:
138 hidden_states = tensor_model_parallel_all_gather(hidden_states, 0)
139 hidden_states = hidden_states[: positions.shape[0]]
140 # Recycle the post-final-norm hidden into the next draft step.
141 # compute_logits applies shared_head (== final norm) to the pre-norm
142 # element, so logits and the recycle each get exactly one final-norm.
143 # Matches SGLang's deepseek_nextn.
144 return hidden_states, self.shared_head(hidden_states)
145
146
147 class DeepSeekMultiTokenPredictor(nn.Module):
148 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
149 super().__init__()
150 config = vllm_config.model_config.hf_config
151 self.mtp_start_layer_idx = config.num_hidden_layers
152 self.num_mtp_layers = config.num_nextn_predict_layers
153 # to map the exact layer index from weights
154
155 self.layers = torch.nn.ModuleDict(
156 {
157 str(idx): DeepSeekMultiTokenPredictorLayer(
158 vllm_config, f"{prefix}.layers.{idx}"
159 )
160 for idx in range(
161 self.mtp_start_layer_idx,
162 self.mtp_start_layer_idx + self.num_mtp_layers,
163 )
164 }
165 )
166 self.embed_tokens = VocabParallelEmbedding(
167 config.vocab_size,
168 config.hidden_size,
169 prefix=maybe_prefix(prefix, "embed_tokens"),
170 )
171 self.logits_processor = LogitsProcessor(config.vocab_size)
172
173 def set_skip_topk(self, skip: bool):
174 """Toggle skip_topk on all MTP layers with sparse attention.
175
176 Called by the proposer to implement index_share_for_mtp_iteration:
177 step 0 sets skip=False (compute own indices), steps 1+ set skip=True
178 (reuse step 0's indices).
179 """
180 for layer in self.layers.values():
181 mtp_block = getattr(layer, "mtp_block", None)
182 if mtp_block is not None:
183 self_attn = getattr(mtp_block, "self_attn", None)
184 if self_attn is not None:
185 mla_attn = getattr(self_attn, "mla_attn", None)
186 if mla_attn is not None and hasattr(mla_attn, "skip_topk"):
187 mla_attn.skip_topk = skip
188
189 def compact_topk_indices(self, slot_ids: torch.Tensor):
190 """Gather the top-k index rows at ``slot_ids`` to the front of the buffer."""
191 num_slots = slot_ids.numel()
192 for layer in self.layers.values():
193 mtp_block = getattr(layer, "mtp_block", None)
194 if mtp_block is not None:
195 self_attn = getattr(mtp_block, "self_attn", None)
196 if self_attn is not None:
197 mla_attn = getattr(self_attn, "mla_attn", None)
198 if mla_attn is not None and hasattr(
199 mla_attn, "topk_indices_buffer"
200 ):
201 topk_indices_buffer = mla_attn.topk_indices_buffer
202 topk_indices_buffer[:num_slots] = topk_indices_buffer[slot_ids]
203
204 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
205 return self.embed_tokens(input_ids)
206
207 def forward(
208 self,
209 input_ids: torch.Tensor,
210 positions: torch.Tensor,
211 previous_hidden_states: torch.Tensor,
212 inputs_embeds: torch.Tensor | None = None,
213 spec_step_idx: int = 0,
214 ) -> torch.Tensor:
215 if inputs_embeds is None:
216 inputs_embeds = self.embed_tokens(input_ids)
217 current_step_idx = spec_step_idx % self.num_mtp_layers
218 return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
219 input_ids,
220 positions,
221 previous_hidden_states,
222 inputs_embeds,
223 current_step_idx,
224 )
225
226 def compute_logits(
227 self,
228 hidden_states: torch.Tensor,
229 spec_step_idx: int = 0,
230 ) -> torch.Tensor:
231 current_step_idx = spec_step_idx % self.num_mtp_layers
232 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
233 logits = self.logits_processor(
234 mtp_layer.shared_head.head, mtp_layer.shared_head(hidden_states)
235 )
236 return logits
237
238
239 @support_torch_compile
240 class DeepSeekMTP(nn.Module, DeepseekV2MixtureOfExperts, SupportsPP):
241 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
242 super().__init__()
243 self.config = vllm_config.model_config.hf_config
244 self.quant_config = vllm_config.quant_config
245 self.model = DeepSeekMultiTokenPredictor(
246 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
247 )
248 # Set MoE hyperparameters
249 self.set_moe_parameters()
250 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
251 ["hidden_states", "residual"], self.config.hidden_size
252 )
253
254 def set_moe_parameters(self):
255 self.num_moe_layers = self.config.num_nextn_predict_layers
256 self.num_expert_groups = self.config.n_group
257
258 self.moe_layers = []
259 self.moe_mlp_layers = []
260 example_moe = None
261 for layer in self.model.layers.values():
262 assert isinstance(layer, DeepSeekMultiTokenPredictorLayer)
263 layer = layer.mtp_block
264 assert isinstance(layer, DeepseekV2DecoderLayer)
265 if isinstance(layer.mlp, DeepseekV2MoE):
266 example_moe = layer.mlp
267 self.moe_mlp_layers.append(layer.mlp)
268 self.moe_layers.append(layer.mlp.experts)
269 self.extract_moe_parameters(example_moe)
270 self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
271 self.model.layers.values(),
272 DeepseekV2MoE,
273 "mtp_block.mlp",
274 )
275
276 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
277 return self.model.embed_input_ids(input_ids)
278
279 def forward( # type: ignore[override]
280 self,
281 input_ids: torch.Tensor | None,
282 positions: torch.Tensor,
283 hidden_states: torch.Tensor,
284 intermediate_tensors: IntermediateTensors | None = None,
285 inputs_embeds: torch.Tensor | None = None,
286 spec_step_idx: int = 0,
287 ) -> torch.Tensor:
288 hidden_states = self.model(
289 input_ids,
290 positions,
291 hidden_states,
292 inputs_embeds,
293 spec_step_idx,
294 )
295 return hidden_states
296
297 def compute_logits(
298 self,
299 hidden_states: torch.Tensor,
300 spec_step_idx: int = 0,
301 ) -> torch.Tensor | None:
302 return self.model.compute_logits(hidden_states, spec_step_idx)
303
304 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
305 stacked_params_mapping = [
306 ("gate_up_proj", "gate_proj", 0),
307 ("gate_up_proj", "up_proj", 1),
308 ("fused_qkv_a_proj", "q_a_proj", 0),
309 ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
310 ]
311
312 # Fused indexer wk + weights_proj (shard 0 = wk, shard 1 = weights_proj)
313 indexer_fused_mapping = [
314 ("wk_weights_proj", "wk", 0),
315 ("wk_weights_proj", "weights_proj", 1),
316 ]
317 stacked_params_mapping.extend(indexer_fused_mapping)
318
319 expert_params_mapping = fused_moe_make_expert_params_mapping(
320 self,
321 ckpt_gate_proj_name="gate_proj",
322 ckpt_down_proj_name="down_proj",
323 ckpt_up_proj_name="up_proj",
324 num_experts=self.config.n_routed_experts
325 + (
326 self.config.n_shared_experts
327 if self.is_fused_shared_expert_enabled
328 else 0
329 ),
330 )
331
332 pp_missing_layer_names = get_pp_missing_layer_names(self)
333 params_dict = dict(self.named_parameters())
334 loaded_params: set[str] = set()
335 _pending_wk_fp8: dict = {} # FP8 indexer wk dequant buffer
336 for name, loaded_weight in weights:
337 if "rotary_emb.inv_freq" in name:
338 continue
339 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
340 if spec_layer is None:
341 # A tied top-level embed_tokens has no spec layer to rewrite
342 # from; the draft needs its own copy under PP.
343 param = params_dict.get(name) if "embed_tokens" in name else None
344 if param is not None:
345 weight_loader = getattr(
346 param, "weight_loader", default_weight_loader
347 )
348 weight_loader(param, loaded_weight)
349 loaded_params.add(name)
350 continue
351 is_fusion_moe_shared_experts_layer = (
352 self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
353 )
354 name = self._rewrite_spec_layer_name(spec_layer, name)
355
356 if _try_load_fp8_indexer_wk(
357 name,
358 loaded_weight,
359 _pending_wk_fp8,
360 params_dict,
361 loaded_params,
362 pp_missing_layer_names,
363 ):
364 continue
365
366 for param_name, weight_name, shard_id in stacked_params_mapping:
367 # Skip non-stacked layers and experts (experts handled below).
368 if weight_name not in name:
369 continue
370 # We have mlp.experts[0].gate_proj in the checkpoint.
371 # Since we handle the experts below in expert_params_mapping,
372 # we need to skip here BEFORE we update the name, otherwise
373 # name will be updated to mlp.experts[0].gate_up_proj, which
374 # will then be updated below in expert_params_mapping
375 # for mlp.experts[0].gate_gate_up_proj, which breaks load.
376 if ("mlp.experts." in name) and name not in params_dict:
377 continue
378 if is_fusion_moe_shared_experts_layer:
379 continue
380 name_mapped = name.replace(weight_name, param_name)
381
382 # QKV fusion is optional, fall back to normal
383 # weight loading if it's not enabled
384 if (
385 param_name == "fused_qkv_a_proj"
386 ) and name_mapped not in params_dict:
387 continue
388 else:
389 name = name_mapped
390
391 # Skip loading extra bias for GPTQ models.
392 if name.endswith(".bias") and name not in params_dict:
393 continue
394
395 param = params_dict[name]
396 weight_loader = param.weight_loader
397 weight_loader(param, loaded_weight, shard_id)
398 break
399 else:
400 # Special handling: when AITER fusion_shared_experts is enabled,
401 # checkpoints may provide a single widened shared_experts tensor
402 # without explicit expert indices
403 # (e.g. ...mlp.shared_experts.gate_proj.weight).
404 # For models with multiple shared experts, split that tensor
405 # evenly into per-shared-expert slices and load them into
406 # appended expert slots mlp.experts.{n_routed_experts + j}.*
407 # accordingly.
408 num_chunks = 1
409 if is_fusion_moe_shared_experts_layer:
410 num_chunks = getattr(self.config, "n_shared_experts", 1) or 1
411 # Determine split axis based on op type
412 # gate/up: ColumnParallel → split along dim 0
413 # down: RowParallel → split along dim 1
414 split_dim = (
415 1
416 if ("down_proj.weight" in name and loaded_weight.ndim > 1)
417 else 0
418 )
419 total = loaded_weight.shape[split_dim]
420 assert total % num_chunks == 0, (
421 f"Shared expert weight dim {total} "
422 f"not divisible by num_chunks {num_chunks}"
423 )
424 chunk_size = total // num_chunks
425
426 for j in range(num_chunks):
427 chunk_name = name
428 weight_to_load = loaded_weight
429
430 if is_fusion_moe_shared_experts_layer:
431 chunk_slice = slice(j * chunk_size, (j + 1) * chunk_size)
432 if loaded_weight.ndim == 1:
433 weight_to_load = loaded_weight[chunk_slice]
434 elif split_dim == 0:
435 weight_to_load = loaded_weight[chunk_slice, :]
436 else:
437 weight_to_load = loaded_weight[:, chunk_slice]
438 # Synthesize an expert-style name so expert mapping
439 # can route it
440 chunk_name = name.replace(
441 "mlp.shared_experts",
442 f"mlp.experts.{self.config.n_routed_experts + j}",
443 )
444
445 # Use expert_params_mapping to locate the destination
446 # param and delegate to its expert-aware weight_loader
447 # with expert_id.
448 is_expert_weight = False
449 for mapping in expert_params_mapping:
450 param_name, weight_name, expert_id, expert_shard_id = mapping
451 if weight_name not in chunk_name:
452 continue
453
454 # Anyway, this is an expert weight and should not be
455 # attempted to load as other weights later
456 is_expert_weight = True
457
458 # Do not modify `name` since the loop may continue here
459 # Instead, create a new variable
460 name_mapped = chunk_name.replace(weight_name, param_name)
461
462 param = params_dict[name_mapped]
463 # We should ask the weight loader to return success or
464 # not here since otherwise we may skip experts with
465 # other available replicas.
466 weight_loader = typing.cast(
467 Callable[..., bool], param.weight_loader
468 )
469 success = weight_loader(
470 param,
471 weight_to_load,
472 name_mapped,
473 shard_id=expert_shard_id,
474 expert_id=expert_id,
475 return_success=True,
476 )
477 if success:
478 if not is_fusion_moe_shared_experts_layer:
479 name = name_mapped
480 else:
481 loaded_params.add(name_mapped)
482 break
483 else:
484 if is_expert_weight:
485 # We've checked that this is an expert weight
486 # However it's not mapped locally to this rank
487 # So we simply skip it
488 continue
489
490 # Skip loading extra bias for GPTQ models.
491 if name.endswith(".bias") and name not in params_dict:
492 continue
493
494 remapped_name = maybe_remap_kv_scale_name(name, params_dict)
495 if remapped_name is None:
496 continue
497 name = remapped_name
498
499 # According to DeepSeek-V3 Technical Report, MTP modules
500 # shares embedding layer. We only load the first weights.
501 if (
502 spec_layer != self.model.mtp_start_layer_idx
503 and ".layers" not in name
504 ):
505 continue
506
507 param = params_dict[name]
508 weight_loader = getattr(
509 param, "weight_loader", default_weight_loader
510 )
511 weight_loader(param, loaded_weight)
512 if not is_fusion_moe_shared_experts_layer:
513 loaded_params.add(name)
514
515 # Validate that weights were loaded for each expected MTP layer.
516 loaded_layers: set[int] = set()
517 for param_name in loaded_params:
518 spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
519 if spec_layer is not None:
520 loaded_layers.add(spec_layer)
521 for layer_idx in range(
522 self.model.mtp_start_layer_idx,
523 self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
524 ):
525 if layer_idx not in loaded_layers and is_mtp_completeness_check_enabled():
526 raise ValueError(
527 f"MTP speculative decoding layer {layer_idx} weights "
528 f"missing from checkpoint. The checkpoint may have "
529 f"been quantized without including the MTP layers. "
530 f"Use a checkpoint that includes MTP layer weights, "
531 f"or disable speculative decoding."
532 )
533
534 return loaded_params
535
536 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
537 """Rewrite the weight name to match the format of the original model.
538 Add .mtp_block for modules in transformer layer block for spec layer
539 and rename shared layer weights to be top level.
540 """
541 spec_layer_weight_names = [
542 "embed_tokens",
543 "enorm",
544 "hnorm",
545 "eh_proj",
546 "shared_head",
547 ]
548 shared_weight_names = ["embed_tokens"]
549 spec_layer_weight = False
550 shared_weight = False
551 for weight_name in spec_layer_weight_names:
552 if weight_name in name:
553 spec_layer_weight = True
554 if weight_name in shared_weight_names:
555 shared_weight = True
556 break
557 if not spec_layer_weight:
558 # treat rest weights as weights for transformer layer block
559 name = name.replace(
560 f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
561 )
562 elif shared_weight:
563 # treat shared weights as top level weights
564 name = name.replace(f"model.layers.{spec_layer}.", "model.")
565 return name