Files
vllm-project--vllm/tests/model_executor/test_weight_utils.py
T
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

286 lines
11 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import tempfile
import huggingface_hub.constants
import pytest
from huggingface_hub.utils import LocalEntryNotFoundError
from vllm.model_executor.model_loader.weight_utils import (
download_weights_from_hf,
maybe_remap_kv_scale_name,
)
def test_download_weights_from_hf():
with tempfile.TemporaryDirectory() as tmpdir:
# assert LocalEntryNotFoundError error is thrown
# if offline is set and model is not cached
huggingface_hub.constants.HF_HUB_OFFLINE = True
with pytest.raises(LocalEntryNotFoundError):
download_weights_from_hf(
"facebook/opt-125m",
allow_patterns=["*.safetensors", "*.bin"],
cache_dir=tmpdir,
)
# download the model
huggingface_hub.constants.HF_HUB_OFFLINE = False
download_weights_from_hf(
"facebook/opt-125m",
allow_patterns=["*.safetensors", "*.bin"],
cache_dir=tmpdir,
)
# now it should work offline
huggingface_hub.constants.HF_HUB_OFFLINE = True
assert (
download_weights_from_hf(
"facebook/opt-125m",
allow_patterns=["*.safetensors", "*.bin"],
cache_dir=tmpdir,
)
is not None
)
class TestMaybeRemapKvScaleName:
"""Tests for maybe_remap_kv_scale_name covering all checkpoint formats."""
PARAMS_DICT = {
"model.layers.0.self_attn.attn.k_scale": None,
"model.layers.0.self_attn.attn.v_scale": None,
"model.layers.0.self_attn.attn.q_scale": None,
"model.layers.0.self_attn.qkv_proj.weight": None,
}
def test_qkv_proj_k_scale(self):
"""Qwen3-MoE / llm-compressor format: qkv_proj.k_scale -> attn.k_scale
Regression test for https://github.com/vllm-project/vllm/issues/25047"""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.qkv_proj.k_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.k_scale"
def test_qkv_proj_v_scale(self):
"""Qwen3-MoE / llm-compressor format: qkv_proj.v_scale -> attn.v_scale
Regression test for https://github.com/vllm-project/vllm/issues/25047"""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.qkv_proj.v_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.v_scale"
def test_modelopt_k_proj_k_scale(self):
"""ModelOpt format: k_proj.k_scale -> attn.k_scale"""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.k_proj.k_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.k_scale"
def test_modelopt_v_proj_v_scale(self):
"""ModelOpt format: v_proj.v_scale -> attn.v_scale"""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.v_proj.v_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.v_scale"
def test_deprecated_kv_scale(self):
"""Old format: kv_scale -> attn.k_scale (deprecated)"""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.kv_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.k_scale"
def test_default_bare_k_scale(self):
"""Default format: .k_scale -> .attn.k_scale"""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.k_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.k_scale"
def test_non_scale_name_unchanged(self):
"""Non-scale names should be returned unchanged."""
name = "model.layers.0.self_attn.qkv_proj.weight"
result = maybe_remap_kv_scale_name(name, self.PARAMS_DICT)
assert result == name
def test_nvfp4_modelopt_k_proj_k_scale(self):
"""ModelOpt NVFP4 format (e.g. nvidia/Qwen3-30B-A3B-NVFP4):
k_proj.k_scale -> attn.k_scale.
Validates that NVFP4 checkpoints are not broken by this change."""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.k_proj.k_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.k_scale"
def test_nvfp4_modelopt_v_proj_v_scale(self):
"""ModelOpt NVFP4 format (e.g. nvidia/Qwen3-30B-A3B-NVFP4):
v_proj.v_scale -> attn.v_scale.
Validates that NVFP4 checkpoints are not broken by this change."""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.v_proj.v_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.v_scale"
def test_qwen3_vl_moe_qkv_proj_k_scale(self):
"""Qwen3-VL-MoE uses the same fused qkv_proj naming as Qwen3-MoE.
Regression test for qwen3_vl_moe.py fix (same bug as #25047)."""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.qkv_proj.k_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.k_scale"
def test_qwen3_vl_moe_qkv_proj_v_scale(self):
"""Qwen3-VL-MoE uses the same fused qkv_proj naming as Qwen3-MoE.
Regression test for qwen3_vl_moe.py fix (same bug as #25047)."""
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.qkv_proj.v_scale", self.PARAMS_DICT
)
assert result == "model.layers.0.self_attn.attn.v_scale"
def test_nvfp4_weight_scale_not_remapped(self):
"""NVFP4 weight_scale should not be touched by remap (not a kv scale)."""
name = "model.layers.0.self_attn.k_proj.weight_scale"
result = maybe_remap_kv_scale_name(name, self.PARAMS_DICT)
assert result == name
def test_nvfp4_input_scale_not_remapped(self):
"""NVFP4 input_scale should not be touched by remap (not a kv scale)."""
name = "model.layers.0.self_attn.k_proj.input_scale"
result = maybe_remap_kv_scale_name(name, self.PARAMS_DICT)
assert result == name
def test_missing_target_returns_none(self):
"""If remapped name not in params_dict, return None."""
empty_params: dict[str, None] = {}
result = maybe_remap_kv_scale_name(
"model.layers.0.self_attn.qkv_proj.k_scale", empty_params
)
assert result is None
class TestKvCacheScaleMapper:
"""The `WeightsMapper` returned by `get_cache_scale_mapper` replaces the
per-model `maybe_remap_kv_scale_name` calls. It must remap the same set of
checkpoint formats (the non-`params_dict`-dependent ones) and be idempotent
so it composes safely with a model's own qkv/gate_up `hf_to_vllm_mapper`."""
def _mapper(self):
# `get_cache_scale_mapper` does not use `self`; call it on the base
# class to get the default (non-config-specific) mapper.
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig,
)
return QuantizationConfig.get_cache_scale_mapper()
def _map(self, name: str) -> str | None:
return self._mapper()._map_name(name)
@pytest.mark.parametrize(
"name,expected",
[
# Qwen3-MoE / llm-compressor fused qkv_proj
(
"model.layers.0.self_attn.qkv_proj.k_scale",
"model.layers.0.self_attn.attn.k_scale",
),
(
"model.layers.0.self_attn.qkv_proj.v_scale",
"model.layers.0.self_attn.attn.v_scale",
),
# ModelOpt / NVFP4 k_proj/v_proj
(
"model.layers.0.self_attn.k_proj.k_scale",
"model.layers.0.self_attn.attn.k_scale",
),
(
"model.layers.0.self_attn.v_proj.v_scale",
"model.layers.0.self_attn.attn.v_scale",
),
# deprecated fused kv_scale and bare scales
(
"model.layers.0.self_attn.kv_scale",
"model.layers.0.self_attn.attn.k_scale",
),
(
"model.layers.0.self_attn.k_scale",
"model.layers.0.self_attn.attn.k_scale",
),
# NemotronH mixer
(
"model.layers.0.mixer.k_proj.k_scale",
"model.layers.0.mixer.attn.k_scale",
),
# already in vLLM form -> unchanged (idempotent)
(
"model.layers.0.self_attn.attn.k_scale",
"model.layers.0.self_attn.attn.k_scale",
),
# non-kv scales must not be touched
(
"model.layers.0.self_attn.k_proj.weight_scale",
"model.layers.0.self_attn.k_proj.weight_scale",
),
(
"model.layers.0.self_attn.k_proj.input_scale",
"model.layers.0.self_attn.k_proj.input_scale",
),
# regular weights untouched
(
"model.layers.0.self_attn.q_proj.weight",
"model.layers.0.self_attn.q_proj.weight",
),
],
)
def test_remap(self, name, expected):
assert self._map(name) == expected
@pytest.mark.parametrize(
"name",
[
"model.layers.0.self_attn.k_scale",
"model.layers.0.self_attn.k_proj.k_scale",
"model.layers.0.self_attn.qkv_proj.v_scale",
"model.layers.0.mixer.k_proj.k_scale",
],
)
def test_idempotent(self, name):
once = self._map(name)
assert once is not None
assert self._map(once) == once
def test_composes_with_qkv_mapper(self):
"""Applied together with a model's qkv/gate_up mapper, the regex scale
rules run before the substr rename, so scales are normalized to `.attn.`
and regular projections are still fused correctly."""
from vllm.model_executor.models.utils import WeightsMapper
model_mapper = WeightsMapper(
orig_to_new_substr={
".q_proj": ".qkv_proj.q",
".k_proj": ".qkv_proj.k",
".v_proj": ".qkv_proj.v",
}
)
# AutoWeightsLoader does `mapper |= cache_scale_mapper`
combined = model_mapper | self._mapper()
assert (
combined._map_name("model.layers.0.self_attn.q_proj.weight")
== "model.layers.0.self_attn.qkv_proj.q.weight"
)
assert (
combined._map_name("model.layers.0.self_attn.k_proj.k_scale")
== "model.layers.0.self_attn.attn.k_scale"
)
assert (
combined._map_name("model.layers.0.self_attn.k_scale")
== "model.layers.0.self_attn.attn.k_scale"
)
if __name__ == "__main__":
test_download_weights_from_hf()