sglang Qwen3-VL 坐标偏移 Bug:一次系统性排查
背景
我们在 RTX 5090 服务器上部署 Qwen3-VL-8B 游戏 AI 模型,用于实时预测屏幕点击坐标。模型在 transformers 离线评估中表现正常,但通过 sglang 0.5.3 推理服务调用时,坐标输出严重偏移,几乎不可用。相同的模型通过 vLLM 部署则表现正常。
根因(已确认)
SGLang 0.5.3 没有实现 Qwen3-VL 的 mrope_interleaved 特性支持。
模型 config.json 里的 rope 配置:
{
"mrope_interleaved": true,
"mrope_section": [24, 20, 20]
}mrope_interleaved: true 要求 3D rotary position embedding 的 t/h/w 三个维度按交织排列(THWTHWTHW...)方式组织频率分量,而不是标准 Qwen2-VL 的连续分段排列(TTT...HHH...WWW)。
transformers 官方实现(modeling_qwen3_vl.py)里有明确的转换函数:
def apply_interleaved_mrope(self, freqs, mrope_section):
"""Reorganizes frequency layout from chunked [TTT...HHH...WWW] to
interleaved [THWTHWTHW...TT], preserving frequency continuity."""
freqs_t = freqs[0]
for dim, offset in enumerate((1, 2), start=1): # H, W
length = mrope_section[dim] * 3
idx = slice(offset, length, 3)
freqs_t[..., idx] = freqs[dim, ..., idx]
return freqs_tvLLM 同样正确实现了这一特性——专门的 mrope_interleaved.py 模块,并在 forward 中检测标志位:
if self.mrope_interleaved:
cos = apply_interleaved_rope(cos, self.mrope_section)
sin = apply_interleaved_rope(sin, self.mrope_section)而 SGLang 的 MRotaryEmbedding.forward()(srt/layers/rotary_embedding.py)里:
cos = torch.cat(
[m[i] for i, m in enumerate(cos.split(self.mrope_section, dim=-1))],
dim=-1,
)这是纯粹的连续分段切法,完全没有交织重组逻辑。搜索整个 SGLang 代码库,mrope_interleaved 这个字符串只在配置校验的 ignore_keys 里出现过两次(意为"验证时跳过这个字段的检查"),没有任何地方真正读取这个标志去改变实际计算路径。
这个 bug 发生在所有 attention kernel 调用之前的 cos/sin 重组阶段,是模型级别的通用逻辑,跟具体用 flashinfer/triton/sdpa 哪个 attention backend 完全无关——这也解释了为什么此前所有排查轮次里,无论如何切换调度策略、缓存策略、CUDA graph、attention backend,输出坐标都稳定复现同一种错误模式:因为 bug 根本不在这些环节,而是在一个更底层、所有请求都必然经过的通用函数里。
t/h/w 频率被错误地按连续分段方式重组,而不是训练时期望的交织方式,导致 h(对应图像 y/高度方向)和 w(对应 x/宽度方向)两个维度的位置信息被互相污染。由于 mrope_section=[24,20,20] 中 h 和 w 段长相同(都是20),但错误的切分方式对两个维度造成的"污染模式"不完全对称,产生了我们观察到的现象:y 方向系统性偏移更严重,x 方向相对较轻。
完整排查历程(定位到根因之前的排除记录)
在定位到上述根因之前,经过五轮系统性排查,逐一排除了以下假设——完整记录留存作为排查方法论参考:
第一轮:5 个初始假设(均排除)
图片预处理 IMAGE_FACTOR 硬编码差异(被下游 transformers 二次处理覆盖)、pixel_values 插值精度差异(transformers.generate 对比无差异)、mrope position 计算逻辑(静态代码比对一致)、chat template(三处来源完全相同)、模型权重/容器环境(同容器内 transformers.generate 结果正确)。
第二轮:调度器候选机制(均排除)
Chunked Prefill(--chunked-prefill-size -1 无效)、RadixCache(--disable-radix-cache 无效)、多模态 embedding 缓存大小、--mm-attention-backend sdpa。这一轮观察到"不同输入图片产生几乎相同预测坐标",据此推测是 vision embedding 信息丢失——这个推测后来被第五轮的多样本测试证明是过度归纳(样本量不足,主要基于反复测试同一张图得出的错误结论)。
第三轮:CUDA Graph 排除 + mrope 运行时数值验证
补测 --disable-cuda-graph,坐标输出与开启时几乎逐位一致,排除。在 get_rope_index 调用点插入运行时 debug,验证 vision token 的 3D position 标签数值网格结构完全符合预期——但这只验证了 position id 本身算对了,没有验证这些 id 在 RoPE 实际应用时是否被正确处理(真正的 bug 恰恰藏在这一步)。
第四轮:逐层数值验证 embedding 组装全链路
- embedding chunk 切片与 scatter:运行时 dump 验证数量精确对齐,未触发异常修正分支。
- vision tower 本身:直接对比一度显示 mean cosine similarity 仅 0.94(看起来严重),但这是因为对比时输入精度不一致(fp32 vs bf16);用控制变量实验(喂完全相同的 bf16 输入)重新对比后变成 0.998,证明 vision tower 实现本身没问题。
- deepstack 特征注入(Qwen3-VL 特有机制):dump 验证三层注入均被正确触发,非零特征参与计算。
- 补充验证:
--chunked-prefill-size -1下确认图片确实只走一次性完整 scatter(无 chunk 切分),仍复现偏移,排除"跨 chunk 边界"假设。
第五轮:embedding cache 排除 + 多样本测试推翻错误结论
- embedding cache 碰撞:用两张不同图片测试,hash 值完全不同,无碰撞、无污染。
- 关键修正:用 8 张来自不同标注会话的样本重新测试,发现"y 值系统性压缩偏小(8/8 样本无一例外),x 值相对分散"这一更精细的模式,推翻了此前"vision embedding 被整体置空"的假设。这个"h 方向异常、w 方向相对正常"的不对称模式,正是 mrope_interleaved 未处理的直接后果——为最终定位根因提供了关键线索。
生产方案
| 方案 | 延迟 | 坐标精度 | 状态 |
|---|---|---|---|
| vLLM(单卡 5090, cudagraph) | 740ms | ✅ 正确(正确实现 mrope_interleaved) | 采用 |
| sglang 0.5.3 | 880ms | ❌ y 方向系统性偏移(未实现 mrope_interleaved) | 待 SGLang 上游修复或自行 patch |
| transformers.generate | ~2000ms | ✅ 基准 | 仅 eval |
教训
- 推理框架对新模型特性的支持程度不同步,是多引擎部署最隐蔽的坑:vLLM 和 SGLang 都声称支持 Qwen3-VL,但对
mrope_interleaved这个具体子特性的支持进度不一致——这种"部分支持"比"完全不支持"更难发现,因为模型能正常加载、正常生成文本,只是空间位置编码这一个维度悄悄错了。 - 配置文件里被忽略的字段值得认真读:
ignore_keys_at_rope_validation = {"mrope_section", "mrope_interleaved"}这行代码的字面意思是"验证时跳过",但顺着这个线索去查,才发现这恰恰是整个推理引擎唯一没有真正处理这个特性的地方。 - "y 异常、x 正常"这种坐标轴不对称的信号,比"全局误差很大"更有诊断价值:一旦发现误差在某个特定维度上有系统性方向,就应该立刻联想到"处理该维度的专属逻辑",而不是继续在通用的调度器/kernel/scheduler 里排查。
- 控制变量法能把"假 bug"从"真 bug"中筛出来:vision tower 输出的相似度对比,从"看起来严重"(cos_sim=0.011)到"基本一致"(cos_sim=0.998),只是控制了输入精度这一个变量。
- 小样本归纳的陷阱:反复测试同一张图在不同调度器配置下输出一致,被误判为"不同输入产生相同输出"的证据。8 样本测试才揭示了真实模式。任何"所有输入都表现出某种规律"的结论,都要用真正不同的输入样本去验证。
- 交叉对比两个独立实现是定位"其中一个缺特性"的有效方法:vLLM 和 SGLang 各自独立实现 Qwen3-VL,当怀疑某个特定机制时,直接去对方的代码库搜索同名的处理逻辑(这里是搜索
mrope_interleaved字符串),如果一边有专门模块、另一边完全没有,几乎就能一次性定位到差异点。