Prhub

#49030 [Bugfix][Multimodal] Fix video temporal padding estimates

原始 PR 作者 labAxiaoming 合并时间 2026-07-29 01:57 文件变更 7 提交数 1 评论 4 代码增减 +34 / -9

执行摘要

修正视频帧数向上取整公式,修复 6 个多模态模型的 token 估算

Issue #47866 指出 _get_vision_info 用于 vLLM 侧 token 与预算估算、dummy 输入和 warmup,多个模型沿用了旧版 Qwen2-VL image processor 的取整公式 num_frames + num_frames % temporal_patch_size,该公式只在常见的 temporal_patch_size == 2 时碰巧正确。以 17 帧、patch=4 为例,当前公式得到 18 而非 20,grid_t 与 num_vision_tokens 被低估,vLLM 的估算与 HF 处理器真实输出不一致。

值得阅读。这是一个典型的「公式语义错误」修复,num_frames + (-num_frames % temporal_patch_size) 是标准向上取整写法,可用于识别同类 bug;PR 对「是否引入共享 helper」做了明确取舍(最小化替代 #47876),展示了在低风险合入与长期可维护性之间权衡的决策样例。后续可考虑把 6 处重复公式收敛为公共工具函数,并补充其余 5 个模型的直接测试。

讨论亮点

Isotr0py 提问(glm4_1v.py:1086):QQ: I think we have padded frames in video loader? 并引用 vllm/multimodal/video.py#L1636-L1663,质疑视频加载器是否已经补齐帧数、此修正是否必要。

labAxiaoming 回应:_get_vision_info 只用于 vLLM 侧 token/预算估算、dummy 输入与 warmup,真实输入仍由 HF 处理器处理;加载器中 if frames.shape[0] & 1: frames = np.concatenate([frames, frames[-1:]], axis=0) 只把奇数帧复制成偶数帧(保证为 2 的倍数),当 temporal_patch_size 为 4 或 8 时仍不能得到正确的倍数,因此修正仍然必要。

结论:该对话澄清了「加载器补齐」与「估算取整」的职责边界,Isotr0py 随后给出 APPROVED。

实现拆解

  1. 定位根因:在 qwen2_vl.py、glm4_1v.py、kanana_v.py、keye.py、llava_onevision2.py、mimo_v2_omni.py 的 _get_vision_info 中发现相同的旧公式,其引用注释指向 Transformers v4.48.3 的 qwen2_vl image_processing,该公式对 patch_size > 2 不成立。
  2. 统一替换公式:将 padded_num_frames 改为 num_frames + (-num_frames % temporal_patch_size),并把注释链接更新为 Transformers v5.13.0 的 video_processing_qwen2_vl.py 对应实现,保证 vLLM 侧估算与新版 HF 处理器行为对齐。
  3. 处理 MiMo 特例:mimo_v2_omni.py 中 effective_frames 由 num_frames * tokens_per_second 计算得到,取整后的帧数需显式转换为 int,避免浮点值进入 grid_t 计算。
  4. 新增回归测试:在 tests/models/multimodal/processing/test_glm4_1v.py 新增 test_vision_info_rounds_up_temporal_frames,参数化覆盖 temporal_patch_size 为 2/4/8、17 帧视频的场景,断言 grid_t 分别为 9/5/3,纯 CPU 运行、无需模型权重。
  5. 刻意保持最小改动:PR body 说明这是 #47876 的最小化替代方案,只修改 6 个调用点并补充聚焦回归测试,不新增共享 helper 或修改 math_utils,降低合入风险。
文件 模块 状态 重要度
vllm/model_executor/models/glm4_1v.py 多模态 modified 5.68
vllm/model_executor/models/qwen2_vl.py 多模态 modified 5.68
vllm/model_executor/models/kanana_v.py 多模态 modified 5.28
vllm/model_executor/models/keye.py 多模态 modified 4.7
vllm/model_executor/models/llava_onevision2.py 多模态 modified 4.7
vllm/model_executor/models/mimo_v2_omni.py 多模态 modified 5.1
tests/models/multimodal/processing/test_glm4_1v.py 多模态测试 modified 5.58

关键符号

_get_vision_info test_vision_info_rounds_up_temporal_frames

关键源码片段

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

GLM4V 的 `_get_vision_info` 是本次修复的代表实现,且回归测试的直接对象。

def _get_vision_info(
    self,
    *,
    image_width: int,
    image_height: int,
    num_frames: int = 16,
    do_resize: bool = True,
    max_image_pixels: int = 28 * 28 * 2 * 30000,
) -> tuple[ImageSize, int]:
    hf_config = self.get_hf_config()
    vision_config = hf_config.vision_config
    patch_size = vision_config.patch_size
    merge_size = vision_config.spatial_merge_size
    temporal_patch_size = vision_config.temporal_patch_size
​
    # 空间维度沿用 HF 的 smart_resize,帧数超过 temporal_patch_size 时才按帧数约束分辨率
    if do_resize:
        resized_height, resized_width = smart_resize(
            num_frames=num_frames
            if num_frames > temporal_patch_size
            else temporal_patch_size,
            height=image_height,
            width=image_width,
            factor=patch_size * merge_size,
            max_pixels=max_image_pixels,
        )
        preprocessed_size = ImageSize(width=resized_width, height=resized_height)
    else:
        preprocessed_size = ImageSize(width=image_width, height=image_height)
​
    # 帧数需向上取整为 temporal_patch_size 的倍数,(-num_frames % temporal_patch_size)
    # 是补足到下一个倍数所需的帧数;旧公式 num_frames % temporal_patch_size
    # 在 patch > 2 时结果偏小,例如 17 帧 + 17 % 4 = 18,而正确值为 20
    padded_num_frames = num_frames + (-num_frames % temporal_patch_size)
​
    # grid_t 是时间维 patch 数,再乘空间 patch 数得到总 patch 数,
    # 除以 merge_size^2 得到最终 token 数,供 vLLM 预算估算与 dummy 输入使用
    grid_t = max(padded_num_frames // temporal_patch_size, 1)
    grid_h = preprocessed_size.height // patch_size
    grid_w = preprocessed_size.width // patch_size
​
    num_patches = grid_t * grid_h * grid_w
    num_vision_tokens = num_patches // (merge_size**2)
​
    return preprocessed_size, num_vision_tokens
tests/models/multimodal/processing/test_glm4_1v.py test-coverage

新增参数化 CPU 回归测试,直接验证 temporal_patch_size 2/4/8 下 17 帧视频的 grid_t 估算,是本次修复的核心证据。

@pytest.mark.parametrize(
    ("temporal_patch_size", "expected_grid_t"),
    [(2, 9), (4, 5), (8, 3)],
)
def test_vision_info_rounds_up_temporal_frames(
    temporal_patch_size: int,
    expected_grid_t: int,
):
    # 用 Mock 构造 Glm4vProcessingInfo,绕过真实模型加载,
    # 让 CPU 测试即可覆盖纯估算逻辑
    info = Mock(spec=Glm4vProcessingInfo)
    vision_config = info.get_hf_config.return_value.vision_config
    vision_config.patch_size = 14
    vision_config.spatial_merge_size = 2
    vision_config.temporal_patch_size = temporal_patch_size
​
    # 17 帧视频在 patch_size 为 2/4/8 时分别应得到 grid_t = 9/5/3,
    # 旧公式在 patch = 4 时会算出 18 帧,导致 grid_t 偏小
    _, num_vision_tokens = Glm4vProcessingInfo._get_vision_info(
        info,
        image_width=28,
        image_height=28,
        num_frames=17,
        do_resize=False,
    )
​
    assert num_vision_tokens == expected_grid_t

评论区精华

视频加载器是否已对帧数补齐,估算公式修正是否必要 question

Isotr0py 在 glm4_1v.py 第 1086 行提问:视频加载器 vllm/multimodal/video.py 中似乎已补齐帧数,这里是否还需修正?

结论:labAxiaoming 解释:加载器只把奇数帧复制为偶数帧(保证为 2 的倍数),并不能保证是 temporal_patch_size(如 4、8)的倍数;而 _get_vision_info 是 vLLM 侧预算估算、dummy 输入与 warmup 专用,两者职责不同,修正仍必要。 · 已解决

风险与影响

  1. 估算值变大:修复后 temporal_patch_size > 2 场景的估算 token 数会增多,dummy 输入与 warmup 开销可能略增,显存紧张环境下需留意 warmup 内存变化。
  2. 无共享 helper:6 处调用点是手工同步修改,后续新增模型或遗漏调用点时可能再次复制旧公式;MiMo 的 int 转换是额外特殊点,需单独维护。
  3. 测试覆盖局限:回归测试只直接断言 GLM4V 的 _get_vision_info,其余 5 个模型未加独立断言,若各模型从 HF 复制的逻辑有细微差异,存在漏网可能。
  4. 外部实现漂移:新公式对齐 Transformers v5.13.0,若 HF 后续调整视频取整逻辑,vLLM 侧需要同步跟进。
  5. CI 噪音:PR 作者反馈 ci/pr 存在与本次改动无关的 Zamba2 OOM 失败,合入时需排除该类不稳定因素。

影响所有使用 Qwen2-VL、GLM-4.1V、Kanana-V、Keye、LLaVA-OneVision2、MiMo-V2-Omni 且视频 temporal_patch_size 大于 2 的服务:修复 vLLM 侧视频 token 预算低估问题,使 dummy 输入、warmup 与真实 HF 预处理更加一致;由于不改真实预处理、模型权重与生成路径,推理输出不变。对团队而言改动面很小(7 个文件、+34/-9),测试为 CPU-only,CI 成本低;对代码库而言是一次向新 Transformers 行为对齐的修正。

影响 6 个多模态模型估算 无共享 helper,手工同步 6 处 回归测试仅覆盖 GLM4V 估算值变化影响 warmup 与 dummy 输入 CI 存在无关 OOM 噪音

关联 Issue

#47866 [Bug][Multi-modal]: Video frames should be paded right by temporal_patch_size
#47876 Fix video temporal padding token estimates

完整报告

参与讨论