main
py 488 lines 19.7 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
9 import vllm.envs as envs
10 from vllm.config import VllmConfig
11 from vllm.forward_context import get_forward_context, is_forward_context_available
12 from vllm.model_executor.layers.fused_embed_norm import (
13 fused_embed_eh_norm,
14 has_full_vocab_on_rank,
15 make_input_embedding,
16 )
17 from vllm.model_executor.layers.fused_moe import (
18 fused_moe_make_expert_params_mapping,
19 )
20 from vllm.model_executor.layers.layernorm import RMSNorm
21 from vllm.model_executor.layers.logits_processor import LogitsProcessor
22 from vllm.model_executor.model_loader.mtp_validation import (
23 is_mtp_completeness_check_enabled,
24 )
25 from vllm.model_executor.model_loader.weight_utils import (
26 default_weight_loader,
27 maybe_remap_kv_scale_name,
28 )
29 from vllm.model_executor.models.deepseek_mtp import SharedHead
30 from vllm.model_executor.models.deepseek_v2 import (
31 DeepseekV2MixtureOfExperts,
32 DeepseekV2MoE,
33 _try_load_fp8_indexer_wk,
34 get_spec_layer_idx_from_weight_name,
35 )
36 from vllm.model_executor.models.interfaces import SupportsPP
37 from vllm.model_executor.models.utils import (
38 get_pp_missing_layer_names,
39 make_empty_intermediate_tensors_factory,
40 maybe_prefix,
41 )
42 from vllm.models.common.ops.fused_allreduce_rms_norm import fused_allreduce_rms_norm
43 from vllm.models.common.ops.sequence_parallel import (
44 sp_all_gather,
45 sp_padding_mask,
46 sp_shard,
47 )
48 from vllm.models.deepseek_v32.common.kernels import fused_eh_norm
49 from vllm.platforms import current_platform
50 from vllm.sequence import IntermediateTensors
51 from vllm.v1.attention.backends.mla.index_group import (
52 SparseMLAIndexGroupBuilder,
53 get_sparse_mla_index_group_max_rows,
54 )
55
56 from .glm52_low_latency_gemm import (
57 build_glm52_plan,
58 enable_glm52_low_latency_gemm,
59 run_glm52_plan,
60 )
61 from .model import DeepseekV32DecoderLayer
62
63
64 class DeepseekV32MultiTokenPredictorLayer(nn.Module):
65 def __init__(self, vllm_config: VllmConfig, prefix: str) -> None:
66 super().__init__()
67 assert vllm_config.speculative_config is not None
68 config = vllm_config.speculative_config.draft_model_config.hf_config
69 self.config = config
70 quant_config = vllm_config.quant_config
71
72 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
73 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
74 self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
75 self._eh_plan = (
76 build_glm52_plan(self.eh_proj.weight, vllm_config.model_config.dtype)
77 if config.model_type == "glm_moe_dsa"
78 else None
79 )
80
81 topk_indices_buffer = torch.empty(
82 vllm_config.scheduler_config.max_num_batched_tokens,
83 config.index_topk,
84 dtype=torch.int32,
85 device=current_platform.device_type,
86 )
87 index_group_builder = SparseMLAIndexGroupBuilder(
88 topk_indices_buffer,
89 get_sparse_mla_index_group_max_rows(vllm_config),
90 )
91 self.shared_head = SharedHead(
92 config=config, prefix=prefix, quant_config=quant_config
93 )
94 self.mtp_block = DeepseekV32DecoderLayer(
95 vllm_config,
96 prefix,
97 config=config,
98 topk_indices_buffer=topk_indices_buffer,
99 index_group_builder=index_group_builder,
100 )
101
102 def forward(
103 self,
104 input_ids: torch.Tensor,
105 positions: torch.Tensor,
106 previous_hidden_states: torch.Tensor,
107 inputs_embeds: torch.Tensor | None = None,
108 embed_table: torch.Tensor | None = None,
109 spec_step_index: int = 0,
110 ) -> torch.Tensor:
111 # Fused zero pos-0 + enorm(embeds) + hnorm(prev) + cat -> [N, 2H]. With a
112 # replicated table the caller passes ``embed_table`` so the embedding
113 # lookup is folded in too (fused_embed_eh_norm); otherwise the embeds are
114 # precomputed and go through the model-local fused_eh_norm.
115 if embed_table is not None:
116 eh_input = fused_embed_eh_norm(
117 positions,
118 input_ids,
119 embed_table,
120 previous_hidden_states,
121 self.enorm.weight,
122 self.hnorm.weight,
123 self.enorm.variance_epsilon,
124 )
125 else:
126 assert inputs_embeds is not None
127 eh_input = fused_eh_norm(
128 positions,
129 inputs_embeds,
130 previous_hidden_states,
131 self.enorm.weight,
132 self.hnorm.weight,
133 self.enorm.variance_epsilon,
134 )
135 is_sequence_parallel = self.mtp_block.use_sequence_parallel
136 if is_sequence_parallel:
137 if envs.VLLM_MOE_SKIP_PADDING and is_forward_context_available():
138 forward_context = get_forward_context()
139 forward_context.is_padding = sp_padding_mask(
140 forward_context.is_padding, eh_input
141 )
142 eh_input = sp_shard(eh_input)
143 hidden_states = run_glm52_plan(self._eh_plan, eh_input, self.eh_proj.weight)
144 if hidden_states is None:
145 hidden_states = self.eh_proj(eh_input)
146 hidden_states, residual = self.mtp_block(
147 positions=positions, hidden_states=hidden_states, residual=None
148 )
149 # Recycle the POST-final-norm hidden into the next draft step. The
150 # residual-add is fused into the final RMSNorm so it is computed
151 # exactly once, and the result is returned for both tuple positions:
152 # the draft-logits hidden (compute_logits applies the LM head only) and
153 # the recycled previous_hidden_states. Recycling the pre-final-norm
154 # hidden mismatches the draft model's hnorm and lowers MTP acceptance;
155 # post-norm recycle matches deepseek_mtp.py (PR #45895). The tuple form
156 # is understood by both the V2 speculator (isinstance-tuple check) and
157 # the legacy proposer (model_returns_tuple is True for the
158 # DeepSeekMTPModel architecture).
159 if is_sequence_parallel:
160 hidden_states, _ = self.shared_head.norm(hidden_states, residual)
161 hidden_states = sp_all_gather(hidden_states)[: positions.shape[0]]
162 else:
163 # The MoE output is left un-reduced; fuse its all-reduce into the
164 # final norm, as the main model does at layer boundaries.
165 hidden_states, _ = fused_allreduce_rms_norm(
166 hidden_states, residual, self.shared_head.norm
167 )
168 return hidden_states, hidden_states
169
170
171 class DeepseekV32MultiTokenPredictor(nn.Module):
172 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
173 super().__init__()
174 config = vllm_config.model_config.hf_config
175 self.mtp_start_layer_idx = config.num_hidden_layers
176 self.num_mtp_layers = config.num_nextn_predict_layers
177 self.layers = torch.nn.ModuleDict(
178 {
179 str(idx): DeepseekV32MultiTokenPredictorLayer(
180 vllm_config, f"{prefix}.layers.{idx}"
181 )
182 for idx in range(
183 self.mtp_start_layer_idx,
184 self.mtp_start_layer_idx + self.num_mtp_layers,
185 )
186 }
187 )
188 self.embed_tokens = make_input_embedding(
189 config.vocab_size,
190 config.hidden_size,
191 quant_config=vllm_config.quant_config,
192 prefix=maybe_prefix(prefix, "embed_tokens"),
193 tie_word_embeddings=getattr(config, "tie_word_embeddings", False),
194 )
195 # A full on-rank table lets the eh_norm fusion fold in the embedding gather.
196 self.replicated_embed = has_full_vocab_on_rank(self.embed_tokens)
197 self.logits_processor = LogitsProcessor(config.vocab_size)
198
199 def set_skip_topk(self, skip: bool):
200 # index_share_for_mtp_iteration: step 0 computes top-k, steps 1+ reuse.
201 for layer in self.layers.values():
202 self_attn = getattr(layer.mtp_block, "self_attn", None)
203 if self_attn is not None and hasattr(self_attn, "skip_topk"):
204 self_attn.skip_topk = skip
205
206 def compact_topk_indices(self, slot_ids: torch.Tensor):
207 """Gather the top-k index rows at ``slot_ids`` to the front of the buffer."""
208 num_slots = slot_ids.numel()
209 for layer in self.layers.values():
210 self_attn = getattr(layer.mtp_block, "self_attn", None)
211 if self_attn is not None and hasattr(self_attn, "topk_indices_buffer"):
212 topk_indices_buffer = self_attn.topk_indices_buffer
213 topk_indices_buffer[:num_slots] = topk_indices_buffer[slot_ids]
214
215 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
216 return self.embed_tokens(input_ids)
217
218 def forward(
219 self,
220 input_ids: torch.Tensor,
221 positions: torch.Tensor,
222 previous_hidden_states: torch.Tensor,
223 inputs_embeds: torch.Tensor | None = None,
224 spec_step_idx: int = 0,
225 ) -> torch.Tensor:
226 # With a replicated table, defer the embedding gather to fused_eh_norm
227 # (folded into the enorm/hnorm/cat launch); otherwise gather it here.
228 embed_table = None
229 if inputs_embeds is None:
230 if self.replicated_embed:
231 embed_table = self.embed_tokens.weight
232 else:
233 inputs_embeds = self.embed_tokens(input_ids)
234 current_step_idx = spec_step_idx % self.num_mtp_layers
235 return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
236 input_ids,
237 positions,
238 previous_hidden_states,
239 inputs_embeds,
240 embed_table,
241 current_step_idx,
242 )
243
244 def compute_logits(
245 self,
246 hidden_states: torch.Tensor,
247 spec_step_idx: int = 0,
248 ) -> torch.Tensor:
249 current_step_idx = spec_step_idx % self.num_mtp_layers
250 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
251 # hidden_states is already post-final-norm (produced in the layer
252 # forward and recycled as-is); apply the LM head only, without a
253 # second RMSNorm.
254 return self.logits_processor(mtp_layer.shared_head.head, hidden_states)
255
256 def get_top_tokens(
257 self,
258 hidden_states: torch.Tensor,
259 spec_step_idx: int = 0,
260 ) -> torch.Tensor:
261 """Greedy draft token ids via per-rank argmax over the vocab shard.
262
263 Saves the full-vocab all-gather ``compute_logits`` does; same tokens.
264 Name is fixed by the protocol the proposer probes for
265 (``use_local_argmax_reduction``).
266 """
267 current_step_idx = spec_step_idx % self.num_mtp_layers
268 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
269 return self.logits_processor.get_top_tokens(
270 mtp_layer.shared_head.head, hidden_states
271 )
272
273
274 class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts, SupportsPP):
275 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
276 super().__init__()
277 self.config = vllm_config.model_config.hf_config
278 self.quant_config = vllm_config.quant_config
279 self.model = DeepseekV32MultiTokenPredictor(
280 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
281 )
282 if self.config.model_type == "glm_moe_dsa":
283 enable_glm52_low_latency_gemm(self, vllm_config.model_config.dtype)
284 self.set_moe_parameters()
285 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
286 ["hidden_states", "residual"], self.config.hidden_size
287 )
288
289 def set_moe_parameters(self):
290 self.num_moe_layers = self.config.num_nextn_predict_layers
291 self.num_expert_groups = self.config.n_group
292 self.moe_layers = []
293 self.moe_mlp_layers = []
294 example_moe = None
295 for layer in self.model.layers.values():
296 mlp = layer.mtp_block.mlp
297 if isinstance(mlp, DeepseekV2MoE):
298 example_moe = mlp
299 self.moe_mlp_layers.append(mlp)
300 self.moe_layers.append(mlp.experts)
301 self.extract_moe_parameters(example_moe)
302
303 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
304 return self.model.embed_input_ids(input_ids)
305
306 def forward( # type: ignore[override]
307 self,
308 input_ids: torch.Tensor | None,
309 positions: torch.Tensor,
310 hidden_states: torch.Tensor,
311 intermediate_tensors: IntermediateTensors | None = None,
312 inputs_embeds: torch.Tensor | None = None,
313 spec_step_idx: int = 0,
314 ) -> torch.Tensor:
315 return self.model(
316 input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
317 )
318
319 def compute_logits(
320 self,
321 hidden_states: torch.Tensor,
322 spec_step_idx: int = 0,
323 ) -> torch.Tensor | None:
324 return self.model.compute_logits(hidden_states, spec_step_idx)
325
326 def get_top_tokens(
327 self,
328 hidden_states: torch.Tensor,
329 spec_step_idx: int = 0,
330 ) -> torch.Tensor:
331 """See ``DeepseekV32MultiTokenPredictor.get_top_tokens``."""
332 return self.model.get_top_tokens(hidden_states, spec_step_idx)
333
334 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
335 spec_layer_weight_names = [
336 "embed_tokens",
337 "enorm",
338 "hnorm",
339 "eh_proj",
340 "shared_head",
341 ]
342 shared_weight_names = ["embed_tokens"]
343 spec_layer_weight = False
344 shared_weight = False
345 for weight_name in spec_layer_weight_names:
346 if weight_name in name:
347 spec_layer_weight = True
348 if weight_name in shared_weight_names:
349 shared_weight = True
350 break
351 if not spec_layer_weight:
352 name = name.replace(
353 f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
354 )
355 elif shared_weight:
356 name = name.replace(f"model.layers.{spec_layer}.", "model.")
357 return name
358
359 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
360 stacked_params_mapping = [
361 ("gate_up_proj", "gate_proj", 0),
362 ("gate_up_proj", "up_proj", 1),
363 ("fused_qkv_a_proj", "q_a_proj", 0),
364 ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
365 ("wk_weights_proj", "wk", 0),
366 ("wk_weights_proj", "weights_proj", 1),
367 ]
368 expert_params_mapping = fused_moe_make_expert_params_mapping(
369 self,
370 ckpt_gate_proj_name="gate_proj",
371 ckpt_down_proj_name="down_proj",
372 ckpt_up_proj_name="up_proj",
373 num_experts=self.config.n_routed_experts,
374 )
375
376 pp_missing_layer_names = get_pp_missing_layer_names(self)
377 params_dict = dict(self.named_parameters())
378 loaded_params: set[str] = set()
379 _pending_wk_fp8: dict = {}
380 for name, loaded_weight in weights:
381 if "rotary_emb.inv_freq" in name:
382 continue
383 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
384 if spec_layer is None:
385 # A tied top-level embed_tokens has no spec layer to rewrite
386 # from; the draft needs its own copy under PP.
387 param = params_dict.get(name) if "embed_tokens" in name else None
388 if param is not None:
389 weight_loader = getattr(
390 param, "weight_loader", default_weight_loader
391 )
392 weight_loader(param, loaded_weight)
393 loaded_params.add(name)
394 continue
395 name = self._rewrite_spec_layer_name(spec_layer, name)
396
397 if _try_load_fp8_indexer_wk(
398 name,
399 loaded_weight,
400 _pending_wk_fp8,
401 params_dict,
402 loaded_params,
403 pp_missing_layer_names,
404 ):
405 continue
406
407 for param_name, weight_name, shard_id in stacked_params_mapping:
408 if weight_name not in name:
409 continue
410 if ("mlp.experts." in name) and name not in params_dict:
411 continue
412 name_mapped = name.replace(weight_name, param_name)
413 if (
414 param_name == "fused_qkv_a_proj"
415 ) and name_mapped not in params_dict:
416 continue
417 else:
418 name = name_mapped
419 if name.endswith(".bias") and name not in params_dict:
420 continue
421 param = params_dict[name]
422 weight_loader = param.weight_loader
423 weight_loader(param, loaded_weight, shard_id)
424 break
425 else:
426 num_chunks = 1
427
428 for j in range(num_chunks):
429 chunk_name = name
430 weight_to_load = loaded_weight
431
432 is_expert_weight = False
433 for mapping in expert_params_mapping:
434 param_name, weight_name, expert_id, shard_id = mapping # type: ignore[assignment]
435 if weight_name not in chunk_name:
436 continue
437 is_expert_weight = True
438 name_mapped = chunk_name.replace(weight_name, param_name)
439 param = params_dict[name_mapped]
440 weight_loader = typing.cast(
441 Callable[..., bool], param.weight_loader
442 )
443 success = weight_loader(
444 param,
445 weight_to_load,
446 name_mapped,
447 shard_id=shard_id,
448 expert_id=expert_id,
449 return_success=True,
450 )
451 if success:
452 name = name_mapped
453 break
454 else:
455 if is_expert_weight:
456 continue
457 if name.endswith(".bias") and name not in params_dict:
458 continue
459 name = maybe_remap_kv_scale_name(name, params_dict) # type: ignore[assignment]
460 if name is None:
461 continue
462 if (
463 spec_layer != self.model.mtp_start_layer_idx
464 and ".layers" not in name
465 ):
466 continue
467 param = params_dict[name]
468 weight_loader = getattr(
469 param, "weight_loader", default_weight_loader
470 )
471 weight_loader(param, loaded_weight)
472 loaded_params.add(name)
473
474 loaded_layers: set[int] = set()
475 for param_name in loaded_params:
476 spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
477 if spec_layer is not None:
478 loaded_layers.add(spec_layer)
479 for layer_idx in range(
480 self.model.mtp_start_layer_idx,
481 self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
482 ):
483 if layer_idx not in loaded_layers and is_mtp_completeness_check_enabled():
484 raise ValueError(
485 f"MTP speculative decoding layer {layer_idx} weights "
486 f"missing from checkpoint."
487 )
488 return loaded_params