大语言模型的文本能力已经被充分挖掘,而让模型"看懂图"的多模态推理,正在成为企业级应用的新战场:文档解析、商品识别、自动驾驶、智能客服。本文从图像如何变成 Token 讲起,深入视觉语言模型(VLM)的联合推理机制、多模态 KV Cache 显存管理,再到生产级多模态推理服务部署。
一、从单模态到多模态:VLM 推理的挑战
1.1 什么是视觉语言模型(VLM)
视觉语言模型(Vision-Language Model,VLM) 是能够同时接受图像与文本输入、输出文本(或结构化信息)的模型。与纯文本 LLM 不同,VLM 需要先"理解"图像内容,再将其与用户的指令进行联合推理。
典型的应用场景包括:
- 文档解析:从 PDF / 扫描件中提取表格、公式与版面信息
- 商品识别与检索:根据商品图片回答材质、颜色、适用场景
- 智能客服:用户上传截图,机器人解析报错信息
- 自动驾驶与具身智能:感知周围环境并输出决策文本
从系统角度看,VLM 推理可以被拆解为两个阶段:视觉编码(Vision Encoding) 与 自回归解码(Autoregressive Decoding)。前者将图像变成一组"视觉 Token",后者让 LLM 像生成文本一样消费这些 Token。
1.2 多模态推理与纯文本 LLM 推理的差异
| 维度 | 纯文本 LLM 推理 | 多模态(VLM)推理 |
|---|---|---|
| 输入表示 | 文本 Token(词表) | 文本 Token + 视觉 Token |
| 编码阶段 | Embedding 查表 | ViT/CLIP 编码器(额外计算) |
| 输入长度 | 通常 1K~128K token | 一张图可占 256~4096 token |
| KV Cache 增长 | 随文本增长 | 视觉 Token 一次性占用大量 KV |
| 峰值显存 | Prefill 阶段 | 视觉编码 + 大图 Prefill |
| 批处理 | 文本长度动态 | 图像数量 + 图像 token 数双重动态 |
关键结论:多模态推理的复杂性主要来自"视觉 Token"的加入。同一张图在推理时消耗的算力与显存,可能相当于数百个文本 Token。
1.3 主流 VLM 架构路线对比
| 模型家族 | 视觉编码器 | 对齐方式 | 视觉 Token/图 | 特点 |
|---|---|---|---|---|
| LLaVA 系列 | CLIP ViT-L/14 | MLP Projector | 576 | 开源生态成熟、训练简单 |
| Qwen-VL | ViT-bigG | Resampler(Q-Former 类) | ~256 | 多语言、文档能力突出 |
| InternVL | InternViT-6B | MLP + PixelShuffle | 128~256 | 大视觉骨干、高分辨率 |
| Gemma 3 | SigLIP | Query Pooler | 256 | 移动端友好、多种尺寸 |
| GPT-4o / Claude | 未公开 | 未公开 | 数千级 | 商业闭源、多模态均衡 |
架构差异直接决定了推理时的 token 数量与显存开销,后面会看到这如何影响 KV Cache 的工程决策。
二、图像 Tokenizer:把像素变成 Token
2.1 视觉编码器(ViT / CLIP / SigLIP)
VLM 的视觉前端几乎都是 Vision Transformer(ViT) 或其变体。ViT 将图像切成固定大小的 Patch,每个 Patch 经线性投影后得到一个向量,再叠加位置编码送入 Transformer Encoder。
import torch
import torch.nn as nn
class PatchEmbed(nn.Module):
"""把 (B, 3, H, W) 的图像切成 patch 并投影为 embedding。"""
def __init__(self, img_size=336, patch_size=14, in_chans=3, embed_dim=1024):
super().__init__()
self.patch_size = patch_size
self.num_patches = (img_size // patch_size) ** 2 # 24x24 = 576
self.proj = nn.Conv2d(in_chans, embed_dim,
kernel_size=patch_size, stride=patch_size)
def forward(self, x):
# x: (B, 3, 336, 336) -> (B, 1024, 24, 24)
x = self.proj(x)
# -> (B, 24*24, 1024)
x = x.flatten(2).transpose(1, 2)
return x # (B, 576, 1024)
对于 336×336 的输入,14×14 的 patch 会得到 24×24 = 576 个 patch。也就是说,一张图最低产生 576 个视觉 token(不含 CLS 与特殊 token)。
2.2 从 Patch Embedding 到视觉 Token
视觉编码器的输出并不直接进入 LLM。因为 ViT 的隐层维度(如 1024/1280)与 LLM 的 hidden size(如 4096)不同,需要一层 Projector 做维度对齐。
- MLP Projector:2 层全连接,把 ViT 输出映射到 LLM 维度(LLaVA 方案)
- Resampler / Q-Former:用可学习的 query 向量从视觉 token 中"抽取"固定数量 token(Qwen-VL 方案)
- PixelShuffle 压缩:把相邻视觉 token 的空间信息合并,4 个 token 合成 1 个(InternVL 方案)
class MLPProjector(nn.Module):
def __init__(self, vit_dim=1024, llm_dim=4096):
super().__init__()
self.fc1 = nn.Linear(vit_dim, llm_dim)
self.fc2 = nn.Linear(llm_dim, llm_dim)
def forward(self, visual_tokens):
# visual_tokens: (B, 576, 1024) -> (B, 576, 4096)
return self.fc2(F.gelu(self.fc1(visual_tokens)))
工程要点:Projector 的计算量相比 ViT 与 LLM 都小,但它决定了视觉 token 的最终数量——576 与 256 之间,KV Cache 开销差一倍以上。
2.3 高分辨率与多尺度图像策略
真实业务中的图片分辨率参差不齐。直接 resize 到 336×336 会丢失细节。主流方案:
- 切片(Tiling):把大图切成多个 336×336 的格子,每格独立编码后再拼接(Qwen-VL、InternVL 均采用)
- 多尺度金字塔:同时编码 1/2、1/4 分辨率,供模型选择
def tile_image(img, tile_size=336, max_tiles=9):
"""把图像切成若干固定大小的子图。"""
tiles = []
h, w = img.shape[-2:]
grid_h = math.ceil(h / tile_size)
grid_w = math.ceil(w / tile_size)
for i in range(min(grid_h * grid_w, max_tiles)):
r, c = divmod(i, grid_w)
patch = img[..., r*tile_size:(r+1)*tile_size,
c*tile_size:(c+1)*tile_size]
tiles.append(patch)
return tiles
切图后一张大图的 token 数会从 576 膨胀到数千,这是多模态推理显存爆炸的第一来源。
三、视觉 Token 与文本的联合推理
3.1 融合架构:Cross-attention 与 Full-attention
视觉信息与文本信息如何融合,决定了模型结构:
| 融合方式 | 代表模型 | 推理成本 | 优点 | 缺点 |
|---|---|---|---|---|
| 拼接进序列(Full-attention) | LLaVA、Qwen-VL | 视觉 token 参与全部自注意力 | 结构简单、视觉信息不丢失 | token 多、KV Cache 大 |
| Cross-attention | Flamingo 类 | 文本层每隔 k 层交叉注意视觉特征 | 文本序列 KV 小 | 结构复杂、视觉信息利用率低 |
当前主流开源 VLM 几乎都采用**拼接(Concatenation)**方式:视觉 token 与文本 token 组成一条序列,由同一个 LLM 统一处理。
[IMAGE]<img_1><img_2>...[img_576>[/IMAGE] <BOS> 请描述这张图片的内容。 [EOS]
LLM 的自回归过程完全不变——它只是在序列开头多了一长串视觉 token。这意味着纯文本 LLM 的整套推理优化(KV Cache、连续批处理、投机解码)都可以复用,这正是 https://plumephp.com/ai-vllm-system/ 中 vLLM 能在多模态场景快速落地的原因。
3.2 图像位置编码与特殊 Token
- 拼接时视觉 token 使用独立的位置编码或与文本相同的位置编码
- 模型通常定义
<image>、<image_token>等特殊 token 标记图像边界 - 部分模型(如 Qwen-VL)在 Resampler 阶段已经加入位置信息,LLM 侧不再重复
3.3 多图与视频输入
- 多图:多张图拼接为
[IMG1 tokens][IMG2 tokens][text],模型需区分"图 1"与"图 2"的指代关系 - 视频:每帧切成若干 token,视频 token 数 = 帧数 × 每帧 token 数。例如 8 帧 × 256 = 2048 视觉 token,这会让 KV Cache 压力陡增
视频推理的常见工程手段是先抽帧、再并行编码,把"视频编码"从 LLM 推理主链路中尽量剥离。
四、多模态 KV Cache 的显存账本
4.1 视觉 Token 带来的 KV Cache 开销
在 https://plumephp.com/ai-attention-optimization/ 中我们分析过 KV Cache 的公式。视觉 token 的最大特点是一次性写入、全程保留:整个生成过程都要携带这批 KV。
KV Cache 显存 = 2 × num_layers × num_kv_heads × head_dim × seq_len × dtype_bytes
假设模型为 Qwen2-VL-7B(28 层,8 KV heads,head_dim=128,FP16=2B):
num_layers = 28
num_kv_heads = 8
head_dim = 128
bytes_per_elem = 2 # FP16
def kv_cache_bytes(seq_len):
return (2 * num_layers * num_kv_heads * head_dim
* seq_len * bytes_per_elem)
print("576 视觉 token 的 KV:", round(kv_cache_bytes(576) / 1024**2, 2), "MiB")
print("2048 视觉 token 的 KV:", round(kv_cache_bytes(2048) / 1024**2, 2), "MiB")
print("4096 文本 token 的 KV:", round(kv_cache_bytes(4096) / 1024**2, 2), "MiB")
输出:
576 视觉 token 的 KV: 3.94 MiB
2048 视觉 token 的 KV: 14.00 MiB
4096 文本 token 的 KV: 28.00 MiB
一个 576-token 的图占用的 KV Cache,抵得上约 1400 个文本 token。当 batch 中有大量图片请求时,KV Cache 会被视觉 token 迅速填满,触达 https://plumephp.com/ai-vllm-system/ 中 PagedAttention 的分页机制。
4.2 视觉 KV Cache 的优化手段
| 手段 | 原理 | 收益 |
|---|---|---|
| 视觉 Token 压缩 | Resampler 把 576 压到 256 | KV 减半以上 |
| 量化 KV Cache | INT8/INT4 存储视觉 KV | 显存 2~4x 下降 |
| 视觉 KV 驱逐 | 生成后期释放不必要视觉 KV | 长文本场景明显 |
| 图像前缀缓存 | 相同图像前缀复用 KV | 多轮对话 + 相同图,省全部重复编码 |
4.3 图像前缀缓存(Image Prefix Caching)
多模态场景下"同一张图反复提问"非常常见(例如用户连续追问同一张报表)。如果每轮都重新编码图像并重新计算 KV,浪费巨大。vLLM 的 Prefix Caching 天然支持视觉 token 前缀:
# vLLM 启用 prefix caching 后,相同图像 token 前缀可被复用
from vllm import LLM, SamplingParams
llm = LLM(
model="Qwen/Qwen2-VL-7B-Instruct",
max_model_len=8192,
enable_prefix_caching=True, # 关键开关
gpu_memory_utilization=0.85,
)
复用条件是"前缀完全一致"——因此部署端应保证同一图像走同一套切图与编码参数(tile 数、resize 尺寸、projector 版本),否则前缀不匹配、缓存全部失效。
五、多模态推理工程部署
5.1 两段式流水线:视觉编码 + LLM
多模态服务最常见的架构是两段式:先由视觉编码服务处理图像,再把视觉 token 交给 LLM 推理引擎。
┌──────────────┐ images ┌────────────────┐ visual tokens ┌──────────────┐
│ 请求入口 │ ──────────> │ 视觉编码服务 │ ────────────────> │ LLM 推理引擎 │
│ API Gateway │ │ ViT + Projector│ 文本 + 视觉 token │ (vLLM/TRT-LLM)│
└──────────────┘ └────────────────┘ └──────────────┘
5.2 引擎适配:vLLM 与 TensorRT-LLM 的多模态支持
- vLLM:内置 LLaVA、Qwen-VL、InternVL 等视觉语言模型支持,API 与文本模型完全一致,
/v1/chat/completions接受image_url字段 - TensorRT-LLM:将视觉编码器与 LLM 编译为两个 engine,需分别 build;适合对延迟极致敏感的场景
from openai import OpenAI
client = OpenAI(base_url="http://llm-server:8000/v1")
resp = client.chat.completions.create(
model="Qwen2-VL-7B",
messages=[{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "https://cdn.example/report.png"}},
{"type": "text", "text": "这张报表的季度营收是多少?"},
],
}],
max_tokens=256,
)
5.3 图像预编码与批处理优化
工程上最有效的优化是把视觉编码从推理热路径中剥离:
- 异步预编码:客户端上传图片后立即编码,编码结果存入缓存服务(Redis/对象存储),用户提问时直接取 token
- 图像批处理:视觉编码器按图批量处理,同一 batch 中不同图像的 token 数差异很大,需要动态 padding 对齐
- LRU 图像缓存:热门图片(商标、报表模板、证件)的视觉 token 直接缓存,命中即省掉 ViT 推理
class ImageTokenCache:
def __init__(self, capacity=10000):
self.cache = OrderedDict() # LRU
self.capacity = capacity
def get(self, image_hash):
if image_hash in self.cache:
self.cache.move_to_end(image_hash)
return self.cache[image_hash]
return None
def put(self, image_hash, tokens):
self.cache[image_hash] = tokens
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # 淘汰最久未用
5.4 一个多模态服务的部署伪代码
@app.post("/v1/vlm")
async def vlm_infer(image_url: str, prompt: str):
image = await download(image_url)
img_hash = sha256(image.bytes)
# 1. 命中缓存直接复用视觉 token
cached = token_cache.get(img_hash)
visual_tokens = cached or await encode_image(image) # ViT + Projector
if cached is None:
token_cache.put(img_hash, visual_tokens)
# 2. 拼接 prompt 后交给 LLM 推理引擎
return await llm_generate(visual_tokens=visual_tokens, prompt=prompt)
六、总结
| 知识点 | 核心要点 |
|---|---|
| VLM 推理本质 | 视觉编码 + 自回归解码两段式 |
| 图像 Tokenizer | ViT 切 patch → Projector 对齐维度 → 特殊 token 拼接 |
| token 数量 | 一张 336px 图 ≈ 256~576 token,切图后可膨胀到数千 |
| KV Cache | 视觉 token 占 KV 大头,量化 / 压缩 / 前缀缓存是三大抓手 |
| 部署模式 | 两段式服务 + 图像预编码缓存 + vLLM 统一接入 |
| 复用已有优化 | FlashAttention、PagedAttention、连续批处理全部可复用 |
多模态推理不是一个"新引擎",而是在成熟的 LLM 推理体系之上叠加了"图像编码"与"视觉 token 管理"两层工程。理解图像如何 token 化,就能理解为什么视觉 token 会吃掉大量 KV Cache,也就知道该在哪里优化。建议从开源 VLM(LLaVA 或 Qwen-VL)入手,用 vLLM 快速起一个最小可用的多模态服务,再逐步加入图像缓存与量化手段。如果你需要为"看图"场景做严格的延迟与成本评估,可参考 https://plumephp.com/ai-inference-benchmark/ 中的指标与方法。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。