Prhub

#54501 [Bugfix][MM] Fix MiniCPM-o image processor reuse on Transformers v5

原始 PR 作者 AndreasKaratzas 合并时间 2026-08-31 17:07 文件变更 3 提交数 2 评论 3 代码增减 +41 / -23

执行摘要

修复 MiniCPM-o 图像处理器复用串扰

Buildkite CI(build #86268)在 Transformers 5.15 下的串行测试中暴露 MiniCPM-o 2.6 复用了 MiniCPM-V 2.5 的不兼容图像处理器。PR body 指出缺口始于 #44282:MiniCPM-o 的 override 绕过基类处理器路径;#48413 只修复了基类路径上的同类 class-reuse bug;#51668 升级 Transformers 5.15 后,被绕过的 override 中的错误处理器类真正触发了失败。

值得精读。该 PR 是一个多模态处理器类复用的典型坑:AutoProcessor 按类名解析 image_processor 导致跨 checkpoint 串用。值得借鉴的设计决策包括:将 checkpoint 特定加载逻辑提取为共享 helper、参数化回归测试直接命中失败路径(get_sliced_grid)并断言处理器类与模块均不同。阅读时可重点关注 minicpmo.py 与基类在 numpy 属性转换上的不一致,以及是否有必要补充端到端测试。

讨论亮点

该 PR 没有实质性的 review 技术争论:DarkLight1337 直接 approve,claude[bot] 仅发送了手动 review 的提示消息。核心讨论沉淀在 PR body 的根因链条中:作者明确指出问题由 #44282 引入(override 绕过基类路径)、#48413 只修了基类路径、#51668(Transformers 5.15 升级)最终暴露遗漏。另外作者声明 OpenAI Codex 辅助了 triage、实现、测试与起草,并强调人类提交者必须在请求 review 前逐行理解所有改动。

实现拆解

  1. 提取共享加载 helper:在 vllm/model_executor/models/minicpmv.py 的 MiniCPMVProcessingInfo 中新增 _get_checkpoint_image_processor,把原先内联在 get_hf_processor 里的 _merge_mm_kwargs 与 cached_get_image_processor(..., processor_cls_overrides=...) 逻辑抽出。helper 依据 checkpoint 的 auto_map['AutoImageProcessor'] 加载正确的处理器类,这是避免跨版本串用的核心。
  2. 基类改走 helper:MiniCPMVProcessingInfo.get_hf_processor 仍调用 self.ctx.get_hf_processor(**kwargs) 但只用于取得 tokenizer,图像处理器改由 _get_checkpoint_image_processor 提供,并继续包裹进 vendored MiniCPMVProcessor。代码注释明确说明 AutoProcessor 的 image_processor 按类名解析,会跨版本选错 checkpoint。
  3. 修复 MiniCPM-o override:vllm/model_executor/models/minicpmo.py 的 MiniCPMOProcessingInfo.get_hf_processor 原先直接复用 hf_processor.image_processor(即被误解析的 2.5 处理器),现改为 _get_checkpoint_image_processor(**kwargs),同时保留 audio feature extractor、tokenizer 与 pool_step 配置,行为契约不变。
  4. 回归测试:tests/models/multimodal/processing/test_minicpmv.py 将原 test_image_processor_for_dif_model 重构为参数化的 test_image_processor_for_different_models,新增 (MiniCPM-V 2.5, MiniCPM-o 2.6) 组合,并调用 second_processor.info.get_sliced_grid(ImageSize(width=128, height=128)) 直接命中 CI 失败路径,最后断言两个模型的处理器类型与所属模块均不相同。
文件 模块 状态 重要度
vllm/model_executor/models/minicpmv.py 处理器加载 modified 6.68
tests/models/multimodal/processing/test_minicpmv.py 回归测试 modified 5.94
vllm/model_executor/models/minicpmo.py 处理器加载 modified 5.61

关键符号

_get_checkpoint_image_processor get_hf_processor test_image_processor_for_different_models

关键源码片段

vllm/model_executor/models/minicpmv.py core-logic

核心修复文件:提取 `_get_checkpoint_image_processor` 共享 helper,基类 `get_hf_processor` 改走该路径,确保按 checkpoint 加载图像处理器而非依赖 AutoProcessor 的类名解析。

# MiniCPMVProcessingInfo 内新增 / 修改的两个方法(其余成员省略)
def _get_checkpoint_image_processor(self, **kwargs: object):
    """按 checkpoint 自身配置加载图像处理器,避免被 AutoProcessor 按类名误选。"""
    model_config = self.ctx.model_config
    processor_cls = self._image_processor_cls
    merged_kwargs = _merge_mm_kwargs(model_config, processor_cls, **kwargs)
​
    # cached_get_image_processor 会读取 checkpoint 的 auto_map['AutoImageProcessor']
    # 并支持 processor_cls_overrides,保证 MiniCPM-V 2.5 / 2.6 / 4 各用各的类。
    return cached_get_image_processor(
        model_config.model,
        revision=model_config.revision,
        trust_remote_code=model_config.trust_remote_code,
        processor_cls_overrides=processor_cls,
        **merged_kwargs,
    )
​
def get_hf_processor(self, **kwargs: object):
    # AutoProcessor 只用于 tokenizer;其 image_processor 按类名解析,
    # 在多版本 MiniCPM 系列中会选错 checkpoint,所以这里显式重新加载。
    hf_processor = self.ctx.get_hf_processor(**kwargs)
    image_processor = self._get_checkpoint_image_processor(**kwargs)
​
    from vllm.transformers_utils.processors.minicpmv import MiniCPMVProcessor
​
    # 用 checkpoint 特定的图像处理器拼装 vendored processor,
    # 同时保留 tokenizer 和版本相关配置。
    vendored_processor = MiniCPMVProcessor(
        image_processor=image_processor,
        tokenizer=hf_processor.tokenizer,
        version=self.get_model_version(),
    )
    hf_processor = vendored_processor
​
    # Transformers v5+ 把 mean/std 改名为 image_mean/image_std,
    # 且 NumPy 数组不算 Sequence,序列化前统一转成 list。
    for attr in ("mean", "std", "image_mean", "image_std"):
        val = getattr(image_processor, attr, None)
        if isinstance(val, np.ndarray):
            setattr(image_processor, attr, val.tolist())
​
    return hf_processor
tests/models/multimodal/processing/test_minicpmv.py test-coverage

回归测试核心:重构为参数化的 `test_image_processor_for_different_models`,新增 MiniCPM-o 2.6 组合并调用 `get_sliced_grid` 命中 CI 失败路径。

@pytest.mark.parametrize(
    "model_ids",
    [
        ("openbmb/MiniCPM-Llama3-V-2_5", "openbmb/MiniCPM-V-4"),
        ("openbmb/MiniCPM-Llama3-V-2_5", "openbmb/MiniCPM-o-2_6"),
    ],
)
def test_image_processor_for_different_models(model_ids):
    first_model_id, second_model_id = model_ids
​
    first_ctx = build_model_context(first_model_id, limit_mm_per_prompt={"image": 1})
    first_processor = MULTIMODAL_REGISTRY.create_processor(first_ctx.model_config)
    first_image_processor = first_processor.info.get_image_processor()
​
    second_ctx = build_model_context(second_model_id, limit_mm_per_prompt={"image": 1})
    second_processor = MULTIMODAL_REGISTRY.create_processor(second_ctx.model_config)
    second_image_processor = second_processor.info.get_image_processor()
​
    # 复现 CI 失败路径:MiniCPM-o 的切片依赖 image processor 的 grid 布局,
    # 若误用 2.5 的处理器,这里会产生错误的切片结果。
    second_processor.info.get_sliced_grid(ImageSize(width=128, height=128))
​
    # 两个模型必须使用不同类型、不同模块的处理器类,防止串行场景下缓存串扰。
    assert type(first_image_processor) is not type(second_image_processor)
    assert (
        type(first_image_processor).__module__
        != type(second_image_processor).__module__
    )
vllm/model_executor/models/minicpmo.py core-logic

缺陷实际所在:MiniCPM-o 的 `get_hf_processor` 修复为使用 `_get_checkpoint_image_processor`,不再复用 AutoProcessor 误解析的处理器。

# MiniCPMOProcessingInfo.get_hf_processor(修复后)
def get_hf_processor(self, **kwargs: object) -> "MiniCPMOProcessor":
    import numpy as np
​
    hf_processor = self.ctx.get_hf_processor(**kwargs)
    # 关键修复:不再复用 AutoProcessor 解析出的 image_processor,
    # 而是从 checkpoint 配置重新加载,避免误用 MiniCPM-V 2.5 的处理器。
    image_processor = self._get_checkpoint_image_processor(**kwargs)
​
    from vllm.transformers_utils.processors.minicpmo import MiniCPMOProcessor
​
    # 用 checkpoint 特定的图像处理器拼装 vendored processor,
    # 音频 feature extractor、tokenizer 与 pool_step 配置保持不变。
    vendored_processor = MiniCPMOProcessor(
        image_processor=image_processor,
        feature_extractor=hf_processor.feature_extractor,
        tokenizer=hf_processor.tokenizer,
        pool_step=self.get_default_audio_pool_step(),
    )
​
    # 序列化兼容:与基类相比这里只覆盖 mean/std,
    # 若 Transformers v5 下还暴露 image_mean/image_std 属性,可能需要同步补充。
    for attr in ("mean", "std"):
        val = getattr(image_processor, attr, None)
        if val is not None and isinstance(val, np.ndarray):
            setattr(image_processor, attr, val.tolist())
​
    return vendored_processor

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 序列化属性覆盖不一致:minicpmo.py 的 numpy 转 list 循环仍只覆盖 mean/std,而基类 minicpmv.py 覆盖 mean/std/image_mean/image_std 四个属性。若 MiniCPM-o 的 checkpoint 处理器在 Transformers v5 下同样暴露 image_mean/image_std 为 NumPy 数组,vendored processor 的序列化兼容性可能仍有缺口。
  2. 行为变化:MiniCPM-o 现在使用 checkpoint 自身的图像处理器,其 normalize 参数与 2.5 不同,图像预处理结果会变化(这是修复而非回归),但依赖旧行为的用户可能观察到输入归一化差异。
  3. 测试覆盖边界:新测试是单元级(build_model_context),未跑真实编码器与 slicing 的端到端链路,也未覆盖 MiniCPM-o 音频路径的处理器复用。
  4. 缓存依赖:修复依赖 cached_get_image_processor 的缓存键(模型名、revision 等)唯一性,若配置中多个模型指向同一路径或模型名不唯一,仍可能串用。

用户侧:MiniCPM-o 2.6 在 Transformers 5.15 及以上版本的推理不再因图像处理器串用而崩溃或错误切片;MiniCPM-V 2.5 与 MiniCPM-V 4 路径为等价重构,行为不变。系统侧:图像处理器加载逻辑统一收敛到基类 helper,后续新增 MiniCPM 变体时不易再犯同类错误。团队侧:新增参数化回归测试常驻 CI,覆盖跨模型串行场景,防止回退。改动面小(3 个文件,+41/-23),维护成本低。

处理器类复用串扰 序列化属性覆盖不一致 单元测试未覆盖端到端 Transformers v5 兼容

关联 Issue

#51668 Bump Transformers version to 5.15.0

完整报告

参与讨论