TT Lab
开始
学习 学习路径 课程

MiniMind — 亲手从头到尾训练一个小型语言模型

中断续训也与一次跑完相同 — 续训文件里放什么

在 TT Lab 中继续学习

目标

让一次性训练 60 步的结果,与在 30 步处用原子方式保存恢复文件(权重、优化器状态、步数、批次随机数状态)后接着训练的结果逐比特一致。测量漏掉优化器状态或数据位置会相差多少,以及半精度保存改变了什么。

为什么重要

只保存权重、再接着训练,结果会在没有报错的情况下变掉。AdamW 为每个参数记着两个矩,丢了这份记忆,接上之后的几步走的步幅完全不同。如果在轮次中途把批次顺序倒回去,前面的部分会看两遍,后面的部分却看不到。这类差别几乎不会在损失曲线上显现,很容易在不知不觉中溜过去。 所以 MiniMind 的 lm_checkpoint 把权重和恢复文件分开放,并且都先写进临时文件再改名。这个实验把这个设计究竟守住了什么,与一次性训练的结果做比较来确认。

步骤

  1. 编写 /root/mm/ckpt/run.py,一次性训练 60 步(随机种子 7、批次 8、长度 128、lr 3e-3),留下 /root/mm/ckpt/straight.pth 和 /root/mm/ckpt/straight_log.csv(step,train_loss)。
  2. 进行同样的训练,在 30 步处停下,把恢复文件 /root/mm/ckpt/resume.pth 先写进 .tmp,再用 os.replace 换名保存(model、optimizer、step、gen_state)。
  3. 从恢复文件接着训练第 31 步到第 60 步,留下 /root/mm/ckpt/resumed.pth 和 /root/mm/ckpt/resumed_log.csv。必须与第 1 步相同。
  4. 只丢掉优化器状态接着训练,留下 /root/mm/ckpt/noopt.pth。
  5. 只丢掉批次随机数状态(用最初的随机种子重来)接着训练,留下 /root/mm/ckpt/noskip.pth。
  6. 把 straight.pth 以半精度保存为 /root/mm/ckpt/straight_fp16.pth,把两个文件的大小和 logits 差异写入 /root/mm/ckpt/fp16.json。
  7. 在恢复文件中数出权重和优化器状态的元素数,写入 /root/mm/ckpt/optim.json。
  8. 在 /root/mm/ckpt/report.md 中写 ## 무엇을 저장하나、## 빠뜨리면、## 반정밀도 三节(三个标题为韩文,依次意为“保存什么”“漏掉会怎样”“半精度”),并放入第 7 步的倍率和第 6 步的 fp16 文件大小(字节)。

参考

一次性训练 60 步——基准

编写 /root/mm/ckpt/run.py,以随机种子 7、批次 8、长度 128、lr 3e-3(MiniMind 余弦,以总共 60 步为基准)一次性训练 60 步,留下 /root/mm/ckpt/straight.pth(state_dict)和 /root/mm/ckpt/straight_log.csv(step,train_loss,第 1–60 步)。

与上一个模块的训练循环相同。这次请把损失写到小数点后第六位——要在第 3 步与接续的记录逐位比较。

在第 30 步原子性地保存

进行同样的训练,在第 30 步停下,把 {"model": state_dict, "optimizer": optimizer.state_dict(), "step": 30, "gen_state": 배치 생성기.get_state()}(占位符为批次生成器)写入 /root/mm/ckpt/resume.pth.tmp,再用 os.replace 改名为 /root/mm/ckpt/resume.pth。学习率仍以总共 60 步为基准来计算。

全部写进临时文件之后再改名,就算保存过程中挂了,也会留下一个完好的文件。这就是 MiniMind 的 lm_checkpoint 用 ckp_tmp → os.replace 所做的事。如果 .tmp 还留着,就是漏掉了改名。

接着训练 60 步——必须与基准相同

在新进程中读取 resume.pth,恢复权重、优化器状态和批次生成器状态,从第 31 步训练到第 60 步,留下 /root/mm/ckpt/resumed.pth 和 /root/mm/ckpt/resumed_log.csv(第 31–60 步)。

模型和优化器要与第 1 步完全一样地创建,再用 load_state_dict 覆盖,并用 g.set_state(…) 把生成器倒回去。评分器会比较两个检查点的所有张量,看最大差是否在 1e-6 以下——在同一个节点上运行的话,就是 0。

丢掉优化器状态会怎样

从恢复文件中只恢复权重和批次生成器状态,优化器重新创建,接着训练第 31–60 步,留下 /root/mm/ckpt/noopt.pth。

AdamW 的步长与梯度除以其滑动平均的平方根之后的值成正比。如果记忆从 0 重新开始,接上之后的几步走的步幅完全不同。评分器会确认这个结果是否与基准明显不同。

把批次顺序倒回去会怎样

从恢复文件中只恢复权重和优化器状态,批次生成器用最初的随机种子(7)重新创建,接着训练第 31–60 步,留下 /root/mm/ckpt/noskip.pth。

如果把生成器放回最初的状态,第 31 步会重新看到第 1 步的批次。这就是在轮次中途恢复时,MiniMind 用 SkipBatchSampler 跳过已经看过的批次的原因。

以半精度保存会怎样

把 straight.pth 的所有张量用 .half() 转换,保存为 /root/mm/ckpt/straight_fp16.pth,并把两个文件的大小(字节)和对 /opt/mm/ref/val.npy 最前面 128 个令牌的 logits 的最大绝对差,以 fp32_bytes、fp16_bytes、max_logit_diff 写入 /root/mm/ckpt/fp16.json。

mmkit.load_model 会把 fp16 权重也展开成 fp32 再放进去。文件缩小将近一半(张量之外还有名称之类的东西,所以不是恰好一半),而 logits 会在小数点后第三位左右出现差异。

恢复文件为什么大

把 resume.pth 中权重的元素数(lm_head.weight 与嵌入共享,所以不算)、优化器状态张量(维度在 1 以上的)的元素数及其倍率,以 model_numel、optimizer_numel、ratio 写入 /root/mm/ckpt/optim.json。

optimizer.state_dict()['state'] 为每个参数装着 step(0 维)、exp_avg、exp_avg_sq。倍率为 2,就说明恢复文件大约是权重文件的三倍——这就是大模型的检查点很沉重的原因。

留下保存什么、为什么保存

在 /root/mm/ckpt/report.md 中写 ## 무엇을 저장하나、## 빠뜨리면、## 반정밀도 三节(三个标题为韩文,依次意为“保存什么”“漏掉会怎样”“半精度”),并以数字放入第 7 步的 ratio 和第 6 步的 fp16_bytes。

把第 4、5 步里拉开的幅度一并写上会更好。半精度一节里,请用一行写出“推理用权重”和“恢复用权重”打算怎么区分。