Prhub

#7539 [ray] fix: skip unused TensorDict consolidation in NumPy DataProto serialization

原始 PR 作者 Sky-Trigger 合并时间 2026-08-25 12:09 文件变更 2 提交数 2 评论 4 代码增减 +34 / -13

执行摘要

修复 DataProto NumPy 序列化中未使用的 TensorDict consolidation 导致的内存浪费。

此 PR 是针对下游项目 verl-omni 中报告的主机 RAM OOM 问题的根因修复。在使用 VERL_DATAPROTO_SERIALIZATION_METHOD=numpy 时,DataProto.__getstate__() 会先执行 self.batch.contiguous().consolidate(),创建一个完整的数据副本,但随后 NumPy 序列化路径却忽略了此副本并直接序列化 self.batch,导致了等同于整个批次大小的、未使用的内存分配。该问题在 verl-omni#402 中被报告,并在 verl-omni#423 中通过临时 monkey patch 进行了规避。此 PR 在 verl 主仓中移除了该冗余分配。

此 PR 值得精读。它清晰展示了一个典型的“热路径”性能问题(无用分配)及其干净利落的修复。关键设计决策在于将序列化方法的选择逻辑前置,这是一个简单但有效的重构。对于关注内存效率和序列化性能的工程师,此 PR 提供了一个很好的实践案例。

讨论亮点

Review 讨论主要集中在两点:

  1. 回归测试的必要性:SamitHuang 建议为不必要的内存分配增加回归测试。这直接导致了 PR 第二个 commit 的加入,为修复添加了针对性的单元测试。
  2. 未来技术方向:wuxibin89 指出 verl-omni 是否计划切换到 TensorQueue (TQ),因为 TQ 的序列化成本为零。作者 Sky-Trigger 回应称迁移到 V1 trainer 需要时间,表明当前修复是一个务实的中间步骤。

实现拆解

  1. 入口变更:在 verl/protocol.py 的 DataProto.__getstate__() 方法中,将检查 VERL_DATAPROTO_SERIALIZATION_METHOD 环境变量的逻辑移至方法开头。
  2. 核心逻辑调整:在 NumPy 序列化分支中,直接返回 serialize_tensordict(self.batch) 的结果(当 self.batch 非空时),完全绕过了 contiguous().consolidate() 调用。这确保了只在需要 torch.save 的默认路径中才执行 consolidation。
  3. 测试配套:在 tests/test_protocol_on_cpu.py 中新增了单元测试 test_numpy_dataproto_serialization_skips_tensordict_consolidation。该测试使用 monkeypatch 禁用 VERL_DATAPROTO_SERIALIZATION_METHOD=numpy,并断言 TensorDict.consolidate() 方法未被调用,以此验证代码路径的正确性。测试覆盖了序列化/反序列化的完整往返,并验证了数据一致性。
文件 模块 状态 重要度
verl/protocol.py 数据协议 modified 6.27
tests/test_protocol_on_cpu.py 协议测试 modified 5.97

关键符号

DataProto.__getstate__ serialize_tensordict

关键源码片段

verl/protocol.py core-logic

核心源码文件,包含本次修复的 `DataProto.__getstate__()` 方法,调整了序列化分支逻辑以消除无效内存分配。

# verl/protocol.py - DataProto.__getstate__ 方法的核心变更
# 原始逻辑:先无条件执行 consolidate(),再检查序列化方法,导致 NumPy 路径下 consolidate 的结果被丢弃。
# 修复后逻辑:先检查序列化方法,如果是 NumPy 模式,则直接序列化原始 batch,避免无效分配。
def __getstate__(self):
    # 优先检查是否使用 NumPy 序列化
    if os.getenv("VERL_DATAPROTO_SERIALIZATION_METHOD") == "numpy":
        # 直接序列化 self.batch,不执行 consolidate()
        return (
            serialize_tensordict(self.batch) if self.batch is not None else None,
            self.non_tensor_batch,
            self.meta_info,
        )
    else:
        # 仅在默认 torch 序列化路径中执行 consolidation,确保 torch.save 获得连续内存块
        if version.parse(tensordict.__version__) >= version.parse("0.5.0") and self.batch is not None:
            # 检查 batch 是否为空,避免 consolidate 时的 torch.cat 错误
            if len(self.batch.keys()) > 0:
                batch = self.batch.contiguous().consolidate()
            else:
                batch = self.batch
        else:
            batch = self.batch
​
        import io
​
        buffer = io.BytesIO()
        torch.save(batch, buffer)
        buffer_bytes = buffer.getvalue()
        return buffer_bytes, self.non_tensor_batch, self.meta_info

评论区精华

是否需要添加针对不必要内存分配的回归测试 测试

SamitHuang 询问:"should we add a regression test for unnecessary full-batch TensorDict allocation?" 这促使作者在第二个 commit 中添加了专门的单元测试。

结论:同意添加测试。PR 随后包含了测试用例。 · 已解决

verl-omni 未来是否会切换到 TensorQueue (TQ) 以彻底解决序列化开销 设计

wuxibin89 询问:"Does verl-omni have plan switching to TQ? TQ has zero serialization cost." 作者 Sky-Trigger 回应确认有计划但需要时间迁移。

结论:确认 TensorQueue 是长期的优化方向,但本 PR 是当前架构下的必要修复。 · 已解决

风险与影响

风险较低。变更严格限制在序列化路径的条件分支重排,没有改变任何数据结构或核心算法。

  • 回归风险:现有测试(包括 test_serialize_dataproto_with_empty_tensordict)和新增测试应能覆盖 torch 序列化和 NumPy 序列化的主要路径。对空 TensorDict 的处理逻辑被保留并移至 torch 分支内。
  • 性能风险:此变更旨在减少内存使用和分配开销,性能影响应为正面。
  • 兼容性风险:行为变更仅对设置 VERL_DATAPROTO_SERIALIZATION_METHOD=numpy 的用户可见,且是优化(跳过无效操作),不改变公共 API 或序列化格式。

影响范围:直接影响所有使用 DataProto 并选择 NumPy 序列化方法(export VERL_DATAPROTO_SERIALIZATION_METHOD=numpy)的用户。
影响程度:显著正面。对于大批次工作负载(如 PR body 中提到的 464 MiB 载荷),可消除一次等量的无用主机内存分配,直接缓解 OOM 压力。此修复为 verl-omni 等下游项目移除临时规避补丁提供了基础。对于使用默认 torch 序列化的用户,行为无任何变化。

序列化路径变更 内存分配行为变更

关联 Issue

#402 It throws some OOM error when we run wan2.2 with Verl-omni on ascend node

完整报告

参与讨论