Prhub

#49417 [Bugfix] MiniCPM-V 4.6: fix ViT self-attn qkv weight loading

原始 PR 作者 arkohut 合并时间 2026-09-14 21:18 文件变更 1 提交数 8 评论 20 代码增减 +6 / -13

执行摘要

MiniCPM-V 4.6 qkv 融合映射上移,修复权重加载失败

PR 描述开门见山:"openbmb/MiniCPM-V-4.6 currently fails to load under vLLM — engine init aborts, so the model cannot be served at all"。在 main 上报 ValueError: There is no module or parameter named 'k_proj' in MiniCPMV4_6ViTWindowAttentionSelfAttn,在已发布的 0.25.x 上同一条代码路径则产出错误的 qkqkv_proj。该回归由 #47058(把显式 stacked 循环换成 WeightsMapper)引入,#49193 只把 qkqkv 变成 k_proj,并未真正修好,因此模型长期处于不可用状态。MiniCPM-V 团队成员 tc-mb 在讨论中催办:"This model cannot be used in vllm. There have been feedback from different people that everyone in the community cannot be used recently.",修复具有明确的社区可用性动机。

值得精读,尤其是想搞清楚 vLLM 权重加载分层的工程师:本 PR 用 6 行新增讲清了"映射该放在哪一层"这个反复踩坑的问题。重点看三处:一是 MiniCPMV4_6ViTWindowAttentionSelfAttn 为什么不能持有自己的 hf_to_vllm_mapper(前缀剥离子模块与映射键层级的错配);二是去掉前导点会级联成 qkqkv_proj 的匹配语义陷阱;三是 hmellor 否决"恢复显式 stacked_params_mapping 循环"的思路、改为上移 mapper 的设计取舍。建议后续补一个纯权重名层面的 CPU 单测(断言 ...self_attn.q_proj.weight → ...self_attn.qkv_proj.weight 且 shard id 为 q/k/v),否则该模块仍会在下一次加载框架重构中静默回归。

讨论亮点

1)最有价值的交锋是实现路线的替换。作者最初主张"Restore the explicit stacked_params_mapping loop with break",即恢复 #47058 之前的显式融合循环;hmellor 直接以 CHANGES_REQUESTED 否决:"I still don't want to add back the load_weights boilerplate. I've just noticed that this load_weights method is on the Attention module, not the model. That's why it's not working, it's ben applied in the wrong place." 他亲自推送 63645932 "Move mapper to right place",把映射搬到模型级并删掉子模块的 load_weights(),最终合入的正是这一版。
2)验证闭环:hmellor "@arkohut please can you try the fix I just pushed? I've tested it locally and it works for me",作者复测确认 "this does the trick... No more k_proj / qkqkv_proj. LGTM"。
3)上游催办:MiniCPM-V 团队成员 tc-mb 两度发声,指出模型对社区完全不可用,并暗示愿意另开 PR 尽快合入;hmellor 回应 CI 卡住并致歉,随后 rebase + 重跑 CI。
4)风格细节:hmellor 留下唯一一条行内 review 建议(给 orig_to_new_prefix 字典补结尾逗号 },),由提交 "Apply suggestion from @hmellor" 落实。
5)自动化审查缺位:claude[bot] 注明 "This pull request is from a fork — automated review is disabled",本 PR 没有机器审查兜底,正确性完全依赖人工复现。

实现拆解

  1. 定位失败入口:问题集中在 vllm/model_executor/models/minicpmv4_6.py 的 MiniCPMV4_6ViTWindowAttentionSelfAttn。该模块把 checkpoint 中分离的 q_proj/k_proj/v_proj 融合为一个 QKVParallelLinear(self.qkv_proj),原先用类属性 hf_to_vllm_mapper = WeightsMapper(orig_to_new_stacked={...}) 配合自定义 load_weights() 内的 AutoWeightsLoader(self) 表达这一融合。
  2. 删除错层的映射与加载钩子:移除该子模块的 hf_to_vllm_mapper 与 load_weights(),模块退回普通 nn.Module。原因是 AutoWeightsLoader 已递归进子模块,load_weights() 收到的是前缀被剥掉的名字("q_proj.weight"),带前导点的键 ".q_proj" 无法命中;而去掉前导点后 orig_to_new_stacked 会在一次替换中反复匹配,"q_proj"→"qkv_proj" 之后 "v_proj" 又在新名字里命中,得到 qkqkv_proj。两种写法都不可用,所以问题不在于映射内容而在于应用层级。
  3. 上移融合规则到模型级 mapper:在 MiniCPMV4_6ForConditionalGeneration.hf_to_vllm_mapper 中新增 orig_to_new_stacked={".q_proj": (".qkv_proj", "q"), ".k_proj": (".qkv_proj", "k"), ".v_proj": (".qkv_proj", "v")},与既有的 orig_to_new_prefix(model.vision_tower.→vpm.、model.language_model.→language_model.model. 等)并存,并补上结尾逗号。模型级 mapper 由顶层 AutoWeightsLoader 应用,看到的仍是完整名字(如 vit_merger.<i>.self_attn.q_proj.weight),.q_proj 能正确命中且三个键互不为子串,分片标识 q/k/v 会被透传给 QKVParallelLinear.weight_loader。
  4. 与既有契约保持一致:模型类上的 packed_modules_mapping 已声明 "qkv_proj": ["q_proj", "k_proj", "v_proj"],融合规则上移后,权重加载、LoRA/adapter 以及量化路径对 qkv_proj 的认知统一在同一个层级,避免子模块再做一次局部映射造成双重解释。
  5. 测试与验证配套:本 PR 未新增任何单测,验证完全依赖人工端到端复现——hmellor 本地验证后推送 "Move mapper to right place" 提交并请作者复测,arkohut 在 RTX 3090(PR 正文记录为 4090)上 serve 官方 checkpoint,确认 Model loading took 2.47 GiB memory、/v1/models 返回 minicpm-v-4_6、文本对话与 video_url base64 视频请求均正确。CI 侧经历多轮 main rebase、mergify 冲突提示、pre-commit 失败重跑,最终由 hmellor 触发 Buildkite #88805 后合并。
位置 改动前 改动后
MiniCPMV4_6ViTWindowAttentionSelfAttn 类属性 hf_to_vllm_mapper + 自定义 load_weights() 两者删除,退回普通模块
MiniCPMV4_6ForConditionalGeneration.hf_to_vllm_mapper 仅 orig_to_new_prefix 新增 orig_to_new_stacked(并补 },)
文件 模块 状态 重要度
vllm/model_executor/models/minicpmv4_6.py 权重加载 modified 6.85

关键符号

MiniCPMV4_6ViTWindowAttentionSelfAttn.load_weights MiniCPMV4_6ForConditionalGeneration.hf_to_vllm_mapper

关键源码片段

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

唯一变更文件。删除 `MiniCPMV4_6ViTWindowAttentionSelfAttn` 上的 `hf_to_vllm_mapper` 与自定义 `load_weights()`,把 q/k/v→`qkv_proj` 的融合规则(`orig_to_new_stacked`)上移到 `MiniCPMV4_6ForConditionalGeneration.hf_to_vllm_mapper`,让顶层 `AutoWeightsLoader` 在完整 checkpoint 名上完成融合,从而修复模型无法加载的问题。

修复后的 ViT window attention 子模块:不再自己持有映射、也不覆写 load_weights,融合交由模型级 mapper 统一处理。

class MiniCPMV4_6ViTWindowAttentionSelfAttn(nn.Module):
    # 修复要点 1:这里不再挂 hf_to_vllm_mapper,也不再覆写 load_weights()。
    # 原实现在子模块内用 AutoWeightsLoader(self) 应用映射,但进入子模块后权重名
    # 已被剥掉前缀(形如 "q_proj.weight"),带前导点的键 ".q_proj" 永远匹配不上,
    # 于是 k_proj 找不到、引擎在加载阶段直接退出;
    # 去掉前导点也不行:"q_proj" 先被替换成 "qkv_proj",随后 "v_proj" 又在新名字里
    # 二次命中,最终得到错误的 "qkqkv_proj"。
    def __init__(
        self,
        config,
        quant_config: QuantizationConfig | None = None,
        prefix: str = "",
    ):
        super().__init__()
        use_data_parallel = is_vit_use_data_parallel()
        self.embed_dim = config.hidden_size
        self.num_heads = config.num_attention_heads
        self.head_dim = self.embed_dim // self.num_heads
        self.scale = self.head_dim**-0.5
​
        tp_size = 1 if use_data_parallel else get_tensor_model_parallel_world_size()
        assert self.num_heads % tp_size == 0
        self.num_heads_per_partition = self.num_heads // tp_size
​
        # checkpoint 中 q_proj / k_proj / v_proj 是分开的,这里合并为一个 qkv_proj;
        # 融合所需的 q/k/v 分片标识由模型级 hf_to_vllm_mapper 的 orig_to_new_stacked 提供
        self.qkv_proj = QKVParallelLinear(
            self.embed_dim,
            self.head_dim,
            self.num_heads,
            quant_config=quant_config,
            prefix=f"{prefix}.qkv_proj",
            disable_tp=use_data_parallel,
        )
        self.out_proj = RowParallelLinear(
            self.embed_dim,
            self.embed_dim,
            bias=True,
            quant_config=quant_config,
            prefix=f"{prefix}.out_proj",
            disable_tp=use_data_parallel,
        )
        self.attn = MMEncoderAttention(
            self.num_heads_per_partition,
            self.head_dim,
            self.scale,
            prefix=f"{prefix}.attn",
        )
​
    def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
        qkv, _ = self.qkv_proj(hidden_states)
        q, k, v = qkv.chunk(3, dim=-1)
        attn_out = self.attn(q, k, v)
        out, _ = self.out_proj(attn_out)
        return out

修复后的模型级 mapper:融合规则与前缀重写放在同一处,由顶层 AutoWeightsLoader 在完整 checkpoint 名上应用。

class MiniCPMV4_6ForConditionalGeneration(
    nn.Module,
    SupportsMultiModal,
    SupportsLoRA,
    SupportsPP,
    HasInnerState,
    IsHybrid,
    SupportsMRoPE,
):
    supports_encoder_tp_data = True
​
    # 修复要点 2:融合规则上移到模型级 mapper。顶层加载器看到的仍是完整 checkpoint
    # 名(如 "vit_merger.<i>.self_attn.q_proj.weight"),".q_proj" 能正确命中;
    # 且 ".q_proj"/".k_proj"/".v_proj" 互不为子串,不会级联成 "qkqkv_proj"。
    hf_to_vllm_mapper = WeightsMapper(
        orig_to_new_stacked={
            ".q_proj": (".qkv_proj", "q"), # 分别写入 qkv_proj 的 q/k/v 分片
            ".k_proj": (".qkv_proj", "k"),
            ".v_proj": (".qkv_proj", "v"),
        },
        orig_to_new_prefix={
            # transformers v5.7+ 使用 `vision_tower`,并把 `vit_merger` 嵌在其中;
            # 顺序敏感:更具体的前缀必须排在前面。
            "model.vision_tower.vit_merger.": "vit_merger.",
            "model.vision_tower.": "vpm.",
            "model.vpm.": "vpm.",
            "model.vit_merger.": "vit_merger.",
            "model.merger.": "merger.",
            "model.language_model.": "language_model.model.",
            "lm_head.": "language_model.lm_head.",
            "mtp.": None,
        },
    )

评论区精华

融合映射该放在子模块还是模型级 设计

作者最初提交的方案是恢复 #47058 之前的显式 `stacked_params_mapping` 循环加 `break`;hmellor 以 CHANGES_REQUESTED 否决:"I still don't want to add back the `load_weights` boilerplate. I've just noticed that this `load_weights` method is on the Attention module, not the model. That's why it's not working, it's ben applied in the wrong place.",随后亲自推送 "Move mapper to right place",把 `orig_to_new_stacked` 搬到 `MiniCPMV4_6ForConditionalGeneration.hf_to_vllm_mapper` 并删除子模块的 `load_weights()`。

结论:采纳维护者方案:映射上移模型级,子模块不再持有 mapper、不再覆写 `load_weights()`;作者随后复测通过。 · 已解决

修复的端到端验证 测试

hmellor:"please can you try the fix I just pushed? I've tested it locally and it works for me";arkohut 复测:"this does the trick. Served the official `openbmb/MiniCPM-V-4.6` on a 3090 ... Model loading took 2.47 GiB memory ... No more `k_proj` / `qkqkv_proj`. LGTM",覆盖了文本对话与 `video_url` base64 视频请求。

结论:两位维护 / 贡献者在真实 checkpoint 上确认加载与服务正常;但验证仅限人工 serve,无自动化测试固化。 · 已解决

上游模型方催办与合并节奏 other

MiniCPM-V 团队成员 tc-mb:"This model cannot be used in vllm. There have been feedback from different people that everyone in the community cannot be used recently... Or a better way is for me to propose a new pr and merge it as soon as possible.";hmellor 致歉并说明 "it seems the previous CI got stuck on something. I've updated from `main` and restarted the CI"。

结论:在外部压力下加快节奏:多轮 rebase、修 pre-commit、重跑 Buildkite #88805 后合并。 · 已解决

字典结尾逗号风格建议 style

hmellor 在文件上留下唯一一条行内建议:为 `orig_to_new_prefix` 加结尾逗号(```suggestion }, ```)。

结论:由提交 "Apply suggestion from @hmellor"(61b5935)采纳。 · 已解决

fork PR 缺少自动化代码审查 测试

claude[bot]:"This pull request is from a fork — automated review is disabled. A repository maintainer can comment `@claude review` to run a one-time review."

结论:未启用一次性机器审查,本次改动的正确性判断完全依赖维护者与作者的本地复现。 · unresolved

风险与影响

  • 映射作用域扩大(主要风险):orig_to_new_stacked 从 ViT window attention 子模块上移到 MiniCPMV4_6ForConditionalGeneration 后,凡是名字里含 .q_proj/.k_proj/.v_proj 的权重都会被这条规则接管,包括语言主干与 merger 中的同名权重。需要确认这些权重同样期望融合进 qkv_proj 且分片顺序为 q/k/v;良性证据是人工验证包含文本对话且回复连贯,说明主干未被打乱,但这一结论没有自动化断言支撑。
  • 缺少测试覆盖:新增 0 个测试文件,也没有针对 MiniCPMV-V 4.6 权重名映射的单元测试。回归已经发生过两次(#47058 引入、#49193 修一半),说明该路径在 CI 中不可见,任何 WeightsMapper 语义调整都可能再次静默破坏加载。
  • 对 WeightsMapper 语义的隐式依赖:修复成立的前提是带前导点的键在完整名字上按预期匹配,且三个键不会互相级联替换。这是对工具类匹配语义的隐性契约,一旦匹配实现改为 endswith 式或收紧子串规则,.q_proj 这类写法需要重新评估。
  • 子模块加载钩子被删除的兼容性:MiniCPMV4_6ViTWindowAttentionSelfAttn.load_weights() 被移除,若 LoRA/adapter 或量化加载依赖该子模块的自定义加载行为,理论上会受影响;PR 明确声称 "no shared machinery is touched",且 supports_encoder_tp_data/is_vit_use_data_parallel() 的 TP/DP 分支未改动,实际风险较低但未在测试中覆盖。
  • 流程风险:8 个提交里多数是 rebase 合并提交,期间出现 merge conflict、pre-commit 失败与 CI 卡住,修复从提出到合入被拖延数周,单模型 bugfix 的反馈链路明显偏长。
  • 用户侧:直接解除 openbmb/MiniCPM-V-4.6 的"完全不可服务"状态,文本与视频(video_url base64)请求均恢复;受影响面是所有尝试部署该模型的社区用户,正是 tc-mb 反馈的群体。
  • 系统侧:改动限定在 vllm/model_executor/models/minicpmv4_6.py 单文件,不触碰 AutoWeightsLoader、WeightsMapper 等共享机制,对其他模型零影响;TP/DP 与量化路径因 QKVParallelLinear 仍通过 stacked shard id 加载而保持行为一致。
  • 维护侧:确立了一条可复用的判断规则——权重融合映射必须放在能看到完整 checkpoint 名的模型级 hf_to_vllm_mapper,而不是子模块的 load_weights(),对后续新增多模态模型有示范意义。
  • 无测试带来的长期影响:模型可用性依赖人工 serve 验证,后续重构缺乏回归护栏,同等事故可能再次发生。
映射作用域扩大到整模型 缺少自动化测试覆盖 依赖 WeightsMapper 匹配语义 删除子模块加载钩子 验证仅靠人工复现

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论