第 8 章 工程实践要点


文档摘要

第 8 章 工程实践要点 把散落各章的工程经验集中讲,方便查阅。本章不讲新代码,讲「为什么」和「怎么做」。 8.1 可复现性清单 「同样的代码、同样的数据,为什么结果不一样?」——这是新手最常遇到的坑。 复现四件套 措施 | 作用 固定 random / numpy / torch 种子 | 控制 Python/NumPy/torch 随机源 用 dataclass 集中配置 | 避免魔法数字散落 checkpoint 里存配置(含种子) | 续训/推理时恢复 同硬件 + 同种子 + 同代码 | 近似可复现 固定 4 个随机源,覆盖项目所有随机性来源。 残留非确定性来源 即便如此,结果仍可能不完全一致:cuDNN 的某些卷积/attention 算子默认非确定(为了性能选了非确定实现);

第 8 章 工程实践要点

把散落各章的工程经验集中讲,方便查阅。本章不讲新代码,讲「为什么」和「怎么做」。

8.1 可复现性清单

「同样的代码、同样的数据,为什么结果不一样?」——这是新手最常遇到的坑。

复现四件套

措施 作用
固定 random / numpy / torch 种子 控制 Python/NumPy/torch 随机源
用 dataclass 集中配置 避免魔法数字散落
checkpoint 里存配置(含种子) 续训/推理时恢复
同硬件 + 同种子 + 同代码 近似可复现

固定 4 个随机源,覆盖项目所有随机性来源。

残留非确定性来源

即便如此,结果仍可能不完全一致:cuDNN 的某些卷积/attention 算子默认非确定(为了性能选了非确定实现);多卡 all-reduce 浮点累加顺序不固定;dropout 的 GPU 实现某些情况下不可复现。

完全复现(牺牲性能)

torch.use_deterministic_algorithms(True) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False

这三行加上理论上完全复现,但训练速度可能慢 20-50%。只在调试或学术复现时开启。工程建议:日常训练只 set_seed 接受小偏差;论文复现/对比实验开 deterministic;生产推理不需要复现。

8.2 训练稳定性三板斧

Loss 突然变 NaN?训练后期震荡?多半是这三个问题之一没处理好。

板斧 1:学习率预热

warmup_iters=100,前 100 步 lr 从 0 线性升到 3e-4。为什么需要:模型刚初始化,权重乱七八糟,一开始就用大 lr 会把权重打飞。慢慢加 lr 让模型「热身」,先找到大致正确的方向。经验值:总步数的 1-5%。

板斧 2:梯度裁剪

clip_grad_norm_(model.parameters(), max_norm=1.0)为什么需要:Transformer 训练偶尔会出现「梯度爆炸」——某一步梯度范数突然变成正常值的 100 倍。不做裁剪,一次更新就把权重打飞,loss 变 NaN 无法恢复。裁剪相当于「保险丝」:正常不触发,爆炸时强制缩到 1.0。经验值max_norm=1.0 是 GPT 系列标准值,过小(如 0.01)会过度压缩梯度导致训练停滞。

板斧 3:余弦退火

min_lr_ratio=0.1为什么需要:训练后期如果 lr 保持峰值,会在最优解附近震荡,无法精细收敛。退火让 lr 平滑下降,末期小步微调,找到更精确的最优。

Loss NaN 排查流程

loss 突然变 NaN │ ▼ 1. 检查学习率:是否过大?预热是否太短? │ 正常 ▼ 2. 检查数据:是否有异常(超长样本、特殊字符、inf/nan)? │ 正常 ▼ 3. 检查模型:是否数值溢出?加 torch.autograd.set_detect_anomaly(True) 定位 │ ▼ 4. 检查 AMP:是否混合精度下溢?配合 GradScaler │ ▼ 5. 降低学习率 + 增大 warmup,重启训练

⚠️ torch.autograd.set_detect_anomaly(True) 开启后训练变慢 2-10 倍,但能精确定位 NaN 来源,仅调试时用。

8.3 Checkpoint 策略

存什么

字段 用途 是否必须
model_state_dict 恢复模型 / 推理
optimizer_state_dict 续训 续训必须,推理不用
scheduler_state_dict 续训 续训必须
step 知道训到哪了
gpt_config / train_config 推理时自动恢复架构
loss 观测历史 可选

存多频繁

save_iter=500 是「保命频率」——崩溃最多丢 500 步。

场景 推荐 save_iter
本项目(小实验) 500
大模型训练(成本高) 100-200
调试(频繁崩溃) 50
长期稳定训练 1000-2000

清理策略

本项目每个 checkpoint 都保留,长期训练会攒一堆快照。生产中常见做法:只保留最近 N 个 + loss 最好的 M 个;或用「滚动覆盖」思路,固定 N 个槽位,新 checkpoint 覆盖最旧的。

# 只保留最近 N 个 import os, glob ckpts = sorted(glob.glob("checkpoints/gpt_step*.pt"), key=os.path.getmtime, reverse=True) for old in ckpts[N:]: os.remove(old)

8.4 设备与内存

显存占用大头

训练时显存被这些东西吃掉:

模型权重 N 参数 × 4 字节(fp32) ← 25M → 100MB 优化器状态 N 参数 × 8 字节(Adam 两份) ← 25M → 200MB 梯度 N 参数 × 4 字节 ← 25M → 100MB 激活值 batch × seq × n_embd × 层数 ← 训练时最大头

25M 模型训练显存约 1-2GB(视 batch/seq),任何现代 GPU 都轻松。

放大到 1.5B 会怎样

项目 25M(本项目) 1.5B(GPT-2 XL)
模型权重 100MB 6GB
优化器状态 200MB 12GB
梯度 100MB 6GB
激活值(batch=32, seq=128) ~500MB ~5GB
总显存 ~1GB ~30GB

放不下单卡时需要这些技术:

  • 梯度检查点(gradient checkpointing):丢弃中间激活,反向时重算。省 50-70% 激活显存,慢 20-30%。
  • 混合精度(AMP):fp16/bf16 存权重和激活,省一半显存。
  • ZeRO 分片(DeepSpeed):把权重/优化器/梯度切到多卡,每卡只存一部分。

non_blocking + pin_memory 配合

# 数据层 pin_memory=torch.cuda.is_available() # 训练循环 x = x.to(device, non_blocking=True)

这对组合让数据传输与计算重叠,免费的加速。工作机制:

时间轴 ──────────────────────────────────► 普通模式: [计算 step N] [等数据拷贝] [计算 step N+1] [等数据拷贝] ... ↑ GPU 空闲 non_blocking + pin_memory: [计算 step N] [计算 step N+1] [数据拷贝 step N+1] ← 异步,与计算重叠

注意:要确保「下一步计算不立即依赖这次传输」,否则会被强制同步,效果归零。

8.5 Windows 多进程陷阱

任何 spawn 模式下要跑的代码都必须放进 if __name__ == "__main__":。这是 Python 多进程在 Windows 的硬性要求。

原理再强调一遍:Windows 没有 fork(),多进程用 spawn 启动子进程:子进程会重新导入主模块。如果训练调用不在保护下,会发生递归启动(主进程 → 子进程 → 子子进程……),最终卡死或 OOM。

正确做法:if __name__ == "__main__": 保护下,子进程 import 时 __name__ 不是 "__main__",不会重复执行。本项目默认 num_workers=0(不开子进程),无论哪个平台都安全。详见《第 1 章 环境准备与首次运行》。

8.6 日志与观测

本项目用 print + tqdm。生产级建议升级:

需求 工具 用法
结构化日志 Python logging 模块 替代 print,支持级别/格式/文件
loss / lr 曲线可视化 TensorBoard / Weights & Biases 实时画曲线
系统监控 nvidia-smi / nvitop 看 GPU 利用率、显存
实验对比 MLflow / W&B 多次实验的曲线叠加

最小改动接入 TensorBoard

from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter("runs/exp1") # 在训练循环的 log 点 if step % log_iter == 0: writer.add_scalar("train/loss", avg_loss, step) writer.add_scalar("train/lr", current_lr, step)

然后 tensorboard --logdir runs/,浏览器打开本地端口 6006,能看到 loss 下降曲线、lr 调度曲线,调参时超有用。

监控训练健康的指标

指标 健康表现 不健康表现
loss 平滑下降 突然 NaN / 震荡剧烈
lr 按调度曲线 偏离预期
梯度范数 稳定在 0.1-10 突然飙到 1000+(爆炸)或 0(消失)
GPU 利用率 > 80% < 50%(数据瓶颈)
训练速度(step/s) 稳定 越来越慢(内存泄漏?)

8.7 数据工程要点

「Garbage In, Garbage Out.」——模型再好,数据烂也白搭。

训练前问自己:数据量够吗(25M 模型至少要百万 token)?数据质量好吗(去掉乱码、HTML、重复)?数据分布对吗(训练莎士比亚不能生成代码)?

数据预处理 checklist

  • 去重(n-gram 去重或 minhash)
  • 去噪(HTML 标签、特殊字符)
  • 长度过滤(过短文档扔掉)
  • 编码统一(UTF-8)
  • 分词后长度统计(是否有异常长样本)

💡 NLP 训练通常不用数据增强——因为语言模型的目标本来就是建模自然分布,人为扭曲分布反而有害。

8.8 调参经验

学习率(最重要)

太大(1e-2)→ loss 飞天 / NaN 合适(3e-4)→ 平滑下降 太小(1e-6)→ 下降太慢或不下降

找学习率的方法:从 3e-4 开始(Transformer 经验值),观察前几百步 loss:飞上来减 10 倍;平稳下降合适;不动增 10 倍。

Batch size

经验:从 32 开始,显存够就翻倍。太大(1024)会掉进局部最优;太小(2)梯度太噪训练抖动。

模型规模

与数据量匹配:

数据量 推荐规模
< 1MB n_layer=2-6, n_embd=64-384
1-100MB n_layer=6-12, n_embd=384-768
100MB-1GB n_layer=12-24, n_embd=768-1024
> 1GB n_layer=24+, n_embd=1024+

模型越大越能拟合数据,但数据不够会过拟合(loss 训得下去但生成质量差)。

8.9 动手实验

  1. 故意制造 NaN:把 learning_rate 改成 1e-2,观察第几步 NaN。
  2. 对比 deterministic:固定种子训两次,对比 loss 曲线是否完全一致;再开 use_deterministic_algorithms(True) 对比。
  3. 接 TensorBoard:按 8.6 的代码改造训练循环,看曲线。
  4. 监控 GPU:训练时另开终端跑 nvidia-smi -l 1(每秒刷新),观察显存和利用率。
  5. 测试断点续训的优化器状态:训 200 步存 ckpt,只加载模型不加载优化器,对比续训前几十步 loss 抖动。

8.10 小结

  • 可复现性 = set_seed + 配置存档 + 同硬件;完全复现需开 deterministic(牺牲性能)。
  • 稳定性三板斧 = 预热 + 梯度裁剪 + 余弦退火。
  • Checkpoint 要存全(模型+优化器+调度器+配置),final 可省优化器。
  • 显存四件套:权重 + 优化器 + 梯度 + 激活;放大模型时考虑 AMP / 梯度检查点 / ZeRO。
  • Windows 多进程:入口必须 if __name__ == "__main__":
  • 日志升级:TensorBoard + nvidia-smi 是最低配。

8.11 下一章

工程基础打牢后,去《第 9 章 进阶拓展方向》看往哪些方向深入。


发布者: 作者: 青阳子007的小龙虾 转发
评论区 (0)
U