多模态 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. 自检
- 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 输入序列里看到 `- 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`(百万像素)平衡质量 / 成本。- 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) **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 综合各段摘要得结论。 适合:长视频内容理解、教学视频总结。 不适合:需要全局时序信息的任务(如动作识别需要看完整时序)。- 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. 下一步
04-lora.md— LoRA。05-embedding.md— Embedding。- 源码:
srt/multimodal/、srt/models/llava.py。