Prhub

#22634 fix(qwen2_5vl): replace in-place += with out-of-place + on expand view in decode path

原始 PR 作者 Dragonliu2018 合并时间 2026-08-07 11:51 文件变更 1 提交数 1 评论 5 代码增减 +6 / -7

执行摘要

修复 Qwen2.5-VL decode 路径对 expand 视图就地写导致的崩溃

PR body 明确指出 Qwen2_5VLModel.forward 的 decode 路径中,position_ids 先调用 .expand() 再执行 += delta 就地修改,而 expand() 创建的视图多个逻辑元素共享同一物理内存地址,PyTorch 会拒绝该操作并抛出 RuntimeError: unsupported operation: more than one element of the written-to tensor refers to a single memory location. Please clone() the tensor before performing the operation.,导致请求在 decode 阶段直接失败。修复思路是重排计算顺序:先 (torch.arange(seq_length) + delta).unsqueeze(0).expand(3, -1, -1),彻底避免对扩展视图就地修改。

值得快速精读(约 10 分钟):该 PR 形象展示了 PyTorch expand() 视图的内存共享语义与就地写限制,以及“先加后 expand”的规避模式。值得关注的设计决策是「用广播替代显式 repeat_interleave」的组合写法,可推广到其他对 expand/广播张量做增量更新的场景。建议后续排查其他模型编码器中是否存在相同模式(先 expand 再就地更新),一并修复。

讨论亮点

alexnails 在 1083 行提出进一步简化:将 delta 统一为 (batch_size, 1) 形状,直接与 torch.arange(seq_length) 广播相加再 unsqueeze(0).expand(3, -1, -1),从而删除 repeat_interleave;该建议被作者采纳,最终 head 版本与其一致。alexnails 同时要求作者先本地验证再批准(“Please test new changes (if you have not) and then I will approve.”),作者回复已本地验证通过后获 approve。gemini-code-assist 机器人无实质反馈,仅确认改动把“生成、reshape、expand 与 delta 叠加”合并为单次赋值。

实现拆解

  1. 定位根因python/sglang/multimodal_gen/runtime/models/encoders/qwen2_5vl.pyQwen2_5VLModel.forward 中,decode 路径(cache_position 可用且 rope_deltas 已缓存)先构造 torch.arange(seq_length).view(1, 1, -1).expand(3, batch_size, -1) 视图,再执行 position_ids += delta,触发 PyTorch 对重叠内存视图的就地写检查。
  2. 重排计算顺序:改为先 position_ids = (torch.arange(seq_length, device=inputs_embeds.device) + delta).unsqueeze(0).expand(3, -1, -1),加法发生在无重叠的 1D 张量上,中间结果独立存储,从根上规避就地写限制。
  3. 简化 delta 构造cache_position 分支保持 cache_position[0] + self.rope_deltas 不变;无缓存分支从 (batch_size, seq_length) 全零张量加 repeat_interleave(batch_size // delta.shape[0], dim=1) 简化为 (batch_size, 1) 全零张量,依赖广播在加法中展开到 (batch_size, seq_length),语义不变且少一次显式张量操作。
  4. 验证与合入:作者本地验证通过(自述 “Tested locally, all good”);PR Test CI 通过,Extra CI 首次失败后经 /rerun-failed-ci 重跑;alexnails 提出“去掉 repeat_interleave”的简化建议被采纳后 approve,BBuf 最终 approve 并合入。
  5. 配套情况:无新增单元测试,无配置、文档或部署改动。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/models/encoders/qwen2_5vl.py 编码器 modified 6.04

关键符号

Qwen2_5VLModel.forward

关键源码片段

python/sglang/multimodal_gen/runtime/models/encoders/qwen2_5vl.py core-logic

唯一变更文件,承载全部修复逻辑:decode 路径中 position_ids 的构造从「先 expand 再就地 +=」改为「先加 delta 再 expand」,并删除 repeat_interleave 对齐。

if position_ids is None:
    # 计算 RoPE 索引:仅在 prefill 阶段生成一次;编译期无法检查张量值,故按输入长度判断阶段
    prefill_compiled_stage = is_torchdynamo_compiling() and (
        (input_ids is not None and input_ids.shape[1] != 1)
        or (inputs_embeds is not None and inputs_embeds.shape[1] != 1)
    )
    prefill_noncompiled_stage = not is_torchdynamo_compiling() and (
        (cache_position is not None and cache_position[0] == 0)
        or (past_key_values is None or past_key_values.get_seq_length() == 0)
    )
    if (prefill_compiled_stage or prefill_noncompiled_stage) or self.rope_deltas is None:
        # prefill:完整计算 rope_index,并把 delta 缓存到 self.rope_deltas 供 decode 复用
        position_ids, rope_deltas = self.get_rope_index(
            input_ids, image_grid_thw, video_grid_thw,
            second_per_grid_ts=second_per_grid_ts, attention_mask=attention_mask,
        )
        self.rope_deltas = rope_deltas
    else:
        # decode:基于缓存的 rope_deltas 增量重建 position_ids
        batch_size, seq_length, _ = inputs_embeds.shape
        if cache_position is not None:
            delta = (cache_position[0] + self.rope_deltas).to(inputs_embeds.device)
        else:
            delta = torch.zeros(batch_size, 1, device=inputs_embeds.device)
​
        # PR #22634 修复点:旧代码先 expand 再就地 += delta,
        # expand 视图共享物理内存导致 PyTorch 拒绝就地写并抛 RuntimeError。
        # 现改为先在 1D arange 上广播加 delta(形状 (batch_size, seq_length)),
        # 再 unsqueeze 到 (1, batch_size, seq_length),最后 expand 到 (3, batch_size, seq_length),
        # 全程无就地写,且省去旧实现里的 repeat_interleave 对齐。
        position_ids = (
            (torch.arange(seq_length, device=inputs_embeds.device) + delta)
            .unsqueeze(0)
            .expand(3, -1, -1)
        )

评论区精华

去掉 repeat_interleave 的进一步简化建议 设计

alexnails 在 1083 行建议将 delta 统一为 (batch_size, 1) 形状并直接广播:`torch.arange(seq_length) + delta` 后 `unsqueeze(0).expand(3, -1, -1)`,从而删除原实现中 `repeat_interleave(batch_size // delta.shape[0], dim=1)` 的显式对齐逻辑。

结论:作者采纳该建议,最终 head 版本与该方案一致;计算语义不变,且省去一次显式张量操作。 · 已解决

本地验证与 CI 要求 测试

alexnails 在 review 中要求作者先本地测试新改动再 approve;作者回复已本地验证通过,并通过 `/rerun-failed-ci` 处理 Extra CI 首次失败。

结论:本地验证通过,PR Test CI 绿色,alexnails 与 BBuf 先后 approve 并合入。 · 已解决

风险与影响

  1. 语义等价性风险:新逻辑依赖 rope_deltas(batch_size, 1) 广播的兼容性。当前 get_rope_index 返回形状匹配、PR Test CI 覆盖通过;但若未来 rope_deltas 引入其他维度(如多模态叠加导致第一维不等于 batch_size),旧代码的 repeat_interleave 对齐与新代码的广播行为可能产生分歧甚至报错。
  2. 测试覆盖缺口:PR 未新增针对该 decode 分支的单元测试,回归保护依赖模型级 CI,后续重构该函数时可能再次踩中 expand 就地写陷阱。
  3. 性能与兼容性:移除 repeat_interleave 减少一次显式张量操作,属微优化;输出 position_ids 形状仍为 (3, batch_size, seq_length),无外部 API 或模型输出契约变化。

直接消除 Qwen2.5-VL 编码器在 decode(续生)阶段因 expand 视图就地写抛出的 RuntimeError,使依赖 Qwen2.5-VL 的多模态生成(diffusion 管线)长序列生成不再崩溃。影响范围限于 multimodal_gen/runtime/models/encoders/qwen2_5vl.py 的 decode 分支,单文件 13 行变更,prefill 路径不受影响。对团队而言属于低风险、易 review 的修正,同时可作为 PyTorch expand 视图内存语义问题的可复现教学范例。

decode 核心路径变更 缺少测试覆盖 广播语义依赖 rope_deltas 形状

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论