Prhub

#39846 [BugFix] Prevent orphaned process on NCCL destroy

原始 PR 作者 jeffreywang-anyscale 合并时间 2026-05-12 03:25 文件变更 2 提交数 14 评论 16 代码增减 +24 / -2

执行摘要

改用 ncclCommAbort 在守护线程中执行,防止不协调关闭时的死锁和孤儿进程

当多进程不协调关闭时,一个 rank 退出后,其他 rank 在 destroy() 中调用 ncclCommDestroy 会永远阻塞,导致父进程残留孤儿 GPU 进程。另外,ncclCommAbort 虽然非集合,但其内部仍可能因等待 CUDA 图释放而阻塞,因此需要在独立线程中执行以避免主线程自死锁。参考 PyTorch 的 ProcessGroupNCCL::abort() 实现。

推荐合并。该 PR 解决了生产环境中棘手的死锁问题,设计借鉴了 PyTorch 的成熟实践。建议后续补充超时时间的可配置性及增加单元测试,以覆盖更多边缘场景。

讨论亮点
  • itayalroy 提出弹性 EP 可能被破坏:先前引入 ncclCommDestroy 是为了在重复扩缩容时清理 stale NCCL 通信器,若跳过清理会导致 GPU 内存泄漏。后来作者改用 ncclCommAbort 后,itayalroy 验证了重复扩缩容场景正常工作。
  • gemini-code-assist[bot] 早期建议采用 ncclCommAbort 而非简单跳过,并指出资源泄漏和并发竞态风险。
  • tlrmchlsmth 询问 ncclCommAbort 是否可行,在得知使用后表示接受并 approve。

实现拆解

  1. 注册 ncclCommAbort 绑定:在 pynccl_wrapper.py 中的 Function 列表里添加 ncclCommAbort 声明,并新增 ncclCommAbort 方法封装原生 NCCL 调用。
  2. 重写 destroy 方法:在 pynccl.py 中导入 threading,将原有 ncclCommDestroy 替换为一个守护线程执行的 _abort 函数,该函数在正确的设备上下文中调用 ncclCommAbort。线程启动后主线程 join 等待最多 5 秒,避免永久阻塞。
  3. 状态更新:线程 join 后立即将 self.available = Falseself.disabled = True,确保后续操作跳过。
  4. 配置与测试:无额外配置变更;未添加新测试,但已在 Ray 的批量场景下验证不出现挂起。
文件 模块 状态 重要度
vllm/distributed/device_communicators/pynccl.py 分布式通信 modified 7.06
vllm/distributed/device_communicators/pynccl_wrapper.py 分布式通信 modified 5.94

关键符号

destroy _abort ncclCommAbort

关键源码片段

vllm/distributed/device_communicators/pynccl.py core-logic

核心修改文件,重写了 destroy 方法,改用守护线程执行 ncclCommAbort 以解决死锁。

    def destroy(self):
        if self.available and not self.disabled:
            # ncclCommAbort 可能阻塞,因为要等所有捕获了 NCCL 操作的 CUDA 图销毁;
            # 这些图在主线程释放链中稍后才会被释放,直接调用会导致自死锁。
            # 所以放入守护线程并设置超时,主线程继续执行,图释放后 abort 返回。
            def _abort():
                with torch.accelerator.device_index(self.device.index):
                    self.nccl.ncclCommAbort(self.comm)
​
            abort_thread = threading.Thread(target=_abort, daemon=True)
            abort_thread.start()
            abort_thread.join(timeout=5.0)
            self.available = False
            self.disabled = True
vllm/distributed/device_communicators/pynccl_wrapper.py core-logic

新增 ncclCommAbort 函数绑定和 Python 封装方法,是 destroy 变更的基础。

        # ncclCommAbort frees resources associated with the communicator
        # without requiring a collective synchronization. Unlike
        # ncclCommDestroy, it is safe to call during an uncoordinated
        # shutdown when peer ranks may already be gone.
        # ncclResult_t ncclCommAbort(ncclComm_t comm);
        Function("ncclCommAbort", ncclResult_t, [ncclComm_t]),
        ...
    def ncclCommAbort(self, comm: ncclComm_t) -> None:
        self.NCCL_CHECK(self._funcs["ncclCommAbort"](comm))

评论区精华

弹性 EP 兼容性担忧 正确性

itayalroy 指出旧的 ncclCommDestroy 本为了清理 stale NCCL 通信器以支持弹性 EP,跳过可能导致 OOM。作者改用 ncclCommAbort 后,itayalroy 验证了重复扩缩容工作正常。

结论:ncclCommAbort 解决了问题且不影响弹性 EP。 · 已解决

ncclCommAbort 可行性 question

tlrmchlsmth 询问 ncclCommAbort 是否可行,作者确认后其接受。

结论:可行,且已实现。 · 已解决

避免资源泄漏的建议 设计

gemini-code-assist[bot] 建议使用 ncclCommAbort 而非简单跳过 destroy 以避免资源泄漏。

结论:被采纳,最终实现使用 ncclCommAbort。 · 已解决

风险与影响

  1. 守护线程超时:5 秒超时可能不足以完成 abort(极端情况下若 CUDA 图释放慢),线程可能被强制终止,导致资源未完全释放(但 NCCL 内部有清理)。
  2. NCCL 版本兼容性ncclCommAbort 在较老 NCCL 版本(<2.18)中可能不存在或行为不同,但 vLLM 通常依赖较新版本。
  3. 缺少单元测试:本次改动无直接测试覆盖,仅靠手动验证。
  4. 并发安全disabled 标志检查与修改之间可能存在竞态,但当前顺序是先 join 后设置标志,其他 collectives 在标志设置前可能仍通过检查并尝试使用已 abort 的 comm。

修复了多 GPU 不协调关闭时进程挂起和孤儿进程的问题,提升了 Ray Actor 和多进程部署的稳定性。对弹性 EP 无负面影响(经验证)。变更范围仅两个文件,属于分布式通信核心层,影响面可控。

核心路径变更 缺少测试覆盖 多线程安全风险 超时不确定性

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论