多模态:图像 / 视频输入
谁该读这一篇? 要在 SGLang 上跑 Qwen2-VL / LLaVA / MiniCPM-V 等多模态模型的开发者。 前置阅读:
01-dsl-walkthrough.md、02-core-concepts/02-frontend-dsl.md。 耗时: 30 分钟 学完能: 1. 用 DSL 写一个图像 QA 程序; 2. 解释图像在 SGLang 内部怎么变成 token; 3. 区分 image / video / audio 三种输入; 4. 配 Qwen2-VL 在 SGLang 跑起来; 5. 排查"图像加载报错 / 输出和图无关"等常见问题。
1. 一个例子:图像 QA
import sglang as sgl
@sgl.function
def image_qa(s, image_path, question):
s += sgl.user(sgl.image(image_path) + question)
s += sgl.assistant_begin()
s += sgl.gen("answer", max_tokens=200)
s += sgl.assistant_end()
state = image_qa.run(image_path="cat.jpg", question="这张图里有什么?")
print(state["answer"])
启动 server 时要用 VLM:
python -m sglang.launch_server \
--model-path Qwen/Qwen2-VL-7B-Instruct \
--chat-template qwen2-vl
2. 多模态在 SGLang 内部的流程
flowchart LR
A[image bytes / path] --> B[Multimodal Processor<br/>ViT encoder]
B --> C[image_embeddings<br/>tensor]
A --> D[prompt with<br/>image placeholder]
D --> E[Tokenizer]
E --> F[input_ids<br/>含 placeholder token id]
C --> G[Scheduler]
F --> G
G --> H[Model.forward]
Note over H: 内部把 placeholder 替换为 image embedding
关键点:
- 图像不是被 tokenizer "看到"的。tokenizer 看到
<image_pad>这类 placeholder token。 - 真正的图像编码在 multimodal processor 里完成(ViT 等)。
- 编码后的
image_embeddings张量跟着请求一起进入 Scheduler。 - Model forward 时把 placeholder 替换为 image embedding。
源码:srt/multimodal/、srt/managers/multimodal_processor.py、srt/managers/mm_utils.py。
3. 输入种类
3.1 Image
sgl.image(path_or_url) # 路径或 URL
sgl.image(pil_image) # PIL Image 对象
sgl.image(bytes_data) # bytes
支持 PNG / JPG / WebP;自动 resize 到模型要求的尺寸。
3.2 Video
sgl.video(path, num_frames=8) # 采样 8 帧
抽帧策略:默认均匀;可以传 frame_idx=[0, 30, 60] 指定。
3.3 Audio(部分模型)
sgl.audio(path)
只有支持音频输入的模型(如 GPT-4o style)才可以。
4. 多图输入
@sgl.function
def compare(s, img1, img2):
s += sgl.user(
"比较这两张图:" +
sgl.image(img1) + "和" + sgl.image(img2)
)
s += sgl.assistant_begin()
s += sgl.gen("comparison", max_tokens=300)
s += sgl.assistant_end()
模型支持多图(如 Qwen2-VL、LLaVA-OneVision)才行。
5. OpenAI API 兼容
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
resp = client.chat.completions.create(
model="qwen2-vl",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "这张图里有什么?"},
{"type": "image_url", "image_url": {"url": "https://.../cat.jpg"}}
]
}],
max_tokens=200
)
SGLang 兼容 OpenAI 的 image_url 协议(URL 或 base64 都支持)。
6. 性能注意
6.1 ViT 编码不是免费的
一张 720p 图像在 Qwen2-VL 上 ViT 编码耗时约 50-100 ms(H100)。 高 QPS 时 ViT 会成为瓶颈。
对策:
- Encoder 分离(
05-distributed/04-disaggregated.md§8):ViT 单独节点跑。 - Encoder cache:相同图像复用 embedding(
srt/multimodal/里有实现)。
6.2 图像 token 数
不同模型展开图像的 token 数不同:
| 模型 | 一张 720p 图像 ≈ token |
|---|---|
| LLaVA-1.5 | 576 |
| Qwen2-VL | 256 - 1280(动态) |
| MiniCPM-V | 64 - 384 |
高分辨率图像 token 多 → KV cache 占用大、TTFT 慢。
6.3 视频 token 爆炸
video, 8 frames, 720p, Qwen2-VL ≈ 8 × 1024 = 8192 token
视频请求容易把 KV pool 吃光,限制并发。
7. 配置启动
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 # 允许 text + image token 混合 chunk
注意:
- 不同 VLM 需要不同的
--chat-template。 - max_running_requests 通常比纯文本少(image token 多)。
--enable-mixed-chunk让 chunked prefill 能切 image token。
8. Encoder Cache
如果同一图像在多个请求里复用(如电商商品图、商品页 RAG):
sgl.image(path, cache_id="product_123")
SGLang 内部维护 image embedding 缓存,避免 ViT 重算。 注:图像 cache 和 RadixCache 是两个不同 cache,前者按 cache_id key,后者按 token 序列。
9. 排障
| 现象 | 排查 |
|---|---|
| 启动报 "no vision tower" | 模型不是 VLM;换正确模型 |
| 图加载失败 | 路径 / URL / base64 格式 |
| 输出和图无关 | chat_template 错误(image placeholder 没替换) |
| 慢 | ViT 编码瓶颈;考虑 encoder 分离 |
| OOM | image token 多;减并发 / 降分辨率 |
10. 多模态 + 受约束输出
@sgl.function
def describe(s, image):
s += sgl.user(sgl.image(image) + "用 JSON 描述")
s += sgl.assistant_begin()
s += sgl.gen("desc", max_tokens=200,
json_schema=json.dumps({
"type": "object",
"properties": {
"objects": {"type": "array", "items": {"type": "string"}},
"colors": {"type": "array", "items": {"type": "string"}}
}
}))
s += sgl.assistant_end()
VLM + JSON schema 是常见做法(结构化提取图像信息)。
11. 小结
- 图像 / 视频 / 音频通过 placeholder + embedding 注入。
- ViT 编码不便宜,高 QPS 考虑 encoder 分离 + encoder cache。
- 视频 token 多,KV pool 容易紧。
- 配置时注意
--chat-template匹配 VLM。 - 多模态 + 受约束输出是结构化抽取的常见 pattern。
12. 自检
- 图像被 tokenizer 看到吗?怎么进入 model 的?
答案
**tokenizer 看不到图像本身**——只看到 `- Qwen2-VL 处理 1080p 图像约多少 token?
答案
Qwen2-VL 用 **动态分辨率**,token 数与图像 pixel 数成正比(每 patch 28×28 → 1 视觉 token)。 1080p = 1920×1080 ≈ 2M pixel → ~2640 visual token(理论值)。 实际 SGLang 会先缩到 `max_pixels` 限制(默认 ~1.3M pixel)下,输出 ~1280 token。 这是为什么 VLM 推荐降 `max_running_requests`:单图就 1k+ token,KV pool 占用大。 对比 LLaVA-1.5 是固定 576 token(不论分辨率),Qwen2-VL 高分辨率更准但 token 多。- Encoder cache 和 RadixCache 是同一个吗?
答案
**不是**。 - **Encoder cache**([`srt/multimodal/`](../sglang/python/sglang/srt/multimodal/)):缓存 ViT 编码结果(image embeddings 张量),key 是图像 byte hash 或 client 传的 `cache_id`。命中跳过 ViT 编码(省 50-120ms / 图)。 - **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 时间。 都开时电商商品图等高复用场景双倍收益。- 视频输入 8 帧 vs 32 帧,对 KV pool 的影响?
答案
token 数线性增长:Qwen2-VL 每帧 ~256-1280 token,8 帧 = 2-10k token;32 帧 = 8-40k token。 KV pool 占用同比例增长。70B 模型 KV ≈ 80 GB 装得下 80k token 总量;32 帧视频请求一个就占 50%,max_running_requests 直接降到 1-2。 实战策略: - 短视频(< 30s):8 帧足够。 - 长视频:分段处理(map-reduce),每段 8 帧分别走一次 forward,避免 KV 爆炸。 - 视频流:用 sliding window,旧帧 KV 主动 evict。 监控 `kv_pool_used`:视频请求时常突然冲 90%。- 图像编码慢怎么优化?
答案
按收益排序: (1) **Encoder cache**:相同图反复编码场景(电商 / RAG 图库),命中后省 100%。 (2) **降分辨率**:Qwen2-VL 设 `min_pixels` / `max_pixels` 限制,1080p 降到 720p ViT 时间砍半,质量损失通常可接受。 (3) **Encoder 分离**:ViT 单独节点跑([`05-distributed/04-disaggregated.md`](../05-distributed/04-disaggregated.md) §8),用便宜 GPU。 (4) **批量 encoding**:同 batch 多图一次 ViT forward,GPU 利用率高。 (5) **量化 ViT**:FP8 ViT 速度 1.5×,精度损失小。 (6) **小 VLM**:MiniCPM-V 每图最低 64 token,ViT 也轻。 不要试图 cache ViT 的 intermediate(开发成本高,收益小)。13. 下一步
03-json-schema.md— 结构化输出。04-tool-use.md— Function calling。09-advanced-features/03-multimodal-vlm.md— 进阶 VLM。- 源码:
srt/multimodal/、srt/managers/multimodal_processor.py。