执行摘要
- 一句话:修正视频帧数向上取整公式,修复 6 个多模态模型的 token 估算
- 推荐动作:值得阅读。这是一个典型的「公式语义错误」修复,
num_frames + (-num_frames % temporal_patch_size) 是标准向上取整写法,可用于识别同类 bug;PR 对「是否引入共享 helper」做了明确取舍(最小化替代 #47876),展示了在低风险合入与长期可维护性之间权衡的决策样例。后续可考虑把 6 处重复公式收敛为公共工具函数,并补充其余 5 个模型的直接测试。
功能与动机
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 处理器真实输出不一致。
实现拆解
- 定位根因:在
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 不成立。
- 统一替换公式:将
padded_num_frames 改为 num_frames + (-num_frames % temporal_patch_size),并把注释链接更新为 Transformers v5.13.0 的 video_processing_qwen2_vl.py 对应实现,保证 vLLM 侧估算与新版 HF 处理器行为对齐。
- 处理 MiMo 特例:
mimo_v2_omni.py 中 effective_frames 由 num_frames * tokens_per_second 计算得到,取整后的帧数需显式转换为 int,避免浮点值进入 grid_t 计算。
- 新增回归测试:在
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 运行、无需模型权重。
- 刻意保持最小改动:PR body 说明这是 #47876 的最小化替代方案,只修改 6 个调用点并补充聚焦回归测试,不新增共享 helper 或修改
math_utils,降低合入风险。
关键文件:
vllm/model_executor/models/glm4_1v.py(模块 多模态;类别 source;类型 data-contract;符号 _get_vision_info): GLM4V 的 _get_vision_info 是本次修复的代表实现,且回归测试的直接对象。
vllm/model_executor/models/qwen2_vl.py(模块 多模态;类别 source;类型 data-contract;符号 _get_vision_info): Qwen2-VL 是问题源头模型,其取整公式曾被其他模型复制,修复此处影响面最大。
vllm/model_executor/models/kanana_v.py(模块 多模态;类别 source;类型 data-contract;符号 _get_vision_info): 与 Qwen2-VL 同样复制的旧公式,作为 6 个受影响模型之一被同步修复。
vllm/model_executor/models/keye.py(模块 多模态;类别 source;类型 data-contract;符号 _get_vision_info): Keye 的 _get_vision_info 同样存在旧取整公式,属于本次批量修复对象。
vllm/model_executor/models/llava_onevision2.py(模块 多模态;类别 source;类型 data-contract;符号 _get_vision_info): LLaVA-OneVision2 的 _get_vision_info 存在同类公式问题,本次一并修复。
vllm/model_executor/models/mimo_v2_omni.py(模块 多模态;类别 source;类型 data-contract;符号 _get_vision_info): MiMo-V2-Omni 除替换公式外,还需把取整后的帧数显式转为 int,是六个模型中的特殊点。
tests/models/multimodal/processing/test_glm4_1v.py(模块 多模态测试;类别 test;类型 test-coverage;符号 test_vision_info_rounds_up_temporal_frames): 新增参数化 CPU 回归测试,直接验证 temporal_patch_size 2/4/8 下 17 帧视频的 grid_t 估算,是本次修复的核心证据。
关键符号:_get_vision_info, test_vision_info_rounds_up_temporal_frames
关键源码片段
vllm/model_executor/models/glm4_1v.py
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
新增参数化 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
评论区精华
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。
- 视频加载器是否已对帧数补齐,估算公式修正是否必要 (question): labAxiaoming 解释:加载器只把奇数帧复制为偶数帧(保证为 2 的倍数),并不能保证是 temporal_patch_size(如 4、8)的倍数;而 _get_vision_info 是 vLLM 侧预算估算、dummy 输入与 warmup 专用,两者职责不同,修正仍必要。
风险与影响
- 风险:
- 估算值变大:修复后 temporal_patch_size > 2 场景的估算 token 数会增多,dummy 输入与 warmup 开销可能略增,显存紧张环境下需留意 warmup 内存变化。
- 无共享 helper:6 处调用点是手工同步修改,后续新增模型或遗漏调用点时可能再次复制旧公式;MiMo 的
int 转换是额外特殊点,需单独维护。
- 测试覆盖局限:回归测试只直接断言 GLM4V 的
_get_vision_info,其余 5 个模型未加独立断言,若各模型从 HF 复制的逻辑有细微差异,存在漏网可能。
- 外部实现漂移:新公式对齐 Transformers v5.13.0,若 HF 后续调整视频取整逻辑,vLLM 侧需要同步跟进。
- 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 噪音
关联脉络
- PR #47876 Fix video temporal padding token estimates: 同一 issue #47866 的草稿方案:引入共享 helper 统一修复;本 PR 是它的最小化替代实现,PR body 明确对比了两者取舍。
- PR #53553 [Bugfix][MM] Fix JinaVL processing cache order: 同属多模态处理路径的估算与缓存一致性 bugfix,且同样落在 tests/models/multimodal/processing 测试域,反映该区域近期在收紧多模态 token 估算正确性。
参与讨论