Prhub

#43617 Fix Qwen3-VL and Qwen3-omni-thinker accuracy degradation from deepstack inputs under torch.compile

原始 PR 作者 andakai 合并时间 2026-05-28 06:34 文件变更 2 提交数 2 评论 5 代码增减 +28 / -22

执行摘要

修复 Qwen3-VL/Omni 在 torch.compile 下的精度退化

作者在 Issue #43602 中报告 Qwen3-VL-2B-Instruct 使用 vLLM 默认 compile 路径时 Geo3K 精度 (0.205) 明显低于 SGLang (0.288),而 eager 模式 (0.291) 接近预期。PR body 指出根本原因是 'compile/profiling path: deepstack_input_embeds = None; real VL request path: deepstack_input_embeds = IntermediateTensors(...)',导致编译图特化忽略视觉分支。

该 PR 值得精读,因为它揭示了一个常见的 torch.compile 陷阱:profile 阶段与 serving 阶段的输入结构不一致会导致编译图特化错误。设计上通过固定返回 tensor 而非 None 来保持图结构稳定的模式值得借鉴。合并前建议考虑的 device/dtype 问题可在后续 PR 中加固。

讨论亮点
  1. Gemini Code Assist 提出的 device/dtype 风险:机器人指出初始化时 deepstack_input_embeds 在 CPU 上创建,若 resize 条件不满足(如 text-only 请求 num_tokens ≤ max_num_batched_tokens),则返回 CPU 张量,后续 GPU 运算会因 device 不匹配崩溃。建议在 resize 条件中增加 device/dtype 检查,并使用 self.visual.device / self.visual.dtype
    - 结论:该风险在 PR 合并前未在代码中处理,但实际运行时 resize 在第一次 GPU 请求时被触发,且后续 resize 继承第一次 GPU 张量的 device/dtype,因此未暴露。建议后续 PR 加固。
  2. Andakai 的分析补充:作者在 PR 评论中解释了 root cause:vLLM 的 pre-profile 阶段 encoder 和 decoder 分开 profiling,decoder dummy run 不含 deepstack_input_embeds,导致 torch.compile 无法捕获 deepstack 分支。

实现拆解

  1. 消除提前返回:在 _get_deepstack_input_embeds 中,移除对 deepstack_input_embeds_num_tokens == 0 时的 return None 分支,改为检查 num_tokens 是否超出缓存容量,若超出则调用新辅助方法 _resize_deepstack_input_embeds 扩容,然后始终返回 IntermediateTensors(零张量或真实张量)。
  2. 提取 resize 辅助方法:将 _set_deepstack_input_embeds 中已有的内联 resize 逻辑抽取为 _resize_deepstack_input_embeds 方法,被 _get_deepstack_input_embeds_set_deepstack_input_embeds 共用,消除重复代码。
  3. 双模型同步修改:在 qwen3_vl.pyqwen3_omni_moe_thinker.py 中做完全相同的修改,因为两个模型共享相同的 deepstack 机制和 bug。
  4. 零张量语义:当没有视觉输入时,返回的 IntermediateTensors 包含全零张量,语义等同于无 deepstack 贡献,但保持了 tensor-based 输入结构,使编译图不因输入结构变化而重编译。
文件 模块 状态 重要度
vllm/model_executor/models/qwen3_vl.py 模型执行 modified 7.39
vllm/model_executor/models/qwen3_omni_moe_thinker.py 模型执行 modified 7.39

关键符号

_get_deepstack_input_embeds _resize_deepstack_input_embeds _set_deepstack_input_embeds

关键源码片段

vllm/model_executor/models/qwen3_vl.py data-contract

核心修复文件之一,修改了 _get_deepstack_input_embeds 的提前返回逻辑,并抽取 _resize_deepstack_input_embeds 方法;Qwen3-VL 模型的精度退化直接由此修复。

def _get_deepstack_input_embeds(
    self,
    num_tokens: int,
) -> IntermediateTensors | None:
    if not getattr(self, "deepstack_input_embeds", None):
        return None # 如果视觉 tower 被跳过(如纯文本模型)
    # 关键变更:不再因无有效 payload 而返回 None,而是确保 buffer 足够大
    if num_tokens > self.deepstack_input_embeds[0].size(0):
        self._resize_deepstack_input_embeds(num_tokens)
    # 始终返回 IntermediateTensors,零张量语义等价于无 deepstack 贡献
    return IntermediateTensors({
        f"deepstack_input_embeds_{idx}": self.deepstack_input_embeds[idx][
            :num_tokens]
        for idx in range(self.deepstack_num_level)
    })
​
​
def _resize_deepstack_input_embeds(self, num_tokens: int) -> None:
    # 新抽出的辅助方法:用零张量重新分配 buffer
    # 注:此处 device 从已有的 self.deepstack_input_embeds[0] 继承
    # 在首次 GPU 操作后该张量会在正确 device 上
    self.deepstack_input_embeds = [
        torch.zeros(
            num_tokens,
            self.config.text_config.hidden_size,
            device=self.deepstack_input_embeds[0].device,
            dtype=self.deepstack_input_embeds[0].dtype,
        )
        for _ in range(self.deepstack_num_level)
    ]
​
​
def _set_deepstack_input_embeds(self, deepstack_input_embeds: torch.Tensor) -> None:
    if not getattr(self, "deepstack_input_embeds", None):
        return
    num_tokens = deepstack_input_embeds.size(1)
    if num_tokens > self.deepstack_input_embeds[0].size(0):
        self._resize_deepstack_input_embeds(num_tokens) # 复用新方法
    for idx in range(self.deepstack_num_level):
        self.deepstack_input_embeds[idx][:num_tokens].copy_(
            deepstack_input_embeds[idx])
    self.deepstack_input_embeds_num_tokens = num_tokens

评论区精华

device/dtype 不匹配风险:_resize_deepstack_input_embeds 应使用 self.visual 的 device/dtype 正确性

gemini-code-assist 指出 self.deepstack_input_embeds 在 __init__ 中初始化为 CPU 张量,_resize_deepstack_input_embeds 从 self.deepstack_input_embeds[0] 继承 device/dtype,可能导致 GPU 运算时 device 不匹配崩溃。建议 resize 时使用 self.visual.device 和 self.visual.dtype。

结论:作者未在本次 PR 中处理,但风险在现有逻辑下被规避:首次 GPU 请求触发 resize 后 buffer 已迁移到 GPU,后续 resize 继承正确 device/dtype。建议后续 PR 加固。 · 已解决

root cause 分析:encoder 与 decoder 分开 profile 导致图特化 设计

andakai 在 PR 评论中解释:vLLM 的 pre-profile 中 encoder 和 decoder 分开 profile,decoder dummy run 不含 deepstack_input_embeds,导致 torch.compile 无法捕获 deepstack 分支,当真实请求携带 deepstack 时其计算被忽略。

结论:通过始终返回 IntermediateTensors 保持输入结构稳定,从而避免图重编译。 · 已解决

风险与影响

  1. device/dtype 不匹配风险(低概率,高影响):如 review 评论指出,初始化时 deepstack_input_embeds 在 CPU 上分配,若第一个请求为 text-only(无视觉)且 num_tokens ≤ max_num_batched_tokens,_resize 不会被调用,返回 CPU 张量导致 GPU 崩溃。实际场景中,text-only 请求通常不会触发 deepstack 逻辑(因 getattr(self, "deepstack_input_embeds", None) 可能为 None),但若视觉 tower 存在但未提供视觉输入,则此风险存在。
  2. 性能影响:返回零张量比返回 None 增加了少量的 GPU memory 和计算开销,但 zero-backed IntermediateTensors 在 decoder 中贡献为零,且被 compiled graph 高效吸收,实测性能反而略有提升(102.6s vs 116.8s)。
  3. 回归风险:修改仅影响 deepstack 路径,不影响纯文本或非 deepstack 模型,回归概率低。

影响范围:Qwen3-VL 和 Qwen3-Omni-Thinker 模型在 torch.compile 默认路径下的精度恢复,对所有使用这些模型的多模态用户有直接正面影响。影响程度:高(精度从接近随机恢复到接近 eager 水平,Geo3K 提升约 0.08~0.06)。性能:略有提升(约 12% 更快)。

核心路径变更 涉及 torch.compile 编译图特化 缺少测试覆盖

关联 Issue

#43602 [Bug]: Qwen3-VL-2B-Instruct Geo3K accuracy score lower than SGLang with deterministic sampling

完整报告

参与讨论