执行摘要
- 一句话:修复 Gemma4 编码器预算未遵循配置的 max_soft_tokens
- 推荐动作:值得精读,尤其是 _get_max_soft_tokens 的工具化设计,展示了如何统一处理嵌套配置键以避免重复逻辑和一致性风险。测试用例也很好地覆盖了正常、边界和异常输入。
功能与动机
Gemma4 支持显式数值 max_soft_tokens(70, 140, 280, 560, 1120),运行时处理已遵循配置,但 get_mm_max_tokens_per_item() 总是返回 280。对于 560 或 1120,编码器预算低估可能导致问题。
实现拆解
- 定义支持值与提取函数:在 gemma4_mm.py 新增模块级常量 _SUPPORTED_SOFT_TOKENS 和工具函数 _get_max_soft_tokens,从 merged_kwargs 中提取 max_soft_tokens,支持顶层和嵌套(images_kwargs)两种路径。
- 修改预算计算:在 get_mm_max_tokens_per_item() 中调用 _get_max_soft_tokens,若返回值为有效整数则替换默认 tokens_per_image,使编码器预算与运行时一致。
- 统一参数解析:重构 _call_hf_processor() 中已有的 max_soft_tokens 提取逻辑,替换为 _get_max_soft_tokens 以避免重复,并正确区分顶层与嵌套来源以决定是否注入 patched_mm_kwargs。
- 新增测试覆盖:在 test_gemma4.py 中新增两个参数化测试,分别验证 get_mm_max_tokens_per_item 返回正确 token 数(包括视频 token 断言)以及嵌套 max_soft_tokens 下的 prompt 替换正确。
关键文件:
vllm/model_executor/models/gemma4_mm.py(模块 模型执行;类别 source;类型 data-contract;符号 _get_max_soft_tokens, _SUPPORTED_SOFT_TOKENS, get_mm_max_tokens_per_item, _call_hf_processor): 核心实现文件:新增 _get_max_soft_tokens 工具函数和 _SUPPORTED_SOFT_TOKENS 常量,修改 get_mm_max_tokens_per_item 和 _call_hf_processor 以使用配置值。
tests/models/multimodal/processing/test_gemma4.py(模块 测试;类别 test;类型 test-coverage;符号 test_get_mm_max_tokens_per_item_respects_configured_max_soft_tokens, test_get_prompt_updates_respects_nested_max_soft_tokens): 新增两个测试函数,覆盖显式 max_soft_tokens 在预算计算和 prompt 替换中的行为,包括嵌套和异常情况。
关键符号:_get_max_soft_tokens, get_mm_max_tokens_per_item, _call_hf_processor, get_replacement_image
关键源码片段
vllm/model_executor/models/gemma4_mm.py
核心实现文件:新增 _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
新增两个测试函数,覆盖显式 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)
评论区精华
- gemini-code-assist[bot] 指出 images_kwargs 非 dict 时可能引发 AttributeError,且 _get_prompt_updates 未处理嵌套参数会导致推理崩溃。作者通过 _get_max_soft_tokens 的 isinstance 检查规避了类型风险,并在 _get_prompt_updates 中统一使用该函数修复了缺失。
- Isotr0py 询问视频项目是否受影响。作者确认视频预算是独立路径(固定 70 tokens/frame),该 PR 仅调整图像预算。
- Isotr0py 建议在测试中同时验证视频 token 数量以确保稳健性。作者采纳并添加了断言。
- images_kwargs 类型安全与嵌套参数一致性 (correctness): 作者通过 _get_max_soft_tokens 中的 isinstance 检查规避了类型风险,并在 _get_prompt_updates 中统一使用该函数,修复了不一致。
- 视频预算是否受 max_soft_tokens 影响 (question): 视频预算不随图像配置变化,保持独立。
- 测试中增加视频 token 断言 (testing): 测试函数新增 assert tokens["video"] == 32 * (70 + 2 + 6) 断言。
风险与影响
关联脉络
- PR #40599 [MM][Gemma4] Support auto max_soft_tokens (upstream issue): PR body 明确说明 #40599 覆盖 'auto' 支持,本 PR 只处理显式数值,两者互补构成完整功能。
参与讨论