Appearance
第15章 多模态推理
"A picture is worth a thousand words — and a thousand tokens."
本章要点
- 理解视觉语言模型(VLM)的推理流程与纯文本 LLM 的差异
- 掌握图像编码器缓存的设计:为什么要缓存编码器输出
- 深入多模态输入的预处理管线:从原始图片到 Token embedding
- 理解多模态输入对调度器的影响
- 认识 vLLM 支持的主流 VLM 架构
15.1 VLM 推理的特殊挑战
源码版本:本章基于 vLLM v0.8.5。多模态注册表
vllm/multimodal/registry.py,模型编码器执行vllm/v1/worker/gpu_model_runner.py:1043-1061。
视觉语言模型(如 Qwen2-VL、LLaVA、Pixtral)在 Transformer 之前增加了一个视觉编码器(通常是 ViT),将图片转换为一组"视觉 Token",然后与文本 Token 一起送入 Transformer。
这带来了三个新挑战:
- 编码器计算——视觉编码器的前向传播是 CPU/GPU 密集型操作,可能耗时数十毫秒
- 变长视觉 Token——不同分辨率的图片产生不同数量的视觉 Token
- KV Cache 膨胀——一张图片可能产生几百甚至上千个视觉 Token,大量消耗 KV Cache
15.2 编码器缓存
V1 引入了编码器缓存(Encoder Cache),将视觉编码器的输出缓存起来:
为什么需要缓存?因为分块预填充。
当一个包含图片的请求被分块预填充时,图片的视觉 Token 可能跨越多个块。如果不缓存编码器输出,每个块都要重新运行视觉编码器——这是巨大的浪费。
有了编码器缓存,视觉编码器只运行一次,输出被存储在缓存中,后续的预填充块直接从缓存中读取视觉 embedding。
15.3 多模态输入的预处理管线
从一张原始图片到 Transformer 能消费的 embedding,要经过四步处理:
步骤 1:图片预处理(CPU)——缩放到模型要求的分辨率、像素归一化、转换为 Tensor。V1 中这一步是非阻塞的——在独立线程中执行,不影响 EngineCore 的主循环。预处理结果可以被缓存,相同图片不需要重复处理。
步骤 2:视觉编码器(GPU)——通常是一个预训练的 ViT(Vision Transformer)。输入一张 336×336 的图片,输出 576 个视觉 Token(每个 14×14 patch 一个 Token)。对于高分辨率图片(如 Qwen2-VL 支持的动态分辨率),输出的 Token 数量可能更多——一张 1344×1344 的图片会产生约 9,000 个视觉 Token。
步骤 3:投影层——视觉编码器输出的维度可能与 LLM 的隐藏维度不匹配。投影层(通常是一两个 Linear 层)将视觉 Token 的维度对齐到 LLM 的 hidden_size。
步骤 4:Token 拼接——视觉 Token 被插入到文本 Token 序列中。不同架构的插入位置不同:LLaVA 替换 <image> 占位符的位置;Qwen2-VL 在特殊标记 <|vision_start|>...<|vision_end|> 之间插入。
15.4 多模态输入对调度的影响
视觉 Token 对调度器的影响比看起来大得多。
KV Cache 预算——视觉 Token 和文本 Token 一样消耗 KV Cache 块。一张高分辨率图片产生 9,000 个视觉 Token,需要 ⌈9000/16⌉ = 563 个 KV Cache 块。这相当于一个 9,000 Token 的纯文本请求的显存消耗——但用户可能只发了一张图片加一句话。
调度器需要在分配预算时将视觉 Token 计入总 Token 数:
python
# 简化
total_tokens = len(text_tokens) + num_image_tokens
num_blocks_needed = ceil(total_tokens / block_size)预填充时间——视觉编码器的计算 + 大量视觉 Token 的注意力计算,使得多模态请求的预填充显著慢于纯文本。调度器的分块策略需要为多模态请求预留更大的时间窗口。
V1 的性能优势——V1 的多进程架构在 VLM 场景下优势更加明显。图片预处理(CPU 密集)在 API Server 进程中完成,不阻塞 EngineCore 的调度循环。V0 中所有这些都在同一个 Python 进程内,GIL 导致图片预处理会阻塞 GPU 计算的编排。vLLM 团队报告 V1 在 VLM 工作负载上的吞吐量提升超过 1.7 倍。
15.5 支持的 VLM 架构
vLLM 支持多种 VLM 架构,它们在视觉 Token 的注入方式上有差异:
拼接型(如 LLaVA、Qwen2-VL、PaliGemma)——视觉 Token 在 embedding 层直接替换文本序列中的占位符,之后所有 Token 一起进入 Transformer。这种方式最简单,也最常见。对 vLLM 来说,视觉 Token 就是普通的 Token,只是 embedding 值来自编码器而非词表。
交叉注意力型(如 Flamingo、一些 BLIP2 变体)——视觉 Token 不进入主序列,而是通过专门的交叉注意力层与文本 Token 交互。这种方式需要在注意力计算中增加额外的 KV 来源。
多模态处理代码位于 vllm/multimodal/,采用了可插拔的预处理管线设计。核心是 MultiModalRegistry(registry.py:80):
python
# vllm/multimodal/registry.py:80-89
class MultiModalRegistry:
"""A registry that dispatches data processing according to the model."""
def __init__(self):
self._processor_factories = ClassRegistry[nn.Module, _ProcessorFactories]()
self._processing_cache = ProcessingCache(VLLM_MM_INPUT_CACHE_GIB)每种模态(图片、音频、视频)通过注册表注入自己的处理器工厂。ProcessingCache 缓存了已处理的多模态输入——如果相同的图片被多个请求引用,预处理只执行一次。
15.6 VLM 推理的实践建议
| 场景 | 建议 | 原因 |
|---|---|---|
| 单图对话 | 默认配置即可 | 单图的视觉 Token 通常 < 1000 |
| 多图/高分辨率 | 降低 max_num_seqs | 视觉 Token 大量占用 KV Cache |
| 视频理解 | 限制帧数 + 降低分辨率 | 避免 KV Cache OOM |
| 批量图片标注 | 启用编码器缓存 | 相同图片模板不重复编码 |
| 延迟敏感场景 | 增大 max_num_batched_tokens | 减少分块次数降低 TTFT |
GPU 上的多模态执行流程
在 GPUModelRunner.execute_model()(gpu_model_runner.py:1043-1061)中,多模态请求的处理流程与纯文本不同:
python
# gpu_model_runner.py:1043-1061 (简化)
if self.is_multimodal_model:
# 1. 运行多模态编码器(如 ViT)
self._execute_mm_encoder(scheduler_output)
# 2. 收集编码器输出
mm_embeds = self._gather_mm_embeddings(scheduler_output)
# 3. 将文本 token 和视觉 embedding 统一为 inputs_embeds
input_ids = self.input_ids[:num_scheduled_tokens]
if mm_embeds:
inputs_embeds = self.model.get_input_embeddings(input_ids, mm_embeds)
else:
inputs_embeds = self.model.get_input_embeddings(input_ids)关键观察:视觉编码器只在预填充阶段运行一次(或分块预填充的第一块)。之后的解码步骤中,视觉 Token 的 KV Cache 已经存在于 GPU 显存中,不需要再运行视觉编码器。这就是编码器缓存(15.2 节)的价值。
15.7 音频与视频支持
除了图片,vLLM 还在扩展对其他模态的支持:
音频——语音模型(如 Whisper 系列的多模态变体)需要将音频波形转换为频谱图,再通过音频编码器产生音频 Token。处理流程与图片类似,但预处理步骤不同(重采样、分帧、STFT)。
视频——视频输入本质上是多帧图片。处理策略有两种:均匀抽帧(每 N 帧取 1 帧),或关键帧提取。每帧独立通过视觉编码器,然后所有帧的视觉 Token 拼接后送入 Transformer。一段 10 秒 30fps 的视频如果抽取 8 帧,可能产生 8 × 576 = 4608 个视觉 Token——这对 KV Cache 是巨大的压力。
多模态推理是 vLLM 快速发展的方向。随着 GPT-4o、Gemini 2.0 等全模态模型的出现,推理引擎对多模态的支持越来越关键。
15.7 本章小结
- VLM 挑战——编码器计算、变长视觉 Token、KV Cache 膨胀
- 编码器缓存——运行一次,缓存输出,支持分块预填充复用
- 调度影响——视觉 Token 计入显存预算,预填充更慢
- 可插拔架构——支持交叉注意力、拼接、MoE 等多种注入方式
源码导航
- 多模态处理:
vllm/multimodal/- VLM 模型实现:
vllm/model_executor/models/(带_vl后缀的文件)