预计阅读 6 分钟

多模态:图像 / 视频输入

谁该读这一篇? 要在 SGLang 上跑 Qwen2-VL / LLaVA / MiniCPM-V 等多模态模型的开发者。 前置阅读: 01-dsl-walkthrough.md02-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.pysrt/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 会成为瓶颈。

对策:

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. 自检

  1. 图像被 tokenizer 看到吗?怎么进入 model 的?
答案 **tokenizer 看不到图像本身**——只看到 `` 这类 placeholder token(特殊 vocab id)。 流程:(1) TM 的 multimodal processor 把图像 bytes → PIL Image → resize → 张量;(2) 跑 ViT encoder 拿 image embeddings `[N_visual_tokens, hidden]`;(3) tokenizer 在 prompt 里看到 `` 标签,编码为 N 个 `` placeholder token id;(4) Scheduler / Worker 在 forward 时,model 的 embedding layer 把 placeholder token id 替换为对应的 image embedding 张量。 关键:image embedding 不走 token vocab,是 hidden-state 级直接注入。
  1. 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 多。
  1. 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 时间。 都开时电商商品图等高复用场景双倍收益。
  1. 视频输入 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. 图像编码慢怎么优化?
答案 按收益排序: (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. 下一步

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