Skip to content

第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。

这带来了三个新挑战:

  1. 编码器计算——视觉编码器的前向传播是 CPU/GPU 密集型操作,可能耗时数十毫秒
  2. 变长视觉 Token——不同分辨率的图片产生不同数量的视觉 Token
  3. 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/,采用了可插拔的预处理管线设计。核心是 MultiModalRegistryregistry.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 后缀的文件)

基于 VitePress 构建