Prhub

#6332 [fsdp, fully_async] fix: fix CI import fast_pos_embed_interpolate in Qwen3-VL

原始 PR 作者 zhihaofang1017 合并时间 2026-05-13 21:45 文件变更 2 提交数 2 评论 2 代码增减 +66 / -3

执行摘要

修复 CI 中 Qwen3-VL 的 fast_pos_embed_interpolate 导入问题

PR body 指出:在将 Qwen3-VL 适配到全异步训练时,需要 fast_pos_embed_interpolate 函数。由于当前 CI 不支持 Qwen3.5(qwen3_5.py 属于 Qwen3.5 模型模块),从 verl/models/transformers/qwen3_5.py 导入该函数会导致 transformers 库报错。因此需要将函数移动到 Qwen3-VL 自己的模块中。

本次 PR 是紧急修复,建议后续跟进:

  1. 将 fast_pos_embed_interpolate 抽取到共享工具模块(如 verl/models/transformers/vision_utils.py),消除重复。
  2. 考虑优化实现性能,避免循环中的 tolist 模式。
  3. 由于 CI 目前不支持 Qwen3.5,可评估是否需要增加对 Qwen3.5 的 CI 支持,从根源避免类似问题。
讨论亮点

代码审查机器人(gemini-code-assist[bot])提出了两个关注点:

  1. 代码重复问题:指出该函数是从 qwen3_5.py 复制而来,导致重复代码。建议将其抽取到共享工具模块(如 vision_utils.py),以提高可维护性。
  2. 性能瓶颈:指出在循环中使用 .tolist() 和 list.extend 会导致频繁的张量与 Python 列表转换,建议使用 torch.cat 和 torch.stack 替代以提升性能。
    这些评论在 PR 合并前未得到进一步回复或解决,最终 PR 依然被批准合并。

实现拆解

  1. 新增函数定义:在 verl/models/transformers/qwen3_vl.py 中新增 fast_pos_embed_interpolate 函数(第 33-95 行),代码逻辑与 qwen3_5.py 中的同名函数一致,唯一修改点是将 self.pos_embed 相关的操作显式放到 grid_thw.device 上,以避免 FSDP2 开启 cpu_offload 时 pos_embed 位于 CPU 导致设备不匹配。
  2. 调整导入来源:在 verl/models/transformers/monkey_patch.py 的 Qwen3-VL monkey patch 分支中,将 fast_pos_embed_interpolate 的导入语句从 from verl.models.transformers.qwen3_5 import ... 改为 from verl.models.transformers.qwen3_vl import ...。这样 monkey patch 无需再依赖 qwen3_5 模块,从而避免 CI 中因缺失 Qwen3.5 支持而失败。
  3. 函数作用:该函数实现了对视觉位置嵌入的双线性插值,用于处理不同分辨率的图像/视频输入。它接收 grid_thw(时间、高度、宽度)张量,计算 4 个最近邻的索引和权重,通过 self.pos_embed(一个 Embedding 层)查找并加权求和,最后按 merge_size 进行 permute 重排,返回插值后的 position embedding。
文件 模块 状态 重要度
verl/models/transformers/qwen3_vl.py 模型层 modified 7.55
verl/models/transformers/monkey_patch.py 模型层 modified 5.96

关键符号

fast_pos_embed_interpolate

关键源码片段

verl/models/transformers/qwen3_vl.py core-logic

新增了 fast_pos_embed_interpolate 函数定义,是 PR 的核心变更。函数逻辑与 qwen3_5.py 中的副本一致,但添加了设备对齐的修正。

def fast_pos_embed_interpolate(self, grid_thw):
    """
    对 Qwen3-VL 的视觉位置嵌入进行快速双线性插值。
    grid_thw: (batch, 3) 每个图像的 (时间, 高度, 宽度) 网格数。
    返回插值后的 patch position embedding。
​
    注意:此函数与 qwen3_5.py 中的版本逻辑相同,
    但显式从 grid_thw 获取 device,避免 FSDP2 cpu_offload 时
    self.pos_embed 位于 CPU 导致设备不匹配。
    """
    grid_thw_list = grid_thw.tolist()
    grid_ts = [row[0] for row in grid_thw_list] # 时间维度
    grid_hs = [row[1] for row in grid_thw_list] # 高度维度
    grid_ws = [row[2] for row in grid_thw_list] # 宽度维度
    # 从 grid_thw 获取 device,防止 FSDP2 cpu_offload 导致设备不匹配
    device = grid_thw.device
​
    idx_list = [[] for _ in range(4)]
    weight_list = [[] for _ in range(4)]
​
    for t, h, w in grid_thw_list:
        # 生成线性插值坐标
        h_idxs = torch.linspace(0, self.num_grid_per_side - 1, h)
        w_idxs = torch.linspace(0, self.num_grid_per_side - 1, w)
​
        h_idxs_floor = h_idxs.int()
        w_idxs_floor = w_idxs.int()
        h_idxs_ceil = (h_idxs.int() + 1).clip(max=self.num_grid_per_side - 1)
        w_idxs_ceil = (w_idxs.int() + 1).clip(max=self.num_grid_per_side - 1)
​
        dh = h_idxs - h_idxs_floor
        dw = w_idxs - w_idxs_floor
​
        # 计算 4 个最近邻索引
        base_h = h_idxs_floor * self.num_grid_per_side
        base_h_ceil = h_idxs_ceil * self.num_grid_per_side
​
        indices = [
            (base_h[None].T + w_idxs_floor[None]).flatten(),
            (base_h[None].T + w_idxs_ceil[None]).flatten(),
            (base_h_ceil[None].T + w_idxs_floor[None]).flatten(),
            (base_h_ceil[None].T + w_idxs_ceil[None]).flatten(),
        ]
​
        weights = [
            ((1 - dh)[None].T * (1 - dw)[None]).flatten(),
            ((1 - dh)[None].T * dw[None]).flatten(),
            (dh[None].T * (1 - dw)[None]).flatten(),
            (dh[None].T * dw[None]).flatten(),
        ]
​
        for i in range(4):
            idx_list[i].extend(indices[i].tolist())
            weight_list[i].extend(weights[i].tolist())
​
    idx_tensor = torch.tensor(idx_list, dtype=torch.long, device=device)
    weight_tensor = torch.tensor(weight_list, dtype=self.pos_embed.weight.dtype, device=device)
    pos_embeds = self.pos_embed(idx_tensor).to(device) * weight_tensor[:, :, None]
    patch_pos_embeds = pos_embeds[0] + pos_embeds[1] + pos_embeds[2] + pos_embeds[3]
​
    # 按实际 grid 尺寸分割
    patch_pos_embeds = patch_pos_embeds.split([h * w for h, w in zip(grid_hs, grid_ws, strict=False)])
​
    # 按 spatial_merge_size 重新排列
    merge_size = self.config.spatial_merge_size
    patch_pos_embeds_permute = []
    for pos_embed, t, h, w in zip(patch_pos_embeds, grid_ts, grid_hs, grid_ws, strict=False):
        pos_embed = pos_embed.repeat(t, 1)
        pos_embed = (
            pos_embed.view(t, h // merge_size, merge_size, w // merge_size, merge_size, -1)
            .permute(0, 1, 3, 2, 4, 5)
            .flatten(0, 4)
        )
        patch_pos_embeds_permute.append(pos_embed)
    patch_pos_embeds = torch.cat(patch_pos_embeds_permute)
    return patch_pos_embeds
verl/models/transformers/monkey_patch.py other

修改了 fast_pos_embed_interpolate 的导入来源,从 qwen3_5 改为 qwen3_vl,解除对 qwen3_5 模块的依赖,是解决 CI 导入错误的关键。

    # 在 monkey_patch.py 的 qwen3_vl 分支中(约第 426 行)
    # 原来:
    # from verl.models.transformers.qwen3_5 import (
    # fast_pos_embed_interpolate,
    # )
    # 现在改为直接从 qwen3_vl 导入:
    from verl.models.transformers.qwen3_vl import (
        fast_pos_embed_interpolate, # 从本模块导入,避免依赖 qwen3_5
        forward_with_normal_backend,
        patch_qwen3_vl_moe_sparse_moe_block_forward,
        qwen3_vl_base_forward,
    )
​
    # 后续 monkey patch 保持不变:
    Qwen3VLMoeVisionModel.fast_pos_embed_interpolate = fast_pos_embed_interpolate
    Qwen3VLVisionModel.fast_pos_embed_interpolate = fast_pos_embed_interpolate

调整后,只要 qwen3_vl 模块本身可导入,monkey patch 就不会因 qwen3_5 缺失而失败。

评论区精华

代码重复问题 设计

审查机器人指出 fast_pos_embed_interpolate 函数是从 qwen3_5.py 复制而来,引入了代码重复,建议抽取到共享工具模块(如 vision_utils.py)以避免维护困难。

结论:未解决;PR 仍以当前方式合并,但建议后续重构。 · unresolved

性能优化建议 性能

审查机器人建议避免循环中使用 `.tolist()` 和 `list.extend`,改为收集张量后使用 `torch.cat` 和 `torch.stack`,以提高性能。

结论:未解决;PR 合并时未采纳该建议。 · unresolved

风险与影响

  1. 代码重复导致的维护风险:fast_pos_embed_interpolate 在 qwen3_vl.py 和 qwen3_5.py 中存在两份副本,未来若修复 bug 或优化性能,需同步两处修改,容易遗漏。
  2. 性能风险:当前实现中反复在 Python 列表与 PyTorch 张量间转换,对于大规模 grid_thw 输入可能成为瓶颈,影响训练效率。
  3. 回归风险低:函数逻辑与原有实现完全一致,仅修改了设备位置的计算方式,对功能无负面影响。
  • 用户/开发者:解决了 CI 中 Qwen3-VL 全异步训练脚本的导入错误,使得相关功能可以在 CI 中正常测试。
  • 系统:无直接影响,属于代码组织调整。
  • 团队:降低了维护便利性,因为引入了重复代码;但短期内避免了更复杂的重构。
代码重复 性能瓶颈风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论