Prhub

#53808 [Bugfix][Multimodal] Honor modality-scoped mm_processor_kwargs

原始 PR 作者 Prudhvivuda 合并时间 2026-08-31 14:37 文件变更 5 提交数 2 评论 11 代码增减 +172 / -9

执行摘要

修复模态级 mm_processor_kwargs 不生效与视频 / 图像预算串扰

Issue #52834 指出两条失败路径:scoped 形式(videos_kwargs)被 vLLM 自身读取点静默忽略,导致编码器缓存按约 12K 视频 token 预留、运行时却可能产生约 229K token 项,属于 #51603 描述的 encoder cache starvation / livelock 故障类;flat 形式(size)则把图像最大项从约 16K 抬到约 448K token,默认 processor cache 下直接中止启动(#52835)。issue 作者强调 HF processor 本身接受模态级嵌套形式(Qwen3VLProcessor._merge_kwargs 会 honor videos_kwargs),vLLM 应与 HF 语义对齐;此前唯一 workaround 是手工改 checkpoint 内的 video_preprocessor_config.json,不可从 serve 配置复现。

值得精读。核心价值在于 overlay_modality_mm_kwargs 的接口设计(modality=None 保旧行为、非 Mapping 忽略、scoped 优先于 flat、保留嵌套键)以及“读取侧按模态展开、HF 侧保留嵌套”的双轨策略;对维护多模态模型的工程师有直接参考价值。建议后续为 GLM4 / Eagle 等其他模型按同一模式接入,并在文档中明确 scoped 与 flat 同时出现时的优先级。

讨论亮点

Review 评论为 0 条,维护者 DarkLight1337 直接批准(“Thanks for fixing!”),说明方案对上了 issue 描述的两条失败路径。Issue 评论区的主要讨论是 CI 分类与 pre-commit 修复:作者将 Buildkite #85589 的两个失败任务归为无关(H200 上 Kimi K3 的 CUDA illegal-address 是已知问题、正被 #53774 回退;MI300 上内存日志断言抖动来自另一进程释放显存),维护者据此 /ci retry 只重跑失败步骤;mergify 两次提示 pre-commit 失败,作者补跑 pre-commit run --all-files 后修复。此外 commit 14891514 记录了一个接口类型权衡:get_merged_mm_kwargs 若声明返回 dict[str, object],会让 GLM4 / Eagle 调用点的 int() 与 **kwargs 被 mypy 拒绝,因此最终放宽为 dict[str, Any]。

实现拆解

变更入口是 vllm/multimodal/processing/context.py 的 InputProcessingContext.get_merged_mm_kwargs,它被所有模型的多模态读取点共用;随后在 Qwen2/3-VL 读取点逐个接入。实现分 5 步:

  1. 新增模态映射与叠加函数:在 context.py 定义 _HF_MODALITY_PROCESSOR_KWARGS(image→images_kwargs、video→videos_kwargs、audio→audio_kwargs)与 overlay_modality_mm_kwargs。函数返回 dict 副本:modality=None 时原样复制保持旧行为;只叠加指定模态的嵌套 dict;非 Mapping 值直接忽略;scoped 键优先于同名扁平键,且保留嵌套键本身供 HF 侧继续读取。
  2. 扩展合并入口:get_merged_mm_kwargs 增加 keyword-only 参数 modality,先走原有 merge_mm_processor_kwargs 合并再 overlay。call_hf_processor 与 processor 构造路径不传 modality,保证嵌套 kwargs 原样到达 HF processor。
  3. Qwen3-VL 读取点接入(vllm/model_executor/models/qwen3_vl.py):_get_vision_info 新增 modality 参数,未指定时按 processor 类型推断(Qwen3VLVideoProcessor → video,否则 image);get_max_video_tokens、get_dummy_mm_data、_apply_hf_processor_main 的视频逐项 size 拷贝显式传 modality="video"。
  4. Qwen2-VL 读取点接入(vllm/model_executor/models/qwen2_vl.py):_get_vision_info 同样加 modality;get_num_image_tokens 传 "image"、get_num_video_tokens 传 "video"、get_image_size_with_most_features 默认分支传 modality="image"。
  5. 测试与类型配套:tests/multimodal/test_processing.py 新增 5 个 overlay 单测;tests/models/multimodal/processing/test_qwen3_vl.py 新增 _probe_mm_token_budgets 与回归测试,复刻 issue 的 CPU 探测脚本。第二个 commit 把返回类型从 dict[str, object] 放宽为 dict[str, Any],因为收紧的类型让 GLM4 / Eagle 等调用点的 int() 与 **kwargs 被 mypy 拒绝。
文件 模块 状态 重要度
vllm/multimodal/processing/context.py 多模态处理 modified 7.69
vllm/model_executor/models/qwen3_vl.py 模型实现 modified 6.2
vllm/model_executor/models/qwen2_vl.py 模型实现 modified 5.44
tests/multimodal/test_processing.py 单元测试 modified 6.34
tests/models/multimodal/processing/test_qwen3_vl.py 回归测试 modified 6.0

关键符号

overlay_modality_mm_kwargs get_merged_mm_kwargs _get_vision_info get_max_video_tokens get_dummy_mm_data _apply_hf_processor_main get_num_image_tokens get_num_video_tokens get_image_size_with_most_features

关键源码片段

vllm/multimodal/processing/context.py core-logic

核心改动:新增 overlay_modality_mm_kwargs 与 _HF_MODALITY_PROCESSOR_KWARGS 映射,get_merged_mm_kwargs 增加模态参数,是全部行为的枢纽。

# HuggingFace processors 除了共享的扁平命名空间,还接受嵌套的
# images_kwargs / videos_kwargs / audio_kwargs。vLLM 的 token 预算与
# dummy-input 读取只认扁平键,因此需要按模态叠加。
_HF_MODALITY_PROCESSOR_KWARGS = {
    "image": "images_kwargs",
    "video": "videos_kwargs",
    "audio": "audio_kwargs",
}
​
​
def overlay_modality_mm_kwargs(
    kwargs: Mapping[str, object],
    modality: str | None,
) -> dict[str, Any]:
    """将 HF 风格嵌套 processor kwargs 叠加到扁平命名空间。
​
    只叠加请求的模态,保证视频 size 上调不会污染图像预算;
    modality 为 None 时保持原共享命名空间行为。
    """
    merged = dict(kwargs)
    if modality is None:
        return merged
​
    scoped_key = _HF_MODALITY_PROCESSOR_KWARGS.get(modality)
    if scoped_key is None:
        return merged
​
    scoped = merged.get(scoped_key)
    if not isinstance(scoped, Mapping):
        # 非 dict 的 scoped 值直接忽略,避免字符串等被误当配置展开
        return merged
​
    # 叠加时保留原嵌套键,HF 侧仍能读到 videos_kwargs 等结构
    return merged | dict(scoped)
​
​
# InputProcessingContext 上的合并入口
def get_merged_mm_kwargs(
    self,
    kwargs: Mapping[str, object],
    *,
    modality: str | None = None,
) -> dict[str, Any]:
    """合并配置与请求维度的 mm_processor_kwargs。
​
    modality 非 None 时,把嵌套的 images_kwargs / videos_kwargs /
    audio_kwargs 叠加到扁平命名空间,供 vLLM 侧读取(token 预算、
    dummy 输入);processor 构造与 HF __call__ 应省略 modality,
    让嵌套 dict 原样传给 HF processor。
    """
    mm_config = self.model_config.get_multimodal_config()
    merged = mm_config.merge_mm_processor_kwargs(kwargs)
    return overlay_modality_mm_kwargs(merged, modality)
tests/models/multimodal/processing/test_qwen3_vl.py test-coverage

端到端回归:用 issue 的 CPU 探测脚本验证 videos_kwargs 只抬视频预算、flat 仍双模态共享,防止 #52834 复现。

# Qwen3-VL / Qwen3.8 长视频理解的像素预算来自模型卡,
# 用于复现 #52834 中 --mm-processor-kwargs 的作用域问题
_LONG_VIDEO_SIZE = {"longest_edge": 469762048, "shortest_edge": 4096}
​
​
def _probe_mm_token_budgets(
    model_id: str, mm_processor_kwargs: dict[str, Any] | None
) -> tuple[int, int]:
    ctx = build_model_context(
        model_id,
        mm_processor_kwargs=mm_processor_kwargs,
        limit_mm_per_prompt={"image": 1, "video": 1},
    )
    info = MULTIMODAL_REGISTRY.create_processor(ctx.model_config).info
    video = info.get_max_video_tokens(
        seq_len=500000, mm_counts={"video": 1, "image": 1}
    )
    return video, info.get_max_image_tokens()
​
​
@pytest.mark.skip_global_cleanup
@pytest.mark.parametrize("model_id", [MODEL_ID])
def test_processor_kwargs_videos_kwargs_does_not_leak_into_image_budget(
    model_id: str,
) -> None:
    """videos_kwargs.size 必须只抬高视频 token 预算。
​
    扁平 size 仍同时作用于两种模态(保持旧共享命名空间行为),
    作为 #52834 的回归测试。
    """
    stock_video, stock_image = _probe_mm_token_budgets(model_id, None)
    scoped_video, scoped_image = _probe_mm_token_budgets(
        model_id, {"videos_kwargs": {"size": _LONG_VIDEO_SIZE}}
    )
    flat_video, flat_image = _probe_mm_token_budgets(
        model_id, {"size": _LONG_VIDEO_SIZE}
    )
​
    assert scoped_video == flat_video
    assert scoped_video > stock_video
    assert scoped_image == stock_image
    assert flat_image > stock_image

评论区精华

CI 失败任务与本 PR 无关的判定 other

作者在 issue 评论区分类 Buildkite #85589 的两个失败:H200 上 Kimi K3 的 CUDA illegal-address(已知问题、正被 #53774 回退)与 MI300 上内存日志断言抖动(另一进程释放显存)。

结论:维护者 DarkLight1337 执行 /ci retry 只重跑失败步骤。 · 已解决

get_merged_mm_kwargs 返回类型收紧引发 mypy 回归 设计

commit 14891514 记录:显式 dict[str, object] 让 GLM4 / Eagle 调用点的 int() 与 **kwargs 被 mypy 拒绝,而这些位置此前是 Any。

结论:改为 dict[str, Any] 保持旧调用点兼容,代价是静态检查强度下降。 · 已解决

pre-commit 检查失败与修复 style

mergify[bot] 两次提示 pre-commit checks failed,要求运行 pre-commit install 与 run --all-files。

结论:作者补跑 pre-commit 并提交修复。 · 已解决

风险与影响

  • 语义优先级新增:当扁平 size 与 videos_kwargs.size 同时存在时,video 读取以 scoped 为准;这是新增的优先级规则,已有单测覆盖,但用户可能对“flat 仍共享、scoped 优先”并存感到意外,建议在文档中明确。
  • 覆盖范围有限:目前只有 Qwen2/3-VL 的读取点接入。GLM4 / Eagle 等模型若使用同样嵌套 kwargs,vLLM 自身读取仍只看扁平键,scoped 配置仍会被吞;该模式可复用,但需要后续逐个模型接入。
  • 类型放宽:dict[str, Any] 降低了静态检查强度,是兼容旧调用点的代价。
  • 嵌套键残留:overlay 后的 dict 仍包含 videos_kwargs 等键;当前读取点只按名取 size / min_pixels / max_pixels 不会冲突,但若未来有人把整个 dict 以 **kwargs 展开传给下游,需要留意去重。
  • 回归面:改动集中在多模态处理上下文与两个 VL 模型,生成、采样、checkpoint 加载路径未触碰;CI 两次运行的相关 Qwen / multimodal 任务均通过,失败项均为无关已知问题。
  • 用户侧:Qwen3-VL / Qwen3.8 长视频 recipe 现在可以直接用 --mm-processor-kwargs '{"videos_kwargs": {"size": ...}}' 生效,无需手工改 checkpoint 里的 video_preprocessor_config.json;消除因编码器缓存低估导致的饥饿 / 活锁,以及 448K 图像 dummy 的分钟级 profiling 与启动中止。
  • 系统侧:encoder 缓存预算与运行时 token 数对齐,对 Qwen2/3-VL 的混合 image + video 推理更稳健,预留在开机 profiling 阶段就与 HF 实际处理一致。
  • 团队侧:沉淀了“HF 嵌套语义 ↔ vLLM 扁平读取”的通用对齐模式,overlay_modality_mm_kwargs 可被后续其他多模态模型直接复用,减少同类 bug 的重复修复成本。
  • 影响程度:中等偏低;改动面仅限多模态处理核心与两个 VL 模型,未触碰生成与采样路径,向后兼容(flat 行为不变)。
配置语义优先级变化 仅 Qwen2/3-VL 接入 返回类型放宽为 Any 嵌套键残留需留意

关联 Issue

#52834 [Bug]: --mm-processor-kwargs cannot scope an override to one modality: videos_kwargs is ignored by profiling while a flat "size" leaks into the image budget (Qwen3-VL)

完整报告

参与讨论