Prhub

#6127 [trainer] fix: preserve jagged tensor layout when rebuilding nested tensors with same sequence length

原始 PR 作者 huaiyizhao 合并时间 2026-04-24 10:05 文件变更 3 提交数 2 评论 1 代码增减 +79 / -17

执行摘要

修复等长序列嵌套张量重建时的布局损坏

在 RL 训练中,position_ids 常为形状 (num_heads, seq_len) 的三维嵌套张量。当多个样本序列长度恰好相同时,原有的 as_nested_tensor 会错误地将最后一维(num_heads)识别为 ragged 维度,破坏 layout。PR 描述及 Review 评论均明确指出此问题。

建议 阅读 本 PR,特别是 nested_tensor_from_tensor_list 的实现和 dataset_utils.py 的简化模式。设计上通过显式控制 _ragged_idx 避免了对 torch 默认可疑行为的依赖。

讨论亮点

Review 评论 (gemini-code-assist[bot]):指出 collate_variable_batch 中的维度条件(if tensors[0].dim() >= 2)是冗余的,因为 nested_tensor_from_tensor_list 内部已处理 1D 分支。建议直接调用新函数。

结论:作者采纳了该建议,在最终代码中将内部逻辑替换为一行调用,消除了冗余分支。无未解决疑虑。

实现拆解

  1. 新建辅助函数 nested_tensor_from_tensor_listverl/utils/tensordict_utils.py
    - 接受张量列表和可选的 ragged_idx 参数,默认 ragged_idx 为样本张量的维数(即最后一维)。
    - 若样本为 1D,直接调用 as_nested_tensor;否则通过 cat + cumsum 构建 offsets,再用 nested_tensor_from_jagged 构造并显式设置 _ragged_idx

  2. 迁移调用点
    - concat_nested_tensors:原使用 as_nested_tensor 重包装,改为调用新函数并传入 ragged_idx=tensors[0].dim()-1
    - chunk_tensordict 的两个分支(正常 unbid 和 padded 回退):均替换为 nested_tensor_from_tensor_list,传递 ragged_idx=nt.dim()-1
    - index_select_tensor_dict:将选中的张量列表传入新函数,保持 ragged_idx=tensor.dim()-1
    - SFTTensorCollator.collate_variable_batchverl/utils/dataset/dataset_utils.py):移除内联的 offset 计算逻辑和维度条件分支,统一调用 nested_tensor_from_tensor_list

  3. 回归测试tests/test_protocol_v2_on_cpu.py
    - test_index_select_tensor_dict_preserves_3d_nested_tensor_layout_with_equal_seq_len:构造 4 个样本(部分等长),验证 index_select 后 _ragged_idx、values、offsets 与预期一致。
    - test_chunk_tensordict_preserves_3d_nested_tensor_layout_with_equal_seq_len_per_chunk:验证 chunk 操作后每个 chunk 内的 position_ids 保持正确 layout。

文件 模块 状态 重要度
verl/utils/tensordict_utils.py 工具层 modified 7.3
tests/test_protocol_v2_on_cpu.py 测试 modified 6.22
verl/utils/dataset/dataset_utils.py 数据层 modified 5.57

关键符号

nested_tensor_from_tensor_list

关键源码片段

verl/utils/dataset/dataset_utils.py dependency-wiring

SFT 数据 collator 中的 collate_variable_batch 方法简化:移除内联 offset 构造和维度分支,统一调用新函数。

# 简化后的 collate_variable_batch 关键片段
if tensors[0].dim() >= 2:
    # 原注释和 offset 计算被替换为一行调用
    final_batch[key] = nested_tensor_from_tensor_list(tensors)
else:
    final_batch[key] = torch.nested.as_nested_tensor(tensors, layout=torch.jagged)

注意:Review 建议将整个 if-else 也替换为同一调用,但当前版本保留了 1D 分支(仍调用 as_nested_tensor)。这是可接受的优化未尽事项。

评论区精华

collate_variable_batch 维度条件冗余 设计

gemini-code-assist[bot] 指出 collate_variable_batch 中 `if tensors[0].dim() >= 2` 分支多余,因为新函数已处理 1D case。建议直接调用。

结论:作者采纳了简化建议,将多行 inline 逻辑替换为单行调用,但保留了 else 分支(1D 仍用 as_nested_tensor)。 · 已解决

风险与影响

  • 回归风险(高):公共函数 concat_nested_tensorschunk_tensordict 在中段有回调行为变化。但测试覆盖了等长和不等长混合场景,且新实现保留了 _ragged_idx,可降低边 case 遗漏。
  • 性能风险(低至中):新增函数多了一次张量维数判断和 cat/cumsum 操作,但总体与旧代码的 offset 构造开销相近,对训练流程无显著影响。
  • 兼容性风险(低):未修改公开 API 签名,仅内部实现替换;dataset_utils.py 的 collator 调用方式相同。
  • 用户可见影响:修复了在等长序列时 position_ids 等张量 layout 错误导致的潜在训练崩溃或梯度错误。
  • 系统影响:波及 TensorDict 工具链的核心操作(index_select、chunk、concat),增强鲁棒性。
  • 团队影响:提供了可复用的 nested_tensor_from_tensor_list 工厂函数,后续开发应优先使用。
核心路径变更 等长序列边界 case

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论