[Mypy] Fix mypy typing for Transformers models (#54320)

Signed-off-by: Taneem Ibrahim <taneem.ibrahim@gmail.com> Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com> Co-authored-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>

Taneem Ibrahim committed Sep 17, 2026 at 11:11 UTC e960ead0aa70362a57f205be831083b68c7a5bdb
16 files changed +224 -145
tests/models/transformers/fusers/test_mla.py
+2 -4
@@ -53,7 +53,6 @@ def _match(q_lora_rank: int | None) -> MLAFuser | None:
53 def test_discovers_modules_without_q_lora():
54 fuser = _match(q_lora_rank=None)
55 assert isinstance(fuser, MLAFuser)
56 - assert not fuser.has_q_lora
56 assert fuser.q_proj_name == "q_proj"
57 assert fuser.kv_a_proj_name == "kv_a_proj_with_mqa"
58 assert fuser.kv_a_layernorm_name == "kv_a_layernorm"
@@ -68,7 +67,6 @@ def test_discovers_modules_without_q_lora():
67 def test_discovers_modules_with_q_lora():
68 fuser = _match(q_lora_rank=64)
69 assert isinstance(fuser, MLAFuser)
71 - assert fuser.has_q_lora
70 assert fuser.q_a_proj_name == "q_a_proj"
71 assert fuser.q_a_layernorm_name == "q_a_layernorm"
72 assert fuser.q_b_proj_name == "q_b_proj"
@@ -107,7 +105,7 @@ def test_update_forward_rewrites_real_attention(q_lora_rank):
105 assert isinstance(fuser, MLAFuser)
106 fuser.update_forward(_attention(q_lora_rank))
107 names = set(fuser.fused_forward.__code__.co_names)
110 - if fuser.has_q_lora:
108 + if fuser.q_a_proj_name is not None:
109 assert _FUSED_QKV_A_PROJ in names
110 assert not {"q_a_proj", "kv_a_proj_with_mqa"} & names
111
@@ -158,7 +156,7 @@ def test_discovers_modules_under_arbitrary_names():
156 module = RenamedMLA()
157 fuser = MLAFuser.match(trace(module), module)
158 assert isinstance(fuser, MLAFuser)
161 - assert not fuser.has_q_lora
159 + assert fuser.q_a_proj_name is None
160 assert fuser.q_proj_name == "alpha"
161 assert fuser.kv_a_proj_name == "beta"
162 assert fuser.kv_a_layernorm_name == "gamma"
tools/pre_commit/mypy.py
-1
@@ -116,7 +116,6 @@ EXCLUDE = [
116 r"vllm/model_executor/models/[nN]",
117 r"vllm/model_executor/models/[oO]",
118 r"vllm/model_executor/models/[qQ]",
119 - r"vllm/model_executor/models/transformers",
119 r"vllm/model_executor/models/[uU]",
120 r"vllm/model_executor/models/[vV]",
121 r"vllm/model_executor/models/[wW]",
vllm/compilation/decorators.py
+8 -5
@@ -7,7 +7,7 @@ import inspect
7 import os
8 import sys
9 from collections.abc import Callable, Generator
10 -from typing import TYPE_CHECKING, Any, TypeVar, overload
10 +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar, overload
11 from unittest.mock import patch
12
13 import torch
@@ -32,6 +32,9 @@ from vllm.utils.torch_utils import is_torch_equal_or_newer
32
33 from .monitor import monitor_profiling_run, monitor_torch_compile
34
35 +DynamicArgDims: TypeAlias = dict[str, int | list[int] | dict[int, str]]
36 +"""Argument name -> the dynamic dimension(s) of that argument."""
37 +
38 # shape_id parameter was added to mark_unbacked in PyTorch 2.11.0
39 _SUPPORTS_SHAPE_ID = is_torch_equal_or_newer("2.11.0")
40
@@ -89,7 +92,7 @@ def support_torch_compile(
92 @overload
93 def support_torch_compile(
94 *,
92 - dynamic_arg_dims: dict[str, int | list[int] | dict[int, str]] | None,
95 + dynamic_arg_dims: DynamicArgDims | None,
96 ) -> Callable[[type[_T]], type[_T]]: ...
97
98
@@ -103,7 +106,7 @@ def support_torch_compile(
106 @overload
107 def support_torch_compile(
108 *,
106 - dynamic_arg_dims: dict[str, int | list[int] | dict[int, str]] | None = None,
109 + dynamic_arg_dims: DynamicArgDims | None = None,
110 mark_unbacked_dims: dict[str, int | list[int]] | None = None,
111 enable_if: Callable[[VllmConfig], bool] | None = None,
112 is_encoder: bool = False,
@@ -117,7 +120,7 @@ def support_torch_compile(cls: type[_T]) -> type[_T]: ...
120 def support_torch_compile(
121 cls: type[_T] | None = None,
122 *,
120 - dynamic_arg_dims: dict[str, int | list[int] | dict[int, str]] | None = None,
123 + dynamic_arg_dims: DynamicArgDims | None = None,
124 mark_unbacked_dims: dict[str, int | list[int]] | None = None,
125 enable_if: Callable[[VllmConfig], bool] | None = None,
126 is_encoder: bool = False,
@@ -328,7 +331,7 @@ def _try_load_aot_compiled_fn(
331
332 def _support_torch_compile(
333 cls: type[_T],
331 - dynamic_arg_dims: dict[str, int | list[int] | dict[int, str]],
334 + dynamic_arg_dims: DynamicArgDims,
335 mark_unbacked_dims: dict[str, int | list[int]] | None = None,
336 enable_if: Callable[[VllmConfig], bool] | None = None,
337 is_encoder: bool = False,
vllm/model_executor/models/transformers/__init__.py
+11 -13
@@ -21,7 +21,7 @@ from typing import TYPE_CHECKING
21 import torch.nn.functional as F
22 from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
23
24 -from vllm.model_executor.models.transformers.base import VLLM_ATTN_ATTR, Base
24 +from vllm.model_executor.models.transformers.base import VLLM_ATTN_ATTR
25 from vllm.model_executor.models.transformers.causal import CausalMixin
26 from vllm.model_executor.models.transformers.legacy import LegacyMixin
27 from vllm.model_executor.models.transformers.moe import MoEMixin
@@ -126,10 +126,10 @@ ALL_ATTENTION_FUNCTIONS.register("vllm_mla", vllm_mla_attention_forward)
126
127
128 # Text only models
129 -class TransformersForCausalLM(CausalMixin, Base): ...
129 +class TransformersForCausalLM(CausalMixin): ...
130
131
132 -class TransformersMoEForCausalLM(MoEMixin, CausalMixin, Base): ...
132 +class TransformersMoEForCausalLM(MoEMixin, CausalMixin): ...
133
134
135 # Multimodal models
@@ -138,7 +138,7 @@ class TransformersMoEForCausalLM(MoEMixin, CausalMixin, Base): ...
138 info=MultiModalProcessingInfo,
139 dummy_inputs=MultiModalDummyInputsBuilder,
140 )
141 -class TransformersMultiModalForCausalLM(MultiModalMixin, CausalMixin, Base): ...
141 +class TransformersMultiModalForCausalLM(MultiModalMixin, CausalMixin): ...
142
143
144 @MULTIMODAL_REGISTRY.register_processor(
@@ -146,16 +146,14 @@ class TransformersMultiModalForCausalLM(MultiModalMixin, CausalMixin, Base): ...
146 info=MultiModalProcessingInfo,
147 dummy_inputs=MultiModalDummyInputsBuilder,
148 )
149 -class TransformersMultiModalMoEForCausalLM(
150 - MoEMixin, MultiModalMixin, CausalMixin, Base
151 -): ...
149 +class TransformersMultiModalMoEForCausalLM(MoEMixin, MultiModalMixin, CausalMixin): ...
150
151
152 # Embedding models
155 -class TransformersEmbeddingModel(EmbeddingMixin, LegacyMixin, Base): ...
153 +class TransformersEmbeddingModel(EmbeddingMixin, LegacyMixin): ...
154
155
158 -class TransformersMoEEmbeddingModel(EmbeddingMixin, MoEMixin, Base): ...
156 +class TransformersMoEEmbeddingModel(EmbeddingMixin, MoEMixin): ...
157
158
159 @MULTIMODAL_REGISTRY.register_processor(
@@ -163,17 +161,17 @@ class TransformersMoEEmbeddingModel(EmbeddingMixin, MoEMixin, Base): ...
161 info=MultiModalProcessingInfo,
162 dummy_inputs=MultiModalDummyInputsBuilder,
163 )
166 -class TransformersMultiModalEmbeddingModel(EmbeddingMixin, MultiModalMixin, Base): ...
164 +class TransformersMultiModalEmbeddingModel(EmbeddingMixin, MultiModalMixin): ...
165
166
167 # Sequence classification models
168 class TransformersForSequenceClassification(
171 - SequenceClassificationMixin, LegacyMixin, Base
169 + SequenceClassificationMixin, LegacyMixin
170 ): ...
171
172
173 class TransformersMoEForSequenceClassification(
176 - SequenceClassificationMixin, MoEMixin, Base
174 + SequenceClassificationMixin, MoEMixin
175 ): ...
176
177
@@ -183,7 +181,7 @@ class TransformersMoEForSequenceClassification(
181 dummy_inputs=MultiModalDummyInputsBuilder,
182 )
183 class TransformersMultiModalForSequenceClassification(
186 - SequenceClassificationMixin, MultiModalMixin, Base
184 + SequenceClassificationMixin, MultiModalMixin
185 ): ...
186
187
vllm/model_executor/models/transformers/base.py
+29 -15
@@ -35,7 +35,7 @@ from transformers.conversion_mapping import (
35 get_model_conversion_mapping,
36 )
37
38 -from vllm.compilation.decorators import support_torch_compile
38 +from vllm.compilation.decorators import DynamicArgDims, support_torch_compile
39 from vllm.config.utils import getattr_iter
40 from vllm.distributed import get_pp_group, get_tp_group
41 from vllm.distributed.utils import get_pp_indices
@@ -76,6 +76,7 @@ from vllm.model_executor.models.transformers.utils import (
76 from vllm.model_executor.models.utils import (
77 AutoWeightsLoader,
78 PPMissingLayer,
79 + ShardId,
80 WeightsMapper,
81 make_empty_intermediate_tensors_factory,
82 maybe_prefix,
@@ -110,10 +111,13 @@ class Base(
111 SupportsEagle,
112 SupportsEagle3,
113 ):
113 - embedding_modules = ["embed_tokens"] # TODO transformers will have a util to get it
114 + hf_to_vllm_mapper: WeightsMapper
115 +
116 + # TODO transformers will have a util to get these
117 + embedding_modules = {"embed_tokens": "input_embeddings"}
118
119 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
116 - super().__init__()
120 + nn.Module.__init__(self)
121 logger.info("Using Transformers modeling backend.")
122
123 self.vllm_config = vllm_config
@@ -248,7 +252,7 @@ class Base(
252 def _decorate_cls_for_torch_compile(
253 self,
254 cls: type["PreTrainedModel"],
251 - dynamic_arg_dims: dict[str, int] | None,
255 + dynamic_arg_dims: DynamicArgDims | None,
256 enable_if: Callable[["VllmConfig"], bool],
257 is_encoder: bool,
258 ):
@@ -284,7 +288,7 @@ class Base(
288 self._decorate_cls_for_torch_compile(
289 cls=self._pre_trained_model_classes.decoder,
290 # Applied to a PreTrainedModel so the batch dimension will exist
287 - dynamic_arg_dims=dict[str, int](
291 + dynamic_arg_dims=DynamicArgDims(
292 input_ids=1, # shape: [1, seq_len]
293 inputs_embeds=1, # shape: [1, seq_len, hidden_size]
294 position_ids=-1, # shape: [1, seq_len] or [3, 1, seq_len] for mrope
@@ -303,9 +307,8 @@ class Base(
307 - Checkpoints saved with no base model prefix
308 - Any quantization config specific mappings
309 """
306 - self.hf_to_vllm_mapper = WeightsMapper()
307 - orig_to_new_renaming = self.hf_to_vllm_mapper.orig_to_new_renaming
308 - orig_to_new_regex = self.hf_to_vllm_mapper.orig_to_new_regex
310 + orig_to_new_renaming: list[WeightRenaming] = []
311 + orig_to_new_regex: dict[re.Pattern, str | None] = {}
312
313 for mapping in get_model_conversion_mapping(self.model):
314 # Handle weights which have been renamed in Transformers
@@ -341,6 +344,11 @@ class Base(
344 nested_lm_head_pattern = re.compile(r"^model\.(.+\.)*(lm_head.+)")
345 orig_to_new_regex[nested_lm_head_pattern] = r"\2"
346
347 + self.hf_to_vllm_mapper = WeightsMapper(
348 + orig_to_new_renaming=orig_to_new_renaming,
349 + orig_to_new_regex=orig_to_new_regex,
350 + )
351 +
352 # Apply mapping to quantization config if needed
353 self._maybe_apply_model_mapping()
354
@@ -371,8 +379,8 @@ class Base(
379 "modeling backend will infer the split from the layers of %s in order "
380 "of declaration and keep parameter-free modules on every rank. This "
381 "may fail if the model's structure is non-standard. %s",
374 - type(self.model),
375 - type(module),
382 + type(self.model).__name__,
383 + type(module).__name__,
384 tip,
385 )
386
@@ -470,7 +478,7 @@ class Base(
478 "backend will shard the model the best it can during graph fusion and "
479 "replicate the rest. This may be suboptimal or fail if the model does "
480 "not fuse cleanly. %s",
473 - type(self.model),
481 + type(self.model).__name__,
482 tip,
483 )
484
@@ -481,6 +489,8 @@ class Base(
489
490 vocab_embeddings = self._vocab_embeddings()
491
492 + orig_to_new_stacked: dict[str, tuple[str, ShardId]] = {}
493 +
494 def register_fusion(fuser: BaseFuser, prefix: str, module: nn.Module):
495 """Register a fused layer's mappings just before it is built."""
496 self.fusers.setdefault(prefix, []).append(fuser)
@@ -498,8 +508,7 @@ class Base(
508 )
509 self.attention_fusers[index] = (prefix, fuser)
510
501 - orig_to_new_stacked = fuser.orig_to_new_stacked(prefix)
502 - self.hf_to_vllm_mapper.orig_to_new_stacked.update(orig_to_new_stacked)
511 + orig_to_new_stacked.update(fuser.orig_to_new_stacked(prefix))
512
513 packed_modules_mapping = fuser.packed_modules_mapping
514 self.packed_modules_mapping.update(packed_modules_mapping)
@@ -564,6 +573,8 @@ class Base(
573
574 _recursive_replace(self.model, prefix="model")
575
576 + self.hf_to_vllm_mapper |= WeightsMapper(orig_to_new_stacked=orig_to_new_stacked)
577 +
578 def _create_attention_instances(self):
579 """Create `Attention` instances to inform KV cache allocation."""
580 text_config = self.text_config
@@ -589,8 +600,11 @@ class Base(
600
601 for i in range(start, end):
602 if i not in self.attention_fusers:
592 - in_range = layer_types and i < len(layer_types)
593 - layer = f"{i} ({layer_types[i]})" if in_range else str(i)
603 + layer = (
604 + f"{i} ({layer_types[i]})"
605 + if layer_types and i < len(layer_types)
606 + else str(i)
607 + )
608 raise ValueError(
609 f"Layer {layer} does not dispatch through the Transformers "
610 "attention interface and vLLM has no other way to handle it."
vllm/model_executor/models/transformers/causal.py
+3 -1
@@ -27,13 +27,15 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
27 from vllm.model_executor.models.interfaces_base import VllmModelForTextGeneration
28 from vllm.model_executor.models.utils import PPMissingLayer, maybe_prefix
29
30 +from .base import Base
31 +
32 if TYPE_CHECKING:
33 import torch
34
35 from vllm.config import VllmConfig
36
37
36 -class CausalMixin(VllmModelForTextGeneration):
38 +class CausalMixin(VllmModelForTextGeneration, Base):
39 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
40 # Skip VllmModelForTextGeneration.__init__ and call the next class in MRO
41 super(VllmModelForTextGeneration, self).__init__(
vllm/model_executor/models/transformers/fusers/attention.py
+7 -5
@@ -5,6 +5,7 @@
5 import ast
6 import inspect
7 import textwrap
8 +from collections.abc import Callable
9 from dataclasses import dataclass
10 from functools import cache
11 from typing import TYPE_CHECKING, ClassVar
@@ -36,16 +37,16 @@ def _is_interface_lookup(node: ast.expr | None) -> bool:
37
38
39 @cache
39 -def interface_call(cls: type[nn.Module]) -> ast.Call | None:
40 - """The attention interface call in `cls.forward`, if it makes exactly one."""
40 +def interface_call(forward: Callable) -> ast.Call | None:
41 + """The attention interface call in `forward`, if it makes exactly one."""
42 try:
42 - source = inspect.getsource(inspect.unwrap(cls.forward))
43 + source = inspect.getsource(inspect.unwrap(forward))
44 tree = ast.parse(textwrap.dedent(source))
45 except (AttributeError, OSError, SyntaxError, TypeError):
46 return None
47
48 # Find the names of the local variables that read from the interface lookup
48 - names = set()
49 + names: set[str] = set()
50 for node in ast.walk(tree):
51 if not isinstance(node, (ast.Assign, ast.AnnAssign)):
52 continue
@@ -97,7 +98,7 @@ class AttentionFuser(BaseFuser):
98 def match(
99 cls, graph: fx.Graph | None, module: nn.Module
100 ) -> "AttentionFuser | None":
100 - if (call := interface_call(type(module))) is None:
101 + if (call := interface_call(type(module).forward)) is None:
102 return None
103 scaling = [kw.value for kw in call.keywords if kw.arg == "scaling"]
104 scale_expr = scaling[0] if len(scaling) == 1 else None
@@ -120,6 +121,7 @@ class AttentionFuser(BaseFuser):
121 self, module: nn.Module, prefix: str, vllm_config: "VllmConfig"
122 ) -> nn.Module:
123 if (sinks := self.sinks(module)) is not None:
124 + assert isinstance(self.s_aux_expr, ast.Attribute)
125 size = sinks.numel() // vllm_config.parallel_config.tensor_parallel_size
126 device = vllm_config.device_config.device
127 data = torch.empty(size, dtype=sinks.dtype, device=device)
vllm/model_executor/models/transformers/fusers/base.py
+5 -2
@@ -130,11 +130,14 @@ class StackedFuser(RewriteFuser):
130 """A fuser that merges sibling projections into one stacked linear and
131 rewrites the forward to call it."""
132
133 - merged_name: ClassVar[str]
134 - """Attribute name of the merged module created by `update_attrs`."""
133 merged_cls_name: ClassVar[str]
134 """Name of the vLLM class the merged projection becomes (for logging)."""
135
136 + @property
137 + @abstractmethod
138 + def merged_name(self) -> str:
139 + """Attribute name of the merged module created by `update_attrs`."""
140 +
141 def info(self, name: str) -> str:
142 sources = " + ".join(shard for shard, _ in self.shards)
143 return (
vllm/model_executor/models/transformers/fusers/mla.py
+18 -18
@@ -24,6 +24,7 @@ from vllm.model_executor.models.transformers.fx_utils import (
24 upstream_linear,
25 )
26 from vllm.model_executor.models.transformers.utils import (
27 + Style,
28 log_replacement,
29 replace_linear_class,
30 )
@@ -128,30 +129,26 @@ class MLAFuser(StackedFuser):
129 merged_name: ClassVar[str] = "fused_qkv_a_proj"
130 merged_cls_name: ClassVar[str] = "MergedColumnParallelLinear"
131
131 - @property
132 - def has_q_lora(self) -> bool:
133 - return self.q_a_proj_name is not None
134 -
132 def info(self, name: str) -> str:
133 info_str = (
134 f"Fused: {name} ({self.source_cls}) -> MLAAttention (attention interface)"
135 )
139 - if self.has_q_lora:
136 + if self.q_a_proj_name is not None:
137 info_str += "; " + super().info(name).removeprefix("Fused: ")
138 return info_str
139
140 @property
141 def shards(self) -> list[tuple[str, ShardId]]:
142 """`q_a_proj` and `kv_a_proj_with_mqa` stack into one down-projection."""
146 - if self.has_q_lora:
147 - return [(self.q_a_proj_name, 0), (self.kv_a_proj_name, 1)]
148 - return []
143 + if self.q_a_proj_name is None:
144 + return []
145 + return [(self.q_a_proj_name, 0), (self.kv_a_proj_name, 1)]
146
147 @property
148 def packed_modules_mapping(self) -> dict[str, list[str]]:
152 - if self.has_q_lora:
153 - return super().packed_modules_mapping
154 - return {}
149 + if self.q_a_proj_name is None:
150 + return {}
151 + return super().packed_modules_mapping
152
153 @classmethod
154 def match(cls, graph: fx.Graph, module: nn.Module) -> "MLAFuser | None":
@@ -234,10 +231,10 @@ class MLAFuser(StackedFuser):
231 Bypass the KV expansion method so the compressed latent reaches the `vllm_mla`
232 attention interface unexpanded."""
233 funcdef, fn = recover_forward(type(module))
237 - if self.has_q_lora:
234 + if (q_a_proj_name := self.q_a_proj_name) is not None:
235 # q_a_proj is usually inside the `else` of `if self.q_lora_rank is None`.
236 # The fused call is inserted at the top-level statement preceding both.
240 - names = [self.q_a_proj_name, self.kv_a_proj_name]
237 + names = [q_a_proj_name, self.kv_a_proj_name]
238 calls = self._unguarded_calls(funcdef, names)
239 if ast.dump(calls[0].args[0]) != ast.dump(calls[1].args[0]):
240 raise ValueError("down-projections read different inputs")
@@ -258,7 +255,7 @@ class MLAFuser(StackedFuser):
255 def update_attrs(self, module: nn.Module, prefix: str, vllm_config: "VllmConfig"):
256 quant_config = vllm_config.quant_config
257
261 - def replace_linear_by_name(name: str, style: str):
258 + def replace_linear_by_name(name: str, style: Style):
259 linear = module.get_submodule(name)
260 replacement = replace_linear_class(
261 linear, style, quant_config, prefix=maybe_prefix(prefix, name)
@@ -266,8 +263,10 @@ class MLAFuser(StackedFuser):
263 setattr(module, name, replacement)
264 log_replacement(maybe_prefix(prefix, name), linear, replacement)
265
269 - if self.has_q_lora:
270 - q_a = module.get_submodule(self.q_a_proj_name)
266 + if (q_a_proj_name := self.q_a_proj_name) is not None:
267 + # `match` sets the q-LoRA names together, or none of them
268 + assert self.q_b_proj_name is not None
269 + q_a = module.get_submodule(q_a_proj_name)
270 kv_a = module.get_submodule(self.kv_a_proj_name)
271 merged = MergedColumnParallelLinear(
272 input_size=q_a.in_features,
@@ -280,7 +279,7 @@ class MLAFuser(StackedFuser):
279 )
280 logger.debug(
281 "%s: %s, %s: %s -> %s: %s",
283 - self.q_a_proj_name,
282 + q_a_proj_name,
283 q_a,
284 self.kv_a_proj_name,
285 kv_a,
@@ -289,10 +288,11 @@ class MLAFuser(StackedFuser):
288 )
289 setattr(module, self.merged_name, merged)
290 # The rewritten forward calls the merged projection instead.
292 - delattr(module, self.q_a_proj_name)
291 + delattr(module, q_a_proj_name)
292 delattr(module, self.kv_a_proj_name)
293 replace_linear_by_name(self.q_b_proj_name, "colwise")
294 else:
295 + assert self.q_proj_name is not None
296 replace_linear_by_name(self.kv_a_proj_name, "replicate")
297 replace_linear_by_name(self.q_proj_name, "colwise")
298
vllm/model_executor/models/transformers/fusers/rms_norm.py
+6 -6
@@ -216,7 +216,7 @@ class RMSNormFuser(BaseFuser):
216 finally:
217 for name, value in candidates.items():
218 setattr(module, name, value)
219 - if (name := markers.get(marked)) is not None:
219 + if marked is not None and (name := markers.get(marked)) is not None:
220 return name, None
221 logger.debug_once(
222 "%s does not hold its eps (%s) in an attribute. Every instance in this "
@@ -251,18 +251,18 @@ class RMSNormFuser(BaseFuser):
251 ) -> nn.Module:
252 """Fuse the matched RMSNorm pattern into a vLLM fused RMSNorm CustomOp."""
253 weight = getattr(module, "weight", None)
254 - has_weight = weight is not None
255 - hidden_size = weight.size(0) if has_weight else 0
254 + hidden_size = weight.size(0) if weight is not None else 0
255 eps = getattr(module, self.eps_attr, None) if self.eps_attr else self.eps
256 if not isinstance(eps, (int, float)):
257 # If eps was not detected, match torch behaviour.
259 - dtype = weight.dtype if has_weight else vllm_config.model_config.dtype
258 + config_dtype = vllm_config.model_config.dtype
259 + dtype = weight.dtype if weight is not None else config_dtype
260 eps = torch.finfo(dtype).eps
261 if self.zero_centered:
262 return TPAwareGemmaRMSNorm(hidden_size=hidden_size, eps=eps)
263 return TPAwareRMSNorm(
264 hidden_size=hidden_size,
265 eps=eps,
266 - has_weight=has_weight,
267 - dtype=weight.dtype if has_weight else None,
266 + has_weight=weight is not None,
267 + dtype=weight.dtype if weight is not None else None,
268 )
vllm/model_executor/models/transformers/fx_utils.py
+14 -11
@@ -15,17 +15,19 @@ import operator
15 import textwrap
16 from collections.abc import Callable
17 from itertools import chain
18 +from typing import Any
19 from unittest import mock
20
21 import torch
22 from torch import fx, nn
23 from torch.nn import functional as F
24 +from typing_extensions import TypeIs
25
26 from vllm.logger import init_logger
27
28 logger = init_logger(__name__)
29
28 -_UNKNOWN = object()
30 +_UNKNOWN: Any = object()
31 """Sentinel meta value for proxies whose concrete value could not be inferred.
32 Distinct from `None`, which is a valid concrete value (e.g. `attn_weights`)."""
33
@@ -34,7 +36,7 @@ _MODULE_CALL = nn.Module.__call__
36 `call_module` nodes; meta execution must call modules for real."""
37
38
37 -def is_leaf_call(node: object) -> bool:
39 +def is_leaf_call(node: object) -> TypeIs[fx.Node]:
40 """Is node a call recorded by `_as_leaf_call` (e.g. an attention interface)."""
41 return isinstance(node, fx.Node) and node.meta.get("leaf_call", False)
42
@@ -60,7 +62,7 @@ class _MetaProxy(fx.Proxy):
62 executing each op on the meta values, so PyTorch's meta kernels are the
63 single source of shape inference — no per-op rules."""
64
63 - meta: object = _UNKNOWN
65 + meta: Any = _UNKNOWN
66
67 def __len__(self) -> int:
68 if self.meta is not _UNKNOWN:
@@ -149,6 +151,7 @@ class _AllLeafTracer(fx.Tracer):
151 if unknown:
152 return _UNKNOWN
153 if kind == "call_function":
154 + assert callable(target)
155 return target(*meta_args, **meta_kwargs)
156 if kind == "call_method":
157 receiver, *rest = meta_args
@@ -576,7 +579,7 @@ def _aliasing_reads(node: ast.expr) -> set[str]:
579 would taint nearly every later name.
580 """
581 names: set[str] = set()
579 - stack = [node]
582 + stack: list[ast.AST] = [node]
583 while stack:
584 current = stack.pop()
585 if isinstance(current, ast.Attribute) and current.attr in _METADATA_ATTRS:
@@ -646,7 +649,7 @@ def aliasing_names(
649 """
650 names = set(seed)
651 assigns = [
649 - node
652 + (node, node.value)
653 for node in ast.walk(funcdef)
654 if isinstance(node, (ast.Assign, ast.AnnAssign))
655 and node.value is not None
@@ -655,10 +658,10 @@ def aliasing_names(
658 changed = True
659 while changed:
660 changed = False
658 - for node in assigns:
659 - if not _aliasing_reads(node.value) & names:
661 + for node, value in assigns:
662 + if not _aliasing_reads(value) & names:
663 continue
661 - targets = getattr(node, "targets", None) or [node.target]
664 + targets = node.targets if isinstance(node, ast.Assign) else [node.target]
665 stores = {
666 child.id
667 for target in targets
@@ -770,7 +773,7 @@ def peel(node: object) -> object:
773 return node
774
775
773 -def is_fn(node: object, target: Callable) -> bool:
776 +def is_fn(node: object, target: object) -> TypeIs[fx.Node]:
777 """Is node `<target>()`."""
778 return (
779 isinstance(node, fx.Node)
@@ -779,14 +782,14 @@ def is_fn(node: object, target: Callable) -> bool:
782 )
783
784
782 -def is_method(node: object, name: str) -> bool:
785 +def is_method(node: object, name: str) -> TypeIs[fx.Node]:
786 """Is node `.<name>()`."""
787 return (
788 isinstance(node, fx.Node) and node.op == "call_method" and node.target == name
789 )
790
791
789 -def is_op(node: object, name: str) -> bool:
792 +def is_op(node: object, name: str) -> TypeIs[fx.Node]:
793 """Is node `<mod>.<name>()` for torch, F, operator, or Tensor."""
794 return any(
795 is_fn(node, getattr(module, name, None)) for module in (torch, F, operator)
vllm/model_executor/models/transformers/legacy.py
+18 -13
@@ -20,35 +20,38 @@ from typing import TYPE_CHECKING
20
21 import torch
22
23 +from vllm.model_executor.models.utils import WeightsMapper
24 from vllm.sequence import IntermediateTensors
25
26 +from .base import Base
27 +
28 if TYPE_CHECKING:
29 from vllm.config import VllmConfig
30
31
29 -class LegacyMixin:
32 +class LegacyMixin(Base):
33 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
34 super().__init__(vllm_config=vllm_config, prefix=prefix)
35
33 - # Drop unsupported/unwanted output embeddings layers.
34 - self.hf_to_vllm_mapper.orig_to_new_prefix.update(
35 - {
36 + self.hf_to_vllm_mapper |= WeightsMapper(
37 + # Drop unsupported/unwanted output embeddings layers.
38 + orig_to_new_prefix={
39 "model.lm_head.": None,
40 "model.predictions.": None,
41 "model.qa_outputs.": None,
42 "model.embeddings_project.": None,
43 "model.discriminator_predictions.": None,
41 - }
44 + },
45 + orig_to_new_substr={
46 + # Some encoder models have the position_ids buffer in the checkpoint.
47 + # vLLM always passes position_ids as an argument, so drop the buffer.
48 + "position_ids": None,
49 + # Some encoder models have the bias of the final classifier layer in
50 + # the checkpoint. vLLM does not use this bias, so drop it.
51 + "score.bias": None,
52 + },
53 )
54
44 - # Some encoder models have the position_ids buffer in the checkpoint. vLLM will
45 - # always pass position_ids as an argument, so we drop the buffer if it exists.
46 - self.hf_to_vllm_mapper.orig_to_new_substr["position_ids"] = None
47 -
48 - # Some encoder models have the bias of the final classifier layer in the
49 - # checkpoint. vLLM does not use this bias, so we drop it if it exists.
50 - self.hf_to_vllm_mapper.orig_to_new_substr["score.bias"] = None
51 -
55 # roberta-like models an extra padding in positions.
56 # FIXME(Isotr0py): This is quite hacky for roberta edge case,
57 # we should find a better way to handle this.
@@ -61,6 +64,7 @@ class LegacyMixin:
64 positions: torch.Tensor,
65 intermediate_tensors: IntermediateTensors | None = None,
66 inputs_embeds: torch.Tensor | None = None,
67 + **kwargs,
68 ) -> torch.Tensor | IntermediateTensors:
69 if self.is_roberta:
70 # RoBERTa positions start at padding_idx + 1.
@@ -72,4 +76,5 @@ class LegacyMixin:
76 positions=positions,
77 intermediate_tensors=intermediate_tensors,
78 inputs_embeds=inputs_embeds,
79 + **kwargs,
80 )
vllm/model_executor/models/transformers/moe.py
+31 -11
@@ -41,6 +41,7 @@ from vllm.model_executor.models.transformers.fusers.moe import MoEBlockFuser
41 from vllm.model_executor.models.utils import extract_layer_index, maybe_prefix
42 from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE, direct_register_custom_op
43
44 +from .base import Base
45 from .utils import log_replacement, maybe_per_layer
46
47 if TYPE_CHECKING:
@@ -66,14 +67,15 @@ class TransformersMoERunner(MoERunner):
67 self._moe_state = moe_state
68 self._moe_state.is_sequence_parallel = self.moe_config.is_sequence_parallel
69
69 - def forward(
70 + def forward( # type: ignore[override]
71 self,
72 hidden_states: torch.Tensor,
73 topk_ids: torch.Tensor,
74 topk_weights: torch.Tensor,
75 **kwargs: Any,
76 ) -> torch.Tensor:
76 - """In Transformers `experts.forward` will have this signature.
77 + """In Transformers `experts.forward` will have this signature, which is why
78 + it deliberately does not match `MoERunner.forward`.
79
80 We discard any extra kwargs because we cannot use them here."""
81 # Note: we need to forward through a custom op so the topk_ids
@@ -126,10 +128,18 @@ direct_register_custom_op(
128
129 class TransformersRoutedExperts(RoutedExperts):
130 def get_expert_mapping(
129 - self, include_fused: bool = False
131 + self,
132 + ckpt_gate_proj_name: str | None = None,
133 + ckpt_down_proj_name: str | None = None,
134 + ckpt_up_proj_name: str | None = None,
135 + include_fused: bool = False,
136 ) -> list[tuple[str, str, int, str]]:
131 - common_names = ("gate_proj", "down_proj", "up_proj")
132 - common_map = super().get_expert_mapping(*common_names, include_fused)
137 + common_map = super().get_expert_mapping(
138 + ckpt_gate_proj_name or "gate_proj",
139 + ckpt_down_proj_name or "down_proj",
140 + ckpt_up_proj_name or "up_proj",
141 + include_fused,
142 + )
143 mixtral_map = super().get_expert_mapping("w1", "w2", "w3", include_fused)
144 if not include_fused:
145 return common_map + mixtral_map
@@ -138,7 +148,13 @@ class TransformersRoutedExperts(RoutedExperts):
148 return common_fused + mixtral_fused + common_unfused + mixtral_unfused
149
150
141 -class MoEMixin(MixtureOfExperts):
151 +class MoEMixin(MixtureOfExperts, Base):
152 + mlp_layers: list[nn.Module]
153 + """MoE blocks whose experts were replaced, for the `MixtureOfExperts` methods."""
154 +
155 + # Redeclared because `recursive_replace` assigns it below the first read
156 + num_local_physical_experts: int
157 +
158 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
159 self.check_version("5.0.0", "MoE models support")
160 # Skip MixtureOfExperts.__init__ and call the next class in MRO
@@ -197,7 +213,8 @@ class MoEMixin(MixtureOfExperts):
213
214 # Dtype the router computes in, if it is not the activation dtype.
215 config_router_dtype = getattr(text_config, "moe_router_dtype", None)
200 - config_router_dtype = STR_DTYPE_TO_TORCH_DTYPE.get(config_router_dtype)
216 + if config_router_dtype is not None:
217 + config_router_dtype = STR_DTYPE_TO_TORCH_DTYPE.get(config_router_dtype)
218
219 # Grouped topk routing kwargs
220 num_expert_group = getattr(text_config, "n_group", None)
@@ -222,7 +239,7 @@ class MoEMixin(MixtureOfExperts):
239 # MixtureOfExperts mixin settings
240 ep_size = get_ep_group().world_size
241
225 - self.mlp_layers = [] # Used for MixtureOfExperts methods
242 + self.mlp_layers = []
243 self.moe_layers = []
244 self.num_expert_groups = 1 if num_expert_group is None else num_expert_group
245 self.num_logical_experts = num_experts
@@ -321,9 +338,10 @@ class MoEMixin(MixtureOfExperts):
338 if shared_experts is not None:
339 hf_shared = shared_experts.shared_experts
340 glu_fuser = get_fuser(hf_shared, GLUFuser)
324 - down_name = glu_fuser and glu_fuser.down_name
325 - if down_name is not None:
326 - shared_down_projs.append((hf_shared, down_name))
341 + if glu_fuser is not None:
342 + down_name = glu_fuser.down_name
343 + if down_name is not None:
344 + shared_down_projs.append((hf_shared, down_name))
345 # Prefer config, otherwise read it from fuser.
346 router_dtype = config_router_dtype or fuser.router_dtype
347 gate = fuser.gate(moe_block, prefix, router_dtype)
@@ -374,7 +392,9 @@ class MoEMixin(MixtureOfExperts):
392 # Handle all gather in expert parallel
393 if topk_ids.size(0) != hidden_states.size(0):
394 dp_metadata = get_forward_context().dp_metadata
395 + assert dp_metadata is not None
396 sizes = dp_metadata.get_chunk_sizes_across_dp_rank()
397 + assert sizes is not None
398 is_sp = moe_state.is_sequence_parallel
399 group = get_ep_group() if is_sp else get_dp_group()
400 assert sizes[group.rank_in_group] == topk_ids.shape[0]
vllm/model_executor/models/transformers/multimodal.py
+36 -18
@@ -17,17 +17,24 @@
17 """Transformers modeling backend mixin for multi-modal models."""
18
19 from collections import defaultdict
20 -from collections.abc import Mapping, Sequence
20 +from collections.abc import Iterable, Mapping, Sequence
21 from contextlib import ExitStack, contextmanager
22 from typing import TYPE_CHECKING, Any
23
24 import torch
25 import transformers
26 from packaging.version import Version
27 +from transformers.utils.generic import ModelOutput
28
29 from vllm.compilation.decorators import should_torch_compile_mm_encoder
30 +from vllm.config.multimodal import AudioDummyOptions
31 from vllm.config.utils import getattr_iter
30 -from vllm.inputs import MultiModalDataDict, MultiModalInput, mm_input
32 +from vllm.inputs import (
33 + MultiModalDataBuiltins,
34 + MultiModalDataDict,
35 + MultiModalInput,
36 + mm_input,
37 +)
38 from vllm.logger import init_logger
39 from vllm.model_executor.models.interfaces import (
40 MultiModalEmbeddings,
@@ -62,6 +69,8 @@ from vllm.sequence import IntermediateTensors
69 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
70 from vllm.utils.torch_utils import async_tensor_h2d
71
72 +from .base import Base
73 +
74 if TYPE_CHECKING:
75 from transformers import BatchFeature, PreTrainedModel
76
@@ -148,7 +157,7 @@ class MultiModalProcessingInfo(BaseProcessingInfo):
157 def get_max_image_tokens(self) -> int:
158 width, height = self.get_image_size_with_most_features()
159 processor = self.get_hf_processor()
151 - multimodal_config = self.ctx.model_config.multimodal_config
160 + multimodal_config = self.ctx.model_config.get_multimodal_config()
161 mm_processor_kwargs = multimodal_config.mm_processor_kwargs or {}
162 mm_tokens = processor._get_num_multimodal_tokens(
163 image_sizes=([height, width],), **mm_processor_kwargs
@@ -188,7 +197,7 @@ class MultiModalDummyInputsBuilder(BaseDummyInputsBuilder[MultiModalProcessingIn
197 mm_counts: Mapping[str, int],
198 mm_options: Mapping[str, "BaseDummyOptions"],
199 ) -> MultiModalDataDict:
191 - data: MultiModalDataDict = {}
200 + data = MultiModalDataBuiltins()
201 if self.info._is_audio_model() and (num_audios := mm_counts.get("audio", 0)):
202 sampling_rate = self.info._get_audio_sampling_rate()
203 sub = self.info._get_audio_processor()
@@ -196,10 +205,14 @@ class MultiModalDummyInputsBuilder(BaseDummyInputsBuilder[MultiModalProcessingIn
205 if chunk_length is None:
206 chunk_length = 30
207 audio_len = int(chunk_length * sampling_rate)
208 + audio_overrides = mm_options.get("audio")
209 + assert audio_overrides is None or isinstance(
210 + audio_overrides, AudioDummyOptions
211 + )
212 data["audio"] = self._get_dummy_audios(
213 length=audio_len,
214 num_audios=num_audios,
202 - overrides=mm_options.get("audio"),
215 + overrides=audio_overrides,
216 )
217 if self.info._is_image_model() and (num_images := mm_counts.get("image", 0)):
218 target_width, target_height = self.info.get_image_size_with_most_features()
@@ -359,7 +372,9 @@ class _MultiModalProcessorBase(BaseMultiModalProcessor[MultiModalProcessingInfo]
372 itself is to process it by itself.
373 """
374 audios = mm_data.get("audio")
362 - if not audios or len({len(audio) for audio in audios}) == 1:
375 + if not isinstance(audios, Iterable) or not audios:
376 + return
377 + if len({len(audio) for audio in audios}) == 1:
378 return
379
380 alone = [
@@ -444,7 +459,8 @@ class LegacyMultiModalProcessor(_MultiModalProcessorBase):
459 token_id = getattr(processor, names[0], getattr_iter(config, names))
460 if token_id is None:
461 token = getattr(processor, f"{modality}_token", None)
447 - token_id = info.get_tokenizer().get_vocab().get(token)
462 + if token is not None:
463 + token_id = info.get_tokenizer().get_vocab().get(token)
464 if token_id is None:
465 raise ValueError(
466 f"Cannot find {modality}_token_id on processor or model config"
@@ -722,13 +738,15 @@ class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
738 for modality, items in out_mm_kwargs.items():
739 # Popped so they are neither cached nor sent to the model; the updates
740 # they produce are cached alongside the item instead
725 - replacements = [
726 - PromptUpdateDetails.select_token_id(
727 - (ids := item.pop(f"{modality}_replacement_ids").data).tolist(),
728 - _get_embed_token_id(ids),
741 + replacements = []
742 + for item in items:
743 + ids = item.pop(f"{modality}_replacement_ids").data
744 + assert isinstance(ids, torch.Tensor)
745 + replacements.append(
746 + PromptUpdateDetails.select_token_id(
747 + ids.tolist(), _get_embed_token_id(ids)
748 + )
749 )
730 - for item in items
731 - ]
750 token = getattr(hf_processor, f"{modality}_token")
751 updates.append(
752 PromptReplacement(
@@ -777,7 +795,7 @@ class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
795 ) -> list[int] | None:
796 """Ask the HF processor how many rows of image data each image produces."""
797 images = mm_data.get("images")
780 - if not images:
798 + if not isinstance(images, Iterable) or not images:
799 return None
800 try:
801 sizes = [(image.height, image.width) for image in images]
@@ -893,7 +911,7 @@ MultiModalProcessor = (
911 )
912
913
896 -class MultiModalMixin(SupportsMultiModal, SupportsMRoPE):
914 +class MultiModalMixin(SupportsMultiModal, SupportsMRoPE, Base):
915 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
916 # Skip SupportsMRoPE.__init__ and call the next class in MRO
917 super(SupportsMRoPE, self).__init__(vllm_config=vllm_config, prefix=prefix)
@@ -925,7 +943,7 @@ class MultiModalMixin(SupportsMultiModal, SupportsMRoPE):
943 if model_config.skip_tokenizer_init:
944 # Determining the supported modalities needs the HF processor, which in
945 # turn needs a tokenizer
928 - mm_config = model_config.multimodal_config
946 + mm_config = model_config.get_multimodal_config()
947 if mm_config.mm_encoder_only or any(
948 mm_config.get_limit_per_prompt(modality) == 0
949 for modality in encoder_classes
@@ -1029,7 +1047,7 @@ class MultiModalMixin(SupportsMultiModal, SupportsMRoPE):
1047 # Exclude MultiModalMixin itself
1048 bases = [b for b in bases if b is not MultiModalMixin]
1049
1032 - class LanguageModel(*bases):
1050 + class LanguageModel(*bases): # type: ignore[misc]
1051 def __init__(self, multimodal_model):
1052 # Don't call super().__init__() to avoid re-initialization
1053 self.__dict__.update(multimodal_model.__dict__)
@@ -1203,7 +1221,7 @@ class MultiModalMixin(SupportsMultiModal, SupportsMRoPE):
1221 # from `self.get_image_features`
1222 if isinstance(features, tuple):
1223 return features[0]
1206 - if isinstance(features, dict):
1224 + if isinstance(features, ModelOutput):
1225 return features.pooler_output
1226 return features
1227
vllm/model_executor/models/transformers/pooling.py
+24 -13
@@ -19,6 +19,7 @@
19 from typing import TYPE_CHECKING
20
21 import torch
22 +import torch.nn as nn
23 from transformers import AutoModelForSequenceClassification
24
25 from vllm.config.utils import getattr_iter
@@ -26,11 +27,25 @@ from vllm.model_executor.layers.pooler import DispatchPooler
27 from vllm.model_executor.models.interfaces import SupportsCrossEncoding
28 from vllm.model_executor.models.interfaces_base import VllmModelForPooling
29
30 +from .base import Base
31 +
32 if TYPE_CHECKING:
33 from vllm.config import VllmConfig
34
35
33 -class EmbeddingMixin(VllmModelForPooling):
36 +class ClassifierWithReshape(nn.Module):
37 + """Token extraction has already been applied in `pooler.pooling`.
38 +
39 + Add dim to match expected input shape of `classifier.forward`.
40 + """
41 +
42 + def forward(self, *args, **kwargs):
43 + if len(args) > 0:
44 + args = (args[0].unsqueeze(1), *args[1:])
45 + return super().forward(*args, **kwargs)
46 +
47 +
48 +class EmbeddingMixin(VllmModelForPooling, Base):
49 default_seq_pooling_type = "CLS"
50
51 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
@@ -45,7 +60,7 @@ class EmbeddingMixin(VllmModelForPooling):
60 self.pooler = DispatchPooler.for_embedding(pooler_config)
61
62
48 -class SequenceClassificationMixin(SupportsCrossEncoding, VllmModelForPooling):
63 +class SequenceClassificationMixin(SupportsCrossEncoding, VllmModelForPooling, Base):
64 default_seq_pooling_type = "CLS"
65
66 def __init__(self, *, vllm_config: "VllmConfig", prefix: str = ""):
@@ -83,17 +98,13 @@ class SequenceClassificationMixin(SupportsCrossEncoding, VllmModelForPooling):
98 )
99 self.init_parameters(self.classifier, dtype=self.model_config.head_dtype)
100
86 - class ClassifierWithReshape(self.classifier.__class__):
87 - """Token extraction has already been applied in `pooler.pooling`.
88 - Add dim to match expected input shape of `classifier.forward`.
89 - """
90 -
91 - def forward(self, *args, **kwargs):
92 - if len(args) > 0:
93 - args = (args[0].unsqueeze(1), *args[1:])
94 - return super().forward(*args, **kwargs)
95 -
96 - self.classifier.__class__ = ClassifierWithReshape
101 + # Order `ClassifierWithReshape` ahead of the classifier's own class so that
102 + # its `super().forward(...)` reaches the original implementation.
103 + self.classifier.__class__ = type(
104 + "ClassifierWithReshape",
105 + (ClassifierWithReshape, type(self.classifier)),
106 + {},
107 + )
108
109 self.pooler = DispatchPooler.for_seq_cls(
110 pooler_config,
vllm/model_executor/models/transformers/utils.py
+12 -9
@@ -18,11 +18,10 @@
18
19 from collections.abc import Callable, Iterator
20 from contextlib import contextmanager
21 -from functools import lru_cache
21 from itertools import chain
22 from operator import attrgetter
23 from pathlib import Path
25 -from typing import TYPE_CHECKING, Literal, TypeVar
24 +from typing import TYPE_CHECKING, Literal, TypeAlias, TypeVar
25
26 import torch
27 from torch import nn
@@ -70,7 +69,7 @@ def init_on_device_without_buffers(device: torch.device):
69 module._parameters[name].to(device), **kwargs
70 )
71
73 - tensor_constructors_to_patch = {}
72 + tensor_constructors_to_patch: dict[str, Callable] = {}
73
74 def patch_tensor_constructor(fn):
75 def wrapper(*args, **kwargs):
@@ -147,8 +146,8 @@ def replace_linear_class(
146 )
147
148
150 -TorchConv = nn.Conv2d | nn.Conv3d
151 -VllmConv = Conv2dLayer | Conv3dLayer
149 +TorchConv: TypeAlias = nn.Conv2d | nn.Conv3d
150 +VllmConv: TypeAlias = Conv2dLayer | Conv3dLayer
151
152
153 def replace_conv_class(conv: TorchConv) -> VllmConv | TorchConv:
@@ -212,7 +211,9 @@ class _VocabParallelEmbeddingBase(VocabParallelEmbedding, _UninitializedEmbeddin
211 `super().forward(...)` in an `nn.Embedding` subclass reaches vLLM's embedding."""
212
213
215 -@lru_cache
214 +_rebased_embedding_classes: dict[type[nn.Embedding], type[VocabParallelEmbedding]] = {}
215 +
216 +
217 def _rebase_on_vocab_parallel(cls: type[nn.Embedding]) -> type[VocabParallelEmbedding]:
218 """Subclass `cls` so that `VocabParallelEmbedding` supersedes its `nn.Embedding`.
219
@@ -224,7 +225,10 @@ def _rebase_on_vocab_parallel(cls: type[nn.Embedding]) -> type[VocabParallelEmbe
225 The new class, to assign to `__class__` of an instance of `cls`.
226
227 """
227 - return type(cls.__name__, (cls, _VocabParallelEmbeddingBase), {})
228 + if cls not in _rebased_embedding_classes:
229 + rebased_cls = type(cls.__name__, (cls, _VocabParallelEmbeddingBase), {})
230 + _rebased_embedding_classes[cls] = rebased_cls
231 + return _rebased_embedding_classes[cls]
232
233
234 def replace_embedding_class(
@@ -278,10 +282,9 @@ def recursive_replace_linear(
282 qual_name = maybe_prefix(prefix, child_name)
283 # Replace modules as needed
284 if isinstance(child_module, nn.Linear):
281 - style = "replicate"
285 new_module = replace_linear_class(
286 child_module,
284 - style,
287 + "replicate",
288 quant_config,
289 prefix=qual_name,
290 )