main
py 485 lines 19.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Copyright 2025 The ZhipuAI Team.
5 # Copyright 2023 The vLLM team.
6 # Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
7 #
8 # This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
9 # and OPT implementations in this library. It has been modified from its
10 # original forms to accommodate minor architectural differences compared
11 # to GPT-NeoX and OPT used by the Meta AI team that trained the model.
12 #
13 # Licensed under the Apache License, Version 2.0 (the "License");
14 # you may not use this file except in compliance with the License.
15 # You may obtain a copy of the License at
16 #
17 # http://www.apache.org/licenses/LICENSE-2.0
18 #
19 # Unless required by applicable law or agreed to in writing, software
20 # distributed under the License is distributed on an "AS IS" BASIS,
21 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 # See the License for the specific language governing permissions and
23 # limitations under the License.
24 """Inference-only GLM-4.7-Flash MTP model compatible with HuggingFace weights."""
25
26 import typing
27 from collections.abc import Callable, Iterable
28
29 import torch
30 import torch.nn as nn
31 from transformers import PreTrainedConfig
32
33 from vllm.config import VllmConfig
34 from vllm.model_executor.layers.fused_moe import (
35 MoERunner,
36 fused_moe_make_expert_params_mapping,
37 )
38 from vllm.model_executor.layers.fused_moe.utils import (
39 is_model_fused_shared_expert_compatible,
40 )
41 from vllm.model_executor.layers.layernorm import RMSNorm
42 from vllm.model_executor.layers.logits_processor import LogitsProcessor
43 from vllm.model_executor.layers.quantization import QuantizationConfig
44 from vllm.model_executor.layers.vocab_parallel_embedding import (
45 ParallelLMHead,
46 VocabParallelEmbedding,
47 )
48 from vllm.model_executor.model_loader.weight_utils import (
49 default_weight_loader,
50 maybe_remap_kv_scale_name,
51 )
52 from vllm.platforms import current_platform
53 from vllm.sequence import IntermediateTensors
54 from vllm.v1.attention.backends.mla.index_group import (
55 SparseMLAIndexGroupBuilder,
56 get_sparse_mla_index_group_max_rows,
57 )
58
59 from .glm4_moe_lite import (
60 Glm4MixtureOfExperts,
61 Glm4MoeLite,
62 Glm4MoeLiteDecoderLayer,
63 )
64 from .interfaces import SupportsPP
65 from .utils import get_spec_layer_idx_from_weight_name, maybe_prefix
66
67
68 class SharedHead(nn.Module):
69 def __init__(
70 self,
71 config: PreTrainedConfig,
72 prefix: str,
73 quant_config: QuantizationConfig | None = None,
74 ) -> None:
75 super().__init__()
76 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
77 self.head = ParallelLMHead(
78 config.vocab_size,
79 config.hidden_size,
80 quant_config=quant_config,
81 prefix=maybe_prefix(prefix, "head"),
82 )
83
84 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
85 return self.norm(hidden_states)
86
87
88 class Glm4MoeLiteMultiTokenPredictorLayer(nn.Module):
89 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
90 super().__init__()
91
92 speculative_config = vllm_config.speculative_config
93 assert speculative_config is not None
94 config = speculative_config.draft_model_config.hf_config
95 self.config = config
96 quant_config = vllm_config.quant_config
97
98 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
99 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
100 self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
101
102 self.device = current_platform.device_type
103
104 self.is_v32 = hasattr(config, "index_topk")
105 if self.is_v32:
106 topk_tokens = config.index_topk
107 topk_indices_buffer = torch.empty(
108 vllm_config.scheduler_config.max_num_batched_tokens,
109 topk_tokens,
110 dtype=torch.int32,
111 device=self.device,
112 )
113 else:
114 topk_indices_buffer = None
115 index_group_builder = (
116 SparseMLAIndexGroupBuilder(
117 topk_indices_buffer,
118 get_sparse_mla_index_group_max_rows(vllm_config),
119 )
120 if topk_indices_buffer is not None
121 else None
122 )
123
124 self.shared_head = SharedHead(
125 config=config, prefix=prefix, quant_config=quant_config
126 )
127 self.mtp_block = Glm4MoeLiteDecoderLayer(
128 vllm_config=vllm_config,
129 prefix=prefix,
130 config=self.config,
131 topk_indices_buffer=topk_indices_buffer,
132 index_group_builder=index_group_builder,
133 )
134
135 def forward(
136 self,
137 input_ids: torch.Tensor,
138 positions: torch.Tensor,
139 previous_hidden_states: torch.Tensor,
140 inputs_embeds: torch.Tensor | None = None,
141 spec_step_index: int = 0,
142 ) -> torch.Tensor:
143 assert inputs_embeds is not None
144 # masking inputs at position 0, as not needed by MTP
145 inputs_embeds[positions == 0] = 0
146 inputs_embeds = self.enorm(inputs_embeds)
147 previous_hidden_states = self.hnorm(previous_hidden_states)
148
149 hidden_states = self.eh_proj(
150 torch.cat([inputs_embeds, previous_hidden_states], dim=-1)
151 )
152
153 hidden_states, residual = self.mtp_block(
154 positions=positions, hidden_states=hidden_states, residual=None
155 )
156 hidden_states = residual + hidden_states
157 return hidden_states
158
159
160 class Glm4MoeLiteMultiTokenPredictor(nn.Module):
161 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
162 super().__init__()
163 config = vllm_config.model_config.hf_config
164 self.mtp_start_layer_idx = config.num_hidden_layers
165 self.num_mtp_layers = config.num_nextn_predict_layers
166 # to map the exact layer index from weights
167 self.layers = torch.nn.ModuleDict(
168 {
169 str(idx): Glm4MoeLiteMultiTokenPredictorLayer(
170 vllm_config=vllm_config,
171 prefix=f"{prefix}.layers.{idx}",
172 )
173 for idx in range(
174 self.mtp_start_layer_idx,
175 self.mtp_start_layer_idx + self.num_mtp_layers,
176 )
177 }
178 )
179 self.embed_tokens = VocabParallelEmbedding(
180 config.vocab_size,
181 config.hidden_size,
182 )
183 self.logits_processor = LogitsProcessor(config.vocab_size)
184
185 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
186 return self.embed_tokens(input_ids)
187
188 def forward(
189 self,
190 input_ids: torch.Tensor,
191 positions: torch.Tensor,
192 previous_hidden_states: torch.Tensor,
193 inputs_embeds: torch.Tensor | None = None,
194 spec_step_idx: int = 0,
195 ) -> torch.Tensor:
196 if inputs_embeds is None:
197 inputs_embeds = self.embed_tokens(input_ids)
198 current_step_idx = spec_step_idx % self.num_mtp_layers
199 return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
200 input_ids,
201 positions,
202 previous_hidden_states,
203 inputs_embeds,
204 current_step_idx,
205 )
206
207 def compute_logits(
208 self,
209 hidden_states: torch.Tensor,
210 spec_step_idx: int = 0,
211 ) -> torch.Tensor:
212 current_step_idx = spec_step_idx % self.num_mtp_layers
213 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
214 logits = self.logits_processor(
215 mtp_layer.shared_head.head, mtp_layer.shared_head(hidden_states)
216 )
217 return logits
218
219
220 class Glm4MoeLiteMTP(nn.Module, SupportsPP, Glm4MixtureOfExperts):
221 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
222 super().__init__()
223 self.config = vllm_config.model_config.hf_config
224 self.model = Glm4MoeLiteMultiTokenPredictor(
225 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
226 )
227
228 # Set MoE hyperparameters
229 self.num_moe_layers = self.config.num_nextn_predict_layers
230 self.num_expert_groups = self.config.n_group
231
232 self.moe_layers: list[MoERunner] = []
233 self.moe_mlp_layers: list[Glm4MoeLite] = []
234 example_moe = None
235 for layer in self.model.layers.values():
236 assert isinstance(layer, Glm4MoeLiteMultiTokenPredictorLayer)
237 layer = layer.mtp_block
238 assert isinstance(layer, Glm4MoeLiteDecoderLayer)
239 if isinstance(layer.mlp, Glm4MoeLite):
240 example_moe = layer.mlp
241 self.moe_mlp_layers.append(layer.mlp)
242 self.moe_layers.append(layer.mlp.experts)
243 self.extract_moe_parameters(example_moe)
244 self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
245 self.model.layers.values(),
246 Glm4MoeLite,
247 "mtp_block.mlp",
248 )
249
250 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
251 return self.model.embed_input_ids(input_ids)
252
253 def forward( # type: ignore[override]
254 self,
255 input_ids: torch.Tensor | None,
256 positions: torch.Tensor,
257 hidden_states: torch.Tensor,
258 intermediate_tensors: IntermediateTensors | None = None,
259 inputs_embeds: torch.Tensor | None = None,
260 spec_step_idx: int = 0,
261 ) -> torch.Tensor:
262 hidden_states = self.model(
263 input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
264 )
265 return hidden_states
266
267 def compute_logits(
268 self,
269 hidden_states: torch.Tensor,
270 spec_step_idx: int = 0,
271 ) -> torch.Tensor | None:
272 return self.model.compute_logits(hidden_states, spec_step_idx)
273
274 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
275 stacked_params_mapping = [
276 ("gate_up_proj", "gate_proj", 0),
277 ("gate_up_proj", "up_proj", 1),
278 ("fused_qkv_a_proj", "q_a_proj", 0),
279 ("fused_qkv_a_proj", "kv_a_proj_with_mqa", 1),
280 ]
281
282 expert_params_mapping = fused_moe_make_expert_params_mapping(
283 self,
284 ckpt_gate_proj_name="gate_proj",
285 ckpt_down_proj_name="down_proj",
286 ckpt_up_proj_name="up_proj",
287 num_experts=self.config.n_routed_experts
288 + (
289 self.config.n_shared_experts
290 if self.is_fused_shared_expert_enabled
291 else 0
292 ),
293 )
294
295 params_dict = dict(self.named_parameters())
296 loaded_params: set[str] = set()
297 for name, loaded_weight in weights:
298 if "rotary_emb.inv_freq" in name:
299 continue
300 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
301 if spec_layer is None:
302 continue
303 is_fusion_moe_shared_experts_layer = (
304 self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
305 )
306 name = self._rewrite_spec_layer_name(spec_layer, name)
307 for param_name, weight_name, shard_id in stacked_params_mapping:
308 # Skip non-stacked layers and experts (experts handled below).
309 if weight_name not in name:
310 continue
311 # We have mlp.experts[0].gate_proj in the checkpoint.
312 # Since we handle the experts below in expert_params_mapping,
313 # we need to skip here BEFORE we update the name, otherwise
314 # name will be updated to mlp.experts[0].gate_up_proj, which
315 # will then be updated below in expert_params_mapping
316 # for mlp.experts[0].gate_gate_up_proj, which breaks load.
317 if ("mlp.experts." in name) and name not in params_dict:
318 continue
319 if is_fusion_moe_shared_experts_layer:
320 continue
321 name_mapped = name.replace(weight_name, param_name)
322
323 # QKV fusion is optional, fall back to normal
324 # weight loading if it's not enabled
325 if (
326 param_name == "fused_qkv_a_proj"
327 ) and name_mapped not in params_dict:
328 continue
329 else:
330 name = name_mapped
331
332 # Skip loading extra bias for GPTQ models.
333 if name.endswith(".bias") and name not in params_dict:
334 continue
335
336 param = params_dict[name]
337 weight_loader = param.weight_loader
338 weight_loader(param, loaded_weight, shard_id)
339 break
340 else:
341 # Special handling: when AITER fusion_shared_experts is enabled,
342 # checkpoints may provide a single widened shared_experts tensor
343 # without explicit expert indices
344 # (e.g. ...mlp.shared_experts.gate_proj.weight).
345 # For models with multiple shared experts, split that tensor
346 # evenly into per-shared-expert slices and load them into
347 # appended expert slots mlp.experts.{n_routed_experts + j}.*
348 # accordingly.
349 num_chunks = 1
350 if is_fusion_moe_shared_experts_layer:
351 num_chunks = getattr(self.config, "n_shared_experts", 1) or 1
352 # Determine split axis based on op type
353 # gate/up: ColumnParallel → split along dim 0
354 # down: RowParallel → split along dim 1
355 split_dim = 1 if "down_proj.weight" in name else 0
356 total = loaded_weight.shape[split_dim]
357 assert total % num_chunks == 0, (
358 f"Shared expert weight dim {total} "
359 f"not divisible by num_chunks {num_chunks}"
360 )
361 chunk_size = total // num_chunks
362
363 for j in range(num_chunks):
364 chunk_name = name
365 weight_to_load = loaded_weight
366
367 if is_fusion_moe_shared_experts_layer:
368 if split_dim == 0:
369 weight_to_load = loaded_weight[
370 j * chunk_size : (j + 1) * chunk_size, :
371 ]
372 else:
373 weight_to_load = loaded_weight[
374 :, j * chunk_size : (j + 1) * chunk_size
375 ]
376 # Synthesize an expert-style name so expert mapping
377 # can route it
378 chunk_name = name.replace(
379 "mlp.shared_experts",
380 f"mlp.experts.{self.config.n_routed_experts + j}",
381 )
382
383 # Use expert_params_mapping to locate the destination
384 # param and delegate to its expert-aware weight_loader
385 # with expert_id.
386 is_expert_weight = False
387 for mapping in expert_params_mapping:
388 param_name, weight_name, mapping_expert_id, mapping_shard_id = (
389 mapping
390 )
391 if weight_name not in chunk_name:
392 continue
393
394 # Anyway, this is an expert weight and should not be
395 # attempted to load as other weights later
396 is_expert_weight = True
397
398 # Do not modify `name` since the loop may continue here
399 # Instead, create a new variable
400 name_mapped = chunk_name.replace(weight_name, param_name)
401
402 param = params_dict[name_mapped]
403 # We should ask the weight loader to return success or
404 # not here since otherwise we may skip experts with
405 # other available replicas.
406 weight_loader = typing.cast(
407 Callable[..., bool], param.weight_loader
408 )
409 success = weight_loader(
410 param,
411 weight_to_load,
412 name_mapped,
413 shard_id=mapping_shard_id,
414 expert_id=mapping_expert_id,
415 return_success=True,
416 )
417 if success:
418 if not is_fusion_moe_shared_experts_layer:
419 name = name_mapped
420 else:
421 loaded_params.add(name_mapped)
422 break
423 else:
424 if is_expert_weight:
425 # We've checked that this is an expert weight
426 # However it's not mapped locally to this rank
427 # So we simply skip it
428 continue
429
430 # Skip loading extra bias for GPTQ models.
431 if name.endswith(".bias") and name not in params_dict:
432 continue
433
434 remapped_name = maybe_remap_kv_scale_name(name, params_dict)
435 if remapped_name is None:
436 continue
437 name = remapped_name
438
439 # According to DeepSeek-V3 Technical Report, MTP modules
440 # shares embedding layer. We only load the first weights.
441 if (
442 spec_layer != self.model.mtp_start_layer_idx
443 and ".layers" not in name
444 ):
445 continue
446
447 param = params_dict[name]
448 weight_loader = getattr(
449 param, "weight_loader", default_weight_loader
450 )
451 weight_loader(param, loaded_weight)
452 if not is_fusion_moe_shared_experts_layer:
453 loaded_params.add(name)
454 return loaded_params
455
456 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
457 """Rewrite the weight name to match the format of the original model.
458 Add .mtp_block for modules in transformer layer block for spec layer
459 and rename shared layer weights to be top level.
460 """
461 spec_layer_weight_names = [
462 "embed_tokens",
463 "enorm",
464 "hnorm",
465 "eh_proj",
466 "shared_head",
467 ]
468 shared_weight_names = ["embed_tokens"]
469 spec_layer_weight = False
470 shared_weight = False
471 for weight_name in spec_layer_weight_names:
472 if weight_name in name:
473 spec_layer_weight = True
474 if weight_name in shared_weight_names:
475 shared_weight = True
476 break
477 if not spec_layer_weight:
478 # treat rest weights as weights for transformer layer block
479 name = name.replace(
480 f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
481 )
482 elif shared_weight:
483 # treat shared weights as top level weights
484 name = name.replace(f"model.layers.{spec_layer}.", "model.")
485 return name