AI Engineering 7 分钟阅读

sglang Qwen3-VL 坐标偏移 Bug:一次系统性排查

· 更新于 2026/8/11

背景

我们在 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_t

vLLM 同样正确实现了这一特性——专门的 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

教训

  1. 推理框架对新模型特性的支持程度不同步,是多引擎部署最隐蔽的坑:vLLM 和 SGLang 都声称支持 Qwen3-VL,但对 mrope_interleaved 这个具体子特性的支持进度不一致——这种"部分支持"比"完全不支持"更难发现,因为模型能正常加载、正常生成文本,只是空间位置编码这一个维度悄悄错了。
  2. 配置文件里被忽略的字段值得认真读ignore_keys_at_rope_validation = {"mrope_section", "mrope_interleaved"} 这行代码的字面意思是"验证时跳过",但顺着这个线索去查,才发现这恰恰是整个推理引擎唯一没有真正处理这个特性的地方。
  3. "y 异常、x 正常"这种坐标轴不对称的信号,比"全局误差很大"更有诊断价值:一旦发现误差在某个特定维度上有系统性方向,就应该立刻联想到"处理该维度的专属逻辑",而不是继续在通用的调度器/kernel/scheduler 里排查。
  4. 控制变量法能把"假 bug"从"真 bug"中筛出来:vision tower 输出的相似度对比,从"看起来严重"(cos_sim=0.011)到"基本一致"(cos_sim=0.998),只是控制了输入精度这一个变量。
  5. 小样本归纳的陷阱:反复测试同一张图在不同调度器配置下输出一致,被误判为"不同输入产生相同输出"的证据。8 样本测试才揭示了真实模式。任何"所有输入都表现出某种规律"的结论,都要用真正不同的输入样本去验证。
  6. 交叉对比两个独立实现是定位"其中一个缺特性"的有效方法:vLLM 和 SGLang 各自独立实现 Qwen3-VL,当怀疑某个特定机制时,直接去对方的代码库搜索同名的处理逻辑(这里是搜索 mrope_interleaved 字符串),如果一边有专门模块、另一边完全没有,几乎就能一次性定位到差异点。