执行摘要
- 一句话:新增配置驱动的 checkpoint 保存回调钩子
- 推荐动作:值得精读。设计上有三个可借鉴点:null-object 模式省去调用点空值守卫、fail-fast 语义保证 checkpoint 副作用不静默失败、
**kwargs 与 no-op 基类为未来扩展事件预留兼容性。评审中“不碰 deprecated trainer、删冗余 UT”的收敛也值得学习。计划在 verl 上做 checkpoint 上传/注册/评估接入的团队可直接照此实现。
功能与动机
PR body 明确指出,用户需要在不 fork 或子类化 trainer 的前提下获得 checkpoint 副作用的官方扩展点(上传对象存储、注册模型 registry、触发评估、保留策略)。方案刻意对齐 HuggingFace transformers 的 TrainerCallback.on_save 语义。PR 作者也通过搜索确认没有现存 PR/issue 提供 trainer 级 checkpoint callback,并与 rollout.checkpoint_manager_class(权重同步引擎而非磁盘 checkpoint)、v1 trainer 的 on_* 方法(子类覆写点而非配置可插拔)做了区分。
实现拆解
- 新增回调基类与工厂:
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 并传入完整配置实例化。
- 接入 v1 trainer:
verl/trainer/ppo/v1/trainer_base.py 中 PPOTrainer.__init__ 增加 self.checkpoint_callback = build_checkpoint_callback(config),使坏的 FQN 在 Ray 资源分配前的 driver 侧立即失败;_save_checkpoint 末尾在两个路径调用 on_save:async_save=True 早退路径携带 async_save=True,正常路径在写入 latest_checkpointed_iteration.txt 后携带 async_save=False。钩子显式放在 worker save 成功之后而非 try/finally,保证保存失败时不触发回调。
- 配置与生成文件同步:
verl/trainer/config/ppo_trainer.yaml 新增带注释的 trainer.checkpoint_callback_class: null,四个 _generated_ppo_*_trainer.yaml 通过 scripts/generate_trainer_config.sh 重新生成,保持配置契约一致。
- 测试配套:新增
tests/trainer/ppo/v1/test_checkpoint_callback_on_cpu.py,用 _StubTrainer 与 _RecordingCallback 覆盖 4 条路径:worker save 后触发 on_save、async_save=True 标志传递、worker 保存抛异常时抑制回调、回调异常向上传播中止训练。
- 文档更新:
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(模块 回调机制;类别 source;类型 core-logic;符号 CheckpointCallback, init, on_save, build_checkpoint_callback): 本 PR 核心:新增 CheckpointCallback 基类与 build_checkpoint_callback 工厂,null-object + load_class_from_fqn 模式。
tests/trainer/ppo/v1/test_checkpoint_callback_on_cpu.py(模块 单元测试;类别 test;类型 test-coverage;符号 _StubTrainer, _RecordingCallback, _make_trainer, test_save_fires_on_save_after_worker_save): 覆盖 v1 训练器钩子接线的 4 条关键路径,验证事件顺序与异常语义。
verl/trainer/ppo/v1/trainer_base.py(模块 训练器;类别 source;类型 dependency-wiring;符号 PPOTrainer.init, PPOTrainer._save_checkpoint): 钩子接线点:init 实例化回调,_save_checkpoint 两处调用 on_save,是变更影响的核心训练路径。
verl/trainer/config/ppo_trainer.yaml(模块 训练配置;类别 config;类型 configuration): 新增配置契约键 checkpoint_callback_class,文档化注释说明 FQN 与语义。
verl/trainer/config/_generated_ppo_megatron_trainer.yaml(模块 生成配置;类别 config;类型 regenerated-config): 由脚本重新生成的训练配置,携带新配置键,保证 Megatron 训练入口契约一致。
verl/trainer/config/_generated_ppo_torchtitan_trainer.yaml(模块 生成配置;类别 config;类型 regenerated-config): 由脚本重新生成的训练配置,携带新配置键,保证 TorchTitan 训练入口契约一致。
verl/trainer/config/_generated_ppo_trainer.yaml(模块 生成配置;类别 config;类型 regenerated-config): 由脚本重新生成的训练配置,携带新配置键,保证默认 PPO 训练入口契约一致。
verl/trainer/config/_generated_ppo_veomni_trainer.yaml(模块 生成配置;类别 config;类型 regenerated-config): 由脚本重新生成的训练配置,携带新配置键,保证 VeOmni 训练入口契约一致。
docs/advance/checkpoint.rst(模块 用户文档;类别 docs;类型 documentation;符号 MyCheckpointCallback, on_save): 新增 Checkpoint Callback 章节,完整定义 on_save 钩子契约、异步保存时序与异常语义。
docs/extend_guide.rst(模块 用户文档;类别 docs;类型 documentation): 扩展指南新增条目,引导用户通过 checkpoint callback 扩展训练器行为。
docs/examples/config.rst(模块 用户文档;类别 docs;类型 documentation): 配置参考文档同步新增 checkpoint_callback_class 键说明。
关键符号:CheckpointCallback, CheckpointCallback.on_save, build_checkpoint_callback, PPOTrainer.init, PPOTrainer._save_checkpoint
关键源码片段
verl/trainer/ppo/v1/trainer_base.py
钩子接线点: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,
)
return
local_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
新增配置契约键 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
评论区精华
评审共 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。
- Legacy RayPPOTrainer 是否应接入 checkpoint callback (design): 移除 legacy trainer 改动,特性明确为 v1-only。
- legacy trainer 的 UT 是否必要 (testing): 删除 legacy 测试文件,保留 v1 测试。
风险与影响
- 风险:
_save_checkpoint 位于 v1 训练核心保存路径,任何回调异常都会中止训练:这是有意的 fail-fast 设计(文档与 docstring 已说明),但第三方回调的 bug 会直接影响训练可用性;
async_save=True(Megatron 异步 checkpoint)时 on_save 在 worker 写入可能仍在途、latest_checkpointed_iteration.txt 未写时触发,用户若将其当作“已持久化”信号可能导致副作用于不可靠数据;
- 仅 v1 trainer 接入,legacy
RayPPOTrainer 与 fully_async trainer 不触发钩子,用户跨后端迁移时行为不一致;
- 新增配置键需要与 4 个生成配置保持同步,后续手工编辑生成文件可能失配。
- 影响:对用户:新增官方扩展点,默认 null 无行为变化,存量配置完全兼容;对系统:每次 checkpoint 保存后 driver 侧多一次回调调用,性能开销可忽略,但回调的 I/O 会叠加在保存路径上;对团队:确立了 trainer 级 checkpoint 副作用的插入规范,未来可在不破坏子类的前提下扩展 load 等事件;对文档:config.rst、checkpoint.rst、extend_guide.rst 三处同步,配置契约清晰。
- 风险标记:核心保存路径变更, 异常中断训练, 异步保存时序, 仅 v1 覆盖
关联脉络
- PR #7363 [ckpt, sft, megatron] fix: finalize Megatron async checkpoint queue on every SFT rank: PR body 列为最接近的 checkpoint bug 修复之一,同属 checkpoint 生命周期完善;其异步保存完成语义与回调的 async_save 时序直接相关。
- PR #7396 [ckpt, fsdp] fix: enforce max-one SFT retention after restart: PR body 列为最接近的 checkpoint bug 修复之一,同属 checkpoint 生命周期与保留策略完善。
- PR #7402 [ckpt, trainer] fix: resume SFT after epoch checkpoints: PR body 列为最接近的 checkpoint bug 修复之一,同属 checkpoint 生命周期完善。
- PR #6537 [RFC] Generic RemoteBackend abstraction for out-of-process RL backends: PR body 论证本特性与 RemoteBackend RFC 不重复,两者同属 verl 可扩展性设计方向,一个在训练器内部扩展点,一个在后端抽象。
- PR #7536 [cfg] fix: drop unused ref router replay config: 同为 trainer 配置变更并同步重新生成 generated_ppo*_trainer.yaml,走同一配置生成链路。
参与讨论