main
py 670 lines 26.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Inkling model implementation for AMD GPUs."""
4
5 from __future__ import annotations
6
7 from collections.abc import Iterable
8 from typing import Any
9
10 import regex as re
11 import torch
12 from torch import nn
13
14 from vllm.config import VllmConfig
15 from vllm.distributed import (
16 get_pp_group,
17 get_tensor_model_parallel_rank,
18 get_tensor_model_parallel_world_size,
19 tensor_model_parallel_all_gather,
20 tensor_model_parallel_reduce_scatter,
21 )
22 from vllm.model_executor.layers.quantization import QuantizationConfig
23 from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
24 from vllm.model_executor.models.interfaces import (
25 MultiModalEmbeddings,
26 SupportsLoRA,
27 SupportsMultiModal,
28 SupportsPP,
29 )
30 from vllm.model_executor.models.utils import (
31 AutoWeightsLoader,
32 WeightsMapper,
33 make_empty_intermediate_tensors_factory,
34 make_layers,
35 maybe_prefix,
36 )
37 from vllm.models.inkling.common.mm_preprocess import (
38 InklingDummyInputsBuilder,
39 InklingMultiModalProcessor,
40 InklingProcessingInfo,
41 inkling_audio_enabled,
42 inkling_vision_enabled,
43 )
44 from vllm.models.inkling.common.towers import InklingAudio, InklingVision
45 from vllm.multimodal import MULTIMODAL_REGISTRY
46 from vllm.sequence import IntermediateTensors
47
48 from ..configs import InklingMMConfig, InklingModelConfig
49 from .attention import InklingAttention, compute_log_scaling_tau
50 from .layernorm import InklingRMSNorm
51 from .logits_processor import InklingLogitsProcessor
52 from .mlp import InklingDenseMLP
53 from .moe import InklingMoE
54 from .ops.norm import add_rmsnorm, embed_rmsnorm
55 from .sconv_swa_attn import _ATTN, _MLP, InklingConvState
56 from .short_conv import InklingShortConv
57
58
59 def _layer_id(name: str) -> int | None:
60 m = re.search(r"\.layers\.(\d+)\.", name)
61 return int(m.group(1)) if m else None
62
63
64 def _sconv_add_norm(
65 delta: torch.Tensor,
66 hidden: torch.Tensor,
67 sconv: InklingShortConv,
68 norm: InklingRMSNorm | None,
69 positions: torch.Tensor,
70 ) -> tuple[torch.Tensor | None, torch.Tensor]:
71 """``h = hidden + sconv(TP-sum(delta)); y = rmsnorm(h)``.
72
73 ROCm uses the portable collective path. The Lamport P2P implementation in
74 the NVIDIA model relies on CUDA GDC and CUDA-specific symmetric-memory
75 publication semantics which cannot be linked into a gfx950 HSACO."""
76 norm_w = norm.weight if norm is not None else None
77 eps = norm.variance_epsilon if norm is not None else 0.0
78
79 # RCCL RS -> shard sconv -> AG -> fused add(+rmsnorm).
80 shard = tensor_model_parallel_reduce_scatter(delta, dim=-1)
81 shard = sconv(shard.contiguous(), positions)
82 full = tensor_model_parallel_all_gather(shard, dim=-1)
83 if norm is None:
84 return None, hidden + full
85 return add_rmsnorm(hidden, full, norm_w, eps)
86
87
88 class InklingDecoderLayer(nn.Module):
89 def __init__(
90 self,
91 config: InklingModelConfig,
92 layer_id: int,
93 is_local: bool,
94 quant_config: QuantizationConfig | None,
95 prefix: str,
96 force_dense_mlp: bool = False,
97 ) -> None:
98 super().__init__()
99 # Per-layer owner of the conv state as a paged SWA cache. The 4 sconv
100 # streams (K/V/attn/mlp) are packed head-major into one block and share
101 # it. Built first so the attention layer can wire its K/V sconv to it.
102 self.conv_state = InklingConvState(
103 num_kv_heads=(
104 config.swa_num_key_value_heads
105 if is_local
106 else config.num_key_value_heads
107 ),
108 head_dim=config.swa_head_dim if is_local else config.head_dim,
109 hidden_size=config.hidden_size,
110 kernel_size=config.sconv_kernel_size,
111 prefix=f"{prefix}.conv_state",
112 )
113 self.attn_norm = InklingRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
114 self.attn = InklingAttention(
115 config,
116 num_heads=(
117 config.swa_num_attention_heads
118 if is_local
119 else config.num_attention_heads
120 ),
121 num_kv_heads=(
122 config.swa_num_key_value_heads
123 if is_local
124 else config.num_key_value_heads
125 ),
126 head_dim=config.swa_head_dim if is_local else config.head_dim,
127 rel_extent=config.rel_extent,
128 local_extent=config.sliding_window_size,
129 is_local=is_local,
130 prefix=f"{prefix}.attn",
131 quant_config=quant_config,
132 conv_owner=self.conv_state,
133 )
134 self.mlp_norm = InklingRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
135 if force_dense_mlp or layer_id < config.dense_mlp_idx:
136 self.mlp: nn.Module = InklingDenseMLP(
137 hidden_size=config.hidden_size,
138 intermediate_size=config.dense_intermediate_size,
139 use_global_scale=config.use_global_scale,
140 quant_config=quant_config,
141 prefix=f"{prefix}.mlp",
142 )
143 else:
144 # InklingMoE decides per layer (from the checkpoint exclude list)
145 # whether the routed experts are NVFP4 or bf16; the shared sink
146 # experts are always bf16.
147 self.mlp = InklingMoE(
148 config,
149 prefix=f"{prefix}.mlp",
150 quant_config=quant_config,
151 )
152
153 # Short convolution on the attention-output and MLP-output residual
154 # streams, hidden-sharded: the sublayer outputs are reduce-scattered
155 # to [T, H/tp], the sconv runs on the shard, and an all-gather
156 # restores the full residual — all fused with the residual add + next
157 # rmsnorm via the Lamport P2P kernels for decode-sized batches.
158 tp_size = get_tensor_model_parallel_world_size()
159 sconv_dim = config.hidden_size // tp_size
160 self.attn_sconv = InklingShortConv(
161 sconv_dim, config.sconv_kernel_size, owner=self.conv_state, stream_idx=_ATTN
162 )
163 self.mlp_sconv = InklingShortConv(
164 sconv_dim, config.sconv_kernel_size, owner=self.conv_state, stream_idx=_MLP
165 )
166
167 def forward(
168 self,
169 positions: torch.Tensor,
170 hidden_states: torch.Tensor,
171 pending: tuple[torch.Tensor | None, InklingShortConv] | None = None,
172 defer_mlp_add: bool = False,
173 attn_in: torch.Tensor | None = None,
174 log_scaling: torch.Tensor | None = None,
175 ) -> (
176 torch.Tensor | tuple[torch.Tensor, tuple[torch.Tensor | None, InklingShortConv]]
177 ):
178 # The previous sublayer's (pre-reduce, pre-sconv) delta is folded in
179 # fused with its RS/sconv/AG and this layer's pre-attention rmsnorm.
180 # A None delta means the partials sit in the NVLS symm buffer.
181 if pending is None:
182 if attn_in is None:
183 # First layer; on the text path attn_norm comes fused with
184 # the embedding gather (chain_weight in embed_rmsnorm).
185 attn_in = self.attn_norm(hidden_states)
186 else:
187 attn_in, hidden_states = _sconv_add_norm(
188 pending[0], hidden_states, pending[1], self.attn_norm, positions
189 )
190 attn_output = self.attn(positions, attn_in, log_scaling)
191 mlp_in, hidden_states = _sconv_add_norm(
192 attn_output, hidden_states, self.attn_sconv, self.mlp_norm, positions
193 )
194 mlp_output = self.mlp(mlp_in)
195 if defer_mlp_add:
196 # Caller folds mlp_output (pre-reduce, pre-sconv) into the next
197 # fused sconv+add+rmsnorm.
198 return hidden_states, (mlp_output, self.mlp_sconv)
199 # Standalone (MTP) tail: finish the sublayer without a norm.
200 return _sconv_add_norm(
201 mlp_output, hidden_states, self.mlp_sconv, None, positions
202 )[1]
203
204
205 class InklingReplicatedEmbedding(nn.Module):
206 """Full-vocab embedding table replicated on every TP rank.
207
208 Trades the full table per rank (~2.3 GiB at V=201k / H=6144 bf16, vs a
209 1/tp shard) for no masked lookup and no per-lookup TP all-reduce — one
210 all-reduce per MTP draft step plus one per verify pass — and keeps the
211 full table on-rank for the fused gather+norm kernels (``embed_rmsnorm``,
212 ``embed_dual_rmsnorm_cat``). Bit-exact vs vocab-parallel: the all-reduce
213 there only ever summed one real row against exact zeros. The LM head
214 stays vocab-sharded.
215 """
216
217 def __init__(self, num_embeddings: int, embedding_dim: int) -> None:
218 super().__init__()
219 self.weight = nn.Parameter(
220 torch.empty(num_embeddings, embedding_dim, dtype=torch.get_default_dtype()),
221 requires_grad=False,
222 )
223
224 def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
225 return embed_rmsnorm(input_ids, self.weight, None, 0.0)
226
227
228 class InklingModel(nn.Module):
229 def __init__(
230 self,
231 *,
232 config: InklingModelConfig,
233 quant_config: QuantizationConfig | None,
234 prefix: str,
235 ) -> None:
236 super().__init__()
237 self.config = config
238 self.embed_tokens = InklingReplicatedEmbedding(
239 config.padded_vocab_size, config.hidden_size
240 )
241 self.embed_norm = (
242 InklingRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
243 if config.use_embed_norm
244 else None
245 )
246 local_ids = set(config.local_layer_ids)
247
248 def get_layer(prefix: str) -> InklingDecoderLayer:
249 idx = _layer_id(prefix + ".") or int(prefix.split(".")[-1])
250 return InklingDecoderLayer(
251 config, idx, idx in local_ids, quant_config, prefix
252 )
253
254 self.start_layer, self.end_layer, self.layers = make_layers(
255 config.num_hidden_layers, get_layer, prefix=f"{prefix}.layers"
256 )
257 self.norm = InklingRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
258 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
259 ["hidden_states"], config.hidden_size
260 )
261
262 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
263 # Row gather + embed_norm in one launch.
264 norm = self.embed_norm
265 return embed_rmsnorm(
266 input_ids,
267 self.embed_tokens.weight,
268 norm.weight if norm is not None else None,
269 norm.variance_epsilon if norm is not None else 0.0,
270 )
271
272 def forward(
273 self,
274 input_ids: torch.Tensor | None,
275 positions: torch.Tensor,
276 intermediate_tensors: IntermediateTensors | None,
277 inputs_embeds: torch.Tensor | None = None,
278 ) -> torch.Tensor | IntermediateTensors:
279 attn_in0: torch.Tensor | None = None
280 if get_pp_group().is_first_rank:
281 if inputs_embeds is not None:
282 # embed_norm was already applied when producing inputs_embeds.
283 hidden_states = inputs_embeds
284 else:
285 # Gather + embed_norm + the first layer's attn_norm, one launch.
286 norm = self.embed_norm
287 hidden_states, attn_in0 = embed_rmsnorm(
288 input_ids,
289 self.embed_tokens.weight,
290 norm.weight if norm is not None else None,
291 self.config.rms_norm_eps,
292 chain_weight=self.layers[self.start_layer].attn_norm.weight,
293 )
294 else:
295 assert intermediate_tensors is not None
296 hidden_states = intermediate_tensors["hidden_states"]
297 hidden_states = hidden_states.view(-1, hidden_states.shape[-1])
298 log_scaling = None
299 if self.config.log_scaling_n_floor is not None:
300 log_scaling = compute_log_scaling_tau(
301 positions,
302 self.config.log_scaling_n_floor,
303 self.config.log_scaling_alpha,
304 )
305
306 pending: tuple[torch.Tensor | None, InklingShortConv] | None = None
307 for layer in self.layers[self.start_layer : self.end_layer]:
308 hidden_states, pending = layer(
309 positions,
310 hidden_states,
311 pending=pending,
312 defer_mlp_add=True,
313 attn_in=attn_in0,
314 log_scaling=log_scaling,
315 )
316 attn_in0 = None
317
318 if not get_pp_group().is_last_rank:
319 if pending is not None:
320 hidden_states = _sconv_add_norm(
321 pending[0], hidden_states, pending[1], None, positions
322 )[1]
323 return IntermediateTensors({"hidden_states": hidden_states})
324 if pending is not None:
325 # Final RS/sconv/AG + residual add fused with the final rmsnorm.
326 norm_out = _sconv_add_norm(
327 pending[0], hidden_states, pending[1], self.norm, positions
328 )[0]
329 assert norm_out is not None
330 return norm_out
331 return self.norm(hidden_states)
332
333
334 class _TmlForCausalLMBase(nn.Module, SupportsPP, SupportsLoRA):
335 """Shared text-backbone causal-LM scaffolding for both entry classes."""
336
337 hf_to_vllm_mapper = WeightsMapper(
338 orig_to_new_substr={
339 ".w13_dn": ".gate_up_proj",
340 ".w2_md": ".down_proj",
341 },
342 orig_to_new_stacked={
343 ".attn.wq_du.": (".attn.qkvr.", 0),
344 ".attn.wk_dv.": (".attn.qkvr.", 1),
345 ".attn.wv_dv.": (".attn.qkvr.", 2),
346 ".attn.wr_du.": (".attn.qkvr.", 3),
347 },
348 orig_to_new_prefix={
349 "model.llm.layers.": "model.layers.",
350 "model.llm.embed_norm": "model.embed_norm",
351 "model.llm.embed": "model.embed_tokens",
352 "model.llm.norm": "model.norm",
353 "model.llm.unembed": "lm_head",
354 "language_model.layers.": "model.layers.",
355 "language_model.lm_head.": "lm_head.",
356 },
357 orig_to_new_suffix={
358 # NVFP4 scale
359 ".w13_weight.scale": ".w13_weight_scale",
360 ".w13_weight.scale2": ".w13_weight_scale_2",
361 ".w2_weight.scale": ".w2_weight_scale",
362 ".w2_weight.scale2": ".w2_weight_scale_2",
363 },
364 )
365 # Quark uses this mapping when resolving quantization exclusions for the
366 # four checkpoint attention projections fused into qkvr.
367 packed_modules_mapping = {
368 "qkvr": ["wq_du", "wk_dv", "wv_dv", "wr_du"],
369 "w13": ["w1", "w3"],
370 }
371 embedding_modules = {
372 "lm_head": "output_embeddings",
373 }
374
375 def _build(
376 self,
377 vllm_config: VllmConfig,
378 text_config: InklingModelConfig,
379 prefix: str,
380 ) -> None:
381 quant_config = vllm_config.quant_config
382 self.config = text_config
383 # ROCm checkpoints use Quark OCP MXFP4. The global Quark config is
384 # passed into each routed MoE and its exclusion list keeps the rest of
385 # Inkling in bf16.
386 # Read by the MRV2 runner to publish per-request short-conv metadata.
387 # Short convolution is intrinsic to Inkling, so this is always set.
388 self.uses_sconv = True
389 self.model = InklingModel(
390 config=text_config,
391 quant_config=quant_config,
392 prefix=maybe_prefix(prefix, "model"),
393 )
394 self.lm_head = ParallelLMHead(
395 text_config.padded_vocab_size,
396 text_config.hidden_size,
397 org_num_embeddings=text_config.padded_vocab_size,
398 quant_config=quant_config,
399 prefix=maybe_prefix(prefix, "lm_head"),
400 )
401 self.logits_processor = InklingLogitsProcessor(
402 text_config.padded_vocab_size,
403 org_vocab_size=text_config.vocab_size,
404 soft_cap=text_config.final_logit_softcapping,
405 logits_mup_width_multiplier=text_config.logits_mup_width_multiplier,
406 )
407 self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
408 self.model.make_empty_intermediate_tensors
409 )
410
411 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
412 return self.model.embed_input_ids(input_ids)
413
414 def forward(
415 self,
416 input_ids: torch.Tensor | None,
417 positions: torch.Tensor,
418 intermediate_tensors: IntermediateTensors | None = None,
419 inputs_embeds: torch.Tensor | None = None,
420 **kwargs: object,
421 ) -> torch.Tensor | IntermediateTensors:
422 return self.model(
423 input_ids,
424 positions,
425 intermediate_tensors,
426 inputs_embeds,
427 )
428
429 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
430 return self.logits_processor(self.lm_head, hidden_states)
431
432 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
433 return _load_inkling_weights(self, weights, self.config)
434
435
436 class InklingForCausalLM(_TmlForCausalLMBase):
437 """Text-only entry point (``inkling_model`` checkpoints)."""
438
439 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
440 super().__init__()
441 self._build(vllm_config, vllm_config.model_config.hf_config, prefix)
442
443
444 @MULTIMODAL_REGISTRY.register_processor(
445 InklingMultiModalProcessor,
446 info=InklingProcessingInfo,
447 dummy_inputs=InklingDummyInputsBuilder,
448 )
449 class InklingForConditionalGeneration(_TmlForCausalLMBase, SupportsMultiModal):
450 """Top-level (multimodal) entry point.
451
452 Builds the vision + audio towers on top of the shared text backbone. Inkling has
453 NO cross-modal fusion (the vision tower emits one token per patch, the audio
454 tower one token per frame), so generation reuses the inherited backbone
455 ``forward`` / ``compute_logits`` (the latter already applies muP) and this
456 class only adds multimodal embedding + merge.
457 """
458
459 hf_to_vllm_mapper = _TmlForCausalLMBase.hf_to_vllm_mapper | WeightsMapper(
460 orig_to_new_prefix={
461 "model.audio.": "audio.",
462 "model.visual.": "visual.vision_encoder.",
463 },
464 )
465
466 @classmethod
467 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
468 if modality.startswith("image"):
469 return "<|content_image|>"
470 if modality.startswith("audio"):
471 return "<|content_audio_input|>"
472 raise ValueError("Only image or audio modality is supported")
473
474 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
475 super().__init__()
476 config: InklingMMConfig = vllm_config.model_config.hf_config
477
478 self.visual = (
479 InklingVision(config.vision_config, prefix=maybe_prefix(prefix, "visual"))
480 if inkling_vision_enabled(config)
481 else None
482 )
483 self.audio = (
484 InklingAudio(config.audio_config, prefix=maybe_prefix(prefix, "audio"))
485 if inkling_audio_enabled(config)
486 else None
487 )
488
489 self._build(vllm_config, config.text_config, prefix)
490
491 # -- multimodal embedding -------------------------------------------
492
493 def _process_image_input(
494 self, pixel_values: Any, num_patches: Any
495 ) -> tuple[torch.Tensor, ...]:
496 assert self.visual is not None
497 # pixel_values is a list (per item) of [P_i, 2, P, P, 3] tensors,
498 # or a single concatenated tensor. Normalize to a flat batch, run the
499 # tower once, then split back per item.
500 if isinstance(pixel_values, (list, tuple)):
501 if not pixel_values:
502 return ()
503 sizes = [int(p.shape[0]) for p in pixel_values]
504 patches = torch.cat(list(pixel_values), dim=0)
505 else:
506 patches = pixel_values
507 sizes = self._sizes_from(num_patches, patches.shape[0])
508
509 patches = patches.to(device=self.visual.device, dtype=self.visual.dtype)
510 embeds = self.visual(patches) # [total_patches, D]
511 return tuple(embeds.split(sizes))
512
513 def _process_audio_input(
514 self, input_audio_features: Any, num_audio_tokens: Any
515 ) -> tuple[torch.Tensor, ...]:
516 assert self.audio is not None
517 if isinstance(input_audio_features, (list, tuple)):
518 if not input_audio_features:
519 return ()
520 sizes = [int(d.shape[0]) for d in input_audio_features]
521 dmel = torch.cat(list(input_audio_features), dim=0)
522 else:
523 dmel = input_audio_features
524 sizes = self._sizes_from(num_audio_tokens, dmel.shape[0])
525
526 dmel = dmel.to(device=self.audio.device)
527 embeds = self.audio(dmel) # [total_frames, D]
528 return tuple(embeds.split(sizes))
529
530 @staticmethod
531 def _sizes_from(counts: Any, total: int) -> list[int]:
532 if counts is None:
533 return [total]
534 if isinstance(counts, torch.Tensor):
535 return [int(c) for c in counts.flatten().tolist()]
536 if isinstance(counts, (list, tuple)):
537 flat: list[int] = []
538 for c in counts:
539 flat.append(int(c.item()) if isinstance(c, torch.Tensor) else int(c))
540 return flat
541 return [int(counts)]
542
543 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
544 # Iterate modalities in a stable order so the returned per-item tensors
545 # line up with their appearance order; the positional merge in
546 # embed_input_ids handles actual placement.
547 pixel_values = kwargs.get("pixel_values")
548 num_patches = kwargs.get("num_patches")
549 input_audio_features = kwargs.get("input_audio_features")
550 num_audio_tokens = kwargs.get("num_audio_tokens")
551
552 embeddings: tuple[torch.Tensor, ...] = ()
553 if pixel_values is not None and self.visual is not None:
554 embeddings += self._process_image_input(pixel_values, num_patches)
555 if input_audio_features is not None and self.audio is not None:
556 embeddings += self._process_audio_input(
557 input_audio_features, num_audio_tokens
558 )
559 return embeddings
560
561 def embed_input_ids(
562 self,
563 input_ids: torch.Tensor,
564 multimodal_embeddings: MultiModalEmbeddings | None = None,
565 *,
566 is_multimodal: torch.Tensor | None = None,
567 ) -> torch.Tensor:
568 # Override the base's 1-arg embed_input_ids: the runner calls this 3-arg
569 # signature for multimodal models. Text embeddings come from the shared
570 # backbone (which applies embed_norm); MM embeddings are scattered in.
571 from vllm.model_executor.models.utils import _merge_multimodal_embeddings
572
573 # Placeholder ids use unused vocabulary slots and these positions are
574 # overwritten by MM embeds below.
575 inputs_embeds = self.model.embed_input_ids(input_ids)
576 if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
577 return inputs_embeds
578 assert is_multimodal is not None
579 return _merge_multimodal_embeddings(
580 inputs_embeds=inputs_embeds,
581 multimodal_embeddings=multimodal_embeddings,
582 is_multimodal=is_multimodal,
583 )
584
585 def get_language_model(self) -> nn.Module:
586 # This class IS the causal LM (the towers are side branches), so the
587 # language model is self — callers expect a module exposing ``.model``
588 # / ``.lm_head`` (e.g. the MTP/eagle loader shares embeddings via
589 # ``get_language_model().model.embed_tokens``).
590 return self
591
592
593 # ===========================================================================
594 # Weight loading
595 # ===========================================================================
596
597
598 _MOE_EXPERT_WEIGHT_RE = re.compile(
599 r"^(?P<mlp>.*\.mlp)\.(?P<rest>(?:shared_)?experts\..+)$"
600 )
601
602
603 def _is_peft_adapter_weight(name: str) -> bool:
604 return ".lora_A." in name or ".lora_B." in name
605
606
607 def _load_inkling_weights(
608 module: nn.Module,
609 weights: Iterable[tuple[str, torch.Tensor]],
610 config: InklingModelConfig,
611 ) -> set[str]:
612 moe_modules = {
613 name: mod for name, mod in module.named_modules() if isinstance(mod, InklingMoE)
614 }
615 loaded: set[str] = set()
616 tp_size = get_tensor_model_parallel_world_size()
617 tp_rank = get_tensor_model_parallel_rank()
618 local_ids = set(config.local_layer_ids)
619
620 def _iter_loadable_weights() -> Iterable[tuple[str, torch.Tensor]]:
621 for name, weight in module.hf_to_vllm_mapper.apply(weights):
622 # LightSeek's MXFP4 conversion bundles a quantized copy of the
623 # standalone PEFT adapter in the base safetensors index. These are
624 # not base-model parameters; adapters remain opt-in at serving.
625 if _is_peft_adapter_weight(name):
626 continue
627 shard_id = getattr(weight, "shard_id", None)
628 # Replicate K/V conv-free GQA heads when tp_size > num_kv_heads.
629 if (
630 shard_id in (1, 2)
631 and name.endswith(".attn.qkvr.weight")
632 and weight.shape[0] > 0
633 ):
634 lid = _layer_id(name)
635 if lid is not None:
636 is_local = lid in local_ids
637 n_kv = (
638 config.swa_num_key_value_heads
639 if is_local
640 else config.num_key_value_heads
641 )
642 head_dim = config.swa_head_dim if is_local else config.head_dim
643 if tp_size > n_kv and weight.shape[0] == n_kv * head_dim:
644 kv_idx = (tp_rank * n_kv) // tp_size
645 weight = weight.narrow(0, kv_idx * head_dim, head_dim)
646 weight.shard_id = shard_id
647
648 # MoE expert tensors (fused stacked, routed + shared sink): translate
649 # the checkpoint layout to per-expert MoERunner loads.
650 moe_match = _MOE_EXPERT_WEIGHT_RE.match(name)
651 if moe_match is not None and moe_match.group("mlp") in moe_modules:
652 moe = moe_modules[moe_match.group("mlp")]
653 for rel in moe.load_expert_weight(moe_match.group("rest"), weight):
654 loaded.add(f"{moe_match.group('mlp')}.{rel}")
655 continue
656
657 yield name, weight
658
659 mapper = WeightsMapper(orig_to_new_prefix={"model.mtp.": None})
660 loader = AutoWeightsLoader(module)
661 loaded |= loader.load_weights(_iter_loadable_weights(), mapper=mapper)
662
663 # Post-load MoE fixups (default input scales, zeroed EP-padding experts).
664 for moe_name, moe in moe_modules.items():
665 for rel in moe.finalize_load():
666 loaded.add(f"{moe_name}.{rel}")
667 return loaded
668
669
670 EntryClass = [InklingForCausalLM, InklingForConditionalGeneration]