多模态推理实战:从图像 Token 化到视觉语言模型联合推理

大语言模型的文本能力已经被充分挖掘,而让模型『看懂图』的多模态推理,正在成为企业级应用的新战场:文档解析、商品识别、自动驾驶、智能客服。本文从图像如何变成 Token 讲起,深入视觉语言模型(VLM)的联合推理机制、多模态 KV Cache 显存管理,再到生产级多模态推理服务部署。一、从单模态到多模态:VLM 推理的挑战

大语言模型的文本能力已经被充分挖掘,而让模型"看懂图"的多模态推理,正在成为企业级应用的新战场:文档解析、商品识别、自动驾驶、智能客服。本文从图像如何变成 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/14MLP Projector576开源生态成熟、训练简单
Qwen-VLViT-bigGResampler(Q-Former 类)~256多语言、文档能力突出
InternVLInternViT-6BMLP + PixelShuffle128~256大视觉骨干、高分辨率
Gemma 3SigLIPQuery Pooler256移动端友好、多种尺寸
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-attentionFlamingo 类文本层每隔 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 压到 256KV 减半以上
量化 KV CacheINT8/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 图像预编码与批处理优化

工程上最有效的优化是把视觉编码从推理热路径中剥离:

  1. 异步预编码:客户端上传图片后立即编码,编码结果存入缓存服务(Redis/对象存储),用户提问时直接取 token
  2. 图像批处理:视觉编码器按图批量处理,同一 batch 中不同图像的 token 数差异很大,需要动态 padding 对齐
  3. 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 推理本质视觉编码 + 自回归解码两段式
图像 TokenizerViT 切 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/ 中的指标与方法。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 时序预测实战:从 ARIMA 到时序基础模型
  2. 模型压缩:量化、剪枝、蒸馏与部署优化实战
  3. 量化感知训练(QAT)与量化微调:伪量化、STE 与 QLoRA 实战