预计阅读 6 分钟

多模态 VLM(进阶):视觉编码器、缓存、视频与音频

谁该读这一篇? 把 VLM 跑到生产、要榨 ViT / 视频处理性能的工程师。 前置阅读: 06-frontend-language/02-multimodal.md耗时: 30 分钟 学完能: 1. 解释 VLM 内部 ViT + LLM 的耦合方式; 2. 配 encoder cache / encoder disaggregation; 3. 处理视频流式 / 长视频; 4. 选对 VLM(Qwen2-VL / LLaVA / MiniCPM-V); 5. 排查"图像不被模型看到"等问题。


1. VLM 内部架构

Image bytes
    │
    ▼
Vision encoder (ViT)
    │
    ▼
Image embeddings [N_visual_tokens, hidden]
    │
    ▼
Projector (linear / MLP / Resampler)
    │
    ▼
Visual tokens 与 text tokens 拼接 (按 placeholder 位置插入)
    │
    ▼
LLM forward

不同 VLM 差异主要在:

  • Vision encoder 选型:CLIP / SigLIP / Eva-CLIP / 自训。
  • 图像 token 数:固定 vs 动态。
  • Projector:linear / cross-attention / Q-Former。
  • 位置编码:interleaved(图像 token 拼进 text 流)vs prefix(图像 token 全在前)。

2. 主流 VLM 对照

模型 Vision 图像 token 多图 视频 中文
Qwen2-VL ViT-bigG 动态 64-1280
LLaVA-1.5 CLIP-L 576 (固定) × × 一般
LLaVA-OneVision SigLIP 动态
MiniCPM-V SigLIP 64-384
Pixtral 自训 ViT 动态 × 一般
Llama-3.2 Vision 自训 ViT 动态 ×

业务推荐:

  • 中文 chat:Qwen2-VL / MiniCPM-V。
  • 英文研究 / 文档:LLaVA-OneVision。
  • 极致省 token:MiniCPM-V(每图最少 64 token)。

3. SGLang 启动 VLM

python -m sglang.launch_server \
    --model-path Qwen/Qwen2-VL-72B-Instruct \
    --chat-template qwen2-vl \
    --tp 4 \
    --max-running-requests 32 \
    --enable-mixed-chunk

注意:

  • --chat-template 必须匹配 VLM(不同 VLM 有不同 image token 位置约定)。
  • --max-running-requests 降低(图 token 多)。
  • --enable-mixed-chunk 允许 text + image token 同 chunk。

4. Vision Encoder 性能

ViT 编码不便宜:

模型 720p 编码 ms (H100)
LLaVA CLIP-L 30-50
Qwen2-VL ViT-bigG 80-120
SigLIP-L 50-80

高 QPS 场景 ViT 会成 CPU/GPU 瓶颈。


5. Encoder Cache

源码:srt/multimodal/

同一图像被 N 个请求复用时,避免每个请求都跑 ViT:

# 客户端带 cache_id(图像 hash 或业务 id)
sgl.image(path, cache_id="product_image_123")

或者 SGLang 自动按图像 byte hash 缓存。 看 metric:sglang_image_encoder_cache_hit_rate


6. Encoder Disaggregation

把 ViT 单独跑一台机器(05-distributed/04-disaggregated.md §8)。 适合:

  • ViT 模型大(如 ViT-bigG),但 LLM 不需要它的全显存。
  • ViT 算力密集,分离后可以用更便宜的 GPU。

启动:

# Encoder 节点(用小 GPU 即可)
python -m sglang.launch_server \
    --model-path Qwen/Qwen2-VL-72B-Instruct \
    --encoder-only \
    --port 30100

# Decoder 节点(大 GPU 跑 LLM)
python -m sglang.launch_server \
    --model-path Qwen/Qwen2-VL-72B-Instruct \
    --disaggregation-mode decode \
    --port 30000

KV transfer 走 RDMA。


7. 视频处理

7.1 抽帧

sgl.video(path, num_frames=8)        # 均匀抽 8 帧
sgl.video(path, frame_idx=[0,30,60]) # 指定帧

帧数选择:

  • 短视频(< 10s):4-8 帧。
  • 长视频(>1 分钟):16-32 帧 + 分段处理。

7.2 帧编码

每帧独立走 ViT,然后 token 拼起来。 8 帧 720p ≈ 64-1280 × 8 = 512-10240 token。 视频请求 KV 占用极大,限制并发。

7.3 长视频分段

@sgl.function
def long_video_qa(s, video_path, question):
    s += sgl.system("...")
    s += sgl.user(question)
    # 把视频分成 6 段,每段 8 帧
    for i in range(6):
        s += sgl.video(video_path, segment=i, num_frames=8)
        s += sgl.gen(f"segment_{i}_summary", max_tokens=100)
    s += "Final answer: "
    s += sgl.gen("answer", max_tokens=300)

类似 "map-reduce on video"。


8. 音频(部分模型)

模型支持:

  • Qwen2-Audio
  • GPT-4o style (闭源)
  • Whisper + LLM 拼接(SGLang 不内置 Whisper)
sgl.audio(path)

音频 token 数比图像少(10-100 token / 秒)。


9. 多模态 + 受约束输出

非常实用的组合:

@sgl.function
def extract_receipt(s, image):
    s += sgl.user(sgl.image(image) + "提取收据信息为 JSON")
    s += sgl.assistant_begin()
    s += sgl.gen("receipt", max_tokens=500, json_schema=json.dumps({
        "type": "object",
        "properties": {
            "merchant": {"type": "string"},
            "total": {"type": "number"},
            "items": {"type": "array", "items": {
                "type": "object",
                "properties": {"name": {"type": "string"}, "price": {"type": "number"}}
            }}
        }
    }))
    s += sgl.assistant_end()

VLM 看图 → 严格 JSON 输出。OCR + 信息抽取一步搞定。


10. 性能要点

指标 健康范围
ViT 编码 mean < 100 ms
Image encoder cache hit > 50%(业务复用度)
视频请求显存峰值 < 20% KV pool
多模态 abort rate < 1%

监控加 multimodal-specific 标签。


11. 常见问题

现象 排查
输出和图无关 chat_template 错 / placeholder 位置不对
图像 OOM 分辨率太高 / 多图过多
ViT 编码巨慢 不在 H100 上跑
视频卡顿 num_frames 太大;分段处理
多图编码顺序错 sgl.image 调用顺序

12. 小结

  • VLM = Vision Encoder + Projector + LLM;SGLang 把 ViT 当 preprocess + token 注入。
  • Encoder cache / disagg 是性能关键。
  • 视频按帧处理,长视频要分段。
  • 受约束输出 + VLM = OCR 替代品。
  • 多模态 KV 占用大,限并发。

13. 自检

  1. VLM 内部 ViT 和 LLM 是什么关系?
答案 **ViT 是 LLM 的"输入预处理器"**——把图像编码成 token-shaped embeddings,注入到 LLM 的输入 embedding 流里。 流程: (1) 图像 → ViT encoder → `[N_visual_tokens, hidden_dim]` embeddings。 (2) Projector(linear / MLP / Q-Former)把 ViT hidden 维度对齐到 LLM hidden 维度。 (3) LLM 输入序列里看到 `` placeholder token id,在 embedding lookup 时把对应位置替换成 ViT embeddings。 (4) LLM 后续 forward 把图像 embeddings 当成普通 text embeddings 处理(self-attention)。 关系:ViT 不是 LLM 的一部分(独立模型),但其输出被 LLM 当 token embedding 消费。两者可以分开训、分开部署。
  1. Qwen2-VL 720p 图像约多少 token?为什么动态?
答案 Qwen2-VL 用 **动态分辨率**:image token 数 ∝ pixel 数 / patch 数。 每 ViT patch = 28×28 像素 → 1 个 visual token。 720p = 1280×720 = 921600 pixels / 784 ≈ **1176 个 token**(理论值)。 实际 SGLang 限制 `min_pixels` / `max_pixels`,720p 通常输出 ~600-900 token。 动态的原因: (a) **高分辨率**(如文档、车牌识别):保留更多细节,token 多但 OCR 准。 (b) **低分辨率**(如缩略图):少 token 即可,省 KV。 对比 LLaVA-1.5 固定 576 token:不论分辨率都一样,高分辨率丢细节,低分辨率浪费 token。 实战:业务图统一缩到 `max_pixels=1024*1024`(百万像素)平衡质量 / 成本。
  1. Encoder cache 和 RadixCache 是同一个吗?
答案 **不同的 cache**。 - **Encoder cache**([`srt/multimodal/`](../sglang/python/sglang/srt/multimodal/)):缓存 ViT 编码结果(image embeddings 张量),key 是图像 byte hash 或 client 传的 `cache_id`。命中跳过 ViT 编码(省 50-120 ms / 图)。 - **RadixCache**([`srt/mem_cache/radix_cache.py`](../sglang/python/sglang/srt/mem_cache/radix_cache.py)):缓存 LLM forward 后的 KV,key 是 token 序列。命中跳过 LLM prefill。 关系:encoder cache 是 RadixCache 的"前置"——encoder cache 命中省 ViT 时间;进 RadixCache 后还可能省 LLM prefill 时间。 实战:电商商品图业务两个 cache 都开,命中率 90%+,ViT 编码 + LLM prefill 几乎全免。
  1. 长视频处理为什么要分段?
答案 长视频整段处理的问题: (1) **token 爆炸**:32 帧 × 1280 token/帧 = 40k+ token,单请求占满 KV pool。 (2) **prefill 超长**:40k token prefill 几秒,TTFT 失控。 (3) **超 context length**:超过模型 max position(如 32k)直接报错。 (4) **注意力稀释**:太长上下文让模型"忘掉"关键内容。 分段处理: ```python # Map-reduce 风格 summaries = [analyze_segment(video[i:i+8]) for i in range(0, len(video), 8)] final = synthesize(summaries) ``` 每段 4-8 帧(< 10k token),单独分析;最后用 LLM 综合各段摘要得结论。 适合:长视频内容理解、教学视频总结。 不适合:需要全局时序信息的任务(如动作识别需要看完整时序)。
  1. VLM + JSON Schema 的应用例子?
答案 经典场景:**OCR + 结构化抽取**。 ```python schema = { "type": "object", "properties": { "merchant": {"type": "string"}, "total": {"type": "number"}, "date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "items": {"type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "price": {"type": "number"} } }} } } sgl.gen("receipt", json_schema=json.dumps(schema)) ``` 用 VLM 看收据图片,直接吐符合 schema 的 JSON。**替代传统 OCR + 规则解析的方案**: - 传统:OCR 文字 → 正则 / NER 抽字段 → 校验 → 数据库。需开发 / 维护多个组件。 - VLM + Schema:一步到位,端到端可靠性高,新模板(不同商家收据)零代码适配。 类似场景:发票识别、ID 卡识别、表单抽取、图表数据提取。 实战 95%+ 准确率,错的也是结构合法 JSON,便于后处理。

14. 下一步

上游源码:sglang/python/sglang/srt/multimodal/