Prhub

#6447 [megatron] fix: Fix GPU memory leak in ref model offload by explicitly releasing storage

原始 PR 作者 ZLiao097 合并时间 2026-06-01 18:09 文件变更 1 提交数 1 评论 3 代码增减 +31 / -2

执行摘要

修复 Megatron ref 模型 GPU 显存泄漏

修复在非 DDP 路径下(用于参考模型),将参数和梯度移到 CPU 时 GPU 显存未及时释放的问题。旧实现 param.data = param.data.to("cpu", non_blocking=True) 导致旧的 GPU 张量仍然被引用直到 Python GC 运行,造成显存尖峰并可能触发 OOM。同时异步拷贝后再释放存储存在数据损坏风险。

建议精读该 PR,特别是 _can_safely_resize_storage 函数的逻辑和同步拷贝 + resize_(0) 的使用模式。该修复对于避免大模型训练中的显存泄漏至关重要。但建议补充单元测试覆盖非 DDP 路径的卸载场景。

讨论亮点

gemini-code-assist[bot] 指出直接对单个参数和梯度调用 storage().resize_(0) 存在风险,因为如果参数共享底层存储(如权重绑定、视图),则会立即失效所有其他张量。虽然 DDP 路径操作的是整个缓冲区所以安全,但非 DDP 路径需要确保参数不共享存储。作者通过引入 _can_safely_resize_storage 函数来防范此风险。ETOgaosion 批准了 PR。

实现拆解

  1. 新增 _can_safely_resize_storage 辅助函数:在 verl/utils/megatron_utils.py 中新增该函数,用于检查一个张量是否独占其整个存储(无共享、无视图偏移、连续),确保调用 storage().resize_(0) 是安全的。
  2. 改造非 DDP 路径的 CPU 卸载逻辑:在 offload_megatron_model_to_cpu 函数的 else 分支(非 DDP 路径)中,先将 param.dataparam.grad 的旧引用保存到 old_dataold_grad,然后使用同步 .to("cpu") 拷贝(默认 non_blocking=False),确保数据拷贝完成后才释放 GPU 存储。
  3. 显式释放 GPU 存储:在同步拷贝后,调用 old_data.storage().resize_(0)old_grad.storage().resize_(0) 立即释放 GPU 显存(仅在 _can_safely_resize_storage 返回 True 时执行)。
  4. 辅助内存回收:添加 gc.collect()torch.cuda.empty_cache() 进一步清理 Python 垃圾回收和缓存碎片。
文件 模块 状态 重要度
verl/utils/megatron_utils.py 工具类 modified 7.28

关键符号

_can_safely_resize_storage offload_megatron_model_to_cpu

关键源码片段

verl/utils/megatron_utils.py core-logic

修复 GPU 显存泄漏的核心文件,新增安全检查函数并改造卸载逻辑。

def _can_safely_resize_storage(tensor: torch.Tensor) -> bool:
    """Check whether it is safe to call ``storage().resize_(0)`` on *tensor*.    Resizing the underlying storage to zero immediately frees the GPU memory
    but also invalidates **every** tensor that shares the same storage
    (e.g. views, tied weights stored as different Python objects, or slices
    of a DDP flat buffer).  This function returns True only when the tensor
    exclusively owns its entire storage, making ``resize_(0)`` safe.
    """
    return (
        # Storage holds exactly the elements of this tensor – no room for
        # other tensors sharing the same storage.
        tensor.storage().size() == tensor.numel()
        # Tensor starts at the beginning of the storage – not a slice/view
        # offset into a larger buffer.
        and tensor.storage_offset() == 0
        # Tensor is contiguous in memory – rules out transposed or
        # non-contiguous views that only occupy part of the storage layout.
        and tensor.is_contiguous()
    )
​
​
@torch.no_grad()
def offload_megatron_model_to_cpu(models):
    # ... 内部 DDP 路径代码不变 ...
    else:
        # we need this for ref module
        for _, param in model_chunk.named_parameters():
            old_data = param.data
            param.data = param.data.to("cpu") # 同步拷贝,确保完成
            if _can_safely_resize_storage(old_data):
                old_data.storage().resize_(0) # 立即释放 GPU 存储
            if param.grad is not None:
                old_grad = param.grad
                param.grad = param.grad.to("cpu")
                if _can_safely_resize_storage(old_grad):
                    old_grad.storage().resize_(0)
        gc.collect()
        get_torch_device().empty_cache()

评论区精华

storage().resize_(0) 安全性 正确性

gemini-code-assist[bot] 指出直接对单个参数调用 storage().resize_(0) 可能存在风险,如果参数共享存储则会导致其他张量失效。

结论:作者通过新增 _can_safely_resize_storage 函数检查存储独占性来降低风险。 · 已解决

风险与影响

主要风险在于 storage().resize_(0) 的正确使用。新增的 _can_safely_resize_storage 函数通过检查存储大小、偏移量和连续性来降低风险,但若存在未预期的共享视图(如高级索引或自定义存储分配)仍可能误判。此外,没有新增测试用例覆盖该新逻辑。

直接影响 Megatron 训练框架下的参考模型(ref module)卸载流程,消除约 1-4GB 显存泄漏,尤其对 NPU 环境(显存紧张)影响显著。不会影响 DDP 路径(该路径已正确处理)。

核心路径变更 缺少测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论