main
py 1,186 lines 41.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import itertools
5 from collections.abc import Callable, Iterable, Mapping
6 from contextlib import contextmanager
7 from dataclasses import dataclass, field, replace
8 from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias, overload
9
10 import regex as re
11 import torch
12 import torch.nn as nn
13 from torch.nn.modules.module import register_module_module_registration_hook
14
15 from vllm.config import VllmConfig
16 from vllm.distributed import (
17 get_tensor_model_parallel_rank,
18 get_tensor_model_parallel_world_size,
19 )
20 from vllm.logger import init_logger
21 from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
22 from vllm.model_executor.model_loader.reload import (
23 support_quantized_model_reload_from_hp_weights,
24 )
25 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
26 from vllm.model_executor.models.interfaces import supports_any_eagle
27 from vllm.multimodal import NestedTensors
28 from vllm.sequence import IntermediateTensors
29 from vllm.utils.math_utils import cdiv
30 from vllm.utils.torch_utils import (
31 direct_register_custom_op,
32 )
33
34 if TYPE_CHECKING:
35 from transformers import PreTrainedConfig
36 from transformers.conversion_mapping import WeightRenaming
37
38 from vllm.model_executor.layers.quantization import QuantizationConfig
39
40 logger = init_logger(__name__)
41
42 ShardId: TypeAlias = str | int | tuple[int, ...]
43 """One shard of a stacked parameter. A tuple is a single contiguous span."""
44 ShardIds: TypeAlias = ShardId | list[ShardId]
45 """One shard, or a list of shards the same weight is loaded into in turn."""
46
47
48 @dataclass
49 class WeightsMapper:
50 """Maps the name of each weight if they match the following patterns.
51
52 If a key maps to a value of `None`, the corresponding weight is ignored."""
53
54 orig_to_new_renaming: list["WeightRenaming"] = field(default_factory=list)
55 orig_to_new_regex: Mapping[re.Pattern, str | None] = field(default_factory=dict)
56 orig_to_new_substr: Mapping[str, str | None] = field(default_factory=dict)
57 orig_to_new_stacked: Mapping[str, tuple[str, ShardIds]] = field(
58 default_factory=dict
59 )
60 orig_to_new_prefix: Mapping[str, str | None] = field(default_factory=dict)
61 orig_to_new_suffix: Mapping[str, str | None] = field(default_factory=dict)
62
63 def __or__(self, other: "WeightsMapper") -> "WeightsMapper":
64 """Combine two `WeightsMapper`s by merging their mappings."""
65 return WeightsMapper(
66 orig_to_new_renaming=[
67 *self.orig_to_new_renaming,
68 *other.orig_to_new_renaming,
69 ],
70 orig_to_new_regex={**self.orig_to_new_regex, **other.orig_to_new_regex},
71 orig_to_new_substr={**self.orig_to_new_substr, **other.orig_to_new_substr},
72 orig_to_new_stacked={
73 **self.orig_to_new_stacked,
74 **other.orig_to_new_stacked,
75 },
76 orig_to_new_prefix={**self.orig_to_new_prefix, **other.orig_to_new_prefix},
77 orig_to_new_suffix={**self.orig_to_new_suffix, **other.orig_to_new_suffix},
78 )
79
80 def map_name(self, key: str) -> str | None:
81 """Map a weight name; returns ``None`` if the weight should be ignored."""
82 result = self._map_name_with_shard(key)
83 return result[0] if result is not None else None
84
85 def _map_name(self, key: str) -> str | None:
86 """Backward-compatible alias for :meth:`map_name`."""
87 return self.map_name(key)
88
89 def _map_name_with_shard(self, key: str) -> tuple[str, ShardIds | None] | None:
90 """Map a weight name and extract any shard_id metadata.
91
92 Returns:
93 (mapped_name, shard_id) if the name should be kept. A list of shard
94 ids means the weight is loaded into each of those shards in turn.
95 None if the name should be dropped.
96
97 """
98 # Deprecation warnings
99 if key.endswith(".kv_scale"):
100 logger.warning_once(
101 "DEPRECATED. Found kv_scale in the checkpoint. "
102 "This format is deprecated in favor of separate k_scale and "
103 "v_scale tensors and will be removed in a future release. "
104 "Functionally, we will remap kv_scale to k_scale and duplicate "
105 "k_scale to v_scale"
106 )
107
108 for renaming in self.orig_to_new_renaming:
109 key, _ = renaming.rename_source_key(key)
110
111 for pattern, new_key in self.orig_to_new_regex.items():
112 if pattern.search(key):
113 if new_key is None:
114 return None
115
116 key = pattern.sub(new_key, key)
117
118 for substr, new_key in self.orig_to_new_substr.items():
119 if substr in key:
120 if new_key is None:
121 return None
122
123 key = key.replace(substr, new_key, 1)
124
125 shard_id: ShardIds | None = None
126 for substr, (new_key, new_shard_id) in self.orig_to_new_stacked.items():
127 if substr in key:
128 key = key.replace(substr, new_key, 1)
129 shard_id = new_shard_id
130
131 for prefix, new_key in self.orig_to_new_prefix.items():
132 if key.startswith(prefix):
133 if new_key is None:
134 return None
135
136 key = key.replace(prefix, new_key, 1)
137
138 for suffix, new_key in self.orig_to_new_suffix.items():
139 if key.endswith(suffix):
140 if new_key is None:
141 return None
142
143 key = new_key.join(key.rsplit(suffix, 1))
144
145 return key, shard_id
146
147 def apply(
148 self, weights: Iterable[tuple[str, torch.Tensor]]
149 ) -> Iterable[tuple[str, torch.Tensor]]:
150 for name, data in weights:
151 result = self._map_name_with_shard(name)
152 if result is None:
153 continue
154 out_name, shard_id = result
155 if shard_id is None:
156 yield out_name, data
157 continue
158 shard_ids = shard_id if isinstance(shard_id, list) else [shard_id]
159 for i, one_shard_id in enumerate(shard_ids):
160 # Each shard carries its own id, so extra shards need their own
161 # tensor object; `detach` aliases the storage instead of copying.
162 shard_data = data if i == 0 else data.detach()
163 shard_data.shard_id = one_shard_id
164 yield out_name, shard_data
165
166 def apply_list(self, values: list[str]) -> list[str]:
167 return [
168 out_name
169 for name in values
170 if (out_name := self._map_name(name)) is not None
171 ]
172
173 def apply_dict(self, values: dict[str, Any]) -> dict[str, Any]:
174 return {
175 out_name: value
176 for name, value in values.items()
177 if (out_name := self._map_name(name)) is not None
178 }
179
180 def get_rename_mapper(self) -> "WeightsMapper":
181 """Mapper variant keeping only the renames.
182
183 This is what consumers that *name* modules rather than load them need:
184 LoRA name parsing and the quantization config's layer lists.
185
186 Stacked maps are dropped so that constituent names (e.g. `q_proj`) survive
187 rather than being rewritten to the stacked vLLM name (`qkv_proj`). Mappings to
188 `None` are dropped because "do not load this weight" is meaningless to such a
189 consumer, and applying it would silently shrink a quantization config's ignore
190 list or make LoRA name parsing fail."""
191 remove_none = lambda d: {k: v for k, v in d.items() if v is not None}
192 return replace(
193 self,
194 orig_to_new_regex=remove_none(self.orig_to_new_regex),
195 orig_to_new_substr=remove_none(self.orig_to_new_substr),
196 orig_to_new_stacked={},
197 orig_to_new_prefix=remove_none(self.orig_to_new_prefix),
198 orig_to_new_suffix=remove_none(self.orig_to_new_suffix),
199 )
200
201
202 def _get_tied_embedding_params(module: nn.Module) -> dict[str, str]:
203 """Map each tied word embedding qualname to the first name it aliases."""
204 canonical = dict[int, str]()
205 aliased = dict[str, str]()
206 for prefix, submodule in module.named_modules(remove_duplicate=False):
207 if not isinstance(submodule, VocabParallelEmbedding):
208 continue
209 for name, param in submodule.named_parameters(remove_duplicate=False):
210 qualname = f"{prefix}.{name}" if prefix else name
211 if (first_name := canonical.setdefault(id(param), qualname)) != qualname:
212 aliased[qualname] = first_name
213 return aliased
214
215
216 class AutoWeightsLoader:
217 """Helper class to load weights into a [`torch.nn.Module`][]. It is able
218 to automatically detect child modules and parameters while iterating over
219 the weights only once.
220
221 The weight loading logic for individual modules can be overridden
222 by defining a `load_weights` method.
223
224 Similarly, the weight loading logic for individual parameters can be
225 overridden by defining a `weight_loader` method.
226
227 Detailed weight loading information can be viewed by setting the
228 environment variable `VLLM_LOGGING_LEVEL=DEBUG`.
229 """
230
231 # Models trained using early version ColossalAI or quantized by
232 # GPTQModel may include these tensors in checkpoint. Drop them.
233 REMOVE_UNUSED_ROTARY_EMBEDS_MAPPER = WeightsMapper(
234 orig_to_new_substr={
235 "rotary_pos_emb.inv_freq": None,
236 "rotary_emb.inv_freq": None,
237 "rotary_emb.cos_cached": None,
238 "rotary_emb.sin_cached": None,
239 }
240 )
241
242 def __init__(
243 self,
244 module: nn.Module,
245 *,
246 ignore_unexpected_prefixes: list[str] | None = None,
247 ignore_unexpected_suffixes: list[str] | None = None,
248 ) -> None:
249 super().__init__()
250
251 self.module = module
252 self.ignore_unexpected_prefixes = ignore_unexpected_prefixes or []
253 self.ignore_unexpected_suffixes = ignore_unexpected_suffixes or []
254
255 # Weight tying makes two qualnames point at the same `nn.Parameter`
256 # (e.g. `lm_head.weight` and `model.embed_tokens.weight`). Loading both
257 # would write the same storage twice, so only the first name reached by
258 # module traversal is loaded and the rest are skipped.
259 self.aliased_params = _get_tied_embedding_params(module)
260 self._skipped_aliases = dict[str, str]()
261 self._loaded_params_are_complete = True
262
263 def _groupby_prefix(
264 self,
265 weights: Iterable[tuple[str, torch.Tensor]],
266 ) -> Iterable[tuple[str, Iterable[tuple[str, torch.Tensor]]]]:
267 weights_by_parts = (
268 (weight_name.split(".", 1), weight_data)
269 for weight_name, weight_data in weights
270 )
271
272 for prefix, group in itertools.groupby(weights_by_parts, key=lambda x: x[0][0]):
273 yield (
274 prefix,
275 # Because maxsplit=1 in weight_name.split(...),
276 # the length of `parts` must either be 1 or 2
277 (
278 ("" if len(parts) == 1 else parts[1], weights_data)
279 for parts, weights_data in group
280 ),
281 )
282
283 def _get_qualname(self, prefix: str, rest: str) -> str:
284 if prefix == "":
285 return rest
286 if rest == "":
287 return prefix
288
289 return ".".join((prefix, rest))
290
291 def _can_skip(self, qualname: str) -> bool:
292 return qualname in self.aliased_params
293
294 def _can_ignore_unexpected(self, qualname: str) -> bool:
295 iup = (qualname.startswith(p) for p in self.ignore_unexpected_prefixes)
296 ius = (qualname.endswith(s) for s in self.ignore_unexpected_suffixes)
297 return any(iup) or any(ius)
298
299 def _load_param(
300 self,
301 base_prefix: str,
302 param: nn.Parameter,
303 weights: Iterable[tuple[str, torch.Tensor]],
304 ) -> Iterable[str]:
305 for weight_name, weight_data in weights:
306 weight_qualname = self._get_qualname(base_prefix, weight_name)
307
308 if self._can_skip(weight_qualname):
309 logger.debug("Skipping weight %s", weight_qualname)
310
311 continue
312
313 if weight_name != "":
314 if self._can_ignore_unexpected(weight_qualname):
315 logger.debug("Ignoring weight %s", weight_qualname)
316
317 continue
318
319 raise ValueError(
320 f"Attempted to load nested weight {weight_qualname!r} "
321 f"into a single parameter {base_prefix!r}"
322 )
323
324 if param.dtype != weight_data.dtype:
325 logger.warning(
326 "Attempted to load weight %s with dtype %s into "
327 "parameter with dtype %s",
328 weight_qualname,
329 weight_data.dtype,
330 param.dtype,
331 )
332 weight_loader = getattr(param, "weight_loader", default_weight_loader)
333 weight_loader(param, weight_data)
334
335 logger.debug("Loaded weight %s with shape %s", weight_qualname, param.shape)
336
337 yield weight_qualname
338
339 def _add_loadable_non_param_tensors(
340 self, module: nn.Module, child_params: dict[str, torch.Tensor]
341 ):
342 """Add tensor names that are not in the model params that may be in the
343 safetensors, e.g., batch normalization stats and registered buffers.
344 """
345 # Add persistent registered buffers.
346 # Non-persistent buffers are excluded, matching PyTorch state_dict().
347 non_persistent: set[str] = getattr(module, "_non_persistent_buffers_set", set())
348 for buf_name, buf in module.named_buffers(recurse=False):
349 if buf_name not in child_params and buf_name not in non_persistent:
350 child_params[buf_name] = buf
351
352 if isinstance(
353 module,
354 (
355 nn.BatchNorm1d,
356 nn.BatchNorm2d,
357 nn.BatchNorm3d,
358 nn.LazyBatchNorm1d,
359 nn.LazyBatchNorm2d,
360 nn.LazyBatchNorm3d,
361 nn.SyncBatchNorm,
362 ),
363 ):
364 module_state_dict = module.state_dict()
365 for stat_name in ("running_mean", "running_var", "num_batches_tracked"):
366 child_params[stat_name] = module_state_dict[stat_name]
367
368 def _load_module(
369 self,
370 base_prefix: str,
371 module: nn.Module,
372 weights: Iterable[tuple[str, torch.Tensor]],
373 ) -> Iterable[str]:
374 if isinstance(module, (StageMissingLayer, PPMissingLayer)):
375 return
376
377 # Avoid infinite recursion since this function is typically
378 # called inside load_weights of the module itself
379 if module != self.module:
380 module_load_weights = getattr(module, "load_weights", None)
381 if callable(module_load_weights):
382 loaded_params = module_load_weights(weights)
383 if loaded_params is None:
384 logger.warning(
385 "Unable to collect loaded parameters for module %s", module
386 )
387 self._loaded_params_are_complete = False
388 else:
389 yield from map(
390 lambda x: self._get_qualname(base_prefix, x),
391 loaded_params,
392 )
393
394 child_modules = dict(module.named_children())
395 child_params = dict(module.named_parameters(recurse=False))
396
397 # Add missing tensors the weight loader needs to be able to load
398 # that aren't registered as params, e.g., batchnorm statistics.
399 self._add_loadable_non_param_tensors(module, child_params)
400
401 for child_prefix, child_weights in self._groupby_prefix(weights):
402 prefix = self._get_qualname(base_prefix, child_prefix)
403
404 if child_prefix in child_modules:
405 yield from self._load_module(
406 prefix, child_modules[child_prefix], child_weights
407 )
408 elif child_prefix in child_params:
409 if self._can_skip(prefix):
410 logger.debug("Skipping param %s", prefix)
411
412 continue
413
414 yield from self._load_param(
415 prefix, child_params[child_prefix], child_weights
416 )
417 else:
418 if self._can_skip(prefix):
419 logger.debug("Skipping missing %s", prefix)
420
421 continue
422
423 can_ignore_module = self._can_ignore_unexpected(prefix + ".")
424 can_ignore_param = self._can_ignore_unexpected(prefix)
425 if can_ignore_module or can_ignore_param:
426 logger.debug("Ignoring missing %s", prefix)
427
428 continue
429
430 named_parameters = module.named_parameters(recurse=True)
431 desc_param_keys = {
432 maybe_prefix(base_prefix, k) for k, _ in named_parameters
433 }
434 msg = (
435 f"There is no module or parameter named {prefix!r} "
436 f"in {self.module._get_name()}. "
437 f"The available parameters belonging to {base_prefix} "
438 f"({module._get_name()}) are: {desc_param_keys}"
439 )
440 raise ValueError(msg)
441
442 @support_quantized_model_reload_from_hp_weights
443 def load_weights(
444 self,
445 weights: Iterable[tuple[str, torch.Tensor]],
446 *,
447 mapper: WeightsMapper | None = None,
448 ) -> set[str]:
449 # Ignore unexpected biases (typically from GPTQ models)
450 self.ignore_unexpected_suffixes.append(".bias")
451
452 mapper = mapper or WeightsMapper()
453 # Many models store quant_config in the base model instead of the causal model.
454 # We look at the causal model's direct children for this reason.
455 modules = (self.module, *self.module.children())
456 iterator = (m.quant_config for m in modules if hasattr(m, "quant_config"))
457 if quant_config := next(iterator, None):
458 # Apply mappings for quantization-specific checkpoint tensors.
459 mapper |= quant_config.get_cache_scale_mapper()
460 mapper |= quant_config.get_checkpoint_weight_mapper()
461 ignore_unexpected_suffixes = quant_config._ignore_unexpected_suffixes
462 self.ignore_unexpected_suffixes.extend(ignore_unexpected_suffixes)
463 mapper |= self.REMOVE_UNUSED_ROTARY_EMBEDS_MAPPER
464
465 weights = mapper.apply(weights)
466 weights = self._filter_skipped(weights)
467
468 autoloaded_weights = set(self._load_module("", self.module, weights))
469 self._check_skipped_aliases(autoloaded_weights)
470 return autoloaded_weights
471
472 def _filter_skipped(
473 self, weights: Iterable[tuple[str, torch.Tensor]]
474 ) -> Iterable[tuple[str, torch.Tensor]]:
475 for name, weight in weights:
476 if (canonical := self.aliased_params.get(name)) is not None:
477 self._skipped_aliases[name] = canonical
478 if self._can_skip(name):
479 continue
480
481 yield name, weight
482
483 def _check_skipped_aliases(self, autoloaded_weights: set[str]) -> None:
484 """Guard against skipping an alias whose canonical name never loads."""
485 if not self._loaded_params_are_complete:
486 return
487 for alias, canonical in self._skipped_aliases.items():
488 if canonical not in autoloaded_weights:
489 raise ValueError(
490 f"{alias!r} was skipped because it is tied to {canonical!r} "
491 f"in {self.module._get_name()}, but {canonical!r} was not "
492 "found in the checkpoint, so the tied weight is "
493 "uninitialized."
494 )
495
496
497 def maybe_fuse_shared_experts(
498 weights: Iterable[tuple[str, torch.Tensor]],
499 *,
500 n_routed_experts: int,
501 n_shared_experts: int,
502 ckpt_prefix: str = "mlp.shared_experts",
503 enabled: bool | None = None,
504 ) -> Iterable[tuple[str, torch.Tensor]]:
505 """Route AITER fused-shared-expert checkpoint weights into fused slots.
506
507 When AITER fused-shared-experts is active, shared experts are packed into
508 the routed expert tensor. The checkpoint stores them under `ckpt_prefix`
509 as a single (possibly widened) tensor; this splits it into
510 `n_shared_experts` chunks named `mlp.experts.{n_routed_experts + j}` so
511 the `RoutedExperts` loader treats them as extra experts. Yields the input
512 unchanged when the fusion is inactive, so callers can wrap unconditionally.
513
514 Args:
515 weights: Iterable of `(name, tensor)` checkpoint pairs.
516 n_routed_experts: Number of routed experts; offsets the fused slots.
517 n_shared_experts: Number of shared experts packed into the tensor.
518 ckpt_prefix: Checkpoint module name of the shared experts.
519 enabled: Whether AITER fused-shared-experts is active. Defaults to
520 `rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()`; pass an
521 explicit value only when the model gates on something more (e.g.
522 quant-spec compatibility) and it must match its construction-time
523 decision.
524
525 Yields:
526 `(name, tensor)` pairs with shared experts routed to fused slots.
527
528 """
529 if enabled is None:
530 from vllm._aiter_ops import rocm_aiter_ops
531
532 enabled = rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
533 if not enabled:
534 yield from weights
535 return
536
537 # Match on the dotted boundary so e.g. "mlp.shared_expert." does not also
538 # catch a sibling "mlp.shared_expert_gate".
539 prefix = f"{ckpt_prefix}."
540 for name, loaded_weight in weights:
541 if prefix not in name:
542 yield name, loaded_weight
543 continue
544 # gate/up split on the output dim; down_proj on the input dim.
545 split_dim = 1 if ("down_proj.weight" in name and loaded_weight.ndim > 1) else 0
546 total = loaded_weight.shape[split_dim]
547 if total % n_shared_experts != 0:
548 raise ValueError(
549 f"FSE shared-expert weight {name!r} has size {total} along axis "
550 f"{split_dim}, not divisible by n_shared_experts={n_shared_experts}."
551 )
552 chunk = total // n_shared_experts
553 for j in range(n_shared_experts):
554 sl = slice(j * chunk, (j + 1) * chunk)
555 if loaded_weight.ndim == 1:
556 chunk_weight = loaded_weight[sl]
557 elif split_dim == 0:
558 chunk_weight = loaded_weight[sl, :]
559 else:
560 chunk_weight = loaded_weight[:, sl]
561 yield (
562 name.replace(prefix, f"mlp.experts.{n_routed_experts + j}."),
563 chunk_weight,
564 )
565
566
567 def get_spec_layer_idx_from_weight_name(
568 config: "PreTrainedConfig", weight_name: str
569 ) -> int | None:
570 """Return the MTP layer index a weight belongs to, or None.
571
572 Args:
573 config: The model config; must expose `num_hidden_layers` and, for MTP
574 checkpoints, `num_nextn_predict_layers`.
575 weight_name: Checkpoint weight name to classify.
576
577 Returns:
578 The absolute layer index for an MTP-layer weight, else None.
579
580 """
581 if not (n := getattr(config, "num_nextn_predict_layers", 0)):
582 return None
583 base = config.num_hidden_layers
584 for i in range(n):
585 if weight_name.startswith(
586 (
587 f"model.layers.{base + i}.",
588 f"layers.{base + i}.",
589 f"model.language_model.layers.{base + i}.",
590 )
591 ):
592 return base + i
593 return None
594
595
596 def skip_spec_layers(
597 weights: Iterable[tuple[str, torch.Tensor]], config: "PreTrainedConfig"
598 ) -> Iterable[tuple[str, torch.Tensor]]:
599 """Drop MTP spec-layer weights (loaded by the MTP head, not the base model).
600
601 Args:
602 weights: Iterable of `(name, tensor)` checkpoint pairs.
603 config: The model config, passed to `get_spec_layer_idx_from_weight_name`.
604
605 Yields:
606 `(name, tensor)` pairs whose weight is not an MTP-layer weight.
607
608 """
609 return (
610 (name, w)
611 for name, w in weights
612 if get_spec_layer_idx_from_weight_name(config, name) is None
613 )
614
615
616 def init_vllm_registered_model(
617 vllm_config: VllmConfig,
618 *,
619 prefix: str = "",
620 hf_config: "PreTrainedConfig | None" = None,
621 architectures: list[str] | None = None,
622 ) -> nn.Module:
623 """Helper function to initialize an inner model registered to vLLM,
624 based on the arguments passed to the outer vLLM model.
625 """
626 from vllm.model_executor.model_loader.utils import initialize_model
627
628 if hf_config is None and architectures is not None:
629 # So that the architectures field is overridden
630 hf_config = vllm_config.model_config.hf_config
631
632 if hf_config is not None:
633 vllm_config = vllm_config.with_hf_config(hf_config, architectures=architectures)
634
635 return initialize_model(vllm_config=vllm_config, prefix=prefix)
636
637
638 @overload
639 def flatten_bn(x: list[torch.Tensor]) -> list[torch.Tensor]: ...
640
641
642 @overload
643 def flatten_bn(x: torch.Tensor) -> torch.Tensor: ...
644
645
646 @overload
647 def flatten_bn(
648 x: list[torch.Tensor] | torch.Tensor,
649 *,
650 concat: Literal[True],
651 ) -> torch.Tensor: ...
652
653
654 @overload
655 def flatten_bn(
656 x: list[torch.Tensor] | torch.Tensor,
657 *,
658 concat: bool = False,
659 ) -> list[torch.Tensor] | torch.Tensor: ...
660
661
662 def flatten_bn(
663 x: list[torch.Tensor] | torch.Tensor,
664 *,
665 concat: bool = False,
666 ) -> list[torch.Tensor] | torch.Tensor:
667 """Flatten the `B` and `N` dimensions of batched multimodal inputs.
668
669 The input tensor should have shape `(B, N, ...)`.
670 """
671 if isinstance(x, torch.Tensor):
672 return x.flatten(0, 1)
673
674 if concat:
675 return torch.cat(x)
676
677 return [x_n for x_b in x for x_n in x_b]
678
679
680 def _flatten_embeddings(embeddings: NestedTensors) -> torch.Tensor:
681 """Recursively flattens and concatenates NestedTensors on all but the last
682 dimension.
683 """
684 if isinstance(embeddings, torch.Tensor):
685 # Flatten all but the last dimension.
686 return embeddings.flatten(0, -2)
687
688 return torch.cat(tuple(_flatten_embeddings(t) for t in embeddings))
689
690
691 def _embedding_count_expression(embeddings: NestedTensors) -> str:
692 """Constructs a debugging representation of the number of embeddings in the
693 NestedTensors.
694 """
695 if isinstance(embeddings, torch.Tensor):
696 return " x ".join([str(dim) for dim in embeddings.shape[:-1]])
697
698 return " + ".join(_embedding_count_expression(inner) for inner in embeddings)
699
700
701 def split_list_into_ranges(lst: torch.Tensor, interval: int) -> list[list[int]]:
702 ranges: list[list[int]] = [[] for _ in range((max(lst) // interval) + 1)]
703 for num in lst:
704 index = num // interval
705 ranges[index].append(num)
706 return ranges
707
708
709 def _merge_multimodal_embeddings(
710 inputs_embeds: torch.Tensor,
711 multimodal_embeddings: NestedTensors,
712 is_multimodal: torch.Tensor,
713 ) -> torch.Tensor:
714 """Merge `multimodal_embeddings` into `inputs_embeds` by overwriting the
715 positions in `inputs_embeds` corresponding to placeholder tokens in
716 `input_ids`.
717
718 Note:
719 This updates `inputs_embeds` in place.
720
721 """
722 if len(multimodal_embeddings) == 0:
723 return inputs_embeds
724
725 mm_embeds_flat = _flatten_embeddings(multimodal_embeddings)
726 input_dtype = inputs_embeds.dtype
727
728 try:
729 # If is_multimodal is on CPU this avoids a D2H sync
730 inputs_embeds[is_multimodal] = mm_embeds_flat.to(dtype=input_dtype)
731 except RuntimeError as e:
732 num_actual_tokens = len(mm_embeds_flat)
733 num_expected_tokens = is_multimodal.sum().item()
734
735 if num_actual_tokens != num_expected_tokens:
736 expr = _embedding_count_expression(multimodal_embeddings)
737
738 raise ValueError(
739 f"Attempted to assign {expr} = {num_actual_tokens} "
740 f"multimodal tokens to {num_expected_tokens} placeholders"
741 ) from e
742
743 raise ValueError("Error during index put operation") from e
744
745 return inputs_embeds
746
747
748 class StageMissingLayer(nn.Module):
749 def __init__(self, stage_name: str, module: nn.Module | None = None) -> None:
750 super().__init__()
751
752 self.stage_name = stage_name
753
754 # Don't register this as a child module in order to
755 # avoid missing keys when loading weights
756 self.__dict__["module"] = module
757
758 def __getattr__(self, name: str):
759 return getattr(self.__dict__["module"], name)
760
761 def __call__(self, *args, **kwargs):
762 raise RuntimeError(f"{self} should not be called")
763
764 def extra_repr(self) -> str:
765 return f"stage_name={self.stage_name!r}"
766
767
768 @contextmanager
769 def collect_children(
770 module: nn.Module,
771 *,
772 targets: type[nn.Module] | tuple[type[nn.Module], ...] | None = None,
773 ):
774 """Within this context, collect all direct child assignments to `module`,
775 returning a list of children names that is internally updated until the
776 context is exited.
777
778 If `targets` is set, instead collect descendents of `module`
779 that are an instance of `targets`, even if they aren't direct children.
780 """
781 children_names = list[str]()
782
783 if targets is None:
784
785 def hook(module_: nn.Module, name: str, submodule: nn.Module):
786 if module_ is module:
787 children_names.append(name)
788
789 with register_module_module_registration_hook(hook):
790 yield children_names
791 else:
792 yield children_names
793
794 for name, module_ in module.named_modules():
795 if isinstance(module_, targets):
796 children_names.append(name)
797
798
799 @contextmanager
800 def no_init_weights(
801 module: nn.Module,
802 placeholder: Callable[[nn.Module], nn.Module],
803 *,
804 targets: type[nn.Module] | tuple[type[nn.Module], ...] | None = None,
805 ):
806 """Within this context, prevent weight initialization from using device memory and
807 replace direct child assignments to `module` with the result of `placeholder()`.
808
809 If `targets` is set, instead prevent weight initialization and
810 replace assignments where the child is an instance of `targets`,
811 even if they aren't direct children of `module`.
812 """
813 if targets is None:
814
815 def hook(module_: nn.Module, name: str, submodule: nn.Module):
816 if module_ is module:
817 return placeholder(submodule)
818
819 return submodule
820
821 with register_module_module_registration_hook(hook), torch.device("meta"):
822 yield
823 else:
824
825 def hook(module_: nn.Module, name: str, submodule: nn.Module):
826 if isinstance(module_, targets):
827 submodule.to("meta") # Free memory
828 if isinstance(submodule, targets):
829 submodule.to("meta") # Free memory
830 return placeholder(submodule)
831
832 return submodule
833
834 # Not all descendents are targeted, so we can't use a blanket
835 # `torch.device("meta")` context
836 with register_module_module_registration_hook(hook):
837 yield
838
839
840 class LayerFn(Protocol):
841 def __call__(self, prefix: str) -> torch.nn.Module: ...
842
843
844 class PPMissingLayer(torch.nn.Identity):
845 """A placeholder layer for missing layers in a pipeline parallel model."""
846
847 def __init__(self, *args, **kwargs):
848 super().__init__()
849
850 def forward(self, *args, **kwargs):
851 """Return the first arg from args or the first value from kwargs."""
852 return args[0] if args else next(iter(kwargs.values()))
853
854
855 def spec_decode_needs_target_embed(
856 vllm_config: VllmConfig, *, include_mtp: bool = False
857 ) -> bool:
858 """Whether the last PP rank needs the target input embedding.
859
860 MTP targets opt in when their drafter supports sharing this embedding.
861 """
862 from vllm.distributed.parallel_state import get_pp_group
863
864 speculative_config = vllm_config.speculative_config
865 if speculative_config is None:
866 return False
867 if speculative_config.method not in (
868 "eagle",
869 "eagle3",
870 "dflash",
871 "dspark",
872 ) and not (include_mtp and speculative_config.method == "mtp"):
873 return False
874 pp = get_pp_group()
875 return pp.world_size > 1 and pp.is_last_rank
876
877
878 def make_layers(
879 num_hidden_layers: int,
880 layer_fn: LayerFn,
881 prefix: str,
882 ) -> tuple[int, int, torch.nn.ModuleList]:
883 """Make a list of layers with the given layer function, taking
884 pipeline parallelism into account.
885
886 Args:
887 num_hidden_layers: Total number of hidden layers in the model.
888 layer_fn: Function to create a layer given its index.
889 prefix: Prefix for layer names.
890
891 Returns:
892 Tuple of (start_layer, end_layer, modules).
893
894 """
895 from vllm.distributed.parallel_state import get_pp_group
896 from vllm.distributed.utils import get_pp_indices
897 from vllm.model_executor.offloader import get_offloader
898
899 start_layer, end_layer = get_pp_indices(
900 num_hidden_layers, get_pp_group().rank_in_group, get_pp_group().world_size
901 )
902
903 modules = torch.nn.ModuleList(
904 [PPMissingLayer() for _ in range(start_layer)]
905 + get_offloader().wrap_modules(
906 layer_fn(prefix=f"{prefix}.{idx}") for idx in range(start_layer, end_layer)
907 )
908 + [PPMissingLayer() for _ in range(end_layer, num_hidden_layers)]
909 )
910
911 return start_layer, end_layer, modules
912
913
914 # NOTE: don't use lru_cache here because it can prevent garbage collection
915 _model_to_pp_missing_layer_names: dict[int, list[str]] = {}
916
917
918 def get_pp_missing_layer_names(model: torch.nn.Module) -> list[str]:
919 """Get the names of the missing layers in a pipeline parallel model."""
920 model_id = id(model)
921 if model_id in _model_to_pp_missing_layer_names:
922 return _model_to_pp_missing_layer_names[model_id]
923
924 missing_layer_names = []
925 for name, module in model.named_modules():
926 if isinstance(module, (StageMissingLayer, PPMissingLayer)):
927 # NOTE: the trailing dot is used to match the prefix of the layer.
928 # without the dot, we could match a layer that is not missing,
929 # e.g., 'encoder.layer.1' would match 'encoder.layer.11'
930 missing_layer_names.append(name + ".")
931 _model_to_pp_missing_layer_names[model_id] = missing_layer_names
932
933 return missing_layer_names
934
935
936 def is_pp_missing_parameter(name: str, model: torch.nn.Module) -> bool:
937 """Check if a parameter is missing in a pipeline parallel model."""
938 if isinstance(model, (StageMissingLayer, PPMissingLayer)):
939 return True
940
941 return any(
942 name.startswith(missing_layer_name)
943 for missing_layer_name in get_pp_missing_layer_names(model)
944 )
945
946
947 def make_empty_intermediate_tensors_factory(keys: list[str], hidden_size: int):
948 def make_empty_intermediate_tensors(
949 batch_size: int,
950 dtype: torch.dtype,
951 device: torch.device,
952 ) -> IntermediateTensors:
953 return IntermediateTensors(
954 {
955 key: torch.zeros((batch_size, hidden_size), dtype=dtype, device=device)
956 for key in keys
957 }
958 )
959
960 return make_empty_intermediate_tensors
961
962
963 def maybe_prefix(prefix: str, name: str) -> str:
964 """Add a prefix to a name if the prefix is non-empty.
965
966 Args:
967 prefix: The prefix to add. If empty, no prefix will be added.
968 name: The name to potentially prefix.
969
970 Returns:
971 The string "prefix.name" if prefix was non-empty, otherwise just "name".
972
973 """
974 return name if not prefix else f"{prefix}.{name}"
975
976
977 def get_draft_quant_config(vllm_config: VllmConfig) -> "QuantizationConfig | None":
978 """Get quantization config for Draft models.
979
980 Draft models should use their own quantization config instead of the verifier/target
981 model's config. This helper retrieves the draft model's quantization config.
982
983 Args:
984 vllm_config: The vLLM configuration object.
985
986 Returns:
987 The draft model's config if available, None otherwise.
988
989 """
990 speculative_config = vllm_config.speculative_config
991 assert speculative_config is not None
992 draft_model_config = speculative_config.draft_model_config
993 draft_load_config = vllm_config.load_config
994
995 return (
996 VllmConfig.get_quantization_config(draft_model_config, draft_load_config)
997 if draft_model_config
998 else None
999 )
1000
1001
1002 def extract_layer_index(layer_name: str, num_attn_module: int = 1) -> int:
1003 """Extract the layer index from the module name.
1004
1005 Examples:
1006 - "encoder.layers.0" -> 0
1007 - "encoder.layers.1.self_attn" -> 1
1008 - "2.self_attn" -> 2
1009 - "model.encoder.layers.0.sub.1" -> ValueError if num_attn_module == 1
1010
1011 """
1012 subnames = layer_name.split(".")
1013 int_vals: list[int] = []
1014 for subname in subnames:
1015 try:
1016 int_vals.append(int(subname))
1017 except ValueError:
1018 continue
1019 if num_attn_module == 1 or "attn" not in layer_name:
1020 assert len(int_vals) == 1, (
1021 f"layer name {layer_name} should only contain one integer"
1022 )
1023
1024 return int_vals[0]
1025 else:
1026 assert len(int_vals) <= 2, (
1027 f"layer name {layer_name} should contain most two integers"
1028 )
1029 layer_index = (
1030 int_vals[0] * num_attn_module + int_vals[1]
1031 if len(int_vals) == 2
1032 else int_vals[0]
1033 )
1034 return layer_index
1035
1036
1037 def cast_overflow_tensors(tensors: torch.Tensor, offset: float = 1000) -> torch.Tensor:
1038 clamp_value = torch.finfo(tensors.dtype).max - offset
1039 return torch.clamp(tensors, min=-clamp_value, max=clamp_value)
1040
1041
1042 def fast_topk(
1043 values: torch.Tensor, topk: int, dim: int
1044 ) -> tuple[torch.Tensor, torch.Tensor]:
1045 """Optimized topk implementation that uses torch.max for k=1 case.
1046
1047 This function provides better performance for the common case of k=1
1048 by using torch.max instead of the more general torch.topk.
1049
1050 Args:
1051 values: Input tensor to find top-k values from
1052 topk: Number of top values to return (k). Must be > 0.
1053 dim: Dimension along which to compute topk
1054
1055 Returns:
1056 Tuple of (values, indices) where values are the top-k values
1057 and indices are their corresponding indices in the input tensor
1058
1059 """
1060 if topk == 1:
1061 # Use max along the specified dimension to get both value and index
1062 return torch.max(values, dim=dim, keepdim=True)
1063 else:
1064 # Use topk for efficiency with larger k values
1065 return torch.topk(values, topk, dim=dim)
1066
1067
1068 # Chunk x along the num_tokens axis for sequence parallelism
1069 # NOTE: This is wrapped in a torch custom op to work around the following issue:
1070 # The output tensor can have a sequence length 0 at small input sequence lengths
1071 # even though we explicitly pad to avoid this.
1072 def sequence_parallel_chunk(x: torch.Tensor) -> torch.Tensor:
1073 return torch.ops.vllm.sequence_parallel_chunk_impl(x)
1074
1075
1076 def sequence_parallel_chunk_impl(x: torch.Tensor) -> torch.Tensor:
1077 tp_size = get_tensor_model_parallel_world_size()
1078 tp_rank = get_tensor_model_parallel_rank()
1079
1080 # all_gather needs the sequence length to be divisible by tp_size
1081 seq_len = x.size(0)
1082 remainder = seq_len % tp_size
1083 if remainder != 0:
1084 pad_len = tp_size - remainder
1085 y = nn.functional.pad(x, (0, 0, 0, pad_len))
1086 else:
1087 y = x
1088
1089 chunk = y.shape[0] // tp_size
1090 start = tp_rank * chunk
1091 out = torch.narrow(y, 0, start, chunk)
1092 # narrow() returns a view; clone when it aliases the input (no-pad case),
1093 # since a functional custom op must not return a view of an input.
1094 return out.clone() if y is x else out
1095
1096
1097 def sequence_parallel_chunk_impl_fake(x: torch.Tensor) -> torch.Tensor:
1098 tp_size = get_tensor_model_parallel_world_size()
1099 seq_len = cdiv(x.size(0), tp_size)
1100 shape = list(x.shape)
1101 shape[0] = seq_len
1102 out = torch.empty(shape, dtype=x.dtype, device=x.device)
1103 return out
1104
1105
1106 direct_register_custom_op(
1107 op_name="sequence_parallel_chunk_impl",
1108 op_func=sequence_parallel_chunk_impl,
1109 fake_impl=sequence_parallel_chunk_impl_fake,
1110 tags=(torch.Tag.needs_fixed_stride_order,),
1111 )
1112
1113
1114 def process_eagle_weight(
1115 model: nn.Module,
1116 name: str,
1117 ) -> None:
1118 """Update EAGLE model flags based on loaded weight name.
1119 This should be called during weight loading to detect if a model
1120 has its own lm_head or embed_tokens weight.
1121
1122 Args:
1123 model: The model instance (must support EAGLE)
1124 name: The name of the weight to process
1125
1126 """
1127 if not supports_any_eagle(model):
1128 return
1129
1130 # To prevent overriding with target model's layers
1131 if "lm_head" in name:
1132 model.has_own_lm_head = True
1133 if "embed_tokens" in name:
1134 model.has_own_embed_tokens = True
1135
1136
1137 def get_layer_index(feature_layer_index: int, num_hidden_layers: int) -> int:
1138 """Given a signed vision feature layer, get the number of hidden layers
1139 needed to leverage it.
1140
1141 Args:
1142 feature_layer_index: Index of a required layer in the visual encoder.
1143 num_hidden_layers: The total number of hidden layers in the visual encoder.
1144
1145 """
1146 if feature_layer_index < 0:
1147 return num_hidden_layers + feature_layer_index + 1
1148 return feature_layer_index
1149
1150
1151 def scatter_output_slices(
1152 output: torch.Tensor,
1153 indices: list[int],
1154 per_item_out_tokens: list[int],
1155 dest: dict[int, torch.Tensor] | list[torch.Tensor | None],
1156 clone: bool = False,
1157 ) -> None:
1158 """Slice a concatenated output tensor and scatter into dest by index."""
1159 offset = 0
1160 for idx in indices:
1161 n_tok = per_item_out_tokens[idx]
1162 sliced = output[offset : offset + n_tok]
1163 dest[idx] = sliced.clone() if clone else sliced
1164 offset += n_tok
1165
1166
1167 def parse_diarized_timestamp(marker: str) -> float | None:
1168 if (
1169 not marker
1170 or not marker.isascii()
1171 or marker.count(".") > 1
1172 or not marker.replace(".", "").isdigit()
1173 ):
1174 return None
1175 return float(marker)
1176
1177
1178 def parse_diarized_speaker(speaker: str) -> str | None:
1179 if (
1180 len(speaker) < 2
1181 or speaker[0] != "S"
1182 or not speaker[1:].isascii()
1183 or not speaker[1:].isdigit()
1184 ):
1185 return None
1186 return speaker