Skip to content

support checkpoint engine[P2P] recovery in colocate mode - #2027

Merged
PengchengShi00 merged 16 commits into
InternLM:mainfrom
PengchengShi00:checkpoint-engine-recovery
Sep 2, 2026
Merged

support checkpoint engine[P2P] recovery in colocate mode#2027
PengchengShi00 merged 16 commits into
InternLM:mainfrom
PengchengShi00:checkpoint-engine-recovery

Conversation

@PengchengShi00

Copy link
Copy Markdown
Collaborator

No description provided.

@PengchengShi00

Copy link
Copy Markdown
Collaborator Author

Checkpoint-Engine P2P 推理引擎故障恢复

在共卡 RL 训练中,rollout worker 负责启动推理引擎并处理生成请求。若某个推理引擎进程异常退出,原有流程会由 RolloutHealthManager 检测失败并重启对应 worker group。
本次故障恢复的目标是:当失败的推理引擎被重启后,不等待本轮 rollout 完全收尾,而是先把该 worker group 标记为 pending_weights,再由训练侧后台线程立即使用 checkpoint-engine 中已注册的权重,通过 P2P 方式把权重推送到恢复后的推理引擎,最后将其重新标记为 active

WorkerLifecycleState新增状态PENDING_WEIGHTS

恢复后的 rollout worker group 不会立刻进入 active,而是先进入 pending_weights。该状态表示:

  • 推理服务已经重启并通过健康检查;
  • 当前服务还没有加载到训练侧最新权重。

checkpoint-engine P2P 更新 pending 目标

RLColocateTrainer 新增了 _update_pending_rollout_weights_from_checkpoint_engine() 路径,用于:

1.. 从 RolloutController 获取 pending_weights 的更新目标;
2.. 调用 bind_rollout_weight_update() 绑定这些 pending target;
3. 调用 weight_update(..., update_pending_only=True) 只更新 pending worker;
4. 调用 pending worker 的 onload_weightsonload_kvcache
5. 更新完成后把对应 group 标记回 active
这样可以避免对所有 active rollout worker 做重复广播,只对刚恢复的失败引擎执行 P2P 权重恢复。

后台监控线程

在 checkpoint-engine 模式下,RLColocateTrainer 会在 rollout 阶段启动后台线程,周期性检查是否存在 pending_weights worker group。

该线程会在满足以下条件时触发恢复:

  • rollout 权重更新锁没有被主流程占用;
  • checkpoint-engine 中已经注册过训练权重;
  • 存在 pending rollout weight-update targets。

权重同步语义

  • update_pending_only=False:正常训练同步,更新所有可更新 rollout target;
  • update_pending_only=True:故障恢复同步,只更新 pending_weights target。

@PengchengShi00
PengchengShi00 force-pushed the checkpoint-engine-recovery branch from 4f909e0 to 5b31599 Compare August 21, 2026 15:48
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

1 similar comment
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 为 colocate 模式引入 checkpoint-engine P2P 故障恢复:新增 WorkerLifecycleState.PENDING_WEIGHTS 状态、RolloutController 上的状态迁移/pending target 查询接口、健康检查线程内的即时 restart,以及 RLColocateTrainer 侧的后台 pending 权重推送线程。方向合理,但恢复闭环中存在若干会导致恢复失效或影响非 checkpoint-engine 路径的问题。

ProduceBatchResult impact: RolloutController.generate 新增宽泛 except Exception 会把此前向上抛出的 worker 异常转成终态 Status.FAILED,直接改变 leftover_failedproduced_samples/produced_tokens 计数;同时 run_once 内同步 restart 会在 rollout 期间占用健康检查线程,可能抬高 group_gen_* 时延统计。

RoutedExperts impact: 新增的 generate 异常兜底把失败请求转为 Status.FAILED 并直接返回入参 rollout_state,该路径不获取 LMDeploy routed-experts object refs,对已转移的 refs 也不新增持有;未发现 ownership 变化。

Ray concurrency impact: 有效分组仍为 {generate: ROLLOUT_RAY_GENERATE_MAX_CONCURRENCY} + default;新增的 RolloutWorker.inject_backend_crash_for_test 是落在 default 组的同步阻塞方法(内部 ray.get(server_task, timeout=60)parent.wait),执行期间会占用 default 组槽位,可能延迟 pause/abort/health 等控制 RPC。

Issues

Critical

  • [xtuner/v1/train/rl_trainer.py _sync_weights_and_save / _update_pending_rollout_weights_from_checkpoint_engine] 用 (target.endpoint_rank,) 当作 lifecycle group ranks,与多节点 engine 的真实 group ranks(如 (0, 1))不一致,mark_worker_groups_lifecycle_state 会静默跳过,恢复后的 worker 永久停留在 PENDING_WEIGHTS 而不再接流量。
  • [xtuner/v1/rl/weight_update/data.py:143,177-183xtuner/v1/rl/weight_update/transport.py:480] 删除 is_active/active_update_targets 过滤后,NCCL 建组的 nccl_engine_infos 与 IPC send() 会把已 INACTIVE 的 endpoint 计入 world_size 并向死 URL 发请求,影响与本次恢复无关的 nccl/ipc 传输路径。
  • [xtuner/v1/rl/weight_update/transport.py register_checkpoint_from_train_engine] register_checkpoint 失败被 except Exception 吞掉后仍赋值 self._checkpoint_name,使 has_registered_checkpoint() 误报成功并让后续 gather_metas/update 推送未注册的 checkpoint(该 error 日志还漏了 f 前缀)。
  • [xtuner/v1/rl/weight_update/transport.py _update_engines] targets 改用全部 update_targets,sync-step 路径下重启失败的 INACTIVE endpoint 也会参与 _get_target_update_ranksreq_func,导致误判 broadcast 并向已失效 server 发起更新。

Warning

  • [xtuner/v1/rl/rollout/health_manager.py run_once] 在健康检查线程内同步执行 _restart_inactive_workers(),restart 期间健康检查完全停摆(代码 TODO 已自述),失败检测延迟不再受 health_check_interval_seconds 约束。
  • [xtuner/v1/rl/rollout/controller.py generate] 新增的 except Exception 把所有 worker 侧异常统一降级为 Status.FAILED,掩盖了原本可区分/可重试的失败并改变批次状态计数。
  • [xtuner/v1/rl/rollout/worker_registry.py get_target_state_worker_groups / set_groups_state] group 匹配用 any()、状态迁移按 rank 过滤 source_state,混合状态的 group 会被整体判定并提升为 ACTIVE,随后 notify_worker_group_recovered 把 entrypoint 重新注册进路由。
  • [xtuner/v1/rl/rollout/worker.py inject_backend_crash_for_testxtuner/v1/rl/rollout/controller.py inject_backend_crash_for_test] 生产 Module 的公开 Interface 上新增了可远程调用的崩溃注入后门(仅靠环境变量兜底),并把测试语义([recovery-test][ImmediateRecoveryExperiment] 日志)留在生产路径。
  • [xtuner/v1/rl/agent_loop_manager/producer.py:236,343,444] 新增的 max_pending_tasks 公开配置与本 PR 的恢复主题无关,违反 .claude/CLAUDE.md 的「一个 PR 一个逻辑变更」。

Main Flowchart after this PR

flowchart TD
    A[RLColocateTrainer._fit] --> B[_rollout_resources_available.set]
    B --> C[_start_check_pending_rollout_worker_thread]
    C --> D[produce_batch]
    D --> E{rollout worker 崩溃?}
    E -- 是 --> F[HealthManager.run_once: mark INACTIVE]
    F --> G[run_once 内同步 _restart_inactive_workers]
    G --> H[set_groups_state -> PENDING_WEIGHTS]
    H --> I[后台线程 _update_pending_rollout_weights_from_checkpoint_engine]
    I --> J[bind pending targets + weight_update need_update]
    J --> K[mark_worker_groups_lifecycle_state PENDING->ACTIVE]
    E -- 否 --> L[_sync_weights_and_save]
    L --> M[restart_inactive_workers + bind_train_rollout]
    M --> N[weight_update: update_targets 含 INACTIVE]
    N --> O[mark pending -> ACTIVE]
    D --> P[controller.generate except Exception -> FAILED]

    classDef changed fill:#dff0d8,stroke:#3c763d
    classDef problem fill:#f2dede,stroke:#a94442,stroke-width:2px
    class B,C,H,I,J,L changed
    class G,K,N,O,P problem
Loading

核心原理实现与单测

  • 恢复闭环的核心是三段状态机:INACTIVE → RECOVERING → PENDING_WEIGHTS → ACTIVE。前两段由 _restart_claimed_recovery_groups 完成并有 test_restart_barrier_marks_recovered_group_pending_weights_after_successtest_pending_weights_listener_runs_outside_lifecycle_operation_lock 覆盖;PENDING_WEIGHTS → ACTIVE 这一段(也是本 PR 最关键的一段)只有 test_registry_sets_groups_state_with_source_filter 覆盖 registry 层,RolloutController.mark_worker_groups_lifecycle_state 与 trainer 侧 rank tuple 的构造方式没有任何真实代码路径覆盖,上述 Critical 的 group ranks 不匹配问题正好落在这个缺口里。
  • checkpoint-engine 侧的 target 选择(update_targets 取代 active_update_targets)、has_registered_checkpoint() 语义、_check_checkpoint_engine_p2p_available() 均无单测;tests/rl/test_qwen35_vl_moe_recover_e2e.py 是唯一端到端验证,但依赖 8 卡与 QWEN3_5_MOE_PATH/GEO3K_* 环境,CI 常态下不会执行。
  • tests/rl/test_rl_colocate_trainer.py 只是给 __new__ 出来的 trainer 补齐新增的 threading 字段并把 weight_transport_type 固定为 ipc,因此没有覆盖任何 checkpoint-engine 恢复分支。

抽象与信息隐藏评估

  • Filesxtuner/v1/rl/rollout/controller.pyworker.pyworker_registry.pyxtuner/v1/train/rl_trainer.py
  • Problem:registry 的状态机细节(source/target state、group ranks 元组)被抬到 RolloutController 和 trainer 两层之上,trainer 需要自己拼 group ranks 才能完成一次状态迁移,规则被拆散到三个 Module;同时 RolloutWorker/RolloutController 的公开 Interface 上新增了崩溃注入后门与 [recovery-test] 日志,测试关注点泄漏进生产 Implementation。
  • Solution:把「恢复后的 group 推完权重就转 ACTIVE,失败就转 INACTIVE」这条规则收进 RolloutController(或 health manager)的一个方法内,只对 trainer 暴露「更新 pending 并返回结果」这一个更小的 Interface;崩溃注入改为测试侧子类或独立 test-only actor 方法,恢复日志去掉 test 标记。
  • Benefits:group ranks 与状态机不变量只在 registry/controller 一处维护(Locality),trainer 不再因为 rank tuple 拼错而静默丢掉恢复(Leverage),且状态迁移可以在不启动 8 卡 e2e 的情况下通过 controller 公开 Interface 测试。

公开 Interface 的线性业务流程评估

  • Filesxtuner/v1/train/rl_trainer.py _sync_weights_and_save
  • Problem:checkpoint-engine 分支内混入了 pending target 查询、rank tuple 拼装、按状态两次 onload_weights、状态回写与日志,抽象层级从「同步权重」下降到「registry 状态迁移」,主流程不再是可读的线性叙述,也使上面的 rank tuple 缺陷难以被发现。
  • Solution:主流程保留 restart → bind → sync → promote_recovered 四步,把 pending 相关细节下沉为一个私有方法(与后台线程复用同一实现),避免同一规则在 sync-step 与后台线程中各写一份。
  • Benefits:两条恢复路径共享同一份规则与不变量,主流程可读性与可测试性同时提升。

单测建议

  • PENDING_WEIGHTS → ACTIVE 缺少跨 RolloutController 公开 Interface 的用例:建议用多节点 lifecycle group(group ranks 与 weight-update endpoint rank 不同,如 sglang nnodes>1)驱动 mark_worker_groups_lifecycle_state,断言 group 真的变为 ACTIVE,这可直接暴露上面的 Critical。
  • checkpoint-engine 的 target 选择与 has_registered_checkpoint() 建议补纯 CPU 单测:包含「存在 INACTIVE target」和「register_checkpoint 抛异常」两种输入,断言不会向 INACTIVE endpoint 发更新、且注册失败后不报告已注册。
  • tests/rl/test_qwen35_vl_moe_recover_e2e.py 依赖 8 GPU 与外部数据集,建议加 @pytest.mark.gpu 标记,避免在无 GPU 环境被当作常规用例执行而失败。

其他 Issues

  • Warning [xtuner/v1/rl/weight_update/update_weighter.py has_registered_weight_checkpoint]:用 getattr(transport, "has_registered_checkpoint", None) 做鸭子类型探测,非 checkpoint-engine transport 会静默返回 False,建议按 transport 类型显式判断以免掩盖误配置。

Verdict

REQUEST_CHANGES

Comment thread xtuner/v1/train/rl_trainer.py Outdated
Comment thread xtuner/v1/rl/weight_update/data.py
Comment on lines +1025 to 1031
try:
self._ps.register_checkpoint(name, files=[], named_tensors=shard, use_shared_memory_pool=True)
if self._sync_after_register:
DEVICE_MODULE.synchronize()
except Exception:
self.logger.error("[checkpoint_engine] register_checkpoint failed rank={self.rank} name={name}")
self._checkpoint_name = name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] register_checkpoint 抛异常后仍执行 self._checkpoint_name = name,导致 has_registered_checkpoint() 误报成功,后台恢复线程会用未注册的 checkpoint 调 gather_metas/update。另外这条 error 日志缺 f 前缀,会原样打印 {self.rank}。建议异常时不设置 _checkpoint_name 并向上抛出。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个也要修一下

Comment thread xtuner/v1/rl/weight_update/transport.py
Comment thread xtuner/v1/rl/weight_update/transport.py
Comment thread xtuner/v1/rl/rollout/health_manager.py Outdated
Comment thread xtuner/v1/rl/rollout/controller.py Outdated
Comment on lines +145 to +149
except Exception as e:
self.logger.exception(f"RolloutController.generate failed: session_id={session_id}")
rollout_state.status = Status.FAILED
rollout_state.error_msg = f"Rollout request failed: {type(e).__name__}: {str(e)[:1024]}"
return rollout_state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 兜底 except Exception 把所有 worker 异常统一降级为终态 Status.FAILED,原本可区分/可重试的失败会被掩盖。ProduceBatchResult impact: 直接改变 leftover_failedproduced_samples/produced_tokens 计数。建议只捕获明确可归类为 FAILED 的异常类型。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里增加这个except的出发点是啥?如果是rollout worker出现了问题,会在rollout worker内部的完成故障处理,将返回的rollout state的status标为FAILED。那么从rollout worker到rollout controller的返回就不应该会有其他的报错了

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这儿加except是因为,在训练时,如果有挂掉的推理引擎,开partial rollout的情况,这里并不会立即把请求转给其他worker,反而是一直在重试,rollout_state.error_msg 这个会很大,然后一直返回这个很大的信息,然后机器就会崩掉。

Comment on lines +134 to +144
def get_target_state_worker_groups(self, target_state: WorkerLifecycleState) -> tuple[WorkerGroup, ...]:
"""Return lifecycle groups containing workers in the requested
state."""
with self._lock:
worker_groups = self._build_worker_groups()
matched_groups = [
group
for group in worker_groups.values()
if any(worker.lifecycle_state is target_state for worker in group.workers)
]
return tuple(sorted(matched_groups, key=lambda group: group.ranks))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] group 用 any() 匹配、set_groups_state 按 rank 过滤 source_state:只要 group 内有一个 rank 是 PENDING_WEIGHTS 就整组被选中,而组内仍为 INACTIVE 的 rank 保持不变,随后整组被 notify_worker_group_recovered 重新注册进路由,entrypoint 会收到实际不可用的流量。建议整组状态一致才允许迁移。

Comment thread xtuner/v1/rl/rollout/worker.py
Comment thread xtuner/v1/rl/rollout/controller.py Outdated
Comment on lines +145 to +149
except Exception as e:
self.logger.exception(f"RolloutController.generate failed: session_id={session_id}")
rollout_state.status = Status.FAILED
rollout_state.error_msg = f"Rollout request failed: {type(e).__name__}: {str(e)[:1024]}"
return rollout_state

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里增加这个except的出发点是啥?如果是rollout worker出现了问题,会在rollout worker内部的完成故障处理,将返回的rollout state的status标为FAILED。那么从rollout worker到rollout controller的返回就不应该会有其他的报错了


def _broadcast_to_active_workers(self, method_name: str, **kwargs):
workers = self.registry.active_workers()
def _broadcast_to_workers(self, method_name: str, target_state: WorkerLifecycleState, **kwargs):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个函数为啥要修改?看上去调用点还是 target_state=WorkerLifecycleState.ACTIVE

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里是因为增加了pending状态,所有给active worker的特性函数,比如onload_weights、onload_kvcache等都需要再给pending状态再写一遍,这里加了一个状态参数,这样就可以使用同一个函数,传递不同的参数控制对哪些状态的worker进行操作了

Comment thread xtuner/v1/rl/rollout/health_manager.py
Comment thread xtuner/v1/rl/trainer/controller.py
@PengchengShi00

Copy link
Copy Markdown
Collaborator Author

@claude review

@PengchengShi00
PengchengShi00 merged commit 8cdebaa into InternLM:main Sep 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants