Prhub

#39366 [BUG] Two phase pause to prevent deadlock

原始 PR 作者 hao-aaron 合并时间 2026-04-30 05:51 文件变更 3 提交数 10 评论 12 代码增减 +232 / -20

执行摘要

两阶段暂停协议修复 DP 引擎死锁

Issue #36594 报告在 DPEP 配置中调用 pause_generation 后执行 collective_rpc 会因 NCCL ALLREDUCE 超时而失败。根因是 START_DP_WAVE 可以绕过暂停状态重新激活引擎,导致集合操作不匹配。本 PR 通过两阶段暂停协议确保所有 rank 一致停止,避免死锁。

值得精读的设计案例:如何在不增加同步开销的前提下利用现有 all-reduce 实现全局共识。建议关注两阶段协议的模式以及 _pause_complete 多态覆盖的设计。未来可考虑在 pending_pause 时触发即时 all-reduce 以降低暂停延迟。

讨论亮点

gemini-code-assist 指出在 pause 共识后若 scheduler 仍有请求,all-reduce 可能引擎恢复,作者回应是预期行为(如 keep 模式需处理已有请求)。
njhill 建议 sync_dp_state 使用 dp_group.size() 替代传参,已采纳。
njhill 提议将重复代码抽取为 _do_pause,最终通过覆盖 _pause_complete 实现,达成相同解耦。

实现拆解

  1. 新增状态标志:在 DPEngineCoreProc.__init__vllm/v1/engine/core.py)中引入 pending_pauseignore_start_dp_wave 布尔标志,并保存 dp_size 供 all-reduce 使用。
  2. 覆盖 _pause_complete 方法DPEngineCoreProc 覆盖基类 _pause_complete。覆盖版本始终返回 False,设置 pending_pause = Trueengines_running = True,确保引擎持续 stepping 以参与后续 all-reduce。
  3. 新增 ParallelConfig.sync_dp_state 方法vllm/config/parallel.py):将“是否有未完成工作”和“是否请求暂停”打包为 2 元素整数张量,通过单个 SUM all-reduce 同时获得 OR 和 AND 语义。
  4. 修改 _has_global_unfinished_reqs 方法vllm/v1/engine/core.py):每 32 步调用 sync_dp_state。当返回 pause_consensus=True 时设置 ignore_start_dp_wave=True 并停止 stepping。非 all-reduce 步时若 pending_pause 为真也返回 True 保持引擎运行。
  5. 处理 START_DP_WAVE 消息:在 _handle_client_request 中,若 ignore_start_dp_wave=True 则忽略 START_DP_WAVE,保持引擎静止。
  6. 新增测试tests/v1/distributed/test_async_llm_dp.py):增加 test_dp_pause_barrier_request_deadlock 验证暂停后 barrier 加请求加 barrier 不会死锁,并适配原有测试至两阶段协议。
文件 模块 状态 重要度
vllm/v1/engine/core.py 引擎核心 modified 8.17
vllm/config/parallel.py 并行配置 modified 6.93
tests/v1/distributed/test_async_llm_dp.py DP 测试 modified 7.49

关键符号

_pause_complete barrier sync_dp_state _has_global_unfinished_reqs

关键源码片段

vllm/v1/engine/core.py core-logic

核心实现:包含两阶段暂停协议的引擎端逻辑,包括状态标志、_pause_complete 覆盖、_has_global_unfinished_reqs 修改、barrier 方法等。

# 在 DPEngineCoreProc 中覆盖基类 _pause_complete,实现两阶段暂停
def _pause_complete(self) -> bool:
    """
    两阶段 DP 感知的暂停判断。
    Phase 1: 设置 pending_pause = True,并强制引擎进入 stepping 循环,
    以便在 all-reduce 检查点与其他 rank 通信。
    Phase 2: 在 _has_global_unfinished_reqs 中执行 all-reduce 后,
    若所有 rank 都 pending_pause,则设置 ignore_start_dp_wave 并返回 False。
    本方法始终返回 False,表示暂停未立即完成,需等待共识。
    """
    self.pending_pause = True
    self.engines_running = True # 确保 rank 进入 stepping 循环
    return False

评论区精华

两阶段暂停逻辑是否导致引擎过早退出暂停状态 正确性

gemini-code-assist 指出当 pause_consensus 达成后,如果 scheduler 中仍有请求,下一个迭代中 _has_global_unfinished_reqs 会再次返回 True,导致引擎恢复。

结论:作者回应这是预期行为,例如 pause mode keep 需要继续运行以处理存留请求。未修改代码。 · 已解决

使用 dp_group.size() 简化 sync_dp_state 参数 设计

njhill 建议 sync_dp_state 接受 dp_group 参数并使用 group.size() 而不是额外传递 dp_size。

结论:已采纳,最终实现中使用 dp_group.size() 动态获取 size。 · 已解决

通过覆盖 _pause_complete 避免重复 pause_scheduler 代码 设计

njhill 建议将 DPEngineCoreProc.pause_scheduler 中的唯一差异抽取为一个 _do_pause 方法,仅在基类中调用。

结论:已采纳,最终实现通过覆盖 _pause_complete 实现(基类 pause_scheduler 调用 self._pause_complete)。 · 已解决

风险与影响

新状态机增加复杂性,可能与其他 DP 路径(如弹性 EP)交互异常;all-reduce 每 32 步检查,若暂停请求在间隔内到达,引擎需空转最多 32 步,对延迟敏感场景可能不理想(已留优化空间);当前仅用于 MoE 模型,扩展到非 MoE 需验证;核心路径变更(core.pyparallel.py)需警惕回归。

修复 DPEP 部署中的致命死锁问题,提升稳定性;影响范围限于 data_parallel_size>1is_moe 的场景,普通用户无感知;引入最多 32 步的暂停延迟(可忽略);测试覆盖了并发暂停、barrier、请求的竞态场景,降低回归风险。

核心路径变更 分布式一致性依赖 引入新状态机 条件竞态风险

关联 Issue

#36594 [Bug]: DPEngineCoreProc may re-arm DP wave while paused (START_DP_WAVE ignores pause state), causing collective timeout after pause_generation + collective_rpc

完整报告

参与讨论