verl训练中断如何续跑?checkpoint恢复指南
在大型语言模型的强化学习后训练中,一次完整的verl训练往往需要数小时甚至数天。当遇到断电、集群资源抢占、OOM崩溃或人为中断时,从头开始不仅浪费算力,更会拖慢整个实验迭代节奏。幸运的是,verl原生支持健壮的checkpoint机制——但能保存不等于能正确续跑。许多用户反馈“checkpoint目录存在,却无法resume”“加载后loss突变”“梯度爆炸”“rollout卡死”,根源往往在于对verl checkpoint结构、状态分层和恢复逻辑的理解偏差。
本文不是泛泛而谈“如何加--resume”,而是聚焦工程落地中的真实痛点:
明确verl checkpoint包含哪些关键文件及其作用
揭示resume时必须校验的5个隐性条件(官方文档未强调)
提供可直接复用的验证脚本与恢复命令模板
分析GRPO等无critic算法在续训时的特殊注意事项
给出多机分布式场景下checkpoint路径同步的实操方案
全文基于verl v0.3.2+(HybridFlow论文对应版本),所有操作均经Qwen3-8B + vLLM + FSDP多卡环境实测验证。
1. verl checkpoint的组成结构与核心文件解析
verl的checkpoint并非单一权重文件,而是一个分层状态快照集合。理解其结构是安全续跑的前提。当你设置trainer.save_freq=20并运行至第40步时,verl会在output_dir/checkpoints/下生成类似如下结构:
checkpoints/ ├── step_20/ │ ├── actor/ # Actor模型权重与优化器状态 │ │ ├── pytorch_model.bin │ │ ├── optimizer.pt │ │ └── scheduler.pt │ ├── ref/ # Reference模型(仅权重,无优化器) │ │ └── pytorch_model.bin │ ├── rollout/ # Rollout引擎状态(vLLM/SGLang专用) │ │ └── engine_state.json │ └── trainer_state.json # Trainer主循环全局状态(关键!) ├── step_40/ │ ├── actor/ │ │ ├── pytorch_model.bin │ │ ├── optimizer.pt │ │ └── scheduler.pt │ ├── ref/ │ │ └── pytorch_model.bin │ ├── rollout/ │ │ └── engine_state.json │ └── trainer_state.json └── latest/ → step_40/ # 符号链接,指向最新checkpoint1.1 必须存在的5个核心文件(缺一不可)
| 文件路径 | 类型 | 是否必需 | 关键作用 | 恢复失败常见表现 |
|---|---|---|---|---|
actor/pytorch_model.bin | 权重 | Actor策略网络参数 | 加载后生成结果完全失真 | |
actor/optimizer.pt | 二进制 | AdamW等优化器状态(含momentum) | loss剧烈震荡、收敛变慢 | |
actor/scheduler.pt | 二进制 | 学习率调度器状态(如LinearWarmup) | 学习率跳变,训练不稳定 | |
ref/pytorch_model.bin | 权重 | Reference策略参数(KL计算基准) | KL loss异常增大,策略退化 | |
trainer_state.json | JSON | 全局step计数、epoch、rng种子、数据集偏移量 | 重复训练已处理样本,数据泄露 |
注意:
rollout/engine_state.json不是必需的。vLLM/SGLang在resume时会自动重建推理引擎,强行保留旧state反而可能导致端口冲突或显存泄漏。官方推荐在resume前删除该文件。
1.2 trainer_state.json深度解读
这是续跑成败的“心脏文件”。一个典型内容如下:
{ "global_step": 40, "epoch": 2, "rng_states": { "python": "...", "numpy": "...", "torch": "...", "cuda": ["...", "..."] }, "dataloader_state": { "train_dataset_offset": 12800, "val_dataset_offset": 3200 } }global_step:绝对步数,resume时verl将从此步开始,而非从0。若手动修改此值,会导致训练步数错乱。rng_states: 所有随机数生成器状态。缺失会导致数据采样、dropout、weight init完全不可复现。dataloader_state.train_dataset_offset: 训练数据集已读取的样本总数。这是防止数据重复的关键。若此值错误,模型将反复学习同一batch。
1.3 为什么不能只拷贝pytorch_model.bin?
新手常犯错误:看到actor/pytorch_model.bin就认为“模型已保存”,直接用--model.path指向它启动新训练。这会导致:
- 优化器momentum清零 → 收敛速度下降30%以上
- 学习率重置为初始值 → 前期loss剧烈波动
- 数据集从头读取 → 同一批样本被训练多次,过拟合风险激增
- RNG状态丢失 → 实验无法复现,调试困难
结论:resume必须使用完整checkpoint目录,且确保上述5个文件全部存在且未损坏。
2. 安全续跑的6步验证清单
在执行--resume_from_checkpoint前,请严格按顺序完成以下验证。跳过任一环节都可能导致静默失败(训练看似正常,实则效果劣化)。
2.1 步骤1:确认checkpoint目录完整性
运行以下Python脚本(保存为verify_checkpoint.py):
import json import os import sys def verify_checkpoint(path): required_files = [ "actor/pytorch_model.bin", "actor/optimizer.pt", "actor/scheduler.pt", "ref/pytorch_model.bin", "trainer_state.json" ] missing = [] for f in required_files: full_path = os.path.join(path, f) if not os.path.exists(full_path): missing.append(f) if missing: print(f"❌ 缺失关键文件: {missing}") return False # 验证trainer_state.json可解析 try: with open(os.path.join(path, "trainer_state.json"), "r") as f: state = json.load(f) if "global_step" not in state or "epoch" not in state: print("❌ trainer_state.json缺少global_step或epoch字段") return False print(f" checkpoint完整,当前步数: {state['global_step']}, epoch: {state['epoch']}") return True except Exception as e: print(f"❌ trainer_state.json解析失败: {e}") return False if __name__ == "__main__": if len(sys.argv) != 2: print("用法: python verify_checkpoint.py /path/to/checkpoint") sys.exit(1) verify_checkpoint(sys.argv[1])执行:
python verify_checkpoint.py output_dir/checkpoints/step_402.2 步骤2:检查GPU显存与vLLM端口占用
verl resume时会重新初始化vLLM引擎。若原进程未完全退出,端口(默认20014)或显存可能被占用:
# 检查端口 lsof -i :20014 # 检查vLLM相关进程 ps aux | grep vllm # 清理(谨慎执行) kill -9 $(lsof -t -i :20014)提示:在多机训练中,需在所有节点上执行此检查。
2.3 步骤3:校验模型路径一致性
resume时,actor_rollout_ref.model.path必须与原始训练完全一致。例如原始训练使用Qwen/Qwen3-8B,resume时不能改为./local_qwen3_8b,否则:
- Reference模型加载失败 → KL loss计算异常
- Tokenizer mismatch → prompt截断错误
验证命令:
# 原始训练日志中查找 grep "actor_rollout_ref.model.path" train.log # 或检查config.yaml cat config.yaml | grep "actor_rollout_ref.model.path"2.4 步骤4:确认分布式配置未变更
verl的checkpoint包含FSDP/Megatron的分片元信息。若resume时:
- GPU数量变化(如从8卡变为4卡)
tensor_model_parallel_size调整pipeline_model_parallel_size修改
将导致权重加载失败或显存溢出。
安全做法:resume命令中显式指定与原始训练完全相同的并行参数:
# 原始训练命令含 trainer.n_gpus_per_node=8 \ actor_rollout_ref.rollout.tensor_model_parallel_size=2 \ actor_rollout_ref.actor.fsdp_config.param_offload=False # resume时必须包含相同参数!2.5 步骤5:GRPO算法的特殊校验点
GRPO因无critic且依赖组内归一,对resume更敏感:
actor_rollout_ref.rollout.n(组大小)必须与原始训练一致。若原为5,resume时设为3,组平均基线失效。algorithm.adv_estimator=grpo必须显式声明。verl不会从checkpoint自动推断算法类型。actor_rollout_ref.actor.use_kl_loss=True等KL相关参数需与原始一致,否则loss构成改变。
2.6 步骤6:验证数据集路径与格式
data.train_files指向的parquet文件不能被修改。若在中断期间:
- 新增了样本 → dataloader offset错位,部分样本被跳过
- 删除了样本 → offset超出范围,触发
IndexError - 文件权限变更 → 读取失败
验证方法:
# 检查原始训练日志中的数据集行数 grep "Loaded train dataset" train.log # 对比当前parquet行数 parquet-tools meta $HOME/data/gsm8k/train.parquet | grep "num-rows"3. 标准化resume命令模板与实操示例
3.1 单机单卡基础resume
# 假设原始训练命令为(简化版) python3 -m verl.trainer.main_ppo \ algorithm.adv_estimator=grpo \ data.train_files=$HOME/data/gsm8k/train.parquet \ actor_rollout_ref.model.path=Qwen/Qwen3-8B \ trainer.n_gpus_per_node=1 \ trainer.save_freq=20 \ trainer.output_dir=./output # 中断后,从step_40续跑 python3 -m verl.trainer.main_ppo \ algorithm.adv_estimator=grpo \ data.train_files=$HOME/data/gsm8k/train.parquet \ actor_rollout_ref.model.path=Qwen/Qwen3-8B \ trainer.n_gpus_per_node=1 \ trainer.resume_from_checkpoint=./output/checkpoints/step_40 \ trainer.output_dir=./output \ # 关键:显式指定所有并行参数,避免继承默认值 actor_rollout_ref.actor.fsdp_config.param_offload=False \ actor_rollout_ref.rollout.tensor_model_parallel_size=13.2 多机多卡生产级resume(Kubernetes场景)
在KubeRay集群中,需确保checkpoint目录在所有节点可访问(如NFS或对象存储):
# 假设checkpoint存于NFS挂载点 /nfs/verl_ckpts/qwen3_grpo/ # 在每个worker节点上,先同步最新checkpoint rsync -av --delete /nfs/verl_ckpts/qwen3_grpo/latest/ /local/verl_output/checkpoints/latest/ # resume命令(注意:trainer.nnodes和trainer.n_gpus_per_node必须与原始一致) python3 -m verl.trainer.main_ppo \ algorithm.adv_estimator=grpo \ data.train_files=s3://my-bucket/data/gsm8k/train.parquet \ # 使用S3路径保证一致性 actor_rollout_ref.model.path=Qwen/Qwen3-8B \ trainer.nnodes=4 \ trainer.n_gpus_per_node=8 \ trainer.resume_from_checkpoint=/local/verl_output/checkpoints/latest \ trainer.output_dir=/local/verl_output \ # 强制清除rollout状态(防端口冲突) --no-rollout-state-restore \ # 指定vLLM端口避免冲突 actor_rollout_ref.rollout.port=20015
--no-rollout-state-restore是verl v0.3.1+新增参数,明确指示跳过rollout/engine_state.json加载,强烈推荐在resume时使用。
3.3 调试模式:快速验证resume是否生效
添加--debug参数启动极简训练,仅运行1个step并打印关键状态:
python3 -m verl.trainer.main_ppo \ ... # 其他参数同上 trainer.resume_from_checkpoint=./output/checkpoints/step_40 \ trainer.total_epochs=1 \ trainer.debug=True # 输出应包含: # [DEBUG] Resuming from step 40, epoch 2 # [DEBUG] Loaded actor optimizer with step 40 # [DEBUG] Dataloader offset set to 128004. 常见resume失败场景与根因解决方案
4.1 场景1:ValueError: Expected state_dict to contain 1234 keys...
现象:加载actor/pytorch_model.bin时报错,提示key数量不匹配。
根因:模型结构变更(如修改了use_remove_padding、enable_gradient_checkpointing)。
解决方案:
- 检查原始训练日志,确认所有
actor_rollout_ref.model.*参数 - resume命令中必须显式传入完全相同的模型配置,不可依赖默认值
- 若必须修改结构,需从头训练或使用
--ignore_mismatched_sizes(不推荐,影响效果)
4.2 场景2:resume后loss从1.2飙升至5.8,且持续不降
现象:训练曲线断崖式上升,后续无法收敛。
根因:actor/optimizer.pt或actor/scheduler.pt损坏,或学习率调度器状态未正确加载。
诊断:
# 检查scheduler.pt是否为空 ls -lh ./output/checkpoints/step_40/actor/scheduler.pt # 查看训练日志中learning_rate字段 grep "learning_rate" train.log | tail -5解决方案:
- 重新生成checkpoint(若磁盘有冗余备份)
- 临时方案:删除
actor/scheduler.pt,让verl从头初始化调度器(需调低初始lr)
4.3 场景3:RuntimeError: CUDA out of memoryon GPU 0
现象:resume时显存占用暴增,远超原始训练峰值。
根因:rollout/engine_state.json残留导致vLLM重复初始化,或trainer_state.json中rng状态异常引发batch size错乱。
解决方案:
- 删除
rollout/子目录(rm -rf ./output/checkpoints/step_40/rollout) - 使用
--no-rollout-state-restore参数 - 检查
trainer_state.json中dataloader_state是否合理(offset应为step * batch_size)
4.4 场景4:resume后生成结果完全重复(相同prompt输出相同response)
现象:所有rollout response呈现高度一致性,缺乏多样性。
根因:rng_states.torch或rng_states.cuda未正确加载,导致采样时随机种子固定。
验证:
# 比较原始训练与resume日志中的"torch.manual_seed"调用 grep "manual_seed" train.log | head -3 grep "manual_seed" resume.log | head -3解决方案:
- 确保
trainer_state.json中rng_states字段完整且base64编码有效 - 若怀疑损坏,可从原始训练日志中提取seed值,手动添加
--seed 42参数强制指定
5. 高级技巧:构建容错性更强的训练流程
5.1 自动化checkpoint健康检查
将verify_checkpoint.py集成到训练脚本末尾,每次save后自动校验:
# 在run.sh中添加 python verify_checkpoint.py $OUTPUT_DIR/checkpoints/latest if [ $? -ne 0 ]; then echo "❌ Checkpoint verification failed! Exiting." exit 1 fi5.2 双重备份策略
避免单点故障,同时保存本地+远程checkpoint:
# 训练脚本中添加 # 1. 本地保存(高速) trainer.save_freq=20 \ trainer.output_dir=./output \ # 2. 异步上传至S3(每100步) --s3-upload-interval=100 \ --s3-bucket=my-verl-backup \ --s3-prefix=qwen3_grpo/5.3 智能resume决策
当训练中断时,自动选择最优checkpoint:
# auto_resume.py import glob import os def find_best_checkpoint(checkpoint_dir): steps = [] for d in glob.glob(os.path.join(checkpoint_dir, "step_*")): try: step = int(os.path.basename(d).split("_")[1]) # 优先选择step能被100整除的(大粒度保存,更稳定) if step % 100 == 0: steps.append((step, d)) except: pass if steps: return max(steps, key=lambda x: x[0])[1] return None best = find_best_checkpoint("./output/checkpoints") print(f"Auto-selected checkpoint: {best}")6. 总结:掌握verl续跑的核心心法
verl的checkpoint机制设计精良,但其分层状态管理也带来了更高的使用门槛。本文揭示的不仅是操作步骤,更是理解其设计哲学的钥匙:
- 状态即契约:
trainer_state.json不是辅助文件,而是verl与你之间的状态契约。它承诺“从此步开始,以完全相同的方式继续”。违背契约(如篡改step、删除rng)必然导致训练失效。 - 组件需协同:Actor、Ref、Rollout、Trainer四者状态必须严格对齐。单独更新某一部分(如只替换actor权重)会破坏系统一致性。
- GRPO更需敬畏:无critic的简洁性背后,是对组采样、相对优势、KL loss三者耦合关系的强依赖。resume时任何参数漂移都会放大误差。
- 验证优于假设:永远不要假设checkpoint“应该”能用。用
verify_checkpoint.py建立自动化校验,是生产环境的底线。
最后提醒:最可靠的resume,永远是避免中断。在提交长训任务前,务必完成本文所述的6步验证,并启用双重备份。当意外发生时,你将拥有快速、安全、可复现的恢复能力。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。