Skip to content

Commit 3136f09

Browse files
hmellornoooop
authored andcommitted
Remove unnecessary load_weights methods (vllm-project#44589)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com> Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>
1 parent 6c9a30f commit 3136f09

54 files changed

Lines changed: 821 additions & 1975 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

tests/model_executor/test_weight_utils.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,126 @@ def test_missing_target_returns_none(self):
160160
assert result is None
161161

162162

163+
class TestKvCacheScaleMapper:
164+
"""The `WeightsMapper` returned by `get_cache_scale_mapper` replaces the
165+
per-model `maybe_remap_kv_scale_name` calls. It must remap the same set of
166+
checkpoint formats (the non-`params_dict`-dependent ones) and be idempotent
167+
so it composes safely with a model's own qkv/gate_up `hf_to_vllm_mapper`."""
168+
169+
def _mapper(self):
170+
# `get_cache_scale_mapper` does not use `self`; call it on the base
171+
# class to get the default (non-config-specific) mapper.
172+
from vllm.model_executor.layers.quantization.base_config import (
173+
QuantizationConfig,
174+
)
175+
176+
return QuantizationConfig.get_cache_scale_mapper()
177+
178+
def _map(self, name: str) -> str | None:
179+
return self._mapper()._map_name(name)
180+
181+
@pytest.mark.parametrize(
182+
"name,expected",
183+
[
184+
# Qwen3-MoE / llm-compressor fused qkv_proj
185+
(
186+
"model.layers.0.self_attn.qkv_proj.k_scale",
187+
"model.layers.0.self_attn.attn.k_scale",
188+
),
189+
(
190+
"model.layers.0.self_attn.qkv_proj.v_scale",
191+
"model.layers.0.self_attn.attn.v_scale",
192+
),
193+
# ModelOpt / NVFP4 k_proj/v_proj
194+
(
195+
"model.layers.0.self_attn.k_proj.k_scale",
196+
"model.layers.0.self_attn.attn.k_scale",
197+
),
198+
(
199+
"model.layers.0.self_attn.v_proj.v_scale",
200+
"model.layers.0.self_attn.attn.v_scale",
201+
),
202+
# deprecated fused kv_scale and bare scales
203+
(
204+
"model.layers.0.self_attn.kv_scale",
205+
"model.layers.0.self_attn.attn.k_scale",
206+
),
207+
(
208+
"model.layers.0.self_attn.k_scale",
209+
"model.layers.0.self_attn.attn.k_scale",
210+
),
211+
# NemotronH mixer
212+
(
213+
"model.layers.0.mixer.k_proj.k_scale",
214+
"model.layers.0.mixer.attn.k_scale",
215+
),
216+
# already in vLLM form -> unchanged (idempotent)
217+
(
218+
"model.layers.0.self_attn.attn.k_scale",
219+
"model.layers.0.self_attn.attn.k_scale",
220+
),
221+
# non-kv scales must not be touched
222+
(
223+
"model.layers.0.self_attn.k_proj.weight_scale",
224+
"model.layers.0.self_attn.k_proj.weight_scale",
225+
),
226+
(
227+
"model.layers.0.self_attn.k_proj.input_scale",
228+
"model.layers.0.self_attn.k_proj.input_scale",
229+
),
230+
# regular weights untouched
231+
(
232+
"model.layers.0.self_attn.q_proj.weight",
233+
"model.layers.0.self_attn.q_proj.weight",
234+
),
235+
],
236+
)
237+
def test_remap(self, name, expected):
238+
assert self._map(name) == expected
239+
240+
@pytest.mark.parametrize(
241+
"name",
242+
[
243+
"model.layers.0.self_attn.k_scale",
244+
"model.layers.0.self_attn.k_proj.k_scale",
245+
"model.layers.0.self_attn.qkv_proj.v_scale",
246+
"model.layers.0.mixer.k_proj.k_scale",
247+
],
248+
)
249+
def test_idempotent(self, name):
250+
once = self._map(name)
251+
assert once is not None
252+
assert self._map(once) == once
253+
254+
def test_composes_with_qkv_mapper(self):
255+
"""Applied together with a model's qkv/gate_up mapper, the regex scale
256+
rules run before the substr rename, so scales are normalized to `.attn.`
257+
and regular projections are still fused correctly."""
258+
from vllm.model_executor.models.utils import WeightsMapper
259+
260+
model_mapper = WeightsMapper(
261+
orig_to_new_substr={
262+
".q_proj": ".qkv_proj.q",
263+
".k_proj": ".qkv_proj.k",
264+
".v_proj": ".qkv_proj.v",
265+
}
266+
)
267+
# AutoWeightsLoader does `mapper |= cache_scale_mapper`
268+
combined = model_mapper | self._mapper()
269+
270+
assert (
271+
combined._map_name("model.layers.0.self_attn.q_proj.weight")
272+
== "model.layers.0.self_attn.qkv_proj.q.weight"
273+
)
274+
assert (
275+
combined._map_name("model.layers.0.self_attn.k_proj.k_scale")
276+
== "model.layers.0.self_attn.attn.k_scale"
277+
)
278+
assert (
279+
combined._map_name("model.layers.0.self_attn.k_scale")
280+
== "model.layers.0.self_attn.attn.k_scale"
281+
)
282+
283+
163284
if __name__ == "__main__":
164285
test_download_weights_from_hf()

vllm/lora/worker_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,13 @@ def _load_adapter(self, lora_request: LoRARequest) -> LoRAModel:
128128
peft_helper.validate_legal(self.lora_config)
129129

130130
# For some models like Qwen2VL, we need to use hf_to_vllm_mapper
131-
# to ensure correct loading of lora weights.
131+
# to ensure correct loading of lora weights. Drop the QKV/MLP fusion
132+
# substr maps so constituent names (e.g. `q_proj`) survive for the
133+
# LoRA manager to pack, while keeping genuine renames/prefixes.
132134
model = self._adapter_manager.model
133135
hf_to_vllm_mapper = getattr(model, "hf_to_vllm_mapper", None)
136+
if hf_to_vllm_mapper is not None:
137+
hf_to_vllm_mapper = hf_to_vllm_mapper.get_unstacked_mapper()
134138

135139
# Get model-defined prefixes to skip during LoRA loading.
136140
lora_skip_prefixes = getattr(model, "lora_skip_prefixes", None)

vllm/model_executor/layers/linear.py

Lines changed: 85 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
import itertools
55
from abc import abstractmethod
6+
from collections.abc import Iterable
7+
from typing import Any
68

79
import torch
810
from torch.nn.parameter import Parameter
11+
from typing_extensions import TypeIs
912

1013
import vllm.envs as envs
1114
from vllm.distributed import (
@@ -632,31 +635,31 @@ def __init__(
632635
disable_tp=disable_tp,
633636
)
634637

635-
def validate_shard_id(self, loaded_shard_id: int | tuple[int, ...] | None):
636-
if loaded_shard_id is None:
637-
return
638-
if isinstance(loaded_shard_id, tuple):
639-
for idx in loaded_shard_id:
638+
def validate_shard_id(self, shard_id: Any) -> TypeIs[int | tuple[int, ...] | None]:
639+
if isinstance(shard_id, int):
640+
if shard_id < 0 or shard_id >= len(self.output_sizes):
641+
raise ValueError(
642+
f"Shard id should be between 0 and {len(self.output_sizes) - 1}. "
643+
f"Got shard id {shard_id}."
644+
)
645+
return True
646+
if shard_id is None:
647+
return True
648+
if isinstance(shard_id, tuple):
649+
for idx in shard_id:
640650
if not (0 <= idx < len(self.output_sizes)):
641651
raise ValueError(
642652
f"Shard id index {idx} should be between 0 and "
643-
f"{len(self.output_sizes) - 1}. Got shard id {loaded_shard_id}."
653+
f"{len(self.output_sizes) - 1}. Got shard id {shard_id}."
644654
)
645-
if len(loaded_shard_id) > 1 and any(
646-
b - a != 1 for a, b in zip(loaded_shard_id[:-1], loaded_shard_id[1:])
655+
if len(shard_id) > 1 and any(
656+
b - a != 1 for a, b in zip(shard_id[:-1], shard_id[1:])
647657
):
648658
raise ValueError(
649659
"Shard id with multiple indices should be consecutive. "
650-
f"Got shard id {loaded_shard_id}."
660+
f"Got shard id {shard_id}."
651661
)
652-
return
653-
elif isinstance(loaded_shard_id, int):
654-
if loaded_shard_id < 0 or loaded_shard_id >= len(self.output_sizes):
655-
raise ValueError(
656-
f"Shard id should be between 0 and {len(self.output_sizes) - 1}. "
657-
f"Got shard id {loaded_shard_id}."
658-
)
659-
return
662+
return True
660663
raise ValueError("This line should not be reached")
661664

662665
def weight_loader(
@@ -910,6 +913,31 @@ def weight_loader_v2(
910913
tp_rank=self.tp_rank,
911914
)
912915

916+
def load_weights(
917+
self, weights: Iterable[tuple[str, torch.Tensor]]
918+
) -> Iterable[str]:
919+
for name, loaded_weight in weights:
920+
shard_id = getattr(loaded_weight, "shard_id", None)
921+
self.validate_shard_id(shard_id)
922+
# Load into self if name is not an attr of self or its submodules
923+
param: Parameter
924+
if "." in name:
925+
submodule, _, attr = name.rpartition(".")
926+
param = getattr(self.get_submodule(submodule), attr, self)
927+
else:
928+
param = getattr(self, name, self)
929+
if param is None and name == "bias":
930+
continue
931+
param.weight_loader(param, loaded_weight, shard_id)
932+
logger.debug(
933+
"Loaded shard %s with shape %s into %s.%s",
934+
shard_id,
935+
loaded_weight.shape,
936+
self.prefix,
937+
name,
938+
)
939+
yield name
940+
913941

914942
class QKVParallelLinear(ColumnParallelLinear):
915943
"""Linear layers for the attention's QKV transformation.
@@ -996,17 +1024,13 @@ def __init__(
9961024
disable_tp=disable_tp,
9971025
)
9981026

999-
def validate_shard_id(self, loaded_shard_id: str | None):
1000-
if loaded_shard_id is None:
1001-
return
1002-
if isinstance(loaded_shard_id, str):
1003-
if loaded_shard_id not in ["q", "k", "v"]:
1004-
raise ValueError(
1005-
"Shard id for QKVParallelLinear should be 'q', 'k', or 'v', "
1006-
f"got shard id {loaded_shard_id}."
1007-
)
1008-
return
1009-
raise ValueError("This line should not be reached")
1027+
def validate_shard_id(self, shard_id: Any) -> TypeIs[str | None]:
1028+
if shard_id in {"q", "k", "v"} or shard_id is None:
1029+
return True
1030+
raise ValueError(
1031+
"Shard id for QKVParallelLinear should be 'q', 'k', or 'v', "
1032+
f"got shard id {shard_id}."
1033+
)
10101034

10111035
def _get_shard_offset_mapping(self, loaded_shard_id: str):
10121036
shard_offset_mapping = {
@@ -1302,6 +1326,31 @@ def weight_loader(
13021326
assert param_data.shape == loaded_weight.shape
13031327
param_data.copy_(loaded_weight)
13041328

1329+
def load_weights(
1330+
self, weights: Iterable[tuple[str, torch.Tensor]]
1331+
) -> Iterable[str]:
1332+
for name, loaded_weight in weights:
1333+
shard_id = getattr(loaded_weight, "shard_id", None)
1334+
self.validate_shard_id(shard_id)
1335+
# Load into self if name is not an attr of self or its submodules
1336+
param: Parameter
1337+
if "." in name:
1338+
submodule, _, attr = name.rpartition(".")
1339+
param = getattr(self.get_submodule(submodule), attr, self)
1340+
else:
1341+
param = getattr(self, name, self)
1342+
if param is None and name == "bias":
1343+
continue
1344+
param.weight_loader(param, loaded_weight, shard_id)
1345+
logger.debug(
1346+
"Loaded shard %s with shape %s into %s.%s",
1347+
shard_id,
1348+
loaded_weight.shape,
1349+
self.prefix,
1350+
name,
1351+
)
1352+
yield name
1353+
13051354

13061355
class MinimaxM3QKVParallelLinearWithIndexer(QKVParallelLinear):
13071356
"""QKV projection fused with a lightning-indexer's index_q/index_k.
@@ -1387,15 +1436,14 @@ def __init__(
13871436
prefix=prefix,
13881437
)
13891438

1390-
def validate_shard_id(self, loaded_shard_id: str | None) -> None:
1391-
if loaded_shard_id is None:
1392-
return
1393-
if loaded_shard_id not in ("q", "k", "v", "index_q", "index_k"):
1394-
raise ValueError(
1395-
"Shard id for MinimaxM3QKVParallelLinearWithIndexer must be one of "
1396-
"'q', 'k', 'v', 'index_q', 'index_k'; got "
1397-
f"{loaded_shard_id}."
1398-
)
1439+
def validate_shard_id(self, shard_id: Any) -> TypeIs[str | None]:
1440+
if shard_id in {"q", "k", "v", "index_q", "index_k"} or shard_id is None:
1441+
return True
1442+
raise ValueError(
1443+
"Shard id for MinimaxM3QKVParallelLinearWithIndexer must be one of "
1444+
"'q', 'k', 'v', 'index_q', 'index_k'; got "
1445+
f"{shard_id}."
1446+
)
13991447

14001448
def _get_shard_offset_mapping(self, loaded_shard_id: str) -> int | None:
14011449
h = self.head_size

0 commit comments

Comments
 (0)