任务与先修:八十次更新必须有身份
现在管道已经可以产生正确目标,完整 decoder 也能反传。本章将训练定义为有状态过程,并让中断恢复成为可验收行为。读者需会向量范数、链式法则和 Python 状态字典;Adam 的具体约定在本章给出。项目固定 CPU 单线程、PyTorch 2.6.0、随机种子七、batch最多四个窗口、学习率0.003、权重衰减0.01、全局梯度范数裁剪阈值一,运行八十次更新。没有学习率调度、混合精度或分布式通信,不能把这些未实现机制写进收据。
一次更新的顺序具有数学含义:先 model.train;抽取下一批并拼成 x、y;optimizer.zero_grad 清除上一步残留;前向得到当批有效目标均值 loss;backward 沿完整计算图求梯度;记录裁剪前范数与各参数张量范数;全局裁剪;optimizer.step 更新。漏清梯度会把不同步的导数累积,先更新再反向则使用错的状态;用验证损失反向会直接破坏留出集合。日志存 float(loss.detach()),不保留带计算图张量,避免无意持有历次图。
裁剪与AdamW的精确对象
把所有参数的梯度展平连接成向量 g,范数 超过阈值 c 时乘缩放因子 c/范数,否则不变。裁剪保留方向但改变幅度,各层不能分别裁成 c 再声称全局范数也不超过 c。实现使用框架函数,其中会加入很小数避免除零,因此本课手算忽略那个保护项,仅对非零且远离阈值的数值比较。梯度非有限应被视为失败;记录所有分量范数是为了发现数值故障,而不是把高梯度一律解释为学习更好。
项目用 AdamW 的默认 beta1=0.9、beta2=0.999、epsilon=1e−8。矩估计初值零,步数 t 从一开始。偏差修正为 与 。权重衰减与梯度矩分开:先使参数乘以 ,再减去 。本项目统一给全部参数施加该权重衰减,包括归一化和bias;这是一项简单实验约定,不声称是所有大模型训练的最佳参数分组。
算例一:一维首步与矩状态
令参数初值二,原始梯度0.5,裁剪阈值一,故梯度不变。采用学习率0.1、衰减0.01,首步 m=0.05、v=0.00025,偏差修正后分别为0.5与0.25。忽略极小epsilon时,自适应步长为0.1;衰减先将二变为1.998,最后参数约1.898。若误把衰减项0.02加入梯度再估计矩,首步虽也可能约减少0.1,但之后轨迹不同,因为矩状态已经不同。仅看一次下降不能区分两种算法。
第二步即使梯度换为−0.25,m仍含上一梯度记忆,v也含其平方记忆。因此只保存参数1.898、重新把m和v设零,与保留原状态继续会走不同方向或步幅。独立检查一是用标准库完整展开首步数值;独立检查二是把连续运行三步与运行两步后序列化全部状态再运行一步比较。只有权重文件相同不是状态等价,因为优化器下一步依赖历史。
算例二:同权重、不同下一批
抽样器有六个窗口,batch大小四,某轮排列为 [3,0,5,1,4,2]。取第一批后游标是四,下一批应为 [4,2]。若恢复时仅用相同种子新建抽样器,游标重置为零,下一批又是 [3,0,5,1],重复了已见数据。即使权重、优化器和全局随机种子都相同,轨迹也会改变,因为抽样器有自己独立的 generator。需要同时保存排列、游标、轮号、生成器内部状态与窗口数、batch大小。
另一个故障是构造模型时消耗全局随机数:先恢复 RNG,再实例化随机初始化模型,构造过程已经推进了 RNG,之后 dropout 或采样将不一致。正确顺序是读取状态,构造模型与优化器,载入权重和状态,构造并恢复抽样器,最后恢复全局 torch RNG。本项目 dropout为零、生成用greedy,当前更新本身不消耗全局随机数;测试仍额外读取随机向量比较,为以后增加随机机制保留一个真实状态校验,而不虚称已测试dropout恢复。
import json, math
def step(state,g):
theta,m,v,t=state
g=g*min(1.,1./abs(g)) if g else g
t+=1; m=.9*m+.1*g; v=.999*v+.001*g*g
mh=m/(1-.9**t); vh=v/(1-.999**t)
theta=theta*(1-.1*.01)-.1*mh/(math.sqrt(vh)+1e-8)
return [theta,m,v,t]
state=[2.,0.,0.,0]
first=step(state,.5)
assert abs(first[0]-1.898)<1e-7
continuous=state
for g in [.5,-.25,.1]: continuous=step(continuous,g)
prefix=state
for g in [.5,-.25]: prefix=step(prefix,g)
resumed=step(json.loads(json.dumps(prefix)),.1)
assert continuous==resumed
wrong=step([prefix[0],0.,0.,0],.1)
assert abs(wrong[0]-continuous[0])>1e-3
print('first',first,'continuous',continuous[0],'resume',resumed[0],'weights-only',wrong[0])
checkpoint契约与真正的恢复测试
项目 checkpoint 保存配置、模型 state_dict、optimizer state_dict、tokenizer规则与摘要、语料摘要、抽样器状态、全局 RNG、已完成步数和 targets_seen。载入时以当前语料重新计算摘要,不匹配即拒绝;编码规则也重算摘要。checkpoint 中的配置用于重建同形状模型,不能默默把词表或窗口改大后继续。这个协议解决本实验已知状态身份,不保证任何来自未知来源的文件都安全,也不把不同框架版本的反序列化结果称为逐位可重现。
真实项目测试先运行三次更新,保存状态;连续执行第四次,记录每个参数、Adam状态、loss、抽样位置与随后随机向量;再从第三步文件恢复,重新执行第四次并逐项比较。当前同一CPU环境下均逐值相等。另加错误语料摘要必须抛异常的故障测试。这比“save后load没有报错”强,因为后者可能只证明文件可读,并未检验后续转移;也比只比loss强,因为不同权重可能给某一批同样的标量。
本次冻结训练的损失与预算
训练收据同时区分初始与最终整套训练损失、逐检查点验证损失、最后一批损失。当前固定实验八十更新,累计4585个有效目标;训练集合单次覆盖只有690个目标,因此4585不是独特语料规模。初始训练NLL为5.682412,最终为1.653204;验证由5.681924降至3.190052。最后一批loss约1.509825,不能拿它替代整套训练平均。曲线说明该环境下优化链路和合成分布的统计拟合在工作,不证明自然语言泛化。
本次计时的范围
墙钟训练段约0.416秒,包含定期验证但不包含解释器启动、导入框架和构造模型;其名称和计时边界记录于文件,机器负载变化时可不同。短运行时间被固定开销和系统抖动主导,不能从这一数推导大规模吞吐。测试通过之后不继续无目标地增加训练预算,只保存给定协议下的真实结果。下一章把冻结 checkpoint 用于验证和一次预先固定配置的测试评估,并把可读生成与定量似然分别记录。
训练状态转移可写成一个确定函数:给定模型参数、优化器矩与步数、数据窗口、抽样器排列游标和随机状态,产生新状态与一条收据。确定性依赖环境与算子,不是只设置一个整数seed就自动成立。本项目设置单线程、手动种子和确定性算法,并把实际torch版本写入training.json;如果将来加上Python随机或其他库的生成器,也必须分别保存其状态。当前源码没有使用那些额外随机源,不能声称已覆盖任意扩展。
保存发生在完整optimizer.step之后、下一次抽样之前。这个边界让已完成步数、权重和游标互相对应:若写在backward之后却未step,必须额外保存尚未应用的梯度,否则恢复会丢失半步。本项目不提供半步恢复,也没有对多个并行写者设计事务。文件写入中断可能产生损坏文件;当前单进程小实验把成功写完的checkpoint作为恢复前提,不宣称具备生产级原子轮换和容错日志。
运行时校验与来源记录
恢复语料摘要通过仍不保证所有超参数适配。模型配置来自文件,optimizer状态中还包含实际学习率与衰减,抽样器状态有窗口数和batch大小;窗口由相同tokenizer和block_size重建。若只改了正文但总窗口数巧合一样,语料摘要仍会拒绝;若只改tokenizer编号,规则摘要会拒绝。对源码实现的变化则必须看source_manifest和测试收据,当前restore不会自动证明两个不同程序语义相同。分清运行时硬校验与旁路来源记录的责任很重要。
有效预算可以用两条独立路径核对:更新返回本批目标数,把历次相加得到targets_seen;抽样器已取窗口索引也能映射回各窗长度求和。若resume把seen恢复了却把游标重置,第一条预算数仍可能看似连续,但训练对象已重复;所以恢复测试同时比较下一批所隐含的loss、全参数、优化器与游标。只盯一个计数无法证明轨迹没断裂。最终固定实验的4585还可与每个检查点1170、2328、3450、4585核对单调性。
拓展:更换学习率调度
学习率固定并不是没有设计。它减少了恢复时遗漏scheduler的状态,但也限制训练效率。若将来采用warmup加余弦衰减,必须首定义时间轴按更新数还是有效目标数推进,并把调度器当前状态或可重算的完整规则保存;两个不同长度batch会让这两种时间轴不同。我们没有为了使曲线更漂亮临时更换学习率,也没有把这份小预算训练称为收敛到全局最优。损失降低是局部经验观察,非凸网络没有由本实验获得最优性证明。
优化器的步数是实际执行更新次数,而非前向次数。一次验证会做许多前向但不推进Adam状态;若误把验证batch也计为训练步,偏差修正时间轴或调度时间轴便会错。项目只在update内部执行step,evaluate返回后不会修改参数或优化器。恢复测试也比较Adam中的step张量,防止权重相同而计数偏移。记录八十次更新与周期验证次数的区别,才能让别人从命令重建相同预算,而不是只复制日志最后一行。
python run.py train
python -m unittest -v tests.test_pipeline.PipelineTests.test_exact_same_environment_resume
TRY IT YOURSELF
练习一:全局裁剪
两个参数的梯度是3与4,阈值为1。求裁剪后梯度,并说明分别裁剪会得到什么。
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习二:恢复缺项
checkpoint已有模型、optimizer、全局RNG,但没有抽样器游标。为什么不能声称精确恢复?写出最小可复现失败。
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习三:定义收据边界
同CPU环境恢复逐值相等,能否宣称换GPU和新版框架也逐位一致?给出足够而不过度的结论。