[Perf][ROCm] Add a ROCm path for Hy4 and compile the backbone (#57526)

Signed-off-by: Aakif Nawaz <aakif.nawaz@amd.com>

akii96 committed Sep 21, 2026 at 11:20 UTC 0ff0477ff955154f37db48791260cdf27fa35e60
4 files changed +782 -7
tests/models/test_hyv4_rocm.py
+48
@@ -51,6 +51,18 @@ def _num_captured_graphs(_worker):
51 return compilation_counter.num_cudagraph_captured
52
53
54 +def _piecewise_compile_state(worker):
55 + from vllm.compilation.counter import compilation_counter
56 +
57 + # `cudagraph_mode` is returned by name: the enum does not survive the
58 + # collective_rpc round trip, it arrives as its bare value.
59 + return (
60 + compilation_counter.num_models_seen,
61 + compilation_counter.num_cudagraph_captured,
62 + worker.vllm_config.compilation_config.cudagraph_mode.name,
63 + )
64 +
65 +
66 @pytest.mark.parametrize(
67 "tp,enforce_eager,mtp,dtype,kv_cache_dtype",
68 [
@@ -135,3 +147,39 @@ def test_hyv4_dummy_generation(
147 and metric.value > 0
148 for metric in model.llm.get_metrics()
149 ), "MTP drafter did not run"
150 +
151 +
152 +def test_hyv4_backbone_reaches_the_compiler(vllm_runner, enable_pickle) -> None:
153 + """Guard against the backbone losing `@support_torch_compile`.
154 +
155 + An undecorated backbone is not a hard failure: vLLM downgrades
156 + `cudagraph_mode` to NONE and serves the whole model eager, which on TP8
157 + gfx942 cost ~4x decode latency.
158 + """
159 + with vllm_runner(
160 + "tencent/Hy4-preview",
161 + trust_remote_code=True,
162 + skip_tokenizer_init=True,
163 + load_format="dummy",
164 + dtype="bfloat16",
165 + hf_overrides=_small_hyv4_config,
166 + max_model_len=256,
167 + max_num_batched_tokens=128,
168 + max_num_seqs=8,
169 + enable_chunked_prefill=True,
170 + enable_prefix_caching=False,
171 + kv_cache_memory_bytes=128 * 1024 * 1024,
172 + enforce_eager=False,
173 + seed=17,
174 + compilation_config=CompilationConfig(
175 + mode=CompilationMode.VLLM_COMPILE,
176 + cudagraph_mode=CUDAGraphMode.PIECEWISE,
177 + cudagraph_capture_sizes=[1, 2, 4],
178 + ),
179 + ) as model:
180 + for models_seen, captured, mode in model.collective_rpc(
181 + _piecewise_compile_state
182 + ):
183 + assert models_seen > 0, "backbone was never handed to the compiler"
184 + assert mode == CUDAGraphMode.PIECEWISE.name
185 + assert captured > 0
vllm/models/hy_v4/__init__.py
+14 -7
@@ -14,20 +14,27 @@ The package is organized like `vllm.models.deepseek_v32`: this module is the
14 only public entry point and dispatches on the current platform, so registry
15 entries never reach into a platform subpackage.
16
17 -NVIDIA and ROCm are supported. The port also drops the reference
18 -implementation's HPC/TPCP fusion paths, which depend on infrastructure that
19 -does not exist in this tree.
17 +NVIDIA and ROCm are supported, the latter through `amd/`. The port also drops
18 +the reference implementation's HPC/TPCP fusion paths, which depend on
19 +infrastructure that does not exist in this tree.
20 """
21
22 +from typing import TYPE_CHECKING
23 +
24 from vllm.platforms import current_platform
25
26 if current_platform.is_xpu():
27 raise NotImplementedError("hy_v4 does not yet support XPU.")
26 -else:
27 - # The implementation is shared by CUDA and ROCm; platform-specific
28 - # attention behavior is selected by the normal attention backend router.
28 +
29 +# The NVIDIA branch is the static default that type-checkers see; the ROCm
30 +# branch overrides it at runtime (kept type-compatible via type: ignore).
31 +if TYPE_CHECKING or not current_platform.is_rocm():
32 from .nvidia.model import HYV4ForCausalLM
30 - from .nvidia.mtp import HYV4MTP
33 +else:
34 + from .amd.model import HYV4ForCausalLM # type: ignore[assignment]
35 +
36 +# The draft head has no ROCm-specific implementation yet.
37 +from .nvidia.mtp import HYV4MTP # noqa: E402
38
39 __all__ = [
40 "HYV4ForCausalLM",
vllm/models/hy_v4/amd/__init__.py new
+2
@@ -0,0 +1,2 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/models/hy_v4/amd/model.py new
+718
@@ -0,0 +1,718 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +# Adapted from
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 HY V4 model compatible with HuggingFace weights (ROCm)."""
25 +
26 +import typing
27 +from collections.abc import Callable, Iterable, MutableSequence, Sequence
28 +from itertools import islice
29 +
30 +import regex as re
31 +import torch
32 +from torch import nn
33 +from transformers import PretrainedConfig
34 +
35 +from vllm.compilation.decorators import support_torch_compile
36 +from vllm.config import CacheConfig, VllmConfig
37 +from vllm.distributed import (
38 + get_pp_group,
39 + get_tensor_model_parallel_rank,
40 + get_tensor_model_parallel_world_size,
41 +)
42 +from vllm.logger import init_logger
43 +from vllm.model_executor.layers.fused_moe import fused_moe_make_expert_params_mapping
44 +from vllm.model_executor.layers.layernorm import RMSNorm
45 +from vllm.model_executor.layers.logits_processor import LogitsProcessor
46 +from vllm.model_executor.layers.quantization import QuantizationConfig
47 +from vllm.model_executor.layers.vocab_parallel_embedding import (
48 + ParallelLMHead,
49 + VocabParallelEmbedding,
50 +)
51 +from vllm.model_executor.model_loader.weight_utils import default_weight_loader
52 +from vllm.model_executor.models.deepseek_v2 import _try_load_fp8_indexer_wk
53 +from vllm.model_executor.models.interfaces import SupportsLoRA, SupportsPP
54 +from vllm.model_executor.models.utils import (
55 + AutoWeightsLoader,
56 + PPMissingLayer,
57 + WeightsMapper,
58 + get_pp_missing_layer_names,
59 + is_pp_missing_parameter,
60 + make_empty_intermediate_tensors_factory,
61 + make_layers,
62 + maybe_prefix,
63 +)
64 +from vllm.platforms import current_platform
65 +from vllm.sequence import IntermediateTensors
66 +
67 +from ..nvidia.attention import (
68 + HYV4MLAAttention,
69 + compute_skip_topk_layers,
70 + is_skip_topk_indexer_weight,
71 +)
72 +from ..nvidia.hc import HYV4HCHeadLayer, HYV4HCLayer
73 +from ..nvidia.moe import HYV4FeedForward, HYV4MoEFused
74 +
75 +logger = init_logger(__name__)
76 +
77 +
78 +def _normalize_hyv4_config(config: PretrainedConfig) -> PretrainedConfig:
79 + """Populate the aliases consumed by the shared MoE implementation."""
80 + config.router_scaling_factor = config.routed_scaling_factor
81 + config.num_experts = config.n_routed_experts
82 + config.expert_hidden_dim = config.moe_intermediate_size
83 + config.num_shared_experts = config.n_shared_experts
84 + config.route_norm = config.norm_topk_prob
85 + return config
86 +
87 +
88 +class HYV4DecoderLayer(nn.Module):
89 + """One HY V4 decoder layer: MLA attention plus a dense or MoE MLP.
90 +
91 + When``config.enable_ihc`` is set the layer runs on ``hc_mult`` residual
92 + channels and each sub-block is wrapped by an `HYV4HCLayer` boundary;
93 + otherwise it uses the standard single-stream residual.
94 + """
95 +
96 + def __init__(
97 + self,
98 + config: PretrainedConfig,
99 + vllm_config: VllmConfig,
100 + cache_config: CacheConfig | None = None,
101 + quant_config: QuantizationConfig | None = None,
102 + prefix: str = "",
103 + topk_indices_buffer: torch.Tensor | None = None,
104 + ) -> None:
105 + super().__init__()
106 + self.hidden_size = config.hidden_size
107 + self.config = config
108 + self.enable_ihc = getattr(config, "enable_ihc", False)
109 + layer_idx = int(prefix.split(".")[-1])
110 + self.layer_idx = layer_idx
111 +
112 + max_position_embeddings = getattr(config, "max_position_embeddings", 8192)
113 + self.self_attn = HYV4MLAAttention(
114 + vllm_config=vllm_config,
115 + config=config,
116 + hidden_size=self.hidden_size,
117 + num_heads=config.num_attention_heads,
118 + qk_nope_head_dim=config.qk_nope_head_dim,
119 + qk_rope_head_dim=config.qk_rope_head_dim,
120 + v_head_dim=config.v_head_dim,
121 + q_lora_rank=config.q_lora_rank,
122 + kv_lora_rank=config.kv_lora_rank,
123 + max_position_embeddings=max_position_embeddings,
124 + cache_config=cache_config,
125 + quant_config=quant_config,
126 + prefix=f"{prefix}.self_attn",
127 + layer_idx=layer_idx,
128 + topk_indices_buffer=topk_indices_buffer,
129 + )
130 + self.input_layernorm = RMSNorm(config.hidden_size, config.rms_norm_eps)
131 + if config.mlp_layer_types[layer_idx] == "dense":
132 + self.mlp = HYV4FeedForward(
133 + hidden_size=config.hidden_size,
134 + intermediate_size=config.intermediate_size,
135 + hidden_act=config.hidden_act,
136 + quant_config=quant_config,
137 + prefix=f"{prefix}.mlp",
138 + )
139 + self.block_type = "feedforward"
140 + else:
141 + self.mlp = HYV4MoEFused(
142 + config=config, quant_config=quant_config, prefix=f"{prefix}.mlp"
143 + )
144 + self.block_type = "moe"
145 + self.post_attention_layernorm = RMSNorm(config.hidden_size, config.rms_norm_eps)
146 + self.hc_attn_layer = HYV4HCLayer(
147 + config, layer_idx, prefix=f"{prefix}.hc_attn_layer"
148 + )
149 + self.hc_mlp_layer = HYV4HCLayer(
150 + config, layer_idx, prefix=f"{prefix}.hc_mlp_layer"
151 + )
152 +
153 + def forward(
154 + self,
155 + positions: torch.Tensor,
156 + hidden_states: torch.Tensor,
157 + residual: torch.Tensor | None,
158 + ) -> tuple[torch.Tensor, torch.Tensor | None]:
159 + if self.enable_ihc:
160 + return self._forward_ihc(positions, hidden_states)
161 + return self._forward_normal(positions, hidden_states, residual)
162 +
163 + def _forward_normal(
164 + self,
165 + positions: torch.Tensor,
166 + hidden_states: torch.Tensor,
167 + residual: torch.Tensor | None,
168 + ) -> tuple[torch.Tensor, torch.Tensor]:
169 + """Standard single-residual-stream forward (iHC disabled)."""
170 + if residual is not None:
171 + hidden_states = hidden_states + residual
172 + residual = hidden_states
173 + hidden_states = self.input_layernorm(hidden_states)
174 +
175 + hidden_states = self.self_attn(
176 + positions=positions,
177 + hidden_states=hidden_states,
178 + )
179 +
180 + hidden_states = hidden_states + residual
181 + residual = hidden_states
182 + hidden_states = self.post_attention_layernorm(hidden_states)
183 +
184 + hidden_states = self.mlp(hidden_states)
185 +
186 + return hidden_states, residual
187 +
188 + def _forward_ihc(
189 + self,
190 + positions: torch.Tensor,
191 + hidden_states: torch.Tensor,
192 + ) -> tuple[torch.Tensor, None]:
193 + """IHC forward: each sub-block reduces and re-scatters the channels."""
194 + hidden_states = self.hc_attn_layer.prepare_input(hidden_states)
195 + hidden_states, post_gates, residual = self.hc_attn_layer.pre(hidden_states)
196 + hidden_states = self.input_layernorm(hidden_states)
197 + hidden_states = self.self_attn(
198 + positions=positions,
199 + hidden_states=hidden_states,
200 + )
201 + hidden_states = self.hc_attn_layer.post(hidden_states, residual, post_gates)
202 +
203 + hidden_states = self.hc_mlp_layer.prepare_input(hidden_states)
204 + hidden_states, post_gates, residual = self.hc_mlp_layer.pre(hidden_states)
205 + hidden_states = self.post_attention_layernorm(hidden_states)
206 + hidden_states = self.mlp(hidden_states)
207 + hidden_states = self.hc_mlp_layer.post(hidden_states, residual, post_gates)
208 +
209 + # Under iHC the residual is carried inside hidden_states.
210 + return hidden_states, None
211 +
212 +
213 +@support_torch_compile
214 +class HYV4Model(nn.Module):
215 + """HY V4 backbone."""
216 +
217 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
218 + super().__init__()
219 +
220 + config = _normalize_hyv4_config(vllm_config.model_config.hf_config)
221 + cache_config = vllm_config.cache_config
222 + quant_config = vllm_config.quant_config
223 + parallel_config = vllm_config.parallel_config
224 + eplb_config = parallel_config.eplb_config
225 + self.num_redundant_experts = eplb_config.num_redundant_experts
226 + self.device = current_platform.device_type
227 + self.vocab_size = config.vocab_size
228 + self.is_sparse = hasattr(config, "index_topk")
229 + if self.is_sparse:
230 + self.topk_indices_buffer = torch.empty(
231 + vllm_config.scheduler_config.max_num_batched_tokens,
232 + config.index_topk,
233 + dtype=torch.int32,
234 + device=self.device,
235 + )
236 + else:
237 + self.topk_indices_buffer = None
238 + self.config = config
239 + self.quant_config = quant_config
240 + self.enable_ihc = getattr(config, "enable_ihc", False)
241 +
242 + if get_pp_group().is_first_rank or (
243 + self.config.tie_word_embeddings and get_pp_group().is_last_rank
244 + ):
245 + self.embed_tokens = VocabParallelEmbedding(
246 + config.vocab_size,
247 + config.hidden_size,
248 + )
249 + else:
250 + self.embed_tokens = PPMissingLayer()
251 + self.start_layer, self.end_layer, self.layers = make_layers(
252 + config.num_hidden_layers,
253 + lambda prefix: HYV4DecoderLayer(
254 + config=config,
255 + vllm_config=vllm_config,
256 + cache_config=cache_config,
257 + quant_config=quant_config,
258 + prefix=prefix,
259 + topk_indices_buffer=self.topk_indices_buffer,
260 + ),
261 + prefix=f"{prefix}.layers",
262 + )
263 + if get_pp_group().is_last_rank:
264 + self.norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
265 + else:
266 + self.norm = PPMissingLayer()
267 + if self.enable_ihc:
268 + # iHC head layer: merge the residual channels into one stream.
269 + if get_pp_group().is_last_rank:
270 + self.hc_head = HYV4HCHeadLayer(
271 + config,
272 + hidden_size=config.hidden_size,
273 + hc_mult=config.hc_mult,
274 + hc_eps=config.hc_eps,
275 + prefix=f"{prefix}.hc_head",
276 + )
277 + else:
278 + self.hc_head = PPMissingLayer()
279 + self.make_empty_intermediate_tensors = (
280 + make_empty_intermediate_tensors_factory(
281 + ["hidden_states"], config.hc_mult * config.hidden_size
282 + )
283 + )
284 + else:
285 + self.make_empty_intermediate_tensors = (
286 + make_empty_intermediate_tensors_factory(
287 + ["hidden_states", "residual"], config.hidden_size
288 + )
289 + )
290 +
291 + # MoE hyperparameters (consumed by EPLB).
292 + self.expert_weights: MutableSequence[Sequence[torch.Tensor]] = []
293 + self.num_expert_groups = 1
294 + self.moe_layers: list[nn.Module] = []
295 + example_layer: HYV4MoEFused | None = None
296 + for layer in self.layers:
297 + if isinstance(layer, PPMissingLayer):
298 + continue
299 +
300 + assert isinstance(layer, HYV4DecoderLayer)
301 + if layer.block_type == "moe":
302 + assert isinstance(layer.mlp, HYV4MoEFused)
303 + example_layer = layer.mlp
304 + self.moe_layers.append(layer.mlp.experts)
305 +
306 + if example_layer is None:
307 + self.num_moe_layers = 0
308 + self.num_logical_experts = 0
309 + self.num_physical_experts = 0
310 + self.num_local_physical_experts = 0
311 + self.num_routed_experts = 0
312 + self.num_redundant_experts = 0
313 + return
314 +
315 + self.num_moe_layers = len(self.moe_layers)
316 + self.num_logical_experts = example_layer.n_logical_experts
317 + self.num_physical_experts = example_layer.n_physical_experts
318 + self.num_local_physical_experts = example_layer.n_local_physical_experts
319 + self.num_routed_experts = example_layer.n_routed_experts
320 + self.num_redundant_experts = example_layer.n_redundant_experts
321 +
322 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
323 + return self.embed_tokens(input_ids)
324 +
325 + def update_physical_experts_metadata(
326 + self,
327 + num_physical_experts: int,
328 + num_local_physical_experts: int,
329 + ) -> None:
330 + assert self.num_local_physical_experts == num_local_physical_experts
331 + self.num_physical_experts = num_physical_experts
332 + self.num_local_physical_experts = num_local_physical_experts
333 + self.num_redundant_experts = num_physical_experts - self.num_logical_experts
334 + for layer in self.layers:
335 + if isinstance(layer, PPMissingLayer):
336 + continue
337 + if isinstance(layer.mlp, HYV4MoEFused):
338 + moe = layer.mlp
339 + moe.n_local_physical_experts = num_local_physical_experts
340 + moe.n_physical_experts = num_physical_experts
341 + moe.n_redundant_experts = self.num_redundant_experts
342 + moe.experts.update_expert_map()
343 +
344 + def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
345 + # (param_name, weight_name, expert_id, shard_id) for weights, fp8
346 + # weight scales and fp8 activation scales.
347 + if not hasattr(self, "_cached_expert_params_mapping"):
348 + self._cached_expert_params_mapping = fused_moe_make_expert_params_mapping(
349 + self,
350 + ckpt_gate_proj_name="gate_proj",
351 + ckpt_down_proj_name="down_proj",
352 + ckpt_up_proj_name="up_proj",
353 + num_experts=self.config.num_experts,
354 + num_redundant_experts=self.num_redundant_experts,
355 + )
356 + return self._cached_expert_params_mapping
357 +
358 + def forward(
359 + self,
360 + input_ids: torch.Tensor,
361 + positions: torch.Tensor,
362 + intermediate_tensors: IntermediateTensors | None = None,
363 + inputs_embeds: torch.Tensor | None = None,
364 + ) -> torch.Tensor | IntermediateTensors:
365 + if get_pp_group().is_first_rank:
366 + if inputs_embeds is not None:
367 + hidden_states = inputs_embeds
368 + else:
369 + hidden_states = self.embed_input_ids(input_ids)
370 + residual = None
371 + else:
372 + assert intermediate_tensors is not None
373 + hidden_states = intermediate_tensors["hidden_states"]
374 + # In iHC mode the flattened [num_tokens, hc*h] tensor from the
375 + # previous PP stage is reshaped back to 3D by the first layer's
376 + # prepare_input, and residual is unused.
377 + residual = None if self.enable_ihc else intermediate_tensors["residual"]
378 +
379 + for layer in islice(self.layers, self.start_layer, self.end_layer):
380 + hidden_states, residual = layer(positions, hidden_states, residual)
381 +
382 + if not get_pp_group().is_last_rank:
383 + if self.enable_ihc:
384 + # hidden_states is [num_tokens, hc, h]; flatten the channel dim
385 + # for PP transfer (matches the 2D receive buffer).
386 + return IntermediateTensors({"hidden_states": hidden_states.flatten(1)})
387 + return IntermediateTensors(
388 + {"hidden_states": hidden_states, "residual": residual}
389 + )
390 + if self.enable_ihc:
391 + hidden_states = self.hc_head(hidden_states)
392 + else:
393 + hidden_states = hidden_states + residual
394 +
395 + return self.norm(hidden_states)
396 +
397 + def load_fused_expert_weights(
398 + self,
399 + name: str,
400 + params_dict: dict,
401 + loaded_weight: torch.Tensor,
402 + shard_id: str,
403 + num_experts: int,
404 + ) -> bool:
405 + param = params_dict[name]
406 + weight_loader = typing.cast(Callable[..., bool], param.weight_loader)
407 + loaded_local_expert = False
408 + for expert_id in range(num_experts):
409 + curr_expert_weight = loaded_weight[expert_id]
410 + success = weight_loader(
411 + param,
412 + curr_expert_weight,
413 + name,
414 + shard_id,
415 + expert_id,
416 + return_success=True,
417 + )
418 + if success:
419 + loaded_local_expert = True
420 +
421 + return loaded_local_expert
422 +
423 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
424 + # FP8 indexer wk dequant buffer (weight and scale arrive separately).
425 + pending_wk_fp8: dict[str, dict[str, torch.Tensor]] = {}
426 + pp_missing_layer_names = get_pp_missing_layer_names(self)
427 + skip_topk_layers = compute_skip_topk_layers(self.config)
428 +
429 + # Must not be cached on `self`: `process_weights_after_loading` swaps in
430 + # kernel-specific expert weights via `replace_parameter`, and a cache
431 + # outliving this call would pin the pre-shuffle storage (OOM on large
432 + # MoE) and make a later reload target orphaned tensors.
433 + params_dict = dict(self.named_parameters())
434 + # Split per-expert mapping (V3 style): experts.0.gate_proj.weight
435 + split_expert_params_mapping = self.get_expert_mapping()
436 + loaded_params: set[str] = set()
437 +
438 + # Sink weights are sharded like the q/k/v linears.
439 + sink_tp_size = get_tensor_model_parallel_world_size()
440 + sink_tp_rank = get_tensor_model_parallel_rank()
441 + n_local_head = self.config.num_attention_heads // sink_tp_size
442 + head_rank_start = n_local_head * sink_tp_rank
443 + head_rank_end = n_local_head * (sink_tp_rank + 1)
444 + base_layer = (
445 + "base_layer." if any(".base_layer." in name for name in params_dict) else ""
446 + )
447 + # Fused expert mapping: experts.gate_up_proj (all experts in one tensor).
448 + # The packed weights are owned by the RoutedExperts submodule, so the
449 + # targets are experts.routed_experts.[base_layer.]w{13,2}_weight.
450 + fused_expert_prefix = f".experts.routed_experts.{base_layer}"
451 + fused_expert_params_mapping = [
452 + (f"{fused_expert_prefix}w13_weight", ".experts.gate_up_proj", 0, "w1"),
453 + (f"{fused_expert_prefix}w2_weight", ".experts.down_proj", 0, "w2"),
454 + ]
455 + num_experts = getattr(self.config, "num_experts", 0)
456 +
457 + def _should_skip_missing_param(param_name: str) -> bool:
458 + # Sparse checkpoints may contain indexer weights for layers that
459 + # fall back to dense MLA on unsupported GPUs.
460 + return ".indexer." in param_name
461 +
462 + def _is_split_expert_weight(weight_name: str) -> bool:
463 + """Whether this weight is in split (per-expert) format."""
464 + # Split format: mlp.experts.<id>.gate_proj.weight
465 + return bool(re.search(r"\.experts\.\d+\.", weight_name))
466 +
467 + def _is_fused_expert_weight(weight_name: str) -> bool:
468 + """Whether this weight is in fused (all-experts-packed) format."""
469 + return ".experts.gate_up_proj" in weight_name or (
470 + ".experts.down_proj" in weight_name
471 + and not _is_split_expert_weight(weight_name)
472 + )
473 +
474 + for name, loaded_weight in weights:
475 + if is_skip_topk_indexer_weight(name, skip_topk_layers):
476 + continue
477 + if _try_load_fp8_indexer_wk(
478 + name,
479 + loaded_weight,
480 + pending_wk_fp8,
481 + params_dict,
482 + loaded_params,
483 + pp_missing_layer_names,
484 + ):
485 + continue
486 + if self.config.tie_word_embeddings and "lm_head.weight" in name:
487 + continue
488 + # Names and stacked shard ids are normalized upstream by
489 + # AutoWeightsLoader, through quant_config.get_cache_scale_mapper()
490 + # and hf_to_vllm_mapper.
491 + stacked_shard_id = getattr(loaded_weight, "shard_id", None)
492 + if ".indexer.wk." in name:
493 + name = name.replace(".indexer.wk.", ".indexer.wk_weights_proj.")
494 + stacked_shard_id = 0
495 + # Skip loading extra bias for GPTQ models.
496 + if name.endswith(".bias") and name not in params_dict:
497 + continue
498 +
499 + # Determine per-weight whether this is fused or split format.
500 + is_fused_expert = _is_fused_expert_weight(name)
501 + expert_params_mapping = (
502 + fused_expert_params_mapping
503 + if is_fused_expert
504 + else split_expert_params_mapping
505 + )
506 +
507 + is_expert_weight = False
508 + loaded_expert_param_names: set[str] = set()
509 + for mapping in expert_params_mapping:
510 + param_name, weight_name, expert_id, shard_id = mapping
511 + if weight_name not in name:
512 + continue
513 +
514 + # This is an expert weight and must not be attempted as any
515 + # other kind of weight later on.
516 + is_expert_weight = True
517 +
518 + # Do not modify `name`: the loop may continue past this point.
519 + name_mapped = name.replace(weight_name, param_name)
520 + if is_pp_missing_parameter(name_mapped, self):
521 + continue
522 + if is_fused_expert:
523 + if "experts.gate_up_proj" in name:
524 + chunks = loaded_weight.chunk(2, dim=-2)
525 + success_w1 = self.load_fused_expert_weights(
526 + name_mapped, params_dict, chunks[0], "w1", num_experts
527 + )
528 + success_w3 = self.load_fused_expert_weights(
529 + name_mapped, params_dict, chunks[1], "w3", num_experts
530 + )
531 + success = success_w1 and success_w3
532 + else:
533 + success = self.load_fused_expert_weights(
534 + name_mapped,
535 + params_dict,
536 + loaded_weight,
537 + shard_id,
538 + num_experts,
539 + )
540 + if success:
541 + name = name_mapped
542 + break
543 + else:
544 + # Split per-expert format (V3 style).
545 + if name_mapped not in params_dict:
546 + if _should_skip_missing_param(name_mapped):
547 + continue
548 + logger.warning_once(
549 + "Skipping unknown checkpoint weight: %s",
550 + name_mapped,
551 + )
552 + continue
553 + param = params_dict[name_mapped]
554 + # Ask the weight loader whether it succeeded, otherwise we
555 + # may skip experts that have other available replicas.
556 + weight_loader: Callable[..., typing.Any] = param.weight_loader
557 + success = weight_loader(
558 + param,
559 + loaded_weight,
560 + name_mapped,
561 + shard_id=shard_id,
562 + expert_id=expert_id,
563 + return_success=True,
564 + )
565 + if success:
566 + if not is_fused_expert:
567 + loaded_expert_param_names.add(name_mapped)
568 + continue
569 + name = name_mapped
570 + break
571 + else:
572 + if loaded_expert_param_names:
573 + loaded_params.update(loaded_expert_param_names)
574 + continue
575 + if "learnable_sink_param" in name:
576 + if is_pp_missing_parameter(name, self):
577 + continue
578 + narrow_weight = loaded_weight[head_rank_start:head_rank_end]
579 + n = narrow_weight.shape[0]
580 + with torch.no_grad():
581 + params_dict[name][:n].copy_(narrow_weight)
582 + else:
583 + if is_expert_weight:
584 + # An expert weight that is not mapped to this rank.
585 + continue
586 + if "gate.e_score_correction_bias" in name:
587 + name = name.replace(
588 + "gate.e_score_correction_bias", "expert_bias"
589 + )
590 + if "router.gate." in name:
591 + name = name.replace("router.", "")
592 + if "hc_fn" in name:
593 + name = name.replace("hc_fn", "hc_fn.weight")
594 + if "hc_head_fn" in name:
595 + name = name.replace("hc_head_fn", "hc_head_fn.weight")
596 +
597 + if is_pp_missing_parameter(name, self):
598 + continue
599 +
600 + if name not in params_dict:
601 + if _should_skip_missing_param(name):
602 + continue
603 + logger.warning_once(
604 + "Skipping unknown checkpoint weight: %s",
605 + name,
606 + )
607 + continue
608 +
609 + param = params_dict[name]
610 + weight_loader = getattr(
611 + param, "weight_loader", default_weight_loader
612 + )
613 + if stacked_shard_id is None:
614 + weight_loader(param, loaded_weight)
615 + else:
616 + weight_loader(param, loaded_weight, stacked_shard_id)
617 + loaded_params.add(name)
618 +
619 + return loaded_params
620 +
621 +
622 +class HYV4ForCausalLM(nn.Module, SupportsPP, SupportsLoRA):
623 + hf_to_vllm_mapper = WeightsMapper(
624 + orig_to_new_stacked={
625 + ".q_a_proj": (".fused_qkv_a_proj", 0),
626 + ".kv_a_proj_with_mqa": (".fused_qkv_a_proj", 1),
627 + ".mlp.gate_proj": (".mlp.gate_up_proj", 0),
628 + ".mlp.up_proj": (".mlp.gate_up_proj", 1),
629 + ".shared_experts.gate_proj": (".shared_experts.gate_up_proj", 0),
630 + ".shared_experts.up_proj": (".shared_experts.gate_up_proj", 1),
631 + ".indexer.weights_proj.": (".indexer.wk_weights_proj.", 1),
632 + }
633 + )
634 + packed_modules_mapping = {
635 + "gate_up_proj": ["gate_proj", "up_proj"],
636 + # MLA runs both latent down-projections as one GEMM.
637 + "fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"],
638 + # The indexer fuses wk and weights_proj into one GEMM.
639 + "wk_weights_proj": ["wk", "weights_proj"],
640 + }
641 +
642 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
643 + super().__init__()
644 + config = vllm_config.model_config.hf_config
645 + quant_config = vllm_config.quant_config
646 + self.config = config
647 + self.quant_config = quant_config
648 + if quant_config is not None:
649 + quant_config.packed_modules_mapping = self.packed_modules_mapping
650 +
651 + parallel_config = vllm_config.parallel_config
652 + eplb_config = parallel_config.eplb_config
653 + self.num_redundant_experts = eplb_config.num_redundant_experts
654 +
655 + self.model = HYV4Model(
656 + vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
657 + )
658 + if get_pp_group().is_last_rank:
659 + # The head stays in the model dtype; ``enable_lm_head_fp32`` is
660 + # surfaced as ``head_dtype`` on the config (see HYV4Config), which
661 + # makes LogitsProcessor accumulate the projection straight into
662 + # fp32 instead of materializing an fp32 copy of the weight.
663 + self.lm_head = ParallelLMHead(
664 + config.vocab_size,
665 + config.hidden_size,
666 + quant_config=quant_config,
667 + prefix=maybe_prefix(prefix, "lm_head"),
668 + )
669 + # With tie_word_embeddings, embed_tokens is kept on the last rank
670 + # (see HYV4Model.__init__) so the weight can be shared here.
671 + if self.config.tie_word_embeddings:
672 + self.lm_head.weight = self.model.embed_tokens.weight
673 + else:
674 + self.lm_head = PPMissingLayer()
675 + self.logits_processor = LogitsProcessor(config.vocab_size)
676 + self.make_empty_intermediate_tensors = (
677 + self.model.make_empty_intermediate_tensors
678 + )
679 +
680 + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
681 + return self.model.embed_input_ids(input_ids)
682 +
683 + def forward(
684 + self,
685 + input_ids: torch.Tensor,
686 + positions: torch.Tensor,
687 + intermediate_tensors: IntermediateTensors | None = None,
688 + inputs_embeds: torch.Tensor | None = None,
689 + ) -> torch.Tensor | IntermediateTensors:
690 + return self.model(input_ids, positions, intermediate_tensors, inputs_embeds)
691 +
692 + def compute_logits(
693 + self,
694 + hidden_states: torch.Tensor,
695 + ) -> torch.Tensor | None:
696 + logits = self.logits_processor(self.lm_head, hidden_states)
697 +
698 + if getattr(self.config, "soft_logits_capping", False):
699 + soft_cap = self.config.soft_logits_capping_logits
700 + logits = soft_cap * torch.nn.functional.tanh(logits / soft_cap)
701 +
702 + return logits
703 +
704 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
705 + # MTP layers are loaded by the MTP head, not by the target model. They
706 + # appear as model.mtp_layers.<i>.* and as the layer ids past the backbone.
707 + mtp_start = self.config.num_hidden_layers
708 + drop_prefixes: dict[str, str | None] = {"model.mtp_layers.": None}
709 + for i in range(getattr(self.config, "num_nextn_predict_layers", 0)):
710 + drop_prefixes[f"model.layers.{mtp_start + i}."] = None
711 + if self.config.tie_word_embeddings:
712 + drop_prefixes["lm_head."] = None
713 + drop_mapper = WeightsMapper(orig_to_new_prefix=drop_prefixes)
714 + loader = AutoWeightsLoader(self)
715 + return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper | drop_mapper)
716 +
717 + def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
718 + return self.model.get_expert_mapping()