---
title: "sglang Qwen3-VL 坐标偏移 Bug：一次系统性排查"
description: "找到根因：SGLang 0.5.3 未实现 Qwen3-VL 的 mrope_interleaved 特性，导致 t/h/w 三维 rotary position 用错误的连续分段方式（而非模型要求的交织方式）重组，产生 h(y) 方向系统性位置编码错误。vLLM 正确实现了这一特性对应分支。完整记录 5 轮排除排查历程。"
pubDate: 2026-07-31
tags: ["sglang","Qwen3-VL","Bug Analysis","Inference","vLLM","mrope","rope-scaling","root-cause"]
category: "AI Engineering"
lang: "zh"
math: false
---
## 背景

我们在 RTX 5090 服务器上部署 Qwen3-VL-8B 游戏 AI 模型，用于实时预测屏幕点击坐标。模型在 transformers 离线评估中表现正常，但通过 sglang 0.5.3 推理服务调用时，坐标输出严重偏移，几乎不可用。相同的模型通过 vLLM 部署则表现正常。

## 根因（已确认）

**SGLang 0.5.3 没有实现 Qwen3-VL 的 `mrope_interleaved` 特性支持。**

模型 `config.json` 里的 rope 配置：

```json
{
  "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`）里有明确的转换函数：

```python
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 中检测标志位：

```python
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`）里：

```python
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` 字符串），如果一边有专门模块、另一边完全没有，几乎就能一次性定位到差异点。