checkpoint怎么选?保存策略与恢复技巧说明
微调大模型时,checkpoint(检查点)不只是训练过程中的一个中间产物,它直接决定了你能否回溯效果、复现结果、快速验证想法,甚至影响最终部署的稳定性和灵活性。尤其在单卡资源有限的场景下——比如用一块 RTX 4090D(24GB)跑 Qwen2.5-7B 的 LoRA 微调——每一轮 save_steps 都在显存、磁盘空间和时间成本之间做权衡。选错 checkpoint,轻则白跑几小时,重则无法还原最佳状态。
本文不讲抽象理论,也不堆参数公式,而是从真实微调现场出发,结合你正在使用的镜像(单卡十分钟完成 Qwen2.5-7B 首次微调),手把手拆解:
checkpoint 到底该不该存?存多少?--save_steps 50是怎么算出来的?为什么不是 10 或 200?
训练中断后,如何精准续训而不是从头再来?
推理时加载checkpoint-xxx和加载最终merged模型,效果真的一样吗?
多个 checkpoint 并存时,怎么一眼识别哪个“最聪明”?
所有答案,都来自你在/root/output/目录下亲手生成的那些文件夹。
1. checkpoint 是什么?它不是“快照”,而是“可执行的状态包”
很多人把 checkpoint 理解成“训练中途截个图”,这是误区。在 ms-swift 框架中,一个典型的checkpoint-100文件夹,实际包含以下核心内容:
adapter_config.json:记录 LoRA 的结构配置(rank=8, alpha=32, target_modules=all-linear)adapter_model.bin:真正的低秩增量权重(通常 10–15MB)trainer_state.json:当前训练步数、loss 历史、optimizer 状态(关键!决定能否续训)global_stepX.bin(可选):梯度或 optimizer 分片(仅当启用 ZeRO 类优化时出现)
划重点:只有同时具备
adapter_model.bin+trainer_state.json,这个 checkpoint 才能既用于推理,也能用于续训。缺一不可。
而像pytorch_model.bin这类全量权重文件,在 LoRA 微调中默认不会生成——因为没必要。Qwen2.5-7B 原始模型本身已固定,我们只学“小补丁”。这也是为什么本镜像能在 24GB 显存里跑起来的根本原因。
2. 保存策略:不是越多越好,而是“够用+可控+可验证”
镜像默认命令中设置了:
--save_steps 50 \ --save_total_limit 2 \ --eval_steps 50这组参数不是随便写的,而是针对self_cognition.json(约 50 条数据)、单卡 4090D、LoRA 微调这一具体场景反复验证后的平衡点。我们来逐条拆解:
2.1--save_steps 50:为什么是 50?不是 10,也不是 100?
先看数据量:50 条样本 ×per_device_train_batch_size 1= 每轮 epoch 实际只走 50 步。
再看gradient_accumulation_steps 16:意味着模型每看到 16 个 batch 才更新一次参数 → 实际参数更新频率是每 16 步一次。
所以:
save_steps=50≈ 每1 个完整 epoch 后保存 1 次(50 ÷ 16 ≈ 3.1 次更新 → 覆盖完整学习周期)- 若设为
10:10 步 ≈ 0.6 次更新 → 保存太密,磁盘写入频繁,且每个 checkpoint 差异极小,无区分度 - 若设为
200:200 步 ≈ 12.5 次更新 → 跨越多个 epoch,一旦在第 150 步崩溃,就得倒退 100 步重训,损失巨大
实操建议:
- 小数据集(<200 条):
save_steps = 数据条数 ÷ 2(如 50 条 → 设 25~50) - 中等数据集(500–2000 条):
save_steps = 100~200,配合eval_steps保持一致 - 每次 save 同时触发 eval → 你能立刻看到
checkpoint-50对应的 loss 和回答质量,而不是靠猜
2.2--save_total_limit 2:为什么只留 2 个?不怕删掉最好的吗?
save_total_limit控制的是/root/output/下同级 checkpoint 文件夹的数量上限。当新 checkpoint 生成,系统会自动删除最旧的那个(FIFO 策略)。
但注意:它只删同级目录,不删子目录。例如:
output/ ├── v2-20250401-102345/ ← 当前训练主目录 │ ├── checkpoint-50/ │ ├── checkpoint-100/ │ └── checkpoint-150/ ← 新生成,触发删除 checkpoint-50所以limit=2实质是:永远保留最近两次完整评估过的状态。
这对 self-cognition 这类目标明确的微调非常友好——你总能对比“前一次”和“这一次”的回答差异,快速判断是否过拟合(比如开始机械重复“我是 CSDN 迪菲赫尔曼 开发的”,但对其他问题答非所问)。
❌ 错误认知:“留越多越保险”。
正确认知:“留够验证窗口,比堆数量更重要”。磁盘空间有限,人工筛选成本高,2 个高质量 checkpoint + 1 份日志,远胜 10 个模糊状态。
2.3--eval_steps 50:保存和评估必须同步,否则 checkpoint 就是“盲盒”
镜像命令中--eval_steps 50与--save_steps 50完全对齐。这意味着:
- 每生成一个
checkpoint-50,系统必跑一次验证(用相同 prompt 测 self-cognition 效果) - 评估结果(loss、accuracy、示例输出)会写入
trainer_state.json和日志文件 - 你打开
output/v2-20250401-102345/checkpoint-100/时,能立刻看到eval_results.json里写着:
{ "eval_loss": 0.214, "eval_samples": 8, "eval_runtime": 12.34, "eval_samples_per_second": 0.65 }这才是 checkpoint 的“可信标签”。没有 eval 结果的 checkpoint,就像没试飞过的飞机——你不知道它能不能平稳落地。
实操动作:每次训练完,别急着 infer,先去对应 checkpoint 目录下cat eval_results.json,扫一眼 loss 是否持续下降。如果checkpoint-100的 loss 比checkpoint-50高,说明可能过拟合,立刻切回去用checkpoint-50推理。
3. 恢复技巧:中断≠重来,3 步精准续训
训练过程中遇到显存溢出、SSH 断连、系统重启?别慌。只要trainer_state.json完整,就能从断点继续,不丢步数、不重算梯度、不浪费 GPU 时间。
3.1 第一步:确认断点位置
进入你的训练主目录(如output/v2-20250401-102345/),执行:
ls -t checkpoint-*输出类似:
checkpoint-150 checkpoint-100 checkpoint-50再查看最新 checkpoint 的trainer_state.json:
jq '.global_step' checkpoint-150/trainer_state.json # 输出:150这个150就是已成功完成的总步数,也是续训的起点。
3.2 第二步:修改微调命令,加入恢复参数
原命令:
swift sft --model Qwen2.5-7B-Instruct ... --output_dir output改为(仅增加两行):
CUDA_VISIBLE_DEVICES=0 \ swift sft \ --model Qwen2.5-7B-Instruct \ --train_type lora \ --dataset self_cognition.json \ --torch_dtype bfloat16 \ --num_train_epochs 10 \ --per_device_train_batch_size 1 \ --per_device_eval_batch_size 1 \ --learning_rate 1e-4 \ --lora_rank 8 \ --lora_alpha 32 \ --target_modules all-linear \ --gradient_accumulation_steps 16 \ --eval_steps 50 \ --save_steps 50 \ --save_total_limit 2 \ --logging_steps 5 \ --max_length 2048 \ --output_dir output \ --system 'You are a helpful assistant.' \ --warmup_ratio 0.05 \ --dataloader_num_workers 4 \ --model_author swift \ --model_name swift-robot \ # 👇 新增两行:指定从 checkpoint-150 恢复 --resume_from_checkpoint output/v2-20250401-102345/checkpoint-150 \ --ignore_mismatched_sizes # 防止因路径微小差异报错关键细节:
--resume_from_checkpoint必须指向完整路径(含output/xxx/checkpoint-xxx),不能只写checkpoint-150--ignore_mismatched_sizes是安全垫:当模型结构微调后略有变化(如新增 token),它跳过校验,避免中断
3.3 第三步:验证续训是否生效
启动后,观察日志首行:
Loading checkpoint from output/v2-20250401-102345/checkpoint-150 Resuming training from global step 150接着看 step 计数是否从 151 开始:
Step: 151/2000 | Loss: 0.211 | ... Step: 152/2000 | Loss: 0.209 | ...如果看到Step: 1/2000,说明路径错了,它当成了全新训练。
经验口诀:
“看三行日志定生死:
第一行有Resuming,第二行有global step XXX,第三行 step 数 > XXX —— 续训成功。”
4. 推理加载:checkpoint vs merged,选哪个更稳?
训练完成后,你会在output/xxx/下看到两类产物:
| 类型 | 路径示例 | 特点 | 适用场景 |
|---|---|---|---|
| LoRA checkpoint | output/v2-20250401-102345/checkpoint-150/ | 仅含 adapter 权重(10MB),依赖原始模型 | 快速验证、A/B 测试、多身份切换 |
| Merged 模型 | output/v2-20250401-102345/merged/ | LoRA 权重已融合进 Qwen2.5-7B,生成完整pytorch_model.bin(~5GB) | 生产部署、离线使用、跨框架迁移 |
4.1 为什么推荐优先用 checkpoint 推理?
- 启动快:加载 10MB 比加载 5GB 快 10 倍以上,
swift infer命令秒响应 - 灵活:你可以在同一基础模型上,同时加载
checkpoint-100(偏通用)和checkpoint-150(偏自认知),对比效果 - 安全:不改动原始模型,避免 merge 错误导致基础模型损坏
命令就是你文档里写的:
swift infer \ --adapters output/v2-20250401-102345/checkpoint-150 \ --stream true \ --temperature 0 \ --max_new_tokens 20484.2 什么时候必须 merge?
- 需要导出给 HuggingFace Transformers、vLLM、Ollama 等不支持 LoRA 动态加载的框架
- 要打包成 Docker 镜像交付给客户,且客户环境无法保证
ms-swift运行时 - 做量化(如 AWQ、GPTQ):多数量化工具要求输入是完整模型
merge 命令(在训练完成后自动触发,或手动运行):
swift export \ --ckpt_dir output/v2-20250401-102345/checkpoint-150 \ --output_dir output/v2-20250401-102345/merged \ --device_map auto重要提醒:merge 后的模型仍需指定--model_type qwen,否则 infer 会报错。因为模型结构信息(如 rotary base、attention mask 处理)不包含在权重里,必须由框架注入。
5. 进阶技巧:用 checkpoint 做“模型体检”
checkpoint 不只是恢复用的,它还是你诊断模型健康度的听诊器。三个实用技巧:
5.1 对比 loss 曲线,识别过拟合拐点
进入output/xxx/,执行:
grep "loss" logs/train.log | tail -n 20你会看到类似:
Step: 130/2000 | Loss: 0.225 | ... Step: 140/2000 | Loss: 0.221 | ... Step: 150/2000 | Loss: 0.218 | ... Step: 160/2000 | Loss: 0.235 | ← 注意!这里上升了 Step: 170/2000 | Loss: 0.242 | ← 持续上升 → 过拟合信号此时,checkpoint-150就是你的“黄金点”。后续所有推理、merge、测试,都基于它,而不是最后那个checkpoint-200。
5.2 提取特定 layer 的 LoRA 变化,定位“记忆焦点”
LoRA 的lora_A和lora_B矩阵,分别作用于 attention 和 FFN 层。你想知道模型在哪一层“最努力记住了自我认知”?用以下代码快速探查:
import torch adapter = torch.load("output/v2-20250401-102345/checkpoint-150/adapter_model.bin") # 查看 attention 层 LoRA 的 L2 norm(越大表示改动越强) attn_norms = [v.norm().item() for k, v in adapter.items() if "self_attn" in k and "lora_B" in k] print("Attention layer LoRA strength:", sorted(attn_norms, reverse=True)[:3]) # 输出类似:[12.4, 11.8, 9.2] → 说明前三层 attention 被重点调整这能帮你理解:为什么模型对“你是谁”回答很准,但对“今天天气”却答得生硬——因为 LoRA 主要强化了 self-attention 的 identity 相关通路。
5.3 一键生成 checkpoint 报告,告别手动翻日志
把下面脚本保存为check_report.py,放在/root/下,每次训练完运行它:
#!/usr/bin/env python3 import os, json, glob from pathlib import Path output_dir = "output" for d in Path(output_dir).glob("v*"): if not d.is_dir(): continue checkpoints = sorted(d.glob("checkpoint-*")) if not checkpoints: continue print(f"\n Report for {d.name}") for cp in checkpoints[-3:]: # 只看最近3个 ts = json.load(open(cp / "trainer_state.json")) eval_f = cp / "eval_results.json" if eval_f.exists(): ev = json.load(open(eval_f)) print(f" {cp.name}: step={ts['global_step']}, loss={ev['eval_loss']:.3f}") else: print(f" {cp.name}: no eval result (run eval manually)")运行python check_report.py,输出即清晰:
Report for v2-20250401-102345 checkpoint-50: step=50, loss=0.312 checkpoint-100: step=100, loss=0.245 checkpoint-150: step=150, loss=0.2186. 总结:checkpoint 选择的本质,是工程节奏的掌控
选 checkpoint,从来不是技术问题,而是项目管理问题。
- 你有多少时间?→ 决定
save_steps密度 - 你有多少磁盘?→ 决定
save_total_limit上限 - 你要快速验证,还是长期部署?→ 决定用
checkpoint还是merged - 你怕中断重来,还是怕效果波动?→ 决定是否开启
eval_steps同步
在单卡微调 Qwen2.5-7B 的实战中,记住这四句口诀:
小数据,步频对半砍(save_steps ≈ 数据量 ÷ 2)
双 checkpoint,够看不占地(save_total_limit = 2)
eval 不离身,loss 是标尺(eval_steps 必须 = save_steps)
续训看三行,global_step 定乾坤
你不需要记住所有参数,只需要养成习惯:每次swift sft后,花 30 秒ls checkpoint-*,再cat eval_results.json—— 这个动作,会帮你避开 80% 的微调返工。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。