Prhub

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

原始 PR 作者 walterbm 合并时间 2026-08-08 03:45 文件变更 2 提交数 4 评论 3 代码增减 +89 / -1

执行摘要

修复 MoE 权重加载时空 bias 参数导致的崩溃

PR body 明确指出:"After the weight loading refactor in #47058 models with unused expert bias tensors started to fail on load with AttributeError: 'RoutedExperts' object has no attribute 'w2_bias'";原因是"some quantized exports (particularly those that use GPTQ or llm-compressor) include empty bias even for models that don't need them",重构移除了相关容错逻辑。因为 AutoWeightsLoader.load_weights 已有同类跳过处理,本 PR 将该模式复制到 RoutedExperts.load_weights,恢复对冗余空 bias 的兼容。

值得精读。改动虽小,却展示了重构后兼容性修复的正确姿势:从 AutoWeightsLoader 复用既有模式,同时用测试明确划定“可跳过”与“必须报错”的边界,特别是 test_fused_bias_names_are_not_skipped 防止静默丢权重。关注 RoutedExperts.load_weights 与 AutoWeightsLoader 的一致性,以及测试如何用 dummy config 低成本复现真实加载路径。

讨论亮点
  • claude[bot] 提示 fork 仓库默认不自动审查,需维护者手动 @claude review;本轮未触发人工 AI 审查,最终由 hmellor、mgoin 两名维护者直接批准。
  • hmellor 在提交 "tighter scope" 中收窄了跳过条件:只有映射后 param_name 以 w13_bias 或 w2_bias 结尾的缺失参数才被跳过,其他缺失参数继续抛 AttributeError,避免静默吞掉任何未知权重名。
  • 新增的 test_fused_bias_names_are_not_skipped 明确断言预融合 bias 命名(如 down_proj_bias)不被跳过,因为这些权重需要模型通过 WeightsMapper 显式重命名,跳过会造成真实 bias 静默丢失,这一边界在 review 中被保留。

实现拆解

  1. 修改 vllm/model_executor/layers/fused_moe/routed_experts.py 的 RoutedExperts.load_weights:将 param = getattr(self, param_name) 替换为 param = getattr(self, param_name, None),当参数缺失且映射后的名字以 w13_bias 或 w2_bias 结尾时直接 continue 跳过;否则抛出带 layer_name 与 checkpoint 权重名的 AttributeError,保留快速失败语义。
  2. 在 tests/kernels/moe/test_moe_weight_loading_padded.py 新增 TestLoadWeightsExpertBias 测试类,并通过 from .utils import make_dummy_moe_config 引入测试工具;_make_experts(has_bias) 使用 RoutedExperts.build_expert_params_mapping 构造带或不带 bias 参数的专家替身,_checkpoint_weights() 则为每个专家生成 gate_proj/up_proj/down_proj 的 weight + bias 张量。
  3. 测试覆盖四类行为:无 bias 参数模型忽略 checkpoint 中的空 bias;有 bias 参数模型正常加载 bias;缺失非 bias 参数(如 new_scale)仍抛 AttributeError 并显示映射后的参数名;预融合命名(down_proj_bias、gate_up_proj_bias、down_proj.bias)不会被跳过,防止静默丢弃真实 bias。
  4. 全程无配置、schema 或部署相关改动,仅源码与测试联动;合并前由 hmellor 提交 "tighter scope" 收窄了跳过范围,并更新了测试断言。
文件 模块 状态 重要度
vllm/model_executor/layers/fused_moe/routed_experts.py 权重加载 modified 6.83
tests/kernels/moe/test_moe_weight_loading_padded.py MoE 测试 modified 7.14

关键符号

RoutedExperts.load_weights TestLoadWeightsExpertBias._make_experts TestLoadWeightsExpertBias._checkpoint_weights TestLoadWeightsExpertBias.test_bias_free_experts_ignore_checkpoint_biases TestLoadWeightsExpertBias.test_experts_with_bias_params_load_checkpoint_biases TestLoadWeightsExpertBias.test_missing_non_bias_param_names_the_weight TestLoadWeightsExpertBias.test_fused_bias_names_are_not_skipped

关键源码片段

vllm/model_executor/layers/fused_moe/routed_experts.py core-logic

核心修复点:`RoutedExperts.load_weights` 对缺失参数改用 `getattr(..., None)`,仅对 `w13_bias`/`w2_bias` 缺失时跳过,其他缺失参数抛出更清晰的 `AttributeError`,避免整个模型加载失败。

def load_weights(
    self, weights: Iterable[tuple[str, torch.Tensor]]
) -> Iterable[str]:
    expert_mapping = self.get_expert_mapping(include_fused=True)
    unpadded_hidden = (
        self.moe_config.hidden_dim_unpadded or self.moe_config.hidden_dim
    )
    for expert_name, loaded_weight in weights:
        qual_name = f"{self.layer_name}.{expert_name}"
        # Fused expert weights 以 3D 张量表示,单专家权重则是 1D/2D
        is_fused = loaded_weight.dim() == 3
        matched = False
        for param_name, weight_name, expert_id, shard_id in expert_mapping:
            if weight_name not in qual_name:
                if matched and is_fused:
                    break
                continue
            matched = True
            weight_name = qual_name.replace(weight_name, param_name)
            param_name = weight_name.removeprefix(f"{self.layer_name}.")
​
            # 部分量化导出(GPTQ、llm-compressor)会给无 bias 参数的模型
            # 写入空 bias 张量;这里对 w13_bias/w2_bias 缺失做容错跳过
            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}"
                )
            # 后续 fused 权重拆分、transpose 与逐一专家加载逻辑保持不变
            ...
tests/kernels/moe/test_moe_weight_loading_padded.py test-coverage

新增 `TestLoadWeightsExpertBias` 与 `_make_experts`/`_checkpoint_weights` 等 4 个测试,覆盖无 bias 跳过、有 bias 加载、非 bias 缺失报错、预融合 bias 名不跳过四类边界,构成防回归护栏。

class TestLoadWeightsExpertBias:
    """验证 RoutedExperts.load_weights 对多余空 bias 张量的容错行为。
​
    某些量化 checkpoint(GPTQ、llm-compressor NVFP4)会给没有 bias 参数的
    模型导出全零 bias;这里要求像 AutoWeightsLoader 一样跳过,而不是抛错。
    """
​
    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
​
        # 有 bias 时额外注册 w13_bias 与 w2_bias 参数
        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]]:
        # 模拟每个专家都带 weight + bias 的 checkpoint
        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"}

评论区精华

跳过范围收紧(tighter scope) 设计

维护者 hmellor 在跟进提交中把初始修复收窄为仅处理 `w13_bias`/`w2_bias` 缺失的情况,避免把所有 `getattr` 返回 None 的参数都静默跳过。

结论:最终实现只跳过 `w13_bias` 与 `w2_bias` 结尾的缺失参数,其余缺失参数显式抛 `AttributeError`,保留失败可见性。 · 已解决

预融合 bias 命名不应被跳过 测试

`test_fused_bias_names_are_not_skipped` 覆盖 `down_proj_bias`、`gate_up_proj_bias`、`down_proj.bias` 等预融合命名,确保跳过逻辑不误伤真实 bias;这些命名需由模型 `WeightsMapper` 显式重命名。

结论:该测试保留在合并版本中,作为防回归护栏。 · 已解决

自动化审查被禁用 other

claude[bot] 提示 fork 仓库默认不自动审查,建议维护者手动触发 `@claude review`;本次未执行人工 AI 审查。

结论:由两名维护者(hmellor、mgoin)直接批准。 · 已解决

风险与影响

风险集中在 vllm/model_executor/layers/fused_moe/routed_experts.py 的 load_weights 路径:跳过条件仅匹配 w13_bias/w2_bias 精确后缀,若未来出现其他命名形式的空 bias,加载仍会失败;这是有意的保守设计,但边缘命名下兼容性可能不足。另一个风险是行为从“必然崩溃”变为“静默跳过缺失 bias”,若某模型确实需要 bias,却因命名或映射问题走到 param is None 分支,则可能被跳过而不会报错;不过对真正有 bias 参数的模型,_make_experts 测试已证明可正常加载,且 test_fused_bias_names_are_not_skipped 防住了最常见的预融合命名。整体回归面小,非缺失参数路径与原逻辑完全一致,运行时推理性能不受影响。

用户侧:此前无法加载的、含冗余空 bias 的量化 MoE checkpoint(GPTQ、llm-compressor 导出)现在可正常加载,示例模型 CohereLabs/command-a-plus-05-2026-w4a4 从启动崩溃变为可用。系统侧:改动仅影响模型启动加载阶段,不影响推理性能;代码与 AutoWeightsLoader 保持对称,降低后续权重加载契约的维护成本。团队侧:新增测试锁定了跳过与不跳过的边界,为后续 MoE 权重加载重构提供了防回归护栏。

核心加载路径分支变更 跳过逻辑仅在精确后缀匹配时生效 测试覆盖到位,回归面小 新行为可能掩盖缺失 bias 参数的权重异常

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论