Prhub

#41799 [MM][Gemma4] Respect max_soft_tokens in encoder budget

原始 PR 作者 lesj0610 合并时间 2026-05-06 20:54 文件变更 2 提交数 4 评论 5 代码增减 +91 / -11

执行摘要

修复 Gemma4 编码器预算未遵循配置的 max_soft_tokens

Gemma4 支持显式数值 max_soft_tokens(70, 140, 280, 560, 1120),运行时处理已遵循配置,但 get_mm_max_tokens_per_item() 总是返回 280。对于 560 或 1120,编码器预算低估可能导致问题。

值得精读,尤其是 _get_max_soft_tokens 的工具化设计,展示了如何统一处理嵌套配置键以避免重复逻辑和一致性风险。测试用例也很好地覆盖了正常、边界和异常输入。

讨论亮点
  1. gemini-code-assist[bot] 指出 images_kwargs 非 dict 时可能引发 AttributeError,且 _get_prompt_updates 未处理嵌套参数会导致推理崩溃。作者通过 _get_max_soft_tokens 的 isinstance 检查规避了类型风险,并在 _get_prompt_updates 中统一使用该函数修复了缺失。
  2. Isotr0py 询问视频项目是否受影响。作者确认视频预算是独立路径(固定 70 tokens/frame),该 PR 仅调整图像预算。
  3. Isotr0py 建议在测试中同时验证视频 token 数量以确保稳健性。作者采纳并添加了断言。

实现拆解

  1. 定义支持值与提取函数:在 gemma4_mm.py 新增模块级常量 _SUPPORTED_SOFT_TOKENS 和工具函数 _get_max_soft_tokens,从 merged_kwargs 中提取 max_soft_tokens,支持顶层和嵌套(images_kwargs)两种路径。
  2. 修改预算计算:在 get_mm_max_tokens_per_item() 中调用 _get_max_soft_tokens,若返回值为有效整数则替换默认 tokens_per_image,使编码器预算与运行时一致。
  3. 统一参数解析:重构 _call_hf_processor() 中已有的 max_soft_tokens 提取逻辑,替换为 _get_max_soft_tokens 以避免重复,并正确区分顶层与嵌套来源以决定是否注入 patched_mm_kwargs。
  4. 新增测试覆盖:在 test_gemma4.py 中新增两个参数化测试,分别验证 get_mm_max_tokens_per_item 返回正确 token 数(包括视频 token 断言)以及嵌套 max_soft_tokens 下的 prompt 替换正确。
文件 模块 状态 重要度
vllm/model_executor/models/gemma4_mm.py 模型执行 modified 7.28
tests/models/multimodal/processing/test_gemma4.py 测试 modified 6.1

关键符号

_get_max_soft_tokens get_mm_max_tokens_per_item _call_hf_processor get_replacement_image

关键源码片段

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

核心实现文件:新增 _get_max_soft_tokens 工具函数和 _SUPPORTED_SOFT_TOKENS 常量,修改 get_mm_max_tokens_per_item 和 _call_hf_processor 以使用配置值。

# 模块顶部新增常量和工具函数
_SUPPORTED_SOFT_TOKENS = (70, 140, 280, 560, 1120) # 显式支持的数值def _get_max_soft_tokens(
    merged_kwargs: Mapping[str, object],
) -> tuple[object | None, bool]:
    """从合并的 mm_kwargs 中提取 max_soft_tokens,返回 (值, 是否顶层)"""
    val = merged_kwargs.get("max_soft_tokens")
    if val is not None:
        return val, True # 顶层设置
​
    images_kwargs = merged_kwargs.get("images_kwargs")
    if isinstance(images_kwargs, Mapping):
        return images_kwargs.get("max_soft_tokens"), False # 嵌套设置
​
    return None, False # 未设置# 修改后的预算计算方法
class Gemma4ProcessingInfo(BaseProcessingInfo):
    def get_mm_max_tokens_per_item(
        self, seq_len: int, mm_counts: Mapping[str, int]
    ) -> Mapping[str, int] | None:
        config = self.get_hf_config()
        # 默认值来自 vision_config
        tokens_per_image = config.vision_config.default_output_length
        merged_kwargs = self.ctx.get_merged_mm_kwargs({})
        val, _ = _get_max_soft_tokens(merged_kwargs)
        if isinstance(val, int) and val in _SUPPORTED_SOFT_TOKENS:
            tokens_per_image = val # 使用用户配置值
        tokens: dict[str, int] = {"image": tokens_per_image}
        if config.audio_config is not None:
            processor = self.get_hf_processor()
            tokens["audio"] = processor.audio_seq_length
        # 视频预算固定
        tokens["video"] = _VIDEO_MAX_FRAMES * (_VIDEO_MAX_SOFT_TOKENS + 2 + 6)
        return tokens
tests/models/multimodal/processing/test_gemma4.py test-coverage

新增两个测试函数,覆盖显式 max_soft_tokens 在预算计算和 prompt 替换中的行为,包括嵌套和异常情况。

@pytest.mark.parametrize(
    ("mm_processor_kwargs", "expected_image_tokens"),
    [
        ({}, 280), # 默认值
        ({"max_soft_tokens": 70}, 70),
        ({"max_soft_tokens": 280}, 280),
        ({"max_soft_tokens": 1120}, 1120),
        ({"images_kwargs": {"max_soft_tokens": 560}}, 560),
        ({"images_kwargs": None}, 280), # 非 dict 回退默认
        ({"images_kwargs": "not-a-dict"}, 280), # 异常值回退默认
    ],
)
@pytest.mark.parametrize("model_id", [GEMMA4_MODEL_ID])
def test_get_mm_max_tokens_per_item_respects_configured_max_soft_tokens(
    model_id: str,
    mm_processor_kwargs: dict[str, object],
    expected_image_tokens: int,
):
    """验证 get_mm_max_tokens_per_item 返回与配置匹配的图像 token 数,同时确认视频 token 不受影响"""
    ctx = build_model_context(
        model_id,
        mm_processor_kwargs=mm_processor_kwargs,
        limit_mm_per_prompt={"image": 1, "video": 1},
    )
    processor = MULTIMODAL_REGISTRY.create_processor(ctx.model_config)
    tokens = processor.info.get_mm_max_tokens_per_item(
        seq_len=ctx.model_config.max_model_len,
        mm_counts={"image": 1, "video": 1},
    )
    assert tokens is not None
    assert tokens["image"] == expected_image_tokens
    # 视频 token 计算公式固定
    assert tokens["video"] == 32 * (70 + 2 + 6)

评论区精华

images_kwargs 类型安全与嵌套参数一致性 正确性

gemini-code-assist 指出 images_kwargs 非 dict 时可能 AttributeError,且 _get_prompt_updates 未处理嵌套参数会导致预算与运行时错配。

结论:作者通过 _get_max_soft_tokens 中的 isinstance 检查规避了类型风险,并在 _get_prompt_updates 中统一使用该函数,修复了不一致。 · 已解决

视频预算是否受 max_soft_tokens 影响 question

Isotr0py 询问视频项目是否会受配置影响。作者回答视频是独立路径,每帧固定 70 tokens,此 PR 仅修改图像预算。

结论:视频预算不随图像配置变化,保持独立。 · 已解决

测试中增加视频 token 断言 测试

Isotr0py 建议在测试中同时验证视频 token 数量以确保稳健性,作者同意。

结论:测试函数新增 assert tokens["video"] == 32 * (70 + 2 + 6) 断言。 · 已解决

风险与影响

  1. 配置项类型风险:若用户传入非 dict 的 images_kwargs(如字符串),直接调用 .get() 会抛出 AttributeError。_get_max_soft_tokens 通过 isinstance 判断已规避此风险。
  2. 嵌套参数一致性:之前 _call_hf_processor 和 _get_prompt_updates 对 max_soft_tokens 的提取逻辑不一致,可能导致预算与运行时错配。统一使用 _get_max_soft_tokens 后风险降低。
  3. 回归风险:修改了 get_mm_max_tokens_per_item 的返回值计算方式,可能影响其他依赖该值的模块(如调度器内存规划)。但仅针对 Gemma4 模型,且测试已覆盖主要路径。

影响范围:仅限于使用 Gemma4 模型并配置 mm_processor_kwargs 中 max_soft_tokens 的用户。对于默认设置(280)无行为变化。对于显式设置较小值(70)可减少内存占用,设置较大值(560/1120)可避免编码器预算不足导致的崩溃。
影响程度:中等。修复了潜在的推理崩溃,但仅影响特定配置场景。

images_kwargs 类型校验 视频预算独立

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论