main
py 431 lines 17.1 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 from vllm.config import VllmConfig
10 from vllm.model_executor.layers.fused_moe import (
11 fused_moe_make_expert_params_mapping,
12 )
13 from vllm.model_executor.layers.layernorm import RMSNorm
14 from vllm.model_executor.layers.logits_processor import LogitsProcessor
15 from vllm.model_executor.layers.vocab_parallel_embedding import (
16 VocabParallelEmbedding,
17 )
18 from vllm.model_executor.model_loader.weight_utils import (
19 default_weight_loader,
20 maybe_remap_kv_scale_name,
21 )
22 from vllm.model_executor.models.deepseek_mtp import SharedHead
23 from vllm.model_executor.models.deepseek_v2 import DeepseekV2MixtureOfExperts
24 from vllm.model_executor.models.utils import maybe_prefix
25 from vllm.models.glm5next.nvidia.ops.fused_eh_norm import fused_eh_norm
26 from vllm.platforms import current_platform
27 from vllm.sequence import IntermediateTensors
28
29 from .model import (
30 Glm5NextDecoderLayer,
31 Glm5NextMLAAttention,
32 Glm5NextMoE,
33 _try_load_fp8_attn_proj,
34 _try_load_fp8_indexer_wk,
35 get_spec_layer_idx_from_weight_name,
36 )
37
38
39 class Glm5NextMultiTokenPredictorLayer(nn.Module):
40 def __init__(self, vllm_config: VllmConfig, prefix: str) -> None:
41 super().__init__()
42 assert vllm_config.speculative_config is not None
43 config = vllm_config.speculative_config.draft_model_config.hf_text_config
44 self.config = config
45
46 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
47 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
48 self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
49
50 # Reserve room for the incomplete pool tail and align the sparse MLA
51 # buffer width to BLOCK_N=128.
52 topk_tokens = config.index_topk
53 assert topk_tokens is not None
54 kpool = config.index_kpool
55 assert kpool is not None
56 buffer_width = topk_tokens + (kpool - 1 if kpool > 1 else 0)
57 sparse_topk_block_n = 128
58 buffer_width = (
59 (buffer_width + sparse_topk_block_n - 1) // sparse_topk_block_n
60 ) * sparse_topk_block_n
61 topk_indices_buffer = torch.empty(
62 vllm_config.scheduler_config.max_num_batched_tokens,
63 buffer_width,
64 dtype=torch.int32,
65 device=current_platform.device_type,
66 )
67 self.shared_head = SharedHead(
68 config=config,
69 prefix=prefix,
70 defer_lm_head=True,
71 )
72 # MTP layers sit past the base model's hidden layers; parse the index
73 # from the prefix (e.g. "...layers.32") so the decoder builds an MLA
74 # (DSA) layer rather than KDA for the MTP path.
75 layer_idx = int(prefix.rsplit(".", 1)[-1])
76 self.mtp_block = Glm5NextDecoderLayer(
77 vllm_config=vllm_config,
78 config=config,
79 layer_idx=layer_idx,
80 prefix=prefix,
81 topk_indices_buffer=topk_indices_buffer,
82 is_mtp_layer=True,
83 )
84
85 def forward(
86 self,
87 input_ids: torch.Tensor,
88 positions: torch.Tensor,
89 previous_hidden_states: torch.Tensor,
90 inputs_embeds: torch.Tensor | None = None,
91 spec_step_index: int = 0,
92 ) -> torch.Tensor:
93 assert inputs_embeds is not None
94 # Fused: zero pos-0 embeds + enorm(embeds) + hnorm(prev) + cat -> [N, 2H].
95 eh_input = fused_eh_norm(
96 positions,
97 inputs_embeds,
98 previous_hidden_states,
99 self.enorm.weight,
100 self.hnorm.weight,
101 self.enorm.variance_epsilon,
102 )
103 hidden_states = self.eh_proj(eh_input)
104 # Fuse the residual add and final RMSNorm. Glm5NextMoE already performs
105 # its all-reduce, so no collective is needed here. The post-norm result
106 # feeds both draft logits and the next recycled hidden state.
107 hidden_states, residual, _, _ = self.mtp_block(
108 positions=positions, hidden_states=hidden_states, residual=None
109 )
110 hidden_states, _ = self.shared_head.norm(hidden_states, residual=residual)
111 return hidden_states, hidden_states
112
113
114 class Glm5NextMultiTokenPredictor(nn.Module):
115 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
116 super().__init__()
117 config = vllm_config.model_config.hf_text_config
118 self.mtp_start_layer_idx = config.num_hidden_layers
119 self.num_mtp_layers = config.num_nextn_predict_layers
120 self.layers = torch.nn.ModuleDict(
121 {
122 str(idx): Glm5NextMultiTokenPredictorLayer(
123 vllm_config, f"{prefix}.layers.{idx}"
124 )
125 for idx in range(
126 self.mtp_start_layer_idx,
127 self.mtp_start_layer_idx + self.num_mtp_layers,
128 )
129 }
130 )
131 self.embed_tokens = VocabParallelEmbedding(
132 config.vocab_size,
133 config.hidden_size,
134 prefix=maybe_prefix(prefix, "embed_tokens"),
135 )
136 # Plain list for the per-propose lookup: ModuleDict[str(...)] builds a
137 # string and hashes it on every draft step.
138 self._mtp_layers = list(self.layers.values())
139 self._mtp_mla_attns = []
140 for layer in self._mtp_layers:
141 self_attn = layer.mtp_block.self_attn
142 assert isinstance(self_attn, Glm5NextMLAAttention)
143 self._mtp_mla_attns.append(self_attn.mla_attn)
144 self.logits_processor = LogitsProcessor(config.vocab_size)
145
146 def set_skip_topk(self, skip: bool):
147 # index_share_for_mtp_iteration: step 0 computes top-k, steps 1+ reuse.
148 for mla_attn in self._mtp_mla_attns:
149 mla_attn.skip_topk = skip
150
151 def compact_topk_indices(self, slot_ids: torch.Tensor):
152 """Gather the top-k index rows at ``slot_ids`` to the front of the buffer."""
153 num_slots = slot_ids.numel()
154 for mla_attn in self._mtp_mla_attns:
155 topk_indices_buffer = mla_attn.topk_indices_buffer
156 assert topk_indices_buffer is not None
157 topk_indices_buffer[:num_slots] = topk_indices_buffer[slot_ids]
158
159 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
160 return self.embed_tokens(input_ids)
161
162 def forward(
163 self,
164 input_ids: torch.Tensor,
165 positions: torch.Tensor,
166 previous_hidden_states: torch.Tensor,
167 inputs_embeds: torch.Tensor | None = None,
168 spec_step_idx: int = 0,
169 ) -> torch.Tensor:
170 if inputs_embeds is None:
171 inputs_embeds = self.embed_tokens(input_ids)
172 current_step_idx = spec_step_idx % self.num_mtp_layers
173 return self._mtp_layers[current_step_idx](
174 input_ids,
175 positions,
176 previous_hidden_states,
177 inputs_embeds,
178 current_step_idx,
179 )
180
181 def compute_logits(
182 self,
183 hidden_states: torch.Tensor,
184 spec_step_idx: int = 0,
185 ) -> torch.Tensor:
186 current_step_idx = spec_step_idx % self.num_mtp_layers
187 mtp_layer = self._mtp_layers[current_step_idx]
188 # hidden_states is already post-final-norm (produced in the layer
189 # forward and recycled as-is); apply the LM head only, without a
190 # second RMSNorm.
191 return self.logits_processor(mtp_layer.shared_head.head, hidden_states)
192
193 def get_top_tokens(
194 self,
195 hidden_states: torch.Tensor,
196 spec_step_idx: int = 0,
197 ) -> torch.Tensor:
198 current_step_idx = spec_step_idx % self.num_mtp_layers
199 mtp_layer = self._mtp_layers[current_step_idx]
200 # Vocab-parallel argmax for the greedy draft: per-rank head projection
201 # + local argmax + a [batch, 2*tp] (value, index) reduce, instead of
202 # materializing and all-gathering full [N, vocab] logits per draft
203 # step. Tie-breaking matches the full argmax (shards are contiguous
204 # and rank-ordered, so the lowest-rank winner is the lowest global
205 # index), so greedy draft tokens are unchanged.
206 return self.logits_processor.get_top_tokens(
207 mtp_layer.shared_head.head, hidden_states
208 )
209
210
211 class Glm5NextMTP(nn.Module, DeepseekV2MixtureOfExperts):
212 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
213 super().__init__()
214 self.config = vllm_config.model_config.hf_text_config
215 self.quant_config = vllm_config.quant_config
216 self.model = Glm5NextMultiTokenPredictor(
217 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
218 )
219 self.set_moe_parameters()
220
221 def set_moe_parameters(self):
222 self.num_moe_layers = self.config.num_nextn_predict_layers
223 self.num_expert_groups = self.config.n_group
224 self.moe_layers = []
225 self.moe_mlp_layers = []
226 example_moe = None
227 for layer in self.model.layers.values():
228 mlp = layer.mtp_block.mlp
229 if isinstance(mlp, Glm5NextMoE):
230 example_moe = mlp
231 self.moe_mlp_layers.append(mlp)
232 self.moe_layers.append(mlp.experts)
233 self.extract_moe_parameters(example_moe)
234
235 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
236 return self.model.embed_input_ids(input_ids)
237
238 def forward(
239 self,
240 input_ids: torch.Tensor | None,
241 positions: torch.Tensor,
242 hidden_states: torch.Tensor,
243 intermediate_tensors: IntermediateTensors | None = None,
244 inputs_embeds: torch.Tensor | None = None,
245 spec_step_idx: int = 0,
246 ) -> torch.Tensor:
247 return self.model(
248 input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
249 )
250
251 def compute_logits(
252 self,
253 hidden_states: torch.Tensor,
254 spec_step_idx: int = 0,
255 ) -> torch.Tensor | None:
256 return self.model.compute_logits(hidden_states, spec_step_idx)
257
258 def get_top_tokens(
259 self,
260 hidden_states: torch.Tensor,
261 spec_step_idx: int = 0,
262 ) -> torch.Tensor:
263 # Greedy-draft path used when use_local_argmax_reduction is enabled:
264 # vocab-parallel argmax, no full-vocab logits.
265 return self.model.get_top_tokens(hidden_states, spec_step_idx)
266
267 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
268 spec_layer_weight_names = [
269 "embed_tokens",
270 "enorm",
271 "hnorm",
272 "eh_proj",
273 "shared_head",
274 ]
275 shared_weight_names = ["embed_tokens"]
276 spec_layer_weight = False
277 shared_weight = False
278 for weight_name in spec_layer_weight_names:
279 if weight_name in name:
280 spec_layer_weight = True
281 if weight_name in shared_weight_names:
282 shared_weight = True
283 break
284 if not spec_layer_weight:
285 name = name.replace(
286 f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
287 )
288 elif shared_weight:
289 name = name.replace(f"model.layers.{spec_layer}.", "model.")
290 return name
291
292 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
293 stacked_params_mapping = [
294 ("gate_up_proj", "gate_proj", 0),
295 ("gate_up_proj", "up_proj", 1),
296 ("fused_qkv_a_proj", "q_a_proj", 0),
297 ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
298 ("wk_weights_proj", "wk", 0),
299 ("wk_weights_proj", "weights_proj", 1),
300 ]
301 expert_params_mapping = fused_moe_make_expert_params_mapping(
302 self,
303 ckpt_gate_proj_name="gate_proj",
304 ckpt_down_proj_name="down_proj",
305 ckpt_up_proj_name="up_proj",
306 num_experts=self.config.n_routed_experts,
307 num_redundant_experts=self.num_redundant_experts,
308 )
309
310 params_dict = dict(self.named_parameters())
311 loaded_params: set[str] = set()
312 _pending_wk_fp8: dict = {}
313 # GLM-5.3-Flash NoPE checkpoints omit the RoPE rows from
314 # ``kv_a_proj_with_mqa``; the FP8-to-BF16 path pads them for the model.
315 kv_a_pad_size = 0
316 if self.config.mla_use_nope and self.config.qk_rope_head_dim > 0:
317 kv_a_pad_size = self.config.qk_rope_head_dim
318 for name, loaded_weight in weights:
319 if "rotary_emb.inv_freq" in name:
320 continue
321 # Multimodal (Glm5NextForConditionalGeneration) checkpoints prefix
322 # the text-tower weights with "model.language_model."; the MTP head
323 # is built as a text-only model (model.layers.*), so strip the
324 # prefix to match.
325 if name.startswith("model.language_model."):
326 name = name.replace("model.language_model.", "model.", 1)
327 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
328 if spec_layer is None:
329 continue
330 name = self._rewrite_spec_layer_name(spec_layer, name)
331
332 if _try_load_fp8_indexer_wk(
333 name,
334 loaded_weight,
335 _pending_wk_fp8,
336 params_dict,
337 loaded_params,
338 ):
339 continue
340
341 # FP8 checkpoint: dequantize the BF16-kept MLA projections
342 # (q_a_proj / kv_a_proj_with_mqa / o_proj) to BF16, mirroring the
343 # target model. The model holds fused_qkv_a_proj / o_proj in BF16,
344 # so the checkpoint's block-FP8 weight + weight_scale_inv for these
345 # has no param home and would KeyError without this dequant.
346 if _try_load_fp8_attn_proj(
347 name,
348 loaded_weight,
349 _pending_wk_fp8,
350 params_dict,
351 loaded_params,
352 kv_a_pad_size,
353 ):
354 continue
355
356 for param_name, weight_name, shard_id in stacked_params_mapping:
357 if weight_name not in name:
358 continue
359 if ("mlp.experts." in name) and name not in params_dict:
360 continue
361 name_mapped = name.replace(weight_name, param_name)
362 if (
363 param_name == "fused_qkv_a_proj"
364 ) and name_mapped not in params_dict:
365 continue
366 else:
367 name = name_mapped
368 if name.endswith(".bias") and name not in params_dict:
369 continue
370 param = params_dict[name]
371 weight_loader = param.weight_loader
372 weight_loader(param, loaded_weight, shard_id)
373 break
374 else:
375 is_expert_weight = False
376 for mapping in expert_params_mapping:
377 param_name, weight_name, expert_id, shard_id = mapping # type: ignore[assignment]
378 if weight_name not in name:
379 continue
380 is_expert_weight = True
381 name_mapped = name.replace(weight_name, param_name)
382 param = params_dict[name_mapped]
383 weight_loader = typing.cast(
384 Callable[..., bool], param.weight_loader
385 )
386 success = weight_loader(
387 param,
388 loaded_weight,
389 name_mapped,
390 shard_id=shard_id,
391 expert_id=expert_id,
392 return_success=True,
393 )
394 if success:
395 name = name_mapped
396 break
397 else:
398 if is_expert_weight:
399 continue
400 if name.endswith(".bias") and name not in params_dict:
401 continue
402 name = maybe_remap_kv_scale_name(name, params_dict) # type: ignore[assignment]
403 if name is None:
404 continue
405 if (
406 spec_layer != self.model.mtp_start_layer_idx
407 and ".layers" not in name
408 ):
409 continue
410 param = params_dict[name]
411 weight_loader = getattr(
412 param, "weight_loader", default_weight_loader
413 )
414 weight_loader(param, loaded_weight)
415 loaded_params.add(name)
416
417 loaded_layers: set[int] = set()
418 for param_name in loaded_params:
419 spec_layer = get_spec_layer_idx_from_weight_name(self.config, param_name)
420 if spec_layer is not None:
421 loaded_layers.add(spec_layer)
422 for layer_idx in range(
423 self.model.mtp_start_layer_idx,
424 self.model.mtp_start_layer_idx + self.model.num_mtp_layers,
425 ):
426 if layer_idx not in loaded_layers:
427 raise ValueError(
428 f"MTP speculative decoding layer {layer_idx} weights "
429 f"missing from checkpoint."
430 )
431 return loaded_params