执行摘要
- 一句话:按 transformers 版本拆分多模态处理器并启用缓存
- 推荐动作:值得精读,特别是关注多模态处理器与上游 transformers 的契约设计:版本门控双处理器、把 HF 展开声明为
PromptReplacement 以复用统一缓存路径、通过 unpad 消除批量 padding 对缓存可复用性的破坏、错误信息直接指导用户上报上游或固定版本。这些设计对理解 vLLM 多模态输入流水线以及后续如何与 HuggingFace 生态协同演进很有价值。
功能与动机
PR body 说明这是对 #51408 和 #51657 的延续,取代 #50107 和 #50899,目标是把 Transformers modeling backend 的多模态路径加固到可缓存、可依赖的程度。关键动机包括:重复图片应命中处理器缓存("A repeated image now hits the cache rather than being reprocessed and re-shipped over IPC");处理器按 batch 公共形状 padding 使单条数据依赖批内其他数据而不可缓存("an item's data depend on what it was processed beside and so uncacheable");以及修复 Idefics3 返回 [1, total_patches, C, H, W] 时 flat_from_sizes 在错误维度切片导致的静默错误("Image fields with a leading batch dimension were sliced on the wrong axis")。
实现拆解
- 处理器按版本分流并抽取公共基类:
vllm/model_executor/models/transformers/multimodal.py 新增 transformers 与 packaging.version 导入,按 Version(transformers.__version__) 选择 LegacyMultiModalProcessor(低于 5.15.0)或 OffsetsMultiModalProcessor(5.15.0 及以上);两者公共逻辑(调用 HF processor、尺寸计算、输出按模态归位)上提为 _MultiModalProcessorBase,占位符定位策略留给子类实现。
- Offsets 路径接入处理器缓存:
OffsetsMultiModalProcessor._get_prompt_updates 把 HF 报告的 text_replacement_offsets 逐个转成 PromptReplacement,因此走基类 apply() 的缓存路径而非重写 apply();新增 _get_embed_token_id(取替换 token 众数作为承载 embedding 的 token)、_get_slice_dim(处理 leading batch 维)、_unpad_images/_unpad_audios(去掉 Emu3 等按 batch 最大形状 padding 造成的尺寸依赖),num_image_patches 在 offsets 路径上改从 processor 的 _get_num_multimodal_tokens 获取而非逐行猜测。
- 行为变更与错误提示:从 5.15.0 起,不报告替换 offsets 的 processor 被直接拒绝,错误信息点名 processor 与其缺失的
replace_<modality>_token hook,并引导用户上报上游或固定 transformers<5.15.0;纯文本提示词走 _apply_hf_processor_text_only(传空 mm_data),只有纯模态提示词才使用 dummy text(_apply_hf_processor_mm_only)。
- Bug 修复:修复 Idefics3
pixel_values=[1, total_patches, C, H, W] 在 flat_from_sizes 上切错轴的问题;Idefics3Processor 拒绝含 <image> 但无图片的提示词时回退 tokenizer(与 idefics3.py 现有处理一致);Emu3 在未展开 prompt 上丢失 BOS token,标记 xfail 等待上游修复。
- 测试配套:新增
tests/models/multimodal/processing/transformers_backend.py,提供 create_processor/create_cached_processor 直接构造两类处理器(绕开注册表的版本门控,保证两条路径都有覆盖),并新增 offsets_only 标记与 PROCESSOR_CLASSES 参数化;图像、音频测试全部改为双路径参数化,新增缓存命中断言(make_stats().hits)、拼接 prompt 与 HF 展开一致性、Emu3 padding 去除、嵌套图像字段按图拆分、Gemma3 结构 token 掩码等测试。
关键文件:
vllm/model_executor/models/transformers/multimodal.py(模块 多模态处理;类别 source;类型 data-contract;符号 _get_embed_token_id, _get_audio_token_id, MultiModalProcessor, _get_prompt_updates): 核心源码文件:新增 _MultiModalProcessorBase 公共基类、LegacyMultiModalProcessor 与 OffsetsMultiModalProcessor 版本分流、_get_embed_token_id、_get_slice_dim、缓存路径与 unpad 逻辑,并包含 Idefics3 切片修复与无 offsets 拒绝的行为变更。
tests/models/multimodal/processing/test_transformers_image.py(模块 图像测试;类别 test;类型 test-coverage;符号 test_multimodal_processor, _process_two_images, test_image_multiple_inputs, test_image_adjacent_inputs): 图像侧测试全套参数化为 legacy/offsets 双路径,新增缓存命中、Emu3 padding 去除、Gemma3 结构 token 掩码等关键用例。
tests/models/multimodal/processing/test_transformers_audio.py(模块 音频测试;类别 test;类型 test-coverage;符号 test_audio_multimodal_processor, _process_granite_speech, test_audio_multiple_inputs, process): 音频侧测试双路径参数化,多音频输入按占位符逐一拆分,Voxtral 的 xfail 原因更新为新行为。
tests/models/multimodal/processing/transformers_backend.py(模块 测试工具;类别 test;类型 test-coverage;符号 create_processor, create_cached_processor): 新增测试工具模块:直接构造两类处理器与真实缓存,保证版本门控下两条路径都有 CI 覆盖。
关键符号:_get_embed_token_id, _get_prompt_updates, _get_hf_mm_data, _get_slice_dim, _call_hf_processor, create_processor, create_cached_processor, test_repeated_image_hits_the_processor_cache, test_spliced_prompt_matches_hf_expansion, test_batch_padding_removed_from_image_items
关键源码片段
vllm/model_executor/models/transformers/multimodal.py
核心源码文件:新增 _MultiModalProcessorBase 公共基类、LegacyMultiModalProcessor 与 OffsetsMultiModalProcessor 版本分流、_get_embed_token_id、_get_slice_dim、缓存路径与 unpad 逻辑,并包含 Idefics3 切片修复与无 offsets 拒绝的行为变更。
# vllm/model_executor/models/transformers/multimodal.py
# 按 transformers 版本选择多模态处理器:低于 5.15.0 用 Legacy,5.15.0 及以上用 Offsets。
_MODALITY_SIZE_KEYS = {"audio": "num_audio_tokens", "image": "num_image_patches"}
def _get_embed_token_id(replacement_ids: torch.Tensor) -> int:
"""HF 展开里重复的那个 token 就是承载 embedding 的 token,取众数得到它。
PromptReplacement 展开时会重复同一 token,mode() 拿到出现次数最多的值,
即可作为该模态的 embedding token。
"""
return int(replacement_ids.mode().values)
class _MultiModalProcessorBase(BaseMultiModalProcessor[MultiModalProcessingInfo]):
"""两种后端处理器共享的公共流程:调用 HF processor、计算图像尺寸、
输出按模态归位;占位符定位策略由子类决定。
Legacy 子类:基于 mm_token_type_ids 在扩展后的 prompt 上定位占位符,
无法命中缓存;
Offsets 子类:把每个展开声明为 PromptReplacement,走基类的缓存路径,
重复的图片命中处理器缓存而不是重新处理再走 IPC。
"""
def _get_prompt_updates(self, ...):
# 子类实现:返回 PromptUpdate 列表,描述 prompt 中每个模态占位符
# 如何被展开替换(offsets 路径)或保持原样(legacy 路径)。
...
def _get_hf_mm_data(self, ...):
# 组装喂给 HF processor 的 mm_data,并记录各模态的尺寸信息,
# 供后续 flat_from_sizes 按 item 切回独立数据。
...
def _get_slice_dim(self, field: str) -> int:
# 部分 processor(如 Idefics3)返回 [1, total_patches, C, H, W],
# 带 leading batch 维,切片必须从 dim 1 开始,否则第一张图会拿到
# 每一行的内容而第二张图拿到空张量。
...
class OffsetsMultiModalProcessor(_MultiModalProcessorBase):
"""根据 HF processor 报告的 text_replacement_offsets 定位占位符,
每个展开声明为一个 PromptReplacement。
把展开表达成 update 之后,就能从不含模态的原始 prompt 出发重建,
因此走基类的通用处理路径,也就能享受处理器缓存:重复图片命中缓存,
不再重新处理、不再跨 IPC 传输。
"""
def _get_prompt_updates(self, ...):
# 读取 hf_inputs 里的 text_replacement_offsets,逐个构造 PromptReplacement;
# 按模态取 num_image_patches / num_audio_tokens 决定占位符长度,
# 并剔除仅供结构化 prompt 使用的非 embedding 辅助 token。
...
评论区精华
- zucchini-nlp 在
multimodal.py:763 询问:offsets 路径是否只有 cached-processing 适用?legacy 是否几个月后会被移除?缓存是否是 vLLM 默认模式?hmellor 回答:缓存由 mm_processor_cache_gb > 0 开启,默认值是 4,所以新路径默认即走缓存。
- zucchini-nlp 在
multimodal.py:368 询问:vLLM 对 text-only 或 mm-only 数据是否用 dummy 方式补缺失模态?hmellor 澄清:_apply_hf_processor_mm_only 使用 dummy text,但 _apply_hf_processor_text_only 只是把空 mm_data 传给 HF processor。
- zucchini-nlp 在
multimodal.py:437 以 "🥲" 表达对 legacy/offsets 双路径长期并存的无奈,hmellor 回应 "Hopefully this won't need to coexist for too long"。
-
zucchini-nlp 在 multimodal.py:1156 提醒:get_imagevideo_features 的输出已在 transformers 5.15 起标准化为 list[torch.Tensor](见 huggingface/transformers#46405),需要保持向后兼容;hmellor 确认该 PR 已随 5.15 发布,因此需要保留当前的 normalization 逻辑直到 5.15 成为最低支持版本。
-
offsets 路径是否默认启用缓存 (design): hmellor 回答缓存由 mm_processor_cache_gb > 0 开启,默认值为 4,因此 offsets 路径默认享受缓存。
- text-only 与 mm-only 是否用 dummy 数据补齐 (design): hmellor 澄清:mm-only 走 _apply_hf_processor_mm_only 使用 dummy text,而 text-only 走 _apply_hf_processor_text_only 只传空 mm_data。
- get_imagevideo_features 输出标准化与向后兼容 (correctness): hmellor 确认该上游 PR 已随 5.15 发布,需保留 normalization 直到 5.15 成为最低支持版本。
- legacy 与 offsets 双路径并存周期 (design): hmellor 回应希望两者不需要共存太久。
风险与影响
- 风险:行为变更影响面:5.15.0 及以上不再对无 offsets 的 processor 做兜底搜索,PR 实测 87/145 个
ProcessorMixin 子类未实现替换 hook(尽管多数不是生成式多模态模型或已有 vLLM 原生实现),用户升级 transformers 后可能突然遇到拒绝服务的报错。缓存正确性依赖一个强前提:缓存未命中时单样本调用 image_processor([img_1]) 必须等于批量调用 image_processor([img_0, img_1])[1];_unpad_images/_unpad_audios 覆盖了 padding 型 processor,但未覆盖的 processor 若有 batch 依赖输出,缓存可能返回与上下文相关的错误数据。Idefics3 切片修复翻转了此前被 re-batch 掩盖的隐性错误,单图与多图边界的回归需要重点验证。Emu3 BOS token 依赖上游 huggingface/transformers#47924 发布,当前以 xfail 掩盖。此外 legacy 与 offsets 双实现并存,长期存在行为分叉与维护成本。
- 影响:用户侧:在
transformers>=5.15.0 下,重复多模态输入默认命中处理器缓存(测试中 2 个请求 1 次命中),显著降低重复预处理与跨 IPC 传输开销;但部分模型会因无替换 offsets 直接报错,需要固定版本或等待上游。系统侧:处理器缓存默认开启(mm_processor_cache_gb=4),占用额外内存,同时减少 CPU 侧重复视觉/音频特征计算。团队侧:测试基础设施新增双路径参数化,vLLM 与 HuggingFace transformers 在处理器接口上双向对齐(offsets 契约、embedding token 推断),后续需在最低支持版本到达 5.15 后清理 legacy 路径。
- 风险标记:行为变更:5.15.0+ 拒绝无 offsets 处理器, 缓存正确性依赖单样本与批量处理等价, 上游依赖:Emu3 BOS 待 transformers 修复, 双处理器路径长期并存维护成本, Idefics3 切片修复回归风险
关联脉络
- PR #51408 Harden Transformers modelling backend multi-modal path(系列前序 [1/N],见本 PR body): 本 PR 明确说明 Continues from #51408,属于同一加固系列的第一步。
- PR #51657 Harden Transformers modelling backend multi-modal path(系列前序 [2/N],见本 PR body): 本 PR 明确说明 Continues from #51657,属于同一加固系列的第二步。
- PR #50107 被本 PR 取代的早期尝试(见本 PR body): PR body 声明本 PR Supersedes #50107,是此前方案被取代。
- PR #50899 被本 PR 取代的早期尝试(见本 PR body): PR body 声明本 PR Supersedes #50899,是此前方案被取代。
- PR #47924 huggingface/transformers: Leftover processors(修复 Emu3 BOS token): 上游 transformers PR,修复 Emu3 在未展开 prompt 上的 BOS 丢失,本 PR 以 xfail 等待其发布。
参与讨论