Fix weight tying (#51665)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Harry Mellor committed
Aug 20, 2026 at 11:31 UTC
f0c14b4f776bb976e654b430442067539fbdb2ea
70 files changed
+526
-238
tests/model_executor/model_loader/test_weight_tying.py
new
+86
@@ -0,0 +1,86 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from types import SimpleNamespace
5
+
6
+import pytest
7
+import torch
8
+from torch import nn
9
+
10
+from vllm.model_executor.layers.vocab_parallel_embedding import (
11
+ ParallelLMHead,
12
+ VocabParallelEmbedding,
13
+)
14
+from vllm.model_executor.model_loader.weight_tying import maybe_retie_word_embeddings
15
+
16
+VOCAB_SIZE = 16
17
+HIDDEN_SIZE = 4
18
+
19
+
20
+class UntiedModel(nn.Module):
21
+ """Nests the head like a multimodal model, which is the harder case."""
22
+
23
+ def __init__(self):
24
+ super().__init__()
25
+ self.language_model = nn.Module()
26
+ self.language_model.model = nn.Module()
27
+ self.language_model.model.embed_tokens = VocabParallelEmbedding(
28
+ VOCAB_SIZE, HIDDEN_SIZE
29
+ )
30
+ self.language_model.lm_head = ParallelLMHead(VOCAB_SIZE, HIDDEN_SIZE)
31
+ self.embed_tokens.weight.data.fill_(1.0)
32
+ self.lm_head.weight.data.fill_(1.0)
33
+
34
+ @property
35
+ def embed_tokens(self) -> VocabParallelEmbedding:
36
+ return self.language_model.model.embed_tokens
37
+
38
+ @property
39
+ def lm_head(self) -> ParallelLMHead:
40
+ return self.language_model.lm_head
41
+
42
+
43
+def make_model_config(untied_by_checkpoint=False):
44
+ return SimpleNamespace(
45
+ model="dummy-model",
46
+ word_embeddings_untied_by_checkpoint=untied_by_checkpoint,
47
+ )
48
+
49
+
50
+@pytest.mark.cpu_test
51
+@pytest.mark.usefixtures("dist_init")
52
+@pytest.mark.parametrize("identical", [True, False])
53
+def test_retie_only_when_identical(identical: bool):
54
+ """A redundant copy of a tied lm_head is shared again to reclaim memory."""
55
+ model = UntiedModel()
56
+ if not identical:
57
+ model.lm_head.weight.data.fill_(2.0)
58
+
59
+ maybe_retie_word_embeddings(model, make_model_config(untied_by_checkpoint=True))
60
+
61
+ assert (model.lm_head.weight is model.embed_tokens.weight) is identical
62
+ if not identical:
63
+ assert torch.all(model.lm_head.weight == 2.0)
64
+
65
+
66
+@pytest.mark.cpu_test
67
+@pytest.mark.usefixtures("dist_init")
68
+def test_quantized_lm_head_is_left_alone():
69
+ """A quantized head may store its weights packed under another name."""
70
+ model = UntiedModel()
71
+ model.lm_head.quant_method = SimpleNamespace()
72
+
73
+ maybe_retie_word_embeddings(model, make_model_config(untied_by_checkpoint=True))
74
+
75
+ assert model.lm_head.weight is not model.embed_tokens.weight
76
+
77
+
78
+@pytest.mark.cpu_test
79
+@pytest.mark.usefixtures("dist_init")
80
+def test_no_retie_without_checkpoint_override():
81
+ """Word embeddings the config genuinely unties are left alone."""
82
+ model = UntiedModel()
83
+
84
+ maybe_retie_word_embeddings(model, make_model_config())
85
+
86
+ assert model.lm_head.weight is not model.embed_tokens.weight
tests/models/test_utils.py
+78
@@ -4,6 +4,10 @@
4
import pytest
5
import torch
6
7
+from vllm.model_executor.layers.vocab_parallel_embedding import (
8
+ ParallelLMHead,
9
+ VocabParallelEmbedding,
10
+)
11
from vllm.model_executor.models.utils import (
12
AutoWeightsLoader,
13
_merge_multimodal_embeddings,
@@ -165,6 +169,80 @@ def test_module_skip_substr():
169
assert new_mod.nested_mod.bn.num_batches_tracked.item() == 1
170
171
172
+VOCAB_SIZE = 16
173
+HIDDEN_SIZE = 2
174
+
175
+
176
+class ModuleWithTiedWeights(torch.nn.Module):
177
+ """Mimics how models tie `lm_head` to the input embeddings."""
178
+
179
+ def __init__(self, tie: bool):
180
+ super().__init__()
181
+ self.model = torch.nn.Module()
182
+ self.model.embed_tokens = VocabParallelEmbedding(VOCAB_SIZE, HIDDEN_SIZE)
183
+ self.lm_head = ParallelLMHead(VOCAB_SIZE, HIDDEN_SIZE)
184
+ if tie:
185
+ self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens)
186
+
187
+
188
+def make_embedding_weights(value: float) -> torch.Tensor:
189
+ return torch.full((VOCAB_SIZE, HIDDEN_SIZE), value)
190
+
191
+
192
+@pytest.mark.cpu_test
193
+@pytest.mark.usefixtures("dist_init")
194
+@pytest.mark.parametrize("tie", [True, False])
195
+def test_module_skip_tied_weights(tie: bool):
196
+ """Tied weights must be loaded once, under the first of their names."""
197
+ mod = ModuleWithTiedWeights(tie)
198
+
199
+ weights = [
200
+ ("model.embed_tokens.weight", make_embedding_weights(1.0)),
201
+ ("lm_head.weight", make_embedding_weights(2.0)),
202
+ ]
203
+ loaded = AutoWeightsLoader(mod).load_weights(iter(weights))
204
+
205
+ if tie:
206
+ assert loaded == {"model.embed_tokens.weight"}
207
+ assert torch.all(mod.lm_head.weight[:VOCAB_SIZE] == 1.0)
208
+ else:
209
+ assert loaded == {"model.embed_tokens.weight", "lm_head.weight"}
210
+ assert torch.all(mod.lm_head.weight[:VOCAB_SIZE] == 2.0)
211
+
212
+
213
+@pytest.mark.cpu_test
214
+@pytest.mark.usefixtures("dist_init")
215
+def test_module_skip_tied_weights_without_canonical():
216
+ """Skipping a tied weight must not leave the shared weight uninitialized."""
217
+ mod = ModuleWithTiedWeights(tie=True)
218
+
219
+ weights = [("lm_head.weight", make_embedding_weights(2.0))]
220
+ with pytest.raises(ValueError, match="model.embed_tokens.weight"):
221
+ AutoWeightsLoader(mod).load_weights(iter(weights))
222
+
223
+
224
+class ModuleWithSharedParam(torch.nn.Module):
225
+ """Mimics an MoE router shared between the MLP and its fused experts."""
226
+
227
+ def __init__(self):
228
+ super().__init__()
229
+ self.experts = torch.nn.Module()
230
+ self.experts.gate = torch.nn.Linear(2, 2, bias=False)
231
+ self.gate = self.experts.gate
232
+
233
+
234
+@pytest.mark.cpu_test
235
+def test_module_load_shared_params_that_are_not_tied_embeddings():
236
+ """Only tied embeddings are skipped; other shared params must still load."""
237
+ mod = ModuleWithSharedParam()
238
+
239
+ weights = [("gate.weight", torch.Tensor([[1, 2], [3, 4]]))]
240
+ loaded = AutoWeightsLoader(mod).load_weights(iter(weights))
241
+
242
+ assert loaded == {"gate.weight"}
243
+ assert torch.all(mod.gate.weight == torch.Tensor([[1, 2], [3, 4]]))
244
+
245
+
246
class raise_if_cuda_sync:
247
def __enter__(self):
248
self.previous_debug_mode = torch.cuda.get_sync_debug_mode()
tests/test_config.py
+39
@@ -604,6 +604,45 @@ def test_with_hf_config_leaves_unknown_model_type_without_architectures(
604
assert updated.model_config.hf_config.architectures is None
605
606
607
+@pytest.mark.parametrize(
608
+ "checkpoint_tensors,tied",
609
+ [
610
+ # The checkpoint has an lm_head of its own, so it must win over the config
611
+ (["model.embed_tokens.weight", "lm_head.weight"], False),
612
+ (["model.embed_tokens.weight"], True),
613
+ # Contents unknown (not safetensors), so the config must be left alone
614
+ ([], True),
615
+ ],
616
+)
617
+def test_maybe_untie_word_embeddings(tmp_path, checkpoint_tensors, tied):
618
+ import torch
619
+ from safetensors.torch import save_file
620
+
621
+ if checkpoint_tensors:
622
+ save_file(
623
+ {name: torch.zeros(2, 2) for name in checkpoint_tensors},
624
+ tmp_path / "model.safetensors",
625
+ )
626
+
627
+ text_config = SimpleNamespace(tie_word_embeddings=True)
628
+ model_config = SimpleNamespace(
629
+ model=str(tmp_path),
630
+ revision=None,
631
+ hf_config=SimpleNamespace(
632
+ tie_word_embeddings=True,
633
+ get_text_config=lambda: text_config,
634
+ ),
635
+ word_embeddings_untied_by_checkpoint=False,
636
+ )
637
+
638
+ ModelConfig.maybe_untie_word_embeddings(model_config)
639
+
640
+ # Both levels must agree, since different callers read different ones
641
+ assert model_config.hf_config.tie_word_embeddings is tied
642
+ assert text_config.tie_word_embeddings is tied
643
+ assert model_config.word_embeddings_untied_by_checkpoint is not tied
644
+
645
+
646
def test_async_scheduling_with_pipeline_parallelism_is_allowed():
647
cfg = VllmConfig(
648
scheduler_config=SchedulerConfig(
tests/transformers_utils/test_config.py
+24
-2
@@ -8,14 +8,17 @@ only get the `eos_token_id` from the tokenizer as defined by
8
9
from types import SimpleNamespace
10
from typing import cast
11
-from unittest.mock import patch
11
+from unittest.mock import MagicMock, patch
12
13
from transformers import PretrainedConfig
14
15
from vllm.config.model import ModelConfig
16
from vllm.tokenizers import get_tokenizer
17
from vllm.transformers_utils import config as config_module
18
-from vllm.transformers_utils.config import try_get_generation_config
18
+from vllm.transformers_utils.config import (
19
+ get_safetensors_params_metadata,
20
+ try_get_generation_config,
21
+)
22
23
24
def test_get_llama3_eos_token():
@@ -77,3 +80,22 @@ def test_model_config_generation_fallback_forwards_code_revision():
80
config_format="auto",
81
token=None,
82
)
83
+
84
+
85
+def test_safetensors_metadata_of_repo_without_safetensors():
86
+ """A repo storing its weights in another format is an answer, not a failure,
87
+ so it must not be retried."""
88
+ from huggingface_hub.errors import LocalEntryNotFoundError, NotASafetensorsRepoError
89
+
90
+ get_safetensors_metadata = MagicMock(
91
+ side_effect=NotASafetensorsRepoError("not a safetensors repo")
92
+ )
93
+ api = SimpleNamespace(
94
+ get_safetensors_metadata=get_safetensors_metadata,
95
+ snapshot_download=MagicMock(side_effect=LocalEntryNotFoundError("no cache")),
96
+ )
97
+
98
+ with patch.object(config_module, "hf_api", lambda: api):
99
+ assert get_safetensors_params_metadata("some/pytorch-only-model") == {}
100
+
101
+ get_safetensors_metadata.assert_called_once()
tests/transformers_utils/test_repo_utils.py
+11
@@ -14,6 +14,7 @@ from vllm.transformers_utils.repo_utils import (
14
get_hf_file_to_dict,
15
is_mistral_model_repo,
16
list_filtered_repo_files,
17
+ with_retry,
18
)
19
20
@@ -185,3 +186,13 @@ def test_is_mistral_model_repo(files: list[str], expected_bool: bool):
186
repo_type="model",
187
token="token",
188
)
189
+
190
+
191
+def test_with_retry_does_not_retry_fatal_errors():
192
+ """A definitive answer must not be retried, which costs a delay per attempt."""
193
+ func = MagicMock(side_effect=FileNotFoundError("no such file"))
194
+
195
+ with pytest.raises(FileNotFoundError):
196
+ with_retry(func, "Error", fatal_errors=(FileNotFoundError,))
197
+
198
+ func.assert_called_once()
vllm/config/model.py
+33
@@ -30,6 +30,7 @@ from vllm.platforms import current_platform
30
from vllm.tasks import PoolingTask, ScoreType, SupportedTask
31
from vllm.transformers_utils.config import (
32
ConfigFormat,
33
+ checkpoint_has_lm_head,
34
get_config,
35
get_hf_image_processor_config,
36
get_hf_text_config,
@@ -185,6 +186,10 @@ class ModelConfig:
186
"""The Hugging Face config of the model."""
187
hf_text_config: PretrainedConfig = field(init=False)
188
"""The Hugging Face config of the text model (same as hf_config for text models)."""
189
+ word_embeddings_untied_by_checkpoint: bool = field(default=False, init=False)
190
+ """Whether `tie_word_embeddings` was overridden to `False` because the checkpoint
191
+ contains an `lm_head` of its own. The two may still turn out to be identical, in
192
+ which case they are re-tied once the weights have been loaded."""
193
hf_config_path: str | None = None
194
"""Name or path of the Hugging Face config to use. If unspecified, model
195
name or path will be used."""
@@ -1109,6 +1114,34 @@ class ModelConfig:
1114
def _get_encoder_config(self) -> dict[str, Any] | None:
1115
return get_sentence_transformer_tokenizer_config(self.model, self.revision)
1116
1117
+ def maybe_untie_word_embeddings(self) -> None:
1118
+ """Stop trusting `tie_word_embeddings` when the checkpoint disagrees.
1119
+
1120
+ A config may claim the word embeddings are tied while the checkpoint
1121
+ ships an `lm_head` of its own. Tying regardless would silently discard
1122
+ that tensor, so build the `lm_head` as if untied and let it load. The
1123
+ two are compared once loaded, and re-tied if they turn out to match, by
1124
+ [maybe_retie_word_embeddings][vllm.model_executor.model_loader.weight_tying.maybe_retie_word_embeddings].
1125
+
1126
+ Transformers makes the same decision in `PreTrainedModel.tie_weights`,
1127
+ where it can compare the two tensors directly.
1128
+ """
1129
+ if not getattr(self.hf_config, "tie_word_embeddings", False):
1130
+ return
1131
+ if not checkpoint_has_lm_head(self.model, revision=self.revision):
1132
+ return
1133
+
1134
+ logger.debug(
1135
+ "The config for %s says the word embeddings are tied, but the checkpoint "
1136
+ "contains an lm_head. Loading it to find out whether they really are tied.",
1137
+ self.model,
1138
+ )
1139
+ # Both levels must be updated: `VllmConfig.with_hf_config` reads the top
1140
+ # level, and the text config is what the language model itself sees.
1141
+ self.hf_config.tie_word_embeddings = False
1142
+ self.hf_config.get_text_config().tie_word_embeddings = False
1143
+ self.word_embeddings_untied_by_checkpoint = True
1144
+
1145
def _get_default_runner_type(
1146
self,
1147
architectures: list[str],
vllm/config/vllm.py
+10
@@ -1170,6 +1170,16 @@ class VllmConfig:
1170
self.model_config, self.load_config
1171
)
1172
1173
+ # "dummy" reads no weights at all, and the sharded formats read a vLLM
1174
+ # state dict, which stores tied word embeddings under the lm_head only.
1175
+ # Neither can tell us what the original checkpoint contained.
1176
+ if self.model_config is not None and self.load_config.load_format not in (
1177
+ "dummy",
1178
+ "sharded_state",
1179
+ "runai_streamer_sharded",
1180
+ ):
1181
+ self.model_config.maybe_untie_word_embeddings()
1182
+
1183
if (
1184
self.quant_config is not None
1185
and self.model_config is not None
vllm/model_executor/model_loader/utils.py
+5
@@ -24,6 +24,7 @@ from vllm.model_executor.model_loader.reload import (
24
record_metadata_for_reloading,
25
set_torchao_reload_attrs,
26
)
27
+from vllm.model_executor.model_loader.weight_tying import maybe_retie_word_embeddings
28
from vllm.model_executor.models.interfaces import SupportsQuant
29
from vllm.tracing import instrument
30
from vllm.utils.mem_utils import release_device_memory_under_pressure
@@ -96,6 +97,10 @@ def initialize_model(
97
def process_weights_after_loading(
98
model: nn.Module, model_config: ModelConfig, target_device: torch.device
99
) -> None:
100
+ # Reclaim memory when an explicit lm_head has been
101
+ # loaded, but it is identical to the input embeddings.
102
+ maybe_retie_word_embeddings(model, model_config)
103
+
104
for _, module in model.named_modules():
105
quant_method = getattr(module, "quant_method", None)
106
if isinstance(quant_method, QuantizeMethodBase):
vllm/model_executor/model_loader/weight_tying.py
new
+99
@@ -0,0 +1,99 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Reconcile word embedding tying with what the checkpoint actually contains."""
4
+
5
+from dataclasses import dataclass
6
+
7
+import torch
8
+from torch import nn
9
+
10
+from vllm.config import ModelConfig
11
+from vllm.logger import init_logger
12
+from vllm.model_executor.layers.vocab_parallel_embedding import (
13
+ ParallelLMHead,
14
+ UnquantizedEmbeddingMethod,
15
+ VocabParallelEmbedding,
16
+)
17
+
18
+logger = init_logger(__name__)
19
+
20
+
21
+@dataclass
22
+class _UntiedLMHead:
23
+ name: str
24
+ lm_head: ParallelLMHead
25
+ embed_tokens: VocabParallelEmbedding
26
+
27
+ @property
28
+ def weight_name(self) -> str:
29
+ return f"{self.name}.weight"
30
+
31
+ def tie(self, model: nn.Module) -> None:
32
+ parent_name, _, attr = self.name.rpartition(".")
33
+ parent = model.get_submodule(parent_name) if parent_name else model
34
+ setattr(parent, attr, self.lm_head.tie_weights(self.embed_tokens))
35
+
36
+
37
+def _get_untied_lm_head(model: nn.Module) -> _UntiedLMHead | None:
38
+ """Locate an `lm_head` that could be tied to the input embeddings.
39
+
40
+ Returns `None` unless the model has exactly one of each, so that models with
41
+ several heads (such as MTP) or with already tied weights are left alone.
42
+ Both are found by type rather than by name, because `get_input_embeddings`
43
+ takes different arguments on multimodal models and the `lm_head` of a
44
+ multimodal model is nested inside its language model.
45
+
46
+ A quantized `lm_head` is also left alone. Its weights may be packed under
47
+ another name, and online quantization creates them after loading, so
48
+ neither their contents nor whether they were loaded can be established here.
49
+ Note that this is a property of the layer, not of the model: most quantized
50
+ models leave the `lm_head` and the input embeddings unquantized.
51
+ """
52
+ heads = list[tuple[str, ParallelLMHead]]()
53
+ embeddings = list[VocabParallelEmbedding]()
54
+ for name, module in model.named_modules():
55
+ if not isinstance(module, VocabParallelEmbedding):
56
+ continue
57
+ if not isinstance(module.quant_method, UnquantizedEmbeddingMethod):
58
+ continue
59
+ if isinstance(module, ParallelLMHead):
60
+ heads.append((name, module))
61
+ else:
62
+ embeddings.append(module)
63
+
64
+ if len(heads) != 1 or len(embeddings) != 1:
65
+ return None
66
+
67
+ (name, lm_head), embed_tokens = heads[0], embeddings[0]
68
+ if lm_head.weight.shape != embed_tokens.weight.shape:
69
+ return None
70
+ return _UntiedLMHead(name, lm_head, embed_tokens)
71
+
72
+
73
+def maybe_retie_word_embeddings(model: nn.Module, model_config: ModelConfig) -> None:
74
+ """Re-tie word embeddings that
75
+ [ModelConfig.maybe_untie_word_embeddings][vllm.config.ModelConfig.maybe_untie_word_embeddings]
76
+ untied, if the loaded `lm_head` turned out to be identical to the input embeddings
77
+ after all.
78
+
79
+ Checkpoints produced by quantization or fine-tuning tooling often keep a redundant
80
+ copy of the tied `lm_head`. Sharing the storage again reclaims the memory it would
81
+ otherwise cost."""
82
+ if not model_config.word_embeddings_untied_by_checkpoint:
83
+ return
84
+ if (untied := _get_untied_lm_head(model)) is None:
85
+ return
86
+
87
+ # On device, torch.equal segfaults on ROCm when sleep mode is enabled
88
+ if not torch.equal(untied.lm_head.weight.cpu(), untied.embed_tokens.weight.cpu()):
89
+ logger.warning(
90
+ "The config for %s says the word embeddings are tied, but the checkpoint "
91
+ "contains a different %s, which has been used instead of tying. "
92
+ "Set `tie_word_embeddings=False` in the config to silence this warning.",
93
+ model_config.model,
94
+ untied.weight_name,
95
+ )
96
+ return
97
+
98
+ logger.debug("Re-tying %s, which is identical to the input embeddings", untied.name)
99
+ untied.tie(model)
vllm/model_executor/models/apertus.py
+1
-4
@@ -491,8 +491,5 @@ class ApertusForCausalLM(
491
return logits
492
493
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
494
- loader = AutoWeightsLoader(
495
- self,
496
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
497
- )
494
+ loader = AutoWeightsLoader(self)
495
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/arcee.py
+1
-5
@@ -356,11 +356,7 @@ class ArceeForCausalLM(
356
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
357
"""Load weights into the model (delegates to inner model and handles
358
tied embeddings)."""
359
- loader = AutoWeightsLoader(
360
- self,
361
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
362
- skip_substrs=["gate_proj"],
363
- )
359
+ loader = AutoWeightsLoader(self, skip_substrs=["gate_proj"])
360
# AutoWeightLoader handles weight name remapping, including fusing
361
# separate q_proj, k_proj, v_proj into qkv_proj
362
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/arctic.py
+1
-4
@@ -589,8 +589,5 @@ class ArcticForCausalLM(nn.Module, SupportsPP, SupportsQuant):
589
return logits
590
591
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
592
- loader = AutoWeightsLoader(
593
- self,
594
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
595
- )
592
+ loader = AutoWeightsLoader(self)
593
return loader.load_weights(weights)
vllm/model_executor/models/bailing_moe.py
+1
-4
@@ -557,10 +557,7 @@ class BailingMoeForCausalLM(nn.Module, SupportsPP, SupportsLoRA):
557
yield name, loaded_weight
558
559
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
560
- loader = AutoWeightsLoader(
561
- self,
562
- skip_prefixes=(["lm_head."] if self.tie_word_embeddings else None),
563
- )
560
+ loader = AutoWeightsLoader(self)
561
return loader.load_weights(self._normalize_lm_head(weights))
562
563
vllm/model_executor/models/bloom.py
+1
-1
@@ -360,7 +360,7 @@ class BloomForCausalLM(nn.Module, SupportsPP, SupportsQuant):
360
return logits
361
362
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
363
- loader = AutoWeightsLoader(self, skip_prefixes=["lm_head.weight"])
363
+ loader = AutoWeightsLoader(self)
364
weights = _add_transformer_prefix(weights)
365
return loader.load_weights(weights)
366
vllm/model_executor/models/chameleon.py
+1
-2
@@ -1059,6 +1059,5 @@ class ChameleonForConditionalGeneration(
1059
return logits
1060
1061
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1062
- skip_prefixes = ["lm_head."] if self.config.tie_word_embeddings else None
1063
- loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
1062
+ loader = AutoWeightsLoader(self)
1063
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/cohere2_moe.py
-1
@@ -493,7 +493,6 @@ class Cohere2MoeForCausalLM(nn.Module, SupportsPP, SupportsQuant):
493
config = vllm_config.model_config.hf_config
494
quant_config = vllm_config.quant_config
495
self.config = config
496
- assert getattr(config, "tie_word_embeddings", True)
496
self.unpadded_vocab_size = config.vocab_size
497
self.quant_config = quant_config
498
self.logits_scale = config.logit_scale
vllm/model_executor/models/commandr.py
-3
@@ -429,9 +429,6 @@ class CohereForCausalLM(nn.Module, SupportsLoRA, SupportsPP, SupportsQuant):
429
quant_config = vllm_config.quant_config
430
431
self.config = config
432
- # currently all existing command R models have `tie_word_embeddings`
433
- # enabled
434
- assert config.tie_word_embeddings
432
433
self.quant_config = quant_config
434
self.logits_processor = LogitsProcessor(
vllm/model_executor/models/ernie45_moe.py
+1
-4
@@ -629,8 +629,5 @@ class Ernie4_5_MoeForCausalLM(nn.Module, SupportsPP, SupportsLoRA, MixtureOfExpe
629
return logits
630
631
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
632
- loader = AutoWeightsLoader(
633
- self,
634
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
635
- )
632
+ loader = AutoWeightsLoader(self)
633
return loader.load_weights(weights)
vllm/model_executor/models/ernie_mtp.py
+1
-2
@@ -214,6 +214,5 @@ class ErnieMTP(nn.Module):
214
if any(k in name for k in ("mtp", "embed_tokens", "lm_head")):
215
yield name, weight
216
217
- skip_prefixes = ["lm_head"] if self.config.tie_word_embeddings else []
218
- loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
217
+ loader = AutoWeightsLoader(self)
218
return loader.load_weights(_filter(weights), mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/exaone.py
+1
-7
@@ -446,11 +446,5 @@ class ExaoneForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
446
return logits
447
448
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
449
- loader = AutoWeightsLoader(
450
- self,
451
- # With tie_word_embeddings, we can skip lm_head.weight
452
- # The weight might appear unnecessarily in the files if the model is
453
- # processed with quantization, LoRA, fine-tuning, etc.
454
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
455
- )
449
+ loader = AutoWeightsLoader(self)
450
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/exaone4.py
+1
-7
@@ -446,11 +446,5 @@ class Exaone4ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
446
return logits
447
448
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
449
- loader = AutoWeightsLoader(
450
- self,
451
- # With tie_word_embeddings, we can skip lm_head.weight
452
- # The weight might appear unnecessarily in the files if the model is
453
- # processed with quantization, LoRA, fine-tuning, etc.
454
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
455
- )
449
+ loader = AutoWeightsLoader(self)
450
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/exaone_moe.py
+1
-6
@@ -560,12 +560,7 @@ class ExaoneMoeForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
560
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
561
loader = AutoWeightsLoader(
562
self,
563
- # With tie_word_embeddings, we can skip lm_head.weight
564
- # The weight might appear unnecessarily in the files if the model is
565
- # processed with quantization, LoRA, fine-tuning, etc.
566
- skip_prefixes=(
567
- ["lm_head.", "mtp."] if self.config.tie_word_embeddings else ["mtp."]
568
- ),
563
+ skip_prefixes=["mtp."],
564
# Skip loading extra parameters for GPTQ/modelopt models.
565
ignore_unexpected_suffixes=[
566
".bias",
vllm/model_executor/models/fairseq2_llama.py
+1
-4
@@ -74,10 +74,7 @@ class Fairseq2LlamaForCausalLM(LlamaForCausalLM):
74
75
params = dict(self.named_parameters())
76
77
- loader = AutoWeightsLoader(
78
- self,
79
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
80
- )
77
+ loader = AutoWeightsLoader(self)
78
return loader.load_weights(
79
self.reshape_fairseq2_weights(name, loaded_weight, params)
80
for name, loaded_weight in weights
vllm/model_executor/models/falcon.py
+1
-4
@@ -535,8 +535,5 @@ class FalconForCausalLM(nn.Module, SupportsPP):
535
return logits
536
537
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
538
- loader = AutoWeightsLoader(
539
- self,
540
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
541
- )
538
+ loader = AutoWeightsLoader(self)
539
return loader.load_weights(weights)
vllm/model_executor/models/falcon_h1.py
+1
-4
@@ -643,8 +643,5 @@ class FalconH1ForCausalLM(
643
return logits
644
645
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
646
- loader = AutoWeightsLoader(
647
- self,
648
- skip_prefixes=(["lm_head."] if self.tie_word_embeddings else None),
649
- )
646
+ loader = AutoWeightsLoader(self)
647
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/gemma.py
+1
-6
@@ -346,8 +346,6 @@ class GemmaForCausalLM(nn.Module, SupportsLoRA, SupportsPP, SupportsQuant):
346
quant_config = vllm_config.quant_config
347
348
self.config = config
349
- # currently all existing Gemma models have `tie_word_embeddings` enabled
350
- assert config.tie_word_embeddings
349
350
self.quant_config = quant_config
351
self.model = GemmaModel(
@@ -381,8 +379,5 @@ class GemmaForCausalLM(nn.Module, SupportsLoRA, SupportsPP, SupportsQuant):
379
return logits
380
381
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
384
- loader = AutoWeightsLoader(
385
- self,
386
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
387
- )
382
+ loader = AutoWeightsLoader(self)
383
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/gemma2.py
+1
-6
@@ -335,8 +335,6 @@ class Gemma2ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
335
336
super().__init__()
337
self.config = config
338
- # currently all existing Gemma models have `tie_word_embeddings` enabled
339
- assert config.tie_word_embeddings
338
self.quant_config = quant_config
339
self.model = Gemma2Model(
340
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
@@ -371,8 +369,5 @@ class Gemma2ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
369
return logits
370
371
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
374
- loader = AutoWeightsLoader(
375
- self,
376
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
377
- )
372
+ loader = AutoWeightsLoader(self)
373
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/gemma3.py
+1
-4
@@ -442,8 +442,5 @@ class Gemma3ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
442
return logits
443
444
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
445
- loader = AutoWeightsLoader(
446
- self,
447
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
448
- )
445
+ loader = AutoWeightsLoader(self)
446
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/gemma4.py
-3
@@ -1694,15 +1694,12 @@ class Gemma4ForCausalLM(
1694
yield name, weight
1695
1696
# Skip multimodal weights — handled by the multimodal wrapper.
1697
- # Also skip lm_head when weights are tied.
1697
skip = [
1698
"audio_tower.",
1699
"vision_tower.",
1700
"embed_audio.",
1701
"embed_vision.",
1702
]
1704
- if self.config.tie_word_embeddings:
1705
- skip.append("lm_head.")
1703
1704
loader = AutoWeightsLoader(self, skip_substrs=skip)
1705
return loader.load_weights(_weight_iterator())
vllm/model_executor/models/glm4.py
+1
-2
@@ -292,11 +292,10 @@ class Glm4ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
292
return logits
293
294
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
295
- skip_prefixes = ["lm_head."] if self.config.tie_word_embeddings else []
295
# Skip the speculative (MTP) layers, which are loaded by the
296
# draft model instead.
297
num_nextn_layers = getattr(self.config, "num_nextn_predict_layers", 0)
299
- skip_prefixes += [
298
+ skip_prefixes = [
299
f"model.layers.{self.config.num_hidden_layers + i}."
300
for i in range(num_nextn_layers)
301
]
vllm/model_executor/models/gpt_oss.py
+1
-4
@@ -1242,8 +1242,5 @@ class GptOssForCausalLM(
1242
return logits
1243
1244
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1245
- loader = AutoWeightsLoader(
1246
- self,
1247
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
1248
- )
1245
+ loader = AutoWeightsLoader(self)
1246
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/granite.py
+1
-6
@@ -467,10 +467,5 @@ class GraniteForCausalLM(nn.Module, SupportsLoRA, SupportsPP, SupportsQuant):
467
)
468
469
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
470
- # With tie_word_embeddings, we can skip lm_head.weight
471
- # The weight might appear unnecessarily in the files if the model is
472
- # processed with quantization, LoRA, fine-tuning, etc.
473
- skip_prefixes = ["lm_head."] if self.config.tie_word_embeddings else None
474
-
475
- loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
470
+ loader = AutoWeightsLoader(self)
471
return loader.load_weights(weights)
vllm/model_executor/models/granitemoe.py
+1
-2
@@ -452,6 +452,5 @@ class GraniteMoeForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
452
)
453
454
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
455
- skip_prefixes = ["lm_head."] if self.config.tie_word_embeddings else None
456
- loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
455
+ loader = AutoWeightsLoader(self)
456
return loader.load_weights(weights)
vllm/model_executor/models/granitemoeshared.py
+1
-2
@@ -289,6 +289,5 @@ class GraniteMoeSharedForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
289
)
290
291
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
292
- skip_prefixes = ["lm_head."] if self.config.tie_word_embeddings else None
293
- loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
292
+ loader = AutoWeightsLoader(self)
293
return loader.load_weights(weights)
vllm/model_executor/models/hrm_text.py
+1
-2
@@ -521,6 +521,5 @@ class HrmTextForCausalLM(nn.Module):
521
return self.logits_processor(self.lm_head, hidden_states)
522
523
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
524
- skip_prefixes = ["lm_head."] if self.config.tie_word_embeddings else None
525
- loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
524
+ loader = AutoWeightsLoader(self)
525
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/hunyuan_v1.py
+1
-4
@@ -970,10 +970,7 @@ class HunyuanV1ModelBase(
970
)
971
972
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
973
- loader = AutoWeightsLoader(
974
- self,
975
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
976
- )
973
+ loader = AutoWeightsLoader(self)
974
return loader.load_weights(weights)
975
976
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
vllm/model_executor/models/hunyuan_vision.py
+1
-4
@@ -1034,10 +1034,7 @@ class HunYuanVLForConditionalGeneration(
1034
return self.language_model.compute_logits(hidden_states)
1035
1036
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1037
- loader = AutoWeightsLoader(
1038
- self,
1039
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
1040
- )
1037
+ loader = AutoWeightsLoader(self)
1038
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1039
1040
def get_mm_mapping(self) -> MultiModelKeys:
vllm/model_executor/models/hy_v3.py
+1
-4
@@ -710,10 +710,7 @@ class HYV3ForCausalLM(nn.Module, SupportsPP, SupportsLoRA):
710
continue
711
yield name, weight
712
713
- loader = AutoWeightsLoader(
714
- self,
715
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
716
- )
713
+ loader = AutoWeightsLoader(self)
714
return loader.load_weights(_filter_weights(weights))
715
716
def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
vllm/model_executor/models/hyperclovax.py
+1
-4
@@ -478,8 +478,5 @@ class HyperCLOVAXForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
478
self,
479
weights: Iterable[tuple[str, torch.Tensor]],
480
) -> set[str]:
481
- loader = AutoWeightsLoader(
482
- self,
483
- skip_prefixes=["lm_head."] if self.config.tie_word_embeddings else None,
484
- )
481
+ loader = AutoWeightsLoader(self)
482
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/internlm2.py
+1
-4
@@ -381,10 +381,7 @@ class InternLM2ForCausalLM(nn.Module, SupportsPP, SupportsLoRA, SupportsQuant):
381
return logits
382
383
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
384
- loader = AutoWeightsLoader(
385
- self,
386
- skip_prefixes=(["output."] if self.config.tie_word_embeddings else None),
387
- )
384
+ loader = AutoWeightsLoader(self)
385
return loader.load_weights(weights)
386
387
vllm/model_executor/models/iquest_loopcoder.py
+1
-4
@@ -575,8 +575,5 @@ class IQuestLoopCoderForCausalLM(nn.Module):
575
return logits
576
577
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
578
- loader = AutoWeightsLoader(
579
- self,
580
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
581
- )
578
+ loader = AutoWeightsLoader(self)
579
return loader.load_weights(weights)
vllm/model_executor/models/jais2.py
+1
-4
@@ -437,8 +437,5 @@ class Jais2ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
437
return logits
438
439
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
440
- loader = AutoWeightsLoader(
441
- self,
442
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
443
- )
440
+ loader = AutoWeightsLoader(self)
441
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/laguna.py
+1
-4
@@ -763,8 +763,5 @@ class LagunaForCausalLM(nn.Module, SupportsPP, SupportsLoRA, SupportsEagle3):
763
return logits
764
765
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
766
- loader = AutoWeightsLoader(
767
- self,
768
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
769
- )
766
+ loader = AutoWeightsLoader(self)
767
return loader.load_weights(weights)
vllm/model_executor/models/lfm2.py
+1
-4
@@ -508,8 +508,5 @@ class Lfm2ForCausalLM(
508
return logits
509
510
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
511
- loader = AutoWeightsLoader(
512
- self,
513
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
514
- )
511
+ loader = AutoWeightsLoader(self)
512
return loader.load_weights(weights)
vllm/model_executor/models/lfm2_moe.py
+1
-4
@@ -671,8 +671,5 @@ class Lfm2MoeForCausalLM(
671
return logits
672
673
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
674
- loader = AutoWeightsLoader(
675
- self,
676
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
677
- )
674
+ loader = AutoWeightsLoader(self)
675
return loader.load_weights(weights)
vllm/model_executor/models/llama.py
+1
-4
@@ -534,10 +534,7 @@ class LlamaForCausalLM(
534
return logits
535
536
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
537
- loader = AutoWeightsLoader(
538
- self,
539
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
540
- )
537
+ loader = AutoWeightsLoader(self)
538
return loader.load_weights(weights)
539
540
vllm/model_executor/models/llama4.py
+1
-4
@@ -792,10 +792,7 @@ class Llama4ForCausalLM(LlamaForCausalLM, MixtureOfExperts):
792
)
793
794
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
795
- loader = AutoWeightsLoader(
796
- self,
797
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
798
- )
795
+ loader = AutoWeightsLoader(self)
796
# Use a generator (not a list comprehension) so the weights iterator is
797
# consumed lazily by AutoWeightsLoader. Materializing it here would hold
798
# the entire language-model checkpoint in host memory at once, which can
vllm/model_executor/models/mimo.py
+1
-3
@@ -119,10 +119,8 @@ class MiMoForCausalLM(Qwen2ForCausalLM, nn.Module):
119
)
120
121
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
122
- skip_prefixes = ["lm_head."] if self.config.tie_word_embeddings else []
122
# MTP layers are loaded by the draft model, not the main model.
124
- skip_prefixes.append("model.mtp_layers.")
125
- loader = AutoWeightsLoader(self, skip_prefixes=skip_prefixes)
123
+ loader = AutoWeightsLoader(self, skip_prefixes=["model.mtp_layers."])
124
return loader.load_weights(weights)
125
126
def compute_logits(
vllm/model_executor/models/minicpm.py
+1
-4
@@ -648,8 +648,5 @@ class MiniCPMForCausalLM(
648
return logits
649
650
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
651
- loader = AutoWeightsLoader(
652
- self,
653
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
654
- )
651
+ loader = AutoWeightsLoader(self)
652
return loader.load_weights(weights)
vllm/model_executor/models/minicpm_eagle.py
+1
-4
@@ -387,8 +387,5 @@ class EagleMiniCPMForCausalLM(nn.Module, SupportsLoRA, SupportsPP, SupportsEagle
387
process_eagle_weight(self, name)
388
return name, loaded_weight
389
390
- loader = AutoWeightsLoader(
391
- self,
392
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
393
- )
390
+ loader = AutoWeightsLoader(self)
391
return loader.load_weights(map(transform, weights))
vllm/model_executor/models/mistral.py
+1
-4
@@ -277,10 +277,7 @@ class MistralForCausalLM(LlamaForCausalLM):
277
)
278
279
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
280
- loader = AutoWeightsLoader(
281
- self,
282
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
283
- )
280
+ loader = AutoWeightsLoader(self)
281
return loader.load_weights(
282
self.maybe_remap_mistral(name, loaded_weight)
283
for name, loaded_weight in weights
vllm/model_executor/models/mpt.py
-1
@@ -279,7 +279,6 @@ class MPTForCausalLM(nn.Module, SupportsPP):
279
config = vllm_config.model_config.hf_config
280
quant_config = vllm_config.quant_config
281
self.config = config
282
- assert config.tie_word_embeddings
282
self.quant_config = quant_config
283
284
self.transformer = MPTModel(
vllm/model_executor/models/nemotron_nas.py
+1
-4
@@ -412,8 +412,5 @@ class DeciLMForCausalLM(nn.Module, SupportsLoRA, SupportsPP, HasNoOps):
412
return logits
413
414
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
415
- loader = AutoWeightsLoader(
416
- self,
417
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
418
- )
415
+ loader = AutoWeightsLoader(self)
416
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/olmo3.py
+1
-6
@@ -408,10 +408,5 @@ class Olmo3ForCausalLM(nn.Module, SupportsPP, SupportsLoRA):
408
return logits
409
410
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
411
- loader = AutoWeightsLoader(
412
- self,
413
- skip_prefixes=(
414
- ["lm_head.weight"] if self.config.tie_word_embeddings else None
415
- ),
416
- )
411
+ loader = AutoWeightsLoader(self)
412
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/olmo_hybrid.py
+1
-6
@@ -474,10 +474,5 @@ class OlmoHybridForCausalLM(
474
return MambaStateCopyFuncCalculator.gated_delta_net_state_copy_func()
475
476
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
477
- loader = AutoWeightsLoader(
478
- self,
479
- skip_prefixes=(
480
- ["lm_head.weight"] if self.config.tie_word_embeddings else None
481
- ),
482
- )
477
+ loader = AutoWeightsLoader(self)
478
return loader.load_weights(weights)
vllm/model_executor/models/openpangu.py
+1
-4
@@ -1153,10 +1153,7 @@ class OpenPanguModelBase(nn.Module, SupportsPP, SupportsLoRA):
1153
return logits
1154
1155
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1156
- loader = AutoWeightsLoader(
1157
- self,
1158
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
1159
- )
1156
+ loader = AutoWeightsLoader(self)
1157
return loader.load_weights(weights)
1158
1159
vllm/model_executor/models/opt.py
+1
-6
@@ -384,10 +384,5 @@ class OPTForCausalLM(nn.Module, SupportsPP, SupportsLoRA):
384
return logits
385
386
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
387
- loader = AutoWeightsLoader(
388
- self,
389
- skip_prefixes=(
390
- ["lm_head.weight"] if self.config.tie_word_embeddings else None
391
- ),
392
- )
387
+ loader = AutoWeightsLoader(self)
388
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/plamo3.py
+1
-4
@@ -430,8 +430,5 @@ class Plamo3ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
430
return logits
431
432
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
433
- loader = AutoWeightsLoader(
434
- self,
435
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
436
- )
433
+ loader = AutoWeightsLoader(self)
434
return loader.load_weights(weights)
vllm/model_executor/models/qwen2.py
+1
-4
@@ -500,8 +500,5 @@ class Qwen2ForCausalLM(
500
return logits
501
502
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
503
- loader = AutoWeightsLoader(
504
- self,
505
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
506
- )
503
+ loader = AutoWeightsLoader(self)
504
return loader.load_weights(weights)
vllm/model_executor/models/qwen3.py
+1
-4
@@ -335,8 +335,5 @@ class Qwen3ForCausalLM(
335
return logits
336
337
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
338
- loader = AutoWeightsLoader(
339
- self,
340
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
341
- )
338
+ loader = AutoWeightsLoader(self)
339
return loader.load_weights(weights)
vllm/model_executor/models/rnj1.py
+1
-4
@@ -395,8 +395,5 @@ class Rnj1ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
395
return logits
396
397
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
398
- loader = AutoWeightsLoader(
399
- self,
400
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
401
- )
398
+ loader = AutoWeightsLoader(self)
399
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/sarvam.py
+1
-4
@@ -679,10 +679,7 @@ class SarvamMLAForCausalLM(nn.Module, SupportsPP, SupportsLoRA, SarvamMixtureOfE
679
self,
680
weights: Iterable[tuple[str, torch.Tensor]],
681
) -> set[str]:
682
- loader = AutoWeightsLoader(
683
- self,
684
- skip_prefixes=(["lm_head."] if self.tie_word_embeddings else None),
685
- )
682
+ loader = AutoWeightsLoader(self)
683
return loader.load_weights(weights)
684
685
vllm/model_executor/models/seed_oss.py
+1
-4
@@ -428,8 +428,5 @@ class SeedOssForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
428
return logits
429
430
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
431
- loader = AutoWeightsLoader(
432
- self,
433
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
434
- )
431
+ loader = AutoWeightsLoader(self)
432
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/telechat2.py
+1
-4
@@ -122,8 +122,5 @@ class TeleChat2ForCausalLM(LlamaForCausalLM):
122
return TeleChat2Model(vllm_config=vllm_config, prefix=prefix)
123
124
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
125
- loader = AutoWeightsLoader(
126
- self,
127
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
128
- )
125
+ loader = AutoWeightsLoader(self)
126
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/transformers/causal.py
-4
@@ -40,11 +40,7 @@ class CausalMixin(VllmModelForTextGeneration):
40
vllm_config=vllm_config, prefix=prefix
41
)
42
43
- # Tell `Base.load_weights` to skip
44
- # `lm_head` if the model has tied word embeddings
43
tie_word_embeddings = self._get_tie_word_embeddings()
46
- if tie_word_embeddings:
47
- self.skip_prefixes.append("lm_head.")
44
45
if self.pp_group.is_last_rank:
46
self.lm_head = ParallelLMHead(
vllm/model_executor/models/utils.py
+54
-6
@@ -18,6 +18,7 @@ from vllm.distributed import (
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
)
@@ -169,6 +170,20 @@ class WeightsMapper:
170
return replace(self, orig_to_new_stacked={})
171
172
173
+def _get_tied_embedding_params(module: nn.Module) -> dict[str, str]:
174
+ """Map each tied word embedding qualname to the first name it aliases."""
175
+ canonical = dict[int, str]()
176
+ aliased = dict[str, str]()
177
+ for prefix, submodule in module.named_modules(remove_duplicate=False):
178
+ if not isinstance(submodule, VocabParallelEmbedding):
179
+ continue
180
+ for name, param in submodule.named_parameters(remove_duplicate=False):
181
+ qualname = f"{prefix}.{name}" if prefix else name
182
+ if (first_name := canonical.setdefault(id(param), qualname)) != qualname:
183
+ aliased[qualname] = first_name
184
+ return aliased
185
+
186
+
187
class AutoWeightsLoader:
188
"""
189
Helper class to load weights into a [`torch.nn.Module`][]. It is able
@@ -213,6 +228,14 @@ class AutoWeightsLoader:
228
# update default skip_substrs
229
self.skip_substrs += self.ROTARY_EMBEDS_UNUSED_WEIGHTS
230
231
+ # Weight tying makes two qualnames point at the same `nn.Parameter`
232
+ # (e.g. `lm_head.weight` and `model.embed_tokens.weight`). Loading both
233
+ # would write the same storage twice, so only the first name reached by
234
+ # module traversal is loaded and the rest are skipped.
235
+ self.aliased_params = _get_tied_embedding_params(module)
236
+ self._skipped_aliases = dict[str, str]()
237
+ self._loaded_params_are_complete = True
238
+
239
def _groupby_prefix(
240
self,
241
weights: Iterable[tuple[str, torch.Tensor]],
@@ -242,8 +265,10 @@ class AutoWeightsLoader:
265
return ".".join((prefix, rest))
266
267
def _can_skip(self, qualname: str) -> bool:
245
- return any(qualname.startswith(p) for p in self.skip_prefixes) or any(
246
- substr in qualname for substr in self.skip_substrs
268
+ return (
269
+ qualname in self.aliased_params
270
+ or any(qualname.startswith(p) for p in self.skip_prefixes)
271
+ or any(substr in qualname for substr in self.skip_substrs)
272
)
273
274
def _can_ignore_unexpected(self, qualname: str) -> bool:
@@ -332,6 +357,7 @@ class AutoWeightsLoader:
357
logger.warning(
358
"Unable to collect loaded parameters for module %s", module
359
)
360
+ self._loaded_params_are_complete = False
361
else:
362
yield from map(
363
lambda x: self._get_qualname(base_prefix, x),
@@ -415,14 +441,36 @@ class AutoWeightsLoader:
441
self.ignore_unexpected_suffixes.extend(ignore_unexpected_suffixes)
442
if mapper is not None:
443
weights = mapper.apply(weights)
418
- # filter out weights with first-prefix/substr to skip in name
419
- weights = (
420
- (name, weight) for name, weight in weights if not self._can_skip(name)
421
- )
444
+ weights = self._filter_skipped(weights)
445
446
autoloaded_weights = set(self._load_module("", self.module, weights))
447
+ self._check_skipped_aliases(autoloaded_weights)
448
return autoloaded_weights
449
450
+ def _filter_skipped(
451
+ self, weights: Iterable[tuple[str, torch.Tensor]]
452
+ ) -> Iterable[tuple[str, torch.Tensor]]:
453
+ for name, weight in weights:
454
+ if (canonical := self.aliased_params.get(name)) is not None:
455
+ self._skipped_aliases[name] = canonical
456
+ if self._can_skip(name):
457
+ continue
458
+
459
+ yield name, weight
460
+
461
+ def _check_skipped_aliases(self, autoloaded_weights: set[str]) -> None:
462
+ """Guard against skipping an alias whose canonical name never loads."""
463
+ if not self._loaded_params_are_complete:
464
+ return
465
+ for alias, canonical in self._skipped_aliases.items():
466
+ if canonical not in autoloaded_weights:
467
+ raise ValueError(
468
+ f"{alias!r} was skipped because it is tied to {canonical!r} "
469
+ f"in {self.module._get_name()}, but {canonical!r} was not "
470
+ "found in the checkpoint, so the tied weight is "
471
+ "uninitialized."
472
+ )
473
+
474
475
def maybe_fuse_shared_experts(
476
weights: Iterable[tuple[str, torch.Tensor]],
vllm/models/kimi_k3/amd/linear.py
+1
-4
@@ -1082,8 +1082,5 @@ class KimiLinearForCausalLM(
1082
return self.logits_processor(self.lm_head, hidden_states)
1083
1084
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1085
- loader = AutoWeightsLoader(
1086
- self,
1087
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
1088
- )
1085
+ loader = AutoWeightsLoader(self)
1086
return loader.load_weights(weights)
vllm/models/kimi_k3/nvidia/model.py
+1
-4
@@ -1676,10 +1676,7 @@ class KimiLinearForCausalLM(
1676
return self.logits_processor(self.lm_head, hidden_states)
1677
1678
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1679
- loader = AutoWeightsLoader(
1680
- self,
1681
- skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
1682
- )
1679
+ loader = AutoWeightsLoader(self)
1680
loaded = loader.load_weights(weights)
1681
self.model.finalize_mega_moe_weights()
1682
# The fused MultiHeadLatentAttention's process_weights_after_loading
vllm/transformers_utils/config.py
+21
-1
@@ -1112,7 +1112,9 @@ def try_get_safetensors_metadata(
1112
1113
try:
1114
return with_retry(
1115
- get_safetensors_metadata_partial, "Error retrieving safetensors"
1115
+ get_safetensors_metadata_partial,
1116
+ "Error retrieving safetensors",
1117
+ fatal_errors=(huggingface_hub.errors.NotASafetensorsRepoError,),
1118
)
1119
except Exception:
1120
return None
@@ -1218,6 +1220,24 @@ def get_safetensors_params_metadata(
1220
return _read_safetensors_metadata_in_dir(Path(local_dir))
1221
1222
1223
+@cache
1224
+def checkpoint_has_lm_head(model: str, *, revision: str | None = None) -> bool | None:
1225
+ """Whether the checkpoint contains an `lm_head` tensor of its own.
1226
+
1227
+ Args:
1228
+ model: Name or path of the model repository.
1229
+ revision: The specific model version to use.
1230
+
1231
+ Returns:
1232
+ `None` if the checkpoint contents could not be determined, for example
1233
+ because it is not stored as safetensors.
1234
+ """
1235
+ metadata = get_safetensors_params_metadata(model, revision=revision)
1236
+ if not metadata:
1237
+ return None
1238
+ return any(name.endswith("lm_head.weight") for name in metadata)
1239
+
1240
+
1241
def _download_mistral_config_file(model, revision) -> dict:
1242
config_file_name = "params.json"
1243
config_dict = get_hf_file_to_dict(config_file_name, model, revision)
vllm/transformers_utils/repo_utils.py
+13
@@ -95,10 +95,23 @@ def with_retry(
95
log_msg: str,
96
max_retries: int = 2,
97
retry_delay: int = 2,
98
+ fatal_errors: tuple[type[Exception], ...] = (),
99
) -> _R:
100
+ """Call `func`, retrying transient failures.
101
+
102
+ Args:
103
+ func: The call to make.
104
+ log_msg: Prefix for the message logged when a call fails.
105
+ max_retries: How many times to call `func` before giving up.
106
+ retry_delay: Seconds to wait after the first failure, doubled each time.
107
+ fatal_errors: Exceptions that are a definitive answer rather than a
108
+ transient failure. These are raised immediately, without logging.
109
+ """
110
for attempt in range(max_retries):
111
try:
112
return func()
113
+ except fatal_errors:
114
+ raise
115
except Exception as e:
116
if attempt == max_retries - 1:
117
logger.error("%s: %s", log_msg, e)