main
py 481 lines 18 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # coding=utf-8
5 # Copyright 2026 The HY team.
6 # Copyright 2023 The vLLM team.
7 # Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
8 #
9 # This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
10 # and OPT implementations in this library. It has been modified from its
11 # original forms to accommodate minor architectural differences compared
12 # to GPT-NeoX and OPT used by the Meta AI team that trained the model.
13 #
14 # Licensed under the Apache License, Version 2.0 (the "License");
15 # you may not use this file except in compliance with the License.
16 # You may obtain a copy of the License at
17 #
18 # http://www.apache.org/licenses/LICENSE-2.0
19 #
20 # Unless required by applicable law or agreed to in writing, software
21 # distributed under the License is distributed on an "AS IS" BASIS,
22 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 # See the License for the specific language governing permissions and
24 # limitations under the License.
25 """Inference-only HY V3 MTP model compatible with HuggingFace weights."""
26
27 from collections.abc import Callable, Iterable
28
29 import regex as re
30 import torch
31 from torch import nn
32 from transformers import PreTrainedConfig
33
34 from vllm.config import CacheConfig, ModelConfig, VllmConfig
35 from vllm.model_executor.layers.fused_moe import (
36 fused_moe_make_expert_params_mapping,
37 )
38 from vllm.model_executor.layers.layernorm import RMSNorm
39 from vllm.model_executor.layers.logits_processor import LogitsProcessor
40 from vllm.model_executor.layers.quantization import QuantizationConfig
41 from vllm.model_executor.layers.vocab_parallel_embedding import (
42 ParallelLMHead,
43 VocabParallelEmbedding,
44 )
45 from vllm.model_executor.model_loader.weight_utils import (
46 default_weight_loader,
47 maybe_remap_kv_scale_name,
48 )
49 from vllm.sequence import IntermediateTensors
50 from vllm.v1.outputs import SamplerOutput
51 from vllm.v1.sample.metadata import SamplingMetadata
52 from vllm.v1.sample.sampler import Sampler
53
54 from .hy_v3 import HYV3DecoderLayer
55 from .utils import (
56 get_spec_layer_idx_from_weight_name,
57 is_pp_missing_parameter,
58 maybe_prefix,
59 )
60
61
62 def _is_moe(config: PreTrainedConfig) -> bool:
63 return bool(
64 getattr(config, "num_experts", None)
65 and (
66 (isinstance(config.num_experts, int) and config.num_experts > 1)
67 or (isinstance(config.num_experts, list) and max(config.num_experts) > 1)
68 )
69 )
70
71
72 def _get_cla_factor(config: PreTrainedConfig) -> int:
73 if not getattr(config, "use_cla", False):
74 return 1
75 return getattr(config, "cla_share_factor", 1)
76
77
78 class HYV3SharedHead(nn.Module):
79 def __init__(
80 self,
81 config: PreTrainedConfig,
82 quant_config: QuantizationConfig | None = None,
83 ) -> None:
84 super().__init__()
85 self.head = ParallelLMHead(
86 config.vocab_size, config.hidden_size, quant_config=quant_config
87 )
88
89 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
90 return hidden_states
91
92
93 class HYV3MultiTokenPredictorLayer(nn.Module):
94 def __init__(
95 self,
96 config: PreTrainedConfig,
97 prefix: str,
98 model_config: ModelConfig,
99 cache_config: CacheConfig | None = None,
100 quant_config: QuantizationConfig | None = None,
101 ) -> None:
102 super().__init__()
103
104 self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
105 self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
106 self.eh_proj = nn.Linear(config.hidden_size * 2, config.hidden_size, bias=False)
107 self.shared_head = HYV3SharedHead(config=config, quant_config=quant_config)
108 self.mtp_block = HYV3DecoderLayer(
109 config=config,
110 cache_config=cache_config,
111 quant_config=quant_config,
112 prefix=prefix,
113 )
114 # Final layernorm applied after transformer block, before logits
115 # projection (matches HF HYV3MTPDecoderLayer.final_layernorm)
116 self.final_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
117
118 def forward(
119 self,
120 input_ids: torch.Tensor,
121 positions: torch.Tensor,
122 previous_hidden_states: torch.Tensor,
123 inputs_embeds: torch.Tensor | None = None,
124 spec_step_index: int = 0,
125 ) -> torch.Tensor:
126 assert inputs_embeds is not None
127 # masking inputs at position 0, as not needed by MTP
128 inputs_embeds[positions == 0] = 0
129 inputs_embeds = self.enorm(inputs_embeds)
130 previous_hidden_states = self.hnorm(previous_hidden_states)
131
132 hidden_states = self.eh_proj(
133 torch.cat([inputs_embeds, previous_hidden_states], dim=-1)
134 )
135
136 # HYV3DecoderLayer returns (hidden_states, residual)
137 hidden_states, residual = self.mtp_block(
138 positions=positions, hidden_states=hidden_states, residual=None
139 )
140 hidden_states = residual + hidden_states
141 hidden_states = self.final_layernorm(hidden_states)
142 return hidden_states
143
144
145 class HYV3MultiTokenPredictor(nn.Module):
146 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
147 super().__init__()
148 config = vllm_config.model_config.hf_config
149 self.mtp_start_layer_idx = config.num_hidden_layers
150 self.num_mtp_layers = config.num_nextn_predict_layers
151
152 # to map the exact layer index from weights
153 self.layers = torch.nn.ModuleDict(
154 {
155 str(idx): HYV3MultiTokenPredictorLayer(
156 config,
157 f"{prefix}.layers.{idx}",
158 model_config=vllm_config.model_config,
159 cache_config=vllm_config.cache_config,
160 quant_config=vllm_config.quant_config,
161 )
162 for idx in range(
163 self.mtp_start_layer_idx,
164 self.mtp_start_layer_idx + self.num_mtp_layers,
165 )
166 }
167 )
168
169 self.embed_tokens = VocabParallelEmbedding(
170 config.vocab_size,
171 config.hidden_size,
172 )
173
174 self.logits_processor = LogitsProcessor(config.vocab_size)
175
176 def forward(
177 self,
178 input_ids: torch.Tensor,
179 positions: torch.Tensor,
180 previous_hidden_states: torch.Tensor,
181 inputs_embeds: torch.Tensor | None = None,
182 spec_step_idx: int = 0,
183 ) -> torch.Tensor:
184 if inputs_embeds is None:
185 inputs_embeds = self.embed_tokens(input_ids)
186 current_step_idx = spec_step_idx % self.num_mtp_layers
187 return self.layers[str(self.mtp_start_layer_idx + current_step_idx)](
188 input_ids,
189 positions,
190 previous_hidden_states,
191 inputs_embeds,
192 current_step_idx,
193 )
194
195 def compute_logits(
196 self,
197 hidden_states: torch.Tensor,
198 spec_step_idx: int = 0,
199 ) -> torch.Tensor:
200 current_step_idx = spec_step_idx % self.num_mtp_layers
201 mtp_layer = self.layers[str(self.mtp_start_layer_idx + current_step_idx)]
202 logits = self.logits_processor(
203 mtp_layer.shared_head.head, mtp_layer.shared_head(hidden_states)
204 )
205 return logits
206
207
208 class HYV3MTP(nn.Module):
209 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
210 super().__init__()
211 self.config = vllm_config.model_config.hf_config
212 self.quant_config = vllm_config.quant_config
213 self.model = HYV3MultiTokenPredictor(
214 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
215 )
216
217 self.sampler = Sampler()
218
219 def forward(
220 self,
221 input_ids: torch.Tensor,
222 positions: torch.Tensor,
223 hidden_states: torch.Tensor,
224 intermediate_tensors: IntermediateTensors | None = None,
225 inputs_embeds: torch.Tensor | None = None,
226 spec_step_idx: int = 0,
227 ) -> torch.Tensor:
228 hidden_states = self.model(
229 input_ids, positions, hidden_states, inputs_embeds, spec_step_idx
230 )
231 return hidden_states
232
233 def compute_logits(
234 self,
235 hidden_states: torch.Tensor,
236 spec_step_idx: int = 0,
237 ) -> torch.Tensor | None:
238 return self.model.compute_logits(hidden_states, spec_step_idx)
239
240 def sample(
241 self,
242 logits: torch.Tensor,
243 sampling_metadata: SamplingMetadata,
244 ) -> SamplerOutput | None:
245 next_tokens = self.sampler(logits, sampling_metadata)
246 return next_tokens
247
248 def _split_qkv_weight(self, qkv: torch.Tensor):
249 num_attention_heads = self.config.num_attention_heads
250 num_kv_heads = getattr(
251 self.config, "num_key_value_heads", self.config.num_attention_heads
252 )
253 num_key_value_groups = num_attention_heads // num_kv_heads
254 hidden_size = self.config.hidden_size
255
256 if hasattr(self.config, "head_dim"):
257 attention_head_dim = self.config.head_dim
258 elif hasattr(self.config, "attention_head_dim"):
259 attention_head_dim = self.config.attention_head_dim
260 else:
261 attention_head_dim = self.config.hidden_size // num_attention_heads
262
263 qkv = qkv.reshape(
264 num_kv_heads, num_key_value_groups + 2, attention_head_dim, hidden_size
265 )
266 q, k, v = torch.split(qkv, (num_key_value_groups, 1, 1), dim=1)
267 q = q.reshape(-1, hidden_size)
268 k = k.reshape(-1, hidden_size)
269 v = v.reshape(-1, hidden_size)
270 return torch.concat((q, k, v))
271
272 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
273 if self.quant_config is not None and (
274 cache_scale_mapper := self.quant_config.get_cache_scale_mapper()
275 ):
276 weights = cache_scale_mapper.apply(weights)
277 cla_factor = _get_cla_factor(self.config)
278 stacked_params_mapping = [
279 # (param_name, shard_name, shard_id)
280 (".qkv_proj", ".q_proj", "q"),
281 (".qkv_proj", ".k_proj", "k"),
282 (".qkv_proj", ".v_proj", "v"),
283 (".gate_up_proj", ".gate_proj", 0),
284 (".gate_up_proj", ".up_proj", 1),
285 ]
286
287 num_attention_heads = self.config.num_attention_heads
288 num_kv_heads = getattr(
289 self.config, "num_key_value_heads", self.config.num_attention_heads
290 )
291 split_params_mapping: list[
292 tuple[
293 str,
294 str,
295 int,
296 list[tuple[str | int, int]],
297 Callable[[torch.Tensor], torch.Tensor] | None,
298 ]
299 ] = [
300 (".gate_up_proj", ".gate_and_up_proj", 2, [(1, 1), (0, 1)], None),
301 (
302 ".qkv_proj",
303 ".qkv_proj",
304 num_attention_heads + num_kv_heads * 2,
305 [("q", num_attention_heads), ("k", num_kv_heads), ("v", num_kv_heads)],
306 self._split_qkv_weight,
307 ),
308 ]
309
310 if _is_moe(self.config):
311 expert_params_mapping = fused_moe_make_expert_params_mapping(
312 self,
313 ckpt_gate_proj_name="gate_proj",
314 ckpt_down_proj_name="down_proj",
315 ckpt_up_proj_name="up_proj",
316 num_experts=self.config.num_experts,
317 )
318 else:
319 expert_params_mapping = []
320
321 params_dict = dict(self.named_parameters())
322
323 # V3 shared weights mapping:
324 # - embed_tokens: from main model's model.embed_tokens.weight
325 # - lm_head: from main model's lm_head.weight → MTP shared_head.head
326 # (HF infer_mtp uses head_weight=self.lm_head.weight, not the
327 # checkpoint's model.layers.<N>.shared_head.weight)
328 # - No norm mapping (V3 MTP has no intermediate norm before lm_head)
329 mtp_start = self.config.num_hidden_layers
330 v3_shared_weights = {
331 "model.embed_tokens.weight": "model.embed_tokens.weight",
332 "lm_head.weight": f"model.layers.{mtp_start}.shared_head.head.weight",
333 }
334
335 for name, loaded_weight in weights:
336 # Intercept shared weights before any other processing
337 if name in v3_shared_weights:
338 target_name = v3_shared_weights[name]
339 if target_name in params_dict:
340 param = params_dict[target_name]
341 weight_loader = getattr(
342 param, "weight_loader", default_weight_loader
343 )
344 weight_loader(param, loaded_weight)
345 continue
346
347 if "rotary_emb.inv_freq" in name:
348 continue
349 if "gate_proj_bias" in name:
350 name = name.replace("gate_proj_bias", "gate_proj.bias")
351 if "up_proj_bias" in name:
352 name = name.replace("up_proj_bias", "up_proj.bias")
353 if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name:
354 continue
355 if self.config.tie_word_embeddings and "lm_head.weight" in name:
356 continue
357 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
358 if spec_layer is None:
359 continue
360 name = self._rewrite_spec_layer_name(spec_layer, name)
361 # Skip weights that _rewrite_spec_layer_name marked for skipping
362 if name == "__skip__":
363 continue
364 if "scale" in name:
365 remapped_name = maybe_remap_kv_scale_name(name, params_dict)
366 if remapped_name is None:
367 continue
368 name = remapped_name
369 is_found = False
370
371 for param_name, weight_name, shard_id in stacked_params_mapping:
372 if weight_name not in name:
373 continue
374 if "mlp.experts" in name:
375 continue
376 if weight_name == ".q_proj":
377 match = re.search(r"layers\.\d+", name)
378 if match:
379 layer_id = int(match.group(0).split(".")[-1])
380 if cla_factor > 1 and layer_id % cla_factor != 0:
381 continue
382 name = name.replace(weight_name, param_name)
383 if name.endswith(".bias") and name not in params_dict:
384 continue
385
386 if is_pp_missing_parameter(name, self):
387 continue
388
389 param = params_dict[name]
390 weight_loader = param.weight_loader
391 weight_loader(param, loaded_weight, shard_id)
392
393 is_found = True
394 break
395 if is_found:
396 continue
397
398 for param_name, weight_name, den, split_param, func in split_params_mapping:
399 if weight_name not in name:
400 continue
401 name = name.replace(weight_name, param_name)
402 if name.endswith(".bias") and name not in params_dict:
403 continue
404
405 if is_pp_missing_parameter(name, self):
406 continue
407
408 assert loaded_weight.shape[0] % den == 0
409 units = loaded_weight.shape[0] // den
410
411 param = params_dict[name]
412 weight_loader = param.weight_loader
413 offset = 0
414 for shard_id, num in split_param:
415 new_offset = offset + num * units
416 if func:
417 weight_loader(
418 param, func(loaded_weight)[offset:new_offset], shard_id
419 )
420 else:
421 weight_loader(param, loaded_weight[offset:new_offset], shard_id)
422 offset = new_offset
423
424 break
425 else:
426 if name.endswith(".bias") and name not in params_dict:
427 continue
428 for mapping in expert_params_mapping:
429 param_name, weight_name, expert_id, shard_id = mapping
430 if weight_name not in name:
431 continue
432 name = name.replace(weight_name, param_name)
433 if is_pp_missing_parameter(name, self):
434 continue
435 param = params_dict[name]
436 weight_loader = param.weight_loader
437 weight_loader(
438 param,
439 loaded_weight,
440 name,
441 shard_id=shard_id,
442 expert_id=expert_id,
443 )
444 break
445 else:
446 if is_pp_missing_parameter(name, self):
447 continue
448
449 if "mlp.gate.wg." in name:
450 name = name.replace("wg.", "")
451 # V3 checkpoint: mlp.router.gate -> mlp.gate
452 if "mlp.router.gate." in name:
453 name = name.replace("router.gate.", "gate.")
454
455 param = params_dict[name]
456 weight_loader = getattr(
457 param, "weight_loader", default_weight_loader
458 )
459 weight_loader(param, loaded_weight)
460
461 def _rewrite_spec_layer_name(self, spec_layer: int, name: str) -> str:
462 """Rewrite spec layer weight names to match vLLM module structure."""
463 # Skip embed_tokens (doesn't exist in V3 MTP checkpoint under spec
464 # layer) and shared_head (we use main model's lm_head instead)
465 if f"model.layers.{spec_layer}.embed_tokens" in name:
466 return "__skip__"
467 if f"model.layers.{spec_layer}.shared_head" in name:
468 return "__skip__"
469
470 spec_layer_weight_names = ["enorm", "hnorm", "eh_proj", "final_layernorm"]
471 spec_layer_weight = False
472 for weight_name in spec_layer_weight_names:
473 if weight_name in name:
474 spec_layer_weight = True
475 break
476 if not spec_layer_weight:
477 # Transformer block weights go under .mtp_block
478 name = name.replace(
479 f"model.layers.{spec_layer}.", f"model.layers.{spec_layer}.mtp_block."
480 )
481 return name