Prhub

#43213 [Model] Fix MiniCPM-V 4.6 vit_merger qkv weight loading

原始 PR 作者 tc-mb 合并时间 2026-05-22 13:44 文件变更 1 提交数 2 评论 3 代码增减 +28 / -0

执行摘要

修复 MiniCPM-V 4.6 vit_merger QKV 权重加载

MiniCPM-V 4.6 的 vit_merger.self_attn 使用 QKVParallelLinear,但 HuggingFace 检查点中 q_proj, k_proj, v_proj 是分离的。顶层的 packed_modules_mapping 声明不足以让 AutoWeightsLoader 覆盖 vit_merger 子树,导致权重加载缺失或静默随机初始化,产生损坏的视觉特征。

建议仔细阅读该 PR 的讨论,尤其是代码审核机器人指出的两个问题。虽已被合并,但可考虑后续修复防御性检查和返回值问题。该 PR 展示了 vLLM 中自定义 weight_loading 的标准模式,值得模型贡献者参考。

讨论亮点

代码审核机器人 [gemini-code-assist] 指出两个潜在问题:

1) params_dict[name] 直接访问可能因检查点中存在意外键而崩溃,应检查键是否存在并跳过未知键;
2) loaded_params 集合存储的是转换后的参数名(如 'qkv_proj...'),而非原始权重名,可能导致模型加载器错误地报告意外键。作者 tc-mb 回复 "ok",但最终合并的代码未解决这些问题。

实现拆解

  1. 添加导入:在 vllm/model_executor/models/minicpmv4_6.py 中导入 default_weight_loader,用于缺省加载无映射参数。
  2. 新增 load_weights 方法:在 MiniCPMV4_6ViTWindowAttentionSelfAttn 类中添加 load_weights 方法,按照 vLLM 的标准 stacked_params_mapping 模式处理权重加载。该方法定义映射 qkv_proj → (q_proj, k_proj, v_proj) 对应 shard_id 'q', 'k', 'v',对匹配到的权重调用 param.weight_loader,未匹配的参数回退到 default_weight_loader。
  3. 无其他文件变更:仅修改一个文件,不涉及测试、配置或部署配套。
文件 模块 状态 重要度
vllm/model_executor/models/minicpmv4_6.py 模型加载 modified 7.26

关键符号

load_weights

关键源码片段

vllm/model_executor/models/minicpmv4_6.py data-contract

唯一变更文件,修复 vit_merger.self_attn 的 QKV 权重加载

def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
    # 定义从多个 shard(q/k/v_proj)到融合参数(qkv_proj)的映射。
    # 每个元素为 (param_name, weight_name, shard_id)。
    stacked_params_mapping = [
        ("qkv_proj", "q_proj", "q"),
        ("qkv_proj", "k_proj", "k"),
        ("qkv_proj", "v_proj", "v"),
    ]
    params_dict = dict(self.named_parameters())
    loaded_params: set[str] = set()
    for name, loaded_weight in weights:
        # 先尝试匹配 stacked 映射,若匹配则调用 weight_loader 传入 shard_id
        for param_name, weight_name, shard_id in stacked_params_mapping:
            if weight_name not in name:
                continue
            # 将权重名中的 weight_name 替换为 param_name,得到实际参数名
            mapped_name = name.replace(weight_name, param_name, 1)
            if mapped_name not in params_dict:
                continue
            param = params_dict[mapped_name]
            # 调用 weight_loader 处理分片加载
            param.weight_loader(param, loaded_weight, shard_id)
            break
        else:
            # 未匹配 stacked 映射,尝试直接按名加载
            if name not in params_dict:
                continue
            param = params_dict[name]
            # 使用参数自带的 weight_loader,缺省为 default_weight_loader
            weight_loader = getattr(param, "weight_loader", default_weight_loader)
            weight_loader(param, loaded_weight)
        loaded_params.add(name)
    return loaded_params

评论区精华

检查点中意外键的处理 正确性

AI 审核指出现有代码直接访问 params_dict[name] 未检查键是否存在,若检查点包含 legacy 参数会崩溃。还指出 loaded_params 存储转换后名称可能误导模型加载器。

结论:作者回应 'ok' 但未修改代码便合并。问题仍存在,建议后续修复。 · unresolved

风险与影响

  1. 缺少防御性检查:load_weights 中未处理检查点出现意外键的场景,若存在 legacy 参数可能触发 KeyError。
  2. loaded_params 返回值不规范:返回转换后的参数名可能导致上层模型加载器误判,引发虚假的 "unexpected keys" 警告或错误。
  3. 无测试覆盖:未添加单元测试验证权重加载逻辑,回归风险较高。

影响范围:仅限于 MiniCPM-V 4.6 模型。
影响程度:中高。修复后 vit_merger 的 qkv_proj 能正确加载预训练权重,模型推理结果恢复正常。无兼容性问题,但对使用旧检查点的用户可能存在遗留键导致的崩溃风险。

缺少测试覆盖 未解决的 review 问题

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论