[Bugfix] when loading weights skip empty expert bias if model does not support them (#50937)

Signed-off-by: walterbm <walter.beller.morales@gmail.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-authored-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
This commit is contained in:
Walter Beller-Morales
2026-08-07 12:45:52 -07:00
committed by GitHub
co-authored by Harry Mellor
parent ac70ce96e0
commit 70456e5e6f
2 changed files with 89 additions and 1 deletions
@@ -14,6 +14,8 @@ import torch
from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
from .utils import make_dummy_moe_config
class TestGetHiddenDim:
"""Unit tests for _get_hidden_dim."""
@@ -376,6 +378,85 @@ class TestWeightLoadingWithPaddedHiddenSize:
)
class TestLoadWeightsExpertBias:
"""Some quantized exports (e.g. GPTQ, llm-compressor NVFP4) materialize
all-zero per-expert `.bias` tensors for models whose experts have no bias
params. `RoutedExperts.load_weights` needs to ignore them like
`AutoWeightsLoader` does, instead of raising AttributeError for the
nonexistent `w13_bias`/`w2_bias` params.
"""
NUM_EXPERTS = 2
def _make_experts(self, has_bias: bool) -> torch.nn.Module:
experts = torch.nn.Module()
experts.layer_name = "model.layers.0.mlp.experts"
experts.moe_config = make_dummy_moe_config(num_experts=self.NUM_EXPERTS)
mapping = RoutedExperts.build_expert_params_mapping(
"gate_proj",
"down_proj",
"up_proj",
num_experts=self.NUM_EXPERTS,
routed_experts_prefix="",
include_fused=True,
)
experts.get_expert_mapping = lambda **_: mapping
def weight_loader(**_):
return True
names = ["w13_weight", "w2_weight"]
if has_bias:
names += ["w13_bias", "w2_bias"]
for name in names:
param = torch.nn.Parameter(torch.zeros(1), requires_grad=False)
param.weight_loader = weight_loader
setattr(experts, name, param)
return experts
def _checkpoint_weights(self) -> list[tuple[str, torch.Tensor]]:
return [
(f"{expert_id}.{proj}.{suffix}", torch.zeros(1, 1))
for expert_id in range(self.NUM_EXPERTS)
for proj in ("gate_proj", "up_proj", "down_proj")
for suffix in ("weight", "bias")
]
def test_bias_free_experts_ignore_checkpoint_biases(self):
experts = self._make_experts(has_bias=False)
loaded = list(RoutedExperts.load_weights(experts, self._checkpoint_weights()))
assert set(loaded) == {"w13_weight", "w2_weight"}
def test_experts_with_bias_params_load_checkpoint_biases(self):
experts = self._make_experts(has_bias=True)
loaded = list(RoutedExperts.load_weights(experts, self._checkpoint_weights()))
assert set(loaded) == {"w13_weight", "w2_weight", "w13_bias", "w2_bias"}
def test_missing_non_bias_param_names_the_weight(self):
experts = self._make_experts(has_bias=False)
weights = [("0.down_proj.new_scale", torch.zeros(1, 1))]
with pytest.raises(AttributeError, match="w2_new_scale"):
list(RoutedExperts.load_weights(experts, weights))
@pytest.mark.parametrize(
"expert_name,param_name",
[
# Pre-fused checkpoints name biases `<proj>_bias` (gpt-oss) or
# `<proj>.bias` (quark), neither of which rewrites to a real param.
# Skipping them would silently drop a bias the layer does have, so
# they must raise; models rename them via WeightsMapper instead.
("down_proj_bias", "w2_weight_bias"),
("gate_up_proj_bias", "w13_weight_bias"),
("down_proj.bias", "w2_weight.bias"),
],
)
def test_fused_bias_names_are_not_skipped(self, expert_name, param_name):
experts = self._make_experts(has_bias=True)
weights = [(expert_name, torch.zeros(self.NUM_EXPERTS, 1))]
with pytest.raises(AttributeError, match=param_name.replace(".", r"\.")):
list(RoutedExperts.load_weights(experts, weights))
class TestPerTensorScaleCoercion:
"""Regression test for shape-(1,) per-tensor scales (issue #43297).
@@ -936,7 +936,14 @@ class RoutedExperts(PluggableLayer):
matched = True
weight_name = qual_name.replace(weight_name, param_name)
param_name = weight_name.removeprefix(f"{self.layer_name}.")
param = getattr(self, param_name)
param = getattr(self, param_name, None)
if param is None:
if param_name.endswith(("w13_bias", "w2_bias")):
continue
raise AttributeError(
f"Layer {self.layer_name} has no parameter {param_name!r} "
f"for checkpoint weight {qual_name!r}"
)
if is_fused:
# w1 and w3 share one fused tensor; use a local copy so the
# transpose below doesn't mutate loaded_weight across