English interface selected · this article is currently written in Chinese.
为什么换一套训练环境结果会变:数值精度、数据顺序与断点恢复
English overview →具备清晰骨架、初步数学推导或工程验证,持续补充论据与实测。
数字花园
🌿 什么是数字花园与思想演进?
遵循 Maggie Appleton 与 Andy Matuschak 的常青笔记理念:这里不是按时间沉底的流水账,而是持续修剪打磨的知识生态。文章按成熟度经历:
- 🌱 萌芽 (Seedling): 发散灵感、读书摘录、开放猜想
- 🌿 演进 (Budding): 结构渐明、初步代码推导、积极修正
- 🌳 常青 (Evergreen): 逻辑闭环、持久经得起检验的基石
- 🔬 实验待证 (Speculative): 结合沙盒与仿真探针的理论探索
代码和随机种子没变,换一张设备或升级框架后,损失曲线却不完全一样。另一种更隐蔽的情况是:训练中断后加载了权重,程序继续跑,但后续轨迹已经不等于原来那次训练。
这些现象不一定都是错误,也不能一概用“随机性”解释。需要先说清想复现到什么程度,再检查哪些状态和运算发生了变化。
本文实际运行的是 Python 标量动量更新与浮点反例,没有运行语言模型、GPU 或分布式训练。因此它验证机制,不提供任何设备间性能或精度结论。
先定义复现的目标
逐位一致要求相同位置的数值完全相同,适合某些严格回归检查。数值接近允许预先规定的误差范围,关注输出、损失或梯度是否足够接近。结论复现则要求多次实验仍支持相同研究判断,例如某种改动在规定任务与预算下稳定优于基线。
三者不是同一项承诺。参数不逐位相同,任务表现可能接近;某次分数接近,也不能证明训练过程一致。验证标准应由使用场景决定,并在看到差异前设定。
PyTorch 官方文档明确说明,不保证跨版本、平台或 CPU/GPU 的完全可复现结果,确定性设置也可能带来性能代价。PyTorch 2.14 可复现性说明
因此,“设了种子”只是控制一个来源,不能覆盖所有实现差异。
浮点加法并不总满足交换求和顺序后的直觉
在精确实数里,先加哪两个数不会改变总和。但浮点数只能保留有限有效位。
本文在 Python 浮点计算中检查:先计算 10¹⁶ 加 -10¹⁶,再加 1,结果为 1;先计算 -10¹⁶ 加 1,再与 10¹⁶ 相加,结果为 0。
原因是后一个顺序里的小量 1 在大数附近无法保留下来。这是刻意放大的教学例子,不代表普通训练每次都会产生同样大的误差。
并行归约、融合算子或不同内核可能改变求和和舍入方式。误差经过多步非线性更新后,也可能改变后面的路径。因此排查时既看数值差异,也看它是否改变实际任务结论,不能只凭最后一位不同判断实现有错。
精度设置不是一个 dtype 就能描述完
模型权重的存储类型、矩阵乘法输入精度、累加精度、梯度类型与优化器状态类型可能不同。混合精度下还可能存在损失缩放与溢出跳步。
如果一次更新因溢出被跳过,实际优化器更新次数就可能少于读取批次数。学习率调度按什么步数推进,也会影响后续训练。
因此记录精度时,不能只写“用了 BF16”或“用了 FP16”。要保存框架与设备版本、自动混合精度配置、相关内核选择,以及实际发生的跳步。本文不推荐某种精度,因为合适选择取决于模型、设备与验证结果。
并行方式会影响批次语义
在简单数据并行下,有效样本批量通常可写成每设备微批量 × 设备数 × 累积次数,但前提是各批大小一致,并且尾批、丢弃与梯度归一化规则匹配。
对可变长度 SFT,更关键的还可能是有效目标 token 数。假设两个微批各自目标损失均值为 0.2、1.0,对应目标数为 10、30。直接平均两个均值是 0.6;按所有目标 token 统一平均则是:
改变累积或并行实现时,如果从一种归一化变成另一种,优化目标已经改变。它不应被归结为微小舍入差异。
还要检查采样器是否重复或遗漏数据、各进程如何处理尾部,以及全局批次顺序是否与原实验对应。卡数相同不自动意味着批次语义相同。
只保存权重,为什么不能原样续训
用一个标量 w 说明。目标是让 w 接近 1,损失为半个平方误差,梯度为 w−1。采用教学动量更新:
从 w=0、v=0 开始,两步后 w=0.28,v=-1.8。现在暂停。
如果同时恢复 w 和 v,下一步的动量是 -2.34,权重变成 0.514。如果只恢复 w,把 v 重置为零,下一步权重却是 0.352。
两种运行都能继续,也都像在往目标走,但已经不是同一条优化轨迹。模型权重没有记录优化器积累的历史。
PyTorch 保存与加载教程因此将恢复训练所需的优化器状态与模型状态一起保存。保存训练检查点
真实训练还可能需要调度器、损失缩放器、已完成更新数和随机数状态。本文的简单动量例子没有这些组件,所以不能把它的恢复成功当成真实训练恢复验证。
数据游标同样属于训练状态
即便权重和优化器都恢复了,如果数据从头再读、跳过一批或改变顺序,梯度也会变。
在刚才 w=0.28、v=-1.8 的检查点,如果下一样本目标由 1 换成 2,下一步权重会变成 0.614,而不是 0.514。参数起点一样,下一条数据不同,结果自然不同。
应保存实际数据版本、当前 epoch、采样顺序或可重建的采样器状态,以及消费到的位置。多进程预取还要区分“已经被工作进程取出”与“已经参与优化更新”的样本。仅记录文件行号或 epoch 数,不一定足够。
流式数据源如果随时间变化,还需要可恢复的快照或稳定引用。重新打开同一个 URL,不保证读到原来的内容。
中途保存还要确认更新边界
若梯度累积到一半就保存,权重和优化器状态仍停留在上一次更新,但内存中可能已有部分累积梯度。恢复后忽略这些梯度,又从后半批继续,等于漏掉前半批贡献。
一种清楚的策略是在完整优化器更新边界保存;另一种是同时保存未完成累积状态并严格恢复。两者都需要验证,不能只依赖“checkpoint 已生成”的日志。
分布式检查点还要确认各分片属于同一个逻辑步,并且写入完整。标记完成之前应核对必要文件;恢复时读取到半写入或混合步数的状态,不能静默接受。
换卡数恢复可能需要重新分片,也可能改变后续数据与归约行为。即便框架支持加载,也应把“成功加载”和“保持原训练语义”分开验收。
用连续运行与分段运行做恢复测试
对一个足够小的配置,先连续执行固定步数,保存每步损失、更新计数、样本标识与关键状态。再从相同起点执行到中间,保存、退出、重新启动并恢复。
对齐后比较恢复后的第一批数据、学习率、梯度和权重。第一步就不同,优先查状态或输入;一段时间后才分开,再检查数值、随机源或内核差异。允许误差时,阈值也应提前定义。
本文脚本把两步后的标量状态写入 JSON,再读取后继续,确认完整恢复与连续运行一致;重置动量、改变下一样本、只保留部分状态则产生不同结果。还检查了浮点结合顺序与微批损失归一化。
这是真实执行的小算例,不是神经网络训练报告。实际项目仍要在所用框架、设备与数据加载器上重复相同思路的检查。
环境清单应服务于定位差异
保存代码提交、依赖版本、设备与驱动、精度与算子设置、并行配置、数据和分词器版本、模板、种子及检查点恢复规则。清单越长不一定越好,关键是能重建影响结果的条件。
发生差异时,每次只改变一个可疑来源,保留最小复现:先固定输入,再检查前向、损失、梯度、更新,最后才比较完整训练成绩。这样能区分协议改变、实现错误与预期数值差异。
复现不是要求所有环境永远给出同一个数字,而是能说明哪些条件相同、哪些不同,以及这些差异是否改变了原本想验证的结论。
文章概念网络
从章节进入正文概念。实线表示所属层级,虚线连接跨章节重复出现的概念,不代表因果关系。
43 / 43 个节点 · 正文标记术语与关键词概念
为什么换一套训练环境结果会变:数值精度、数据顺序与断点恢复
回到原文 ↗最后更新于 2026-10-23
引用本文
@article{2026-10-23-llm-training-eval-04-reproduction,
author = {枫语},
title = {为什么换一套训练环境结果会变:数值精度、数据顺序与断点恢复},
journal = {枫语},
year = {2026},
month = {oct},
url = {https://meiluosi.github.io/posts/2026-10-23-llm-training-eval-04-reproduction/}
}