Prhub

#7513 [trainer, ckpt, cfg] feat: add config-driven checkpoint callback hook

原始 PR 作者 yueyiming2009 合并时间 2026-08-25 11:41 文件变更 11 提交数 1 评论 4 代码增减 +283 / -4

执行摘要

新增配置驱动的 checkpoint 保存回调钩子

PR body 明确指出,用户需要在不 fork 或子类化 trainer 的前提下获得 checkpoint 副作用的官方扩展点(上传对象存储、注册模型 registry、触发评估、保留策略)。方案刻意对齐 HuggingFace transformersTrainerCallback.on_save 语义。PR 作者也通过搜索确认没有现存 PR/issue 提供 trainer 级 checkpoint callback,并与 rollout.checkpoint_manager_class(权重同步引擎而非磁盘 checkpoint)、v1 trainer 的 on_* 方法(子类覆写点而非配置可插拔)做了区分。

值得精读。设计上有三个可借鉴点:null-object 模式省去调用点空值守卫、fail-fast 语义保证 checkpoint 副作用不静默失败、**kwargs 与 no-op 基类为未来扩展事件预留兼容性。评审中“不碰 deprecated trainer、删冗余 UT”的收敛也值得学习。计划在 verl 上做 checkpoint 上传/注册/评估接入的团队可直接照此实现。

讨论亮点

评审共 4 条评论、2 个主题。wuxibin89 在 verl/trainer/ppo/ray_trainer.py 上留言 “Legacy trainer is deprecated, don't modify it.”,作者回复 “Done — reverted all changes to ray_trainer.py; the callback is now wired into the v1 trainer only”,并同步更新测试与文档;在 tests/trainer/ppo/test_checkpoint_callback_on_cpu.py 上留言 “This UT is not needed.”,作者删除该文件、保留 v1 测试。最终 wuxibin89 给出 APPROVED。

实现拆解

  1. 新增回调基类与工厂verl/trainer/ppo/checkpoint_callback.py 定义 CheckpointCallback,唯一事件为 on_save(trainer, global_step, checkpoint_dir, async_save=False, **kwargs)**kwargs 为向前兼容保留;build_checkpoint_callback(config) 在配置未设置时返回 no-op 实例(null-object 模式),否则通过 load_class_from_fqn 加载用户 FQN 并传入完整配置实例化。
  2. 接入 v1 trainerverl/trainer/ppo/v1/trainer_base.pyPPOTrainer.__init__ 增加 self.checkpoint_callback = build_checkpoint_callback(config),使坏的 FQN 在 Ray 资源分配前的 driver 侧立即失败;_save_checkpoint 末尾在两个路径调用 on_saveasync_save=True 早退路径携带 async_save=True,正常路径在写入 latest_checkpointed_iteration.txt 后携带 async_save=False。钩子显式放在 worker save 成功之后而非 try/finally,保证保存失败时不触发回调。
  3. 配置与生成文件同步verl/trainer/config/ppo_trainer.yaml 新增带注释的 trainer.checkpoint_callback_class: null,四个 _generated_ppo_*_trainer.yaml 通过 scripts/generate_trainer_config.sh 重新生成,保持配置契约一致。
  4. 测试配套:新增 tests/trainer/ppo/v1/test_checkpoint_callback_on_cpu.py,用 _StubTrainer_RecordingCallback 覆盖 4 条路径:worker save 后触发 on_saveasync_save=True 标志传递、worker 保存抛异常时抑制回调、回调异常向上传播中止训练。
  5. 文档更新docs/advance/checkpoint.rst 新增 “Checkpoint Callback” 章节说明钩子契约与异步保存时序,docs/examples/config.rst 补充键参考,docs/extend_guide.rst 加入扩展指南条目。评审后移除对 legacy RayPPOTrainer 及对应 UT 的改动,verl/experimental/fully_async_policy/fully_async_trainer.py 不在覆盖范围。
文件 模块 状态 重要度
verl/trainer/ppo/checkpoint_callback.py 回调机制 added 8.7
tests/trainer/ppo/v1/test_checkpoint_callback_on_cpu.py 单元测试 added 7.49
verl/trainer/ppo/v1/trainer_base.py 训练器 modified 6.26
verl/trainer/config/ppo_trainer.yaml 训练配置 modified 3.93
verl/trainer/config/_generated_ppo_megatron_trainer.yaml 生成配置 modified 2.53
verl/trainer/config/_generated_ppo_torchtitan_trainer.yaml 生成配置 modified 2.53
verl/trainer/config/_generated_ppo_trainer.yaml 生成配置 modified 2.53
verl/trainer/config/_generated_ppo_veomni_trainer.yaml 生成配置 modified 2.53
docs/advance/checkpoint.rst 用户文档 modified 4.03
docs/extend_guide.rst 用户文档 modified 2.92
docs/examples/config.rst 用户文档 modified 2.27

关键符号

CheckpointCallback CheckpointCallback.on_save build_checkpoint_callback PPOTrainer.__init__ PPOTrainer._save_checkpoint

关键源码片段

verl/trainer/ppo/v1/trainer_base.py dependency-wiring

钩子接线点:__init__ 实例化回调,_save_checkpoint 两处调用 on_save,是变更影响的核心训练路径。

actor_ckpt_cfg = self.config.actor_rollout_ref.actor.get("checkpoint", {})
if actor_ckpt_cfg.get("async_save", False):
    # Megatron 异步保存:worker 写盘可能仍在途,跳过迭代 tracker 写入
    logger.info("skip write latest_checkpointed_iteration.txt when async_save is True")
    # 显式传 async_save=True,让回调知道此时不能假设 durable
    self.checkpoint_callback.on_save(
        trainer=self,
        global_step=self.global_steps,
        checkpoint_dir=local_global_step_folder,
        async_save=True,
    )
    returnlocal_latest_checkpointed_iteration = os.path.join(
    self.config.trainer.default_local_dir, "latest_checkpointed_iteration.txt"
)
with open(local_latest_checkpointed_iteration, "w") as f:
    f.write(str(self.global_steps))# 同步路径:迭代 tracker 已落盘,表示保存流程完整走完,此时才触发 on_save
self.checkpoint_callback.on_save(
    trainer=self,
    global_step=self.global_steps,
    checkpoint_dir=local_global_step_folder,
    async_save=False,
)
verl/trainer/config/ppo_trainer.yaml configuration

新增配置契约键 checkpoint_callback_class,文档化注释说明 FQN 与语义。

trainer:
  ...
  # 用户自定义 checkpoint 回调类的全限定名,例如 my_pkg.callbacks.MyCheckpointCallback。
  # driver 侧用完整 trainer config 实例化,每次 checkpoint 保存后调用其 on_save 钩子,
  # 语义与 transformers TrainerCallback.on_save 对齐;null 表示不启用任何回调。
  checkpoint_callback_class: null

评论区精华

Legacy RayPPOTrainer 是否应接入 checkpoint callback 设计

wuxibin89 在 ray_trainer.py 的 diff 上留言 “Legacy trainer is deprecated, don't modify it.”;作者回应称已回退 ray_trainer.py 全部改动,回调只接入 v1 训练器(verl/trainer/ppo/v1/trainer_base.py),测试与文档同步更新。

结论:移除 legacy trainer 改动,特性明确为 v1-only。 · 已解决

legacy trainer 的 UT 是否必要 测试

wuxibin89 在 tests/trainer/ppo/test_checkpoint_callback_on_cpu.py 上留言 “This UT is not needed.”;作者删除该文件,保留 v1 的 tests/trainer/ppo/v1/test_checkpoint_callback_on_cpu.py 覆盖钩子接线。

结论:删除 legacy 测试文件,保留 v1 测试。 · 已解决

风险与影响

  1. _save_checkpoint 位于 v1 训练核心保存路径,任何回调异常都会中止训练:这是有意的 fail-fast 设计(文档与 docstring 已说明),但第三方回调的 bug 会直接影响训练可用性;
  2. async_save=True(Megatron 异步 checkpoint)时 on_save 在 worker 写入可能仍在途、latest_checkpointed_iteration.txt 未写时触发,用户若将其当作“已持久化”信号可能导致副作用于不可靠数据;
  3. 仅 v1 trainer 接入,legacy RayPPOTrainer 与 fully_async trainer 不触发钩子,用户跨后端迁移时行为不一致;
  4. 新增配置键需要与 4 个生成配置保持同步,后续手工编辑生成文件可能失配。

对用户:新增官方扩展点,默认 null 无行为变化,存量配置完全兼容;对系统:每次 checkpoint 保存后 driver 侧多一次回调调用,性能开销可忽略,但回调的 I/O 会叠加在保存路径上;对团队:确立了 trainer 级 checkpoint 副作用的插入规范,未来可在不破坏子类的前提下扩展 load 等事件;对文档:config.rst、checkpoint.rst、extend_guide.rst 三处同步,配置契约清晰。

核心保存路径变更 异常中断训练 异步保存时序 仅 v1 覆盖

关联 Issue

#6537 [RFC] Generic `RemoteBackend` abstraction for out-of-process RL backends
#7363 [ckpt, sft, megatron] fix: finalize Megatron async checkpoint queue on every SFT rank
#7396 [ckpt, fsdp] fix: enforce max-one SFT retention after restart
#7402 [ckpt, trainer] fix: resume SFT after epoch checkpoints

完整报告

参与讨论