← 返回学习路线 ◆ 贯穿项目
数学与机器学习 · 阶段 7 · 深度学习基础
Stage 07 / 17 · 数学与机器学习

深度学习基础 Deep Learning

这是从「调库」变成「理解」的阶段。重点不是记住多少网络结构,而是掌握训练这件事的工程化方法:怎么诊断不收敛、怎么判断过拟合、怎么在有限显存下训更大的模型、怎么让实验可复现。2026 年的 LLM 训练配方(RMSNorm + Pre-LN + SwiGLU + BF16 + FSDP2 + torch.compile)全都是这里概念的沿伸——把地基打牢,阶段 10–11 的 Transformer 与预训练你会理解得飞快。

⏱ 4–6 周 🎯 进阶 ◆ 里程碑 M7 2026-09-29
PyTorch反向传播RMSNormSwiGLU混合精度FSDPZeROtorch.compileCNN迁移学习

阶段总览

✔
学完你能做到
  • 理解 MLP 的万能逼近定理与「深度优于宽度」的直觉
  • 掌握 Xavier / He / 正交初始化,能解释全零初始化为何失败与方差守恒推导
  • 讲清 ReLU 死亡神经元、GELU / SiLU、以及为什么 2026 LLM 用 SwiGLU(参数量比例如何调)
  • 理解 BatchNorm / LayerNorm / RMSNorm / GroupNorm 的差异,以及 Pre-LN vs Post-LN 对训练稳定性的影响
  • 熟练使用 PyTorch 张量、自动微分、Dataset / DataLoader、AMP、torch.compile 与完整训练循环
  • 掌握混合精度(BF16/FP8)、梯度累积、梯度裁剪、梯度检查点、断点续训与显存估算
  • 理解 DP / DDP / FSDP / ZeRO-1/2/3 各切什么,能估算 7B 模型全量微调的显存
  • 建立训练调试清单(loss 不降 / NaN / 过拟合 / 验证波动)与实验追踪习惯(里程碑 M7)
阶段知识结构总览 · 从理解网络到训得稳、训得起
阶段 7 · 深度学习基础Deep Learning · 10 大章 · 110+ 知识点
1. 从线性到 MLP万能逼近定理深度 > 宽度参数效率组合泛化与层级特征残差梯度直达Mixture-of-Depths
2. 初始化Xavier 2/(n_in+n_out)He 2/n_in正交初始化 RNN全零初始化失败方差守恒推导
3. 激活函数ReLU 死亡神经元GELU / SiLUGLU 门控SwiGLU hidden=8d/3FFN 占 2/3 参数
4. 归一化BatchNorm running 统计LayerNorm 特征维RMSNorm 只除 RMSPre-LN vs Post-LNQK-Norm
5. PyTorch 核心动态计算图 grad_fnAMP autocast + GradScalertorch.compileDataLoader 调优断点续训六类状态
6. 训练工程FP16 / BF16 / FP8梯度累积 / 裁剪 / 检查点7B 全量显存 112 GBKV Cache 显存公式DDP / FSDP / ZeRO
7. 卷积与归纳偏置局部性 + 平移等变感受野递推 R_lResNet 残差抗退化迁移学习三分法ViT vs CNN
8. RNN → AttentionRNN 三硬伤LSTM / GRU 门控注意力 O(T²)FlashAttention-3流匹配 / rectified flow
9. 训练调试清单先过拟合一个 batch梯度范数监控forward hook 定位 NaNLR range test症状→病因排查表
10. 实验追踪 M7W&B / MLflow 记录要素超参 / 曲线 / 产物 / 版本A/B 对照设计Muon 优化器Newton-Schulz 正交化
贯穿项目 · M7 PyTorch 训练管线与实验追踪第 33–38 周通用训练循环(AMP、梯度累积、梯度裁剪、断点续训、早停)实验追踪封装,记录数据版本哈希 + 配置 + 指标Hydra 风格配置,命令行可覆盖与 M6 基线对比,确认指标不退化
学习路径
周次主题交付物
第 1 周MLP / 初始化 / 激活 / 归一化原理手写各组件并对照消融
第 2 周PyTorch 核心与训练循环 + Dataset/DataLoader不查文档手写完整训练循环
第 3 周训练工程(BF16/FP8/累积/裁剪/检查点)+ 显存估算在 8–24GB 显存上训练一个小模型并算清显存
第 4 周CNN 与视觉 + 迁移学习微调一个预训练视觉模型做分类
第 5 周分布式训练原理(DDP / FSDP / ZeRO)单卡模拟 + 多卡脚本骨架
第 6 周训练调试清单 + 实验追踪 + 综合项目(M7)PyTorch 训练管线 + W&B/MLflow 追踪的可复现实验
✔
给工程背景的人的捷径:你可能会觉得「手写训练循环很啰嗦」。但这一步的价值在于:后面所有复杂框架(HuggingFace Trainer、DeepSpeed、Megatron)都是在这个循环上叠功能。自己写过一次,你才能在它们出问题时知道该看哪里。2026 的 LLM 训练栈(FSDP2 + torch.compile + BF16)也只是把这套循环分片化、编译化而已。

1. 从线性到 MLP:网络为什么能学到东西

知识结构图 · 从线性到 MLP
从线性到 MLP3 大知识域 · 15 个知识点
万能逼近定理单隐层可逼近连续函数能拟合 ≠ 学得高效parity 浅网指数宽度深而窄参数效率更高多层堆叠是主流
学习路径
  1. 读 1.1:理解万能逼近定理的「能拟合≠学得高效」,背下 parity 浅网需指数宽度
  2. 跑 parity 数据集,观察 n 增大时深度 2 所需宽度的指数爆炸
  3. 完成 1.3 自测:解释「深而窄为何优于浅而宽」及换归纳偏置的价值
✔ 能解释万能逼近的局限,并用 parity 实例说明深度相比宽度的参数效率
核心知识点详解
  • 万能逼近的三个局限:①只保证存在一个宽网络,没说梯度下降找得到;②所需宽度随输入维/精度指数增长;③只谈表达力,泛化靠归纳偏置与正则。故现代模型走「深而窄」而非「浅而巨宽」。
  • 深>宽的量化直觉:parity:深度 2 网络拟合 n 位奇偶需 ~2^n 宽度(n=12→4096),深网用 O(n) 宽度 + O(n) 层即可;固定参数量下深而窄的验证误差常明显更低。
  • 层级复用与组合泛化:第 1 层学边缘、第 2 层学部件、第 3 层学物体,同一组原语被反复复用→参数效率高;这正是 CNN/Transformer 多层次堆叠而非单层超宽的根本原因。
深度与组合泛化组合泛化层级复用残差提供恒等捷径CNN 局部 / Attention 全局数据多弱偏置赢Mixture-of-Depths 按需深度
学习路径
  1. 读 1.2:理解组合泛化、残差恒等捷径与 CNN/Attention 的归纳偏置
  2. 跑 first_layer_grad(True/False),对比有无残差时的最浅层梯度范数量级
  3. 完成 1.3 自测:说明残差为何等价于「学残差」,弱/强偏置各举一例
✔ 能跑出残差使梯度保持同量级(无残差指数衰减到 1e-6),解释深度方案的可靠性
核心知识点详解
  • 残差 = 学残差:h=h+blk(h) 让目标退化为学 F(x)=H(x)−x;恒等项提供梯度直达通道,40 层无残差时最浅层梯度可指数衰减到 ~1e-6,加残差后保持同量级 ~1e-1——这是上百层可训的根本原因。
  • 归纳偏置决定深度怎么用:CNN 用「局部+权值共享」、Attention 用「全局+位置编码」,本质都是把深度用在合适处;数据有限加偏置、数据海量走弱偏置(纯 Transformer)+深度。
  • 按需深度(MoD):Mixture-of-Depths 用 router 决定 token 是否经过某层,把算力集中在难 token——不是所有输入都值得同样深度与容量,比堆层更省算力。
动手自测要点first_layer_grad 量级差残差 = 学残差 F(x)弱 / 强偏置各举一例
学习路径
  1. 过一遍 1.3 自测,重点跑 first_layer_grad 看梯度范数
✔ 自测命中判据,残差梯度范数对比能亲手复现
核心知识点详解
  • first_layer_grad 复现:depth=40 下 h=h+blk(h) 使最浅层梯度 ~1e-1,去掉残差则指数衰减到 1e-6 以下;能复现这个量级差即过关。
  • 学残差的等价性:残差把恒等映射变成梯度的恒等捷径,目标退化为 F(x)=H(x)−x,从而深层无须苦学恒等;这是 deep 网络能可靠训练的关键。
  • 偏置举例别空谈(坑):弱偏置+大数据=纯 Transformer 大规模预训练、强偏置+小数据=CNN 小图像集;光背「弱胜强」不举具体对比例子会被判偏题。
学习路径

1.1 万能逼近定理的直觉与局限

1989 年 Cybenko 证明:一个隐藏层、足够宽、用非线性的 MLP,能以任意精度逼近任意紧集上的连续函数。这是「神经网络能拟合一切」的理论根源——但「能拟合」≠「学得高效」。

维度万能逼近的代价现实含义
宽度换能力宽度随精度 / 输入维指数增长单层网络要“巨宽”才能表达复杂函数,参数量爆炸
深度换能力同样表达能力,深度网络所需宽度小得多深层用层级组合,参数效率高

关键局限:① 定理只保证「存在」一个宽网络,没说梯度下降找不找得到;② 需要的宽度随输入维/精度指数增长,单层在高位感知任务上不实用;③ 它解释「表达力」,不解释「泛化」——泛化靠的是归纳偏置与正则,不是逼近定理。所以现代模型普遍走深而窄而非「浅而巨宽」。

★
深度为什么优于宽度:Telgarsky 等证明:有些函数用深度 k 的网络只要 O(k) 宽度就能表达,但限制为深度 2 则需要指数级宽度。直观上,深度提供组合泛化——第 1 层学边缘、第 2 层学部件、第 3 层学物体——这种层级特征复用是卷积与 Transformer 通用的成功密码,也是为什么 7B/70B 模型是「上百层」而非「一层超宽」。
python# 深度优势的经典例子:奇偶校验(parity)。浅网需指数宽度,深网可线性宽度
import numpy as np
from itertools import product
def parity_dataset(n):
    X = np.array(list(product([0, 1], repeat=n)))
    return X, X.sum(1) % 2

for n in [4, 8, 12]:
    X, y = parity_dataset(n)
    print(f"n={n:2d}: 样本数={len(y):5d}  深度 2 网络所需宽度 ~ 2^{n}")

# 预期输出:
#   n= 4: 样本数=   16  深度 2 网络所需宽度 ~ 2^4
#   n= 8: 样本数=  256  深度 2 网络所需宽度 ~ 2^8
#   n=12: 样本数= 4096  深度 2 网络所需宽度 ~ 2^12
# 结论:拟合 parity,浅网参数需求随 n 指数爆炸;深网可用 O(n) 宽度 + O(n) 层达成

一个量化直觉:固定参数量预算时,「深而窄」通常优于「浅而宽」。经验上在图像与语言任务里,把同样的参数从「1 层 × 超宽」改成「多层 × 适中宽度」,验证误差往往明显更低——因为层级组合让同一组原语被反复复用,参数效率更高。这也是所有现代主干网(ResNet / Transformer / Mamba)都是「多层堆叠」而非「单层巨宽」的根本原因。

1.2 深度、组合泛化与层级特征

pythonimport torch, torch.nn as nn
# 残差与否,观察「最浅层」拿到的梯度范数(能否训深的关键)
def first_layer_grad(residual=True, depth=40, dim=64):
    net = nn.Sequential(*[nn.Sequential(nn.Linear(dim, dim), nn.ReLU()) for _ in range(depth)])
    x = torch.randn(8, dim, requires_grad=True)
    h = x
    for blk in net:
        h = h + blk(h) if residual else blk(h)     # 有 / 无残差
    h.sum().backward()
    return x.grad.norm().item()

print("with  residual:", round(first_layer_grad(True), 4))
print("without residual:", round(first_layer_grad(False), 6))
# 预期:有残差梯度保持同量级(~1e-1);无残差随深度指数衰减(~1e-6 甚至更小)
# 结论:残差提供的「恒等捷径」是上百层可训的根本原因,也是 Transformer 每个子层的标配

2026 的一个延伸方向是让深度「按需分配」:Mixture-of-Depths(MoD)对每个 token 学习「是否经过这一层」,用 router 决定跳过或执行,从而在推理时把算力集中在难 token 上;同理 MoE 是「按需激活专家」。二者都在回答同一个问题——不是所有输入都值得同样的深度与容量,这比单纯堆层更省算力。

1.3 动手练习与自测

✔
动手练习与自测(第 1 章):理论题写出结论即可,代码题建议真跑一遍看梯度范数。
  1. 用 parity 数据集说明为什么「浅而宽」需要指数宽度。答案:n 位 parity 用深度 2 拟合需 ~2^n 个线性区,宽度指数增长;深网 O(n) 宽度即可。
  2. 跑 first_layer_grad(True) 与 (False),解释两者量级差异。判据:无残差时梯度随深度指数衰减(可达 1e-6 以下)。
  3. 说明残差连接为什么等价于「学残差而非学完整映射」。答案:恒等项提供梯度直达通道,目标退化为 F(x)=H(x)−x。
  4. 解释固定参数量下「深而窄」为何常优于「浅而宽」。答案:层级组合让原语被复用,参数效率更高。
  5. 描述 Mixture-of-Depths 的核心思想,并说明它省的是什么。答案:router 决定 token 是否经过某层,省的是「不必要的深度算力」。
  6. 举出「弱归纳偏置 + 大数据」胜出与「强偏置 + 小数据」胜出各一例。答案:前者=纯 Transformer 大规模预训练;后者=CNN 小图像数据集。

2. 初始化:信号不爆炸也不消失

知识结构图 · 初始化
初始化3 大知识域 · 14 个知识点
Xavier / He / 正交Xavier 2/(n_in+n_out)He 2/n_in 补偿 ReLU 砍半正交奇异值≈1残差输出缩放 1/√NLLaMA 固定缩放 0.02
学习路径
  1. 读 2.1:背下 Xavier 2/(n_in+n_out)、He 2/n_in、正交奇异值≈1 的方差建议
  2. 用随机网络打印逐层激活方差,验证 He 对 ReLU 的补偿来自 2/n_in
  3. 完成 2.3 自测:推导 σ_w² 表达式并说明先查初始化再调 lr
✔ 能写出各初始化方差公式,并打印逐层激活方差验证守恒或对照
核心知识点详解
  • 三种初始化方差:Xavier Var=2/(n_in+n_out)(tanh/sigmoid)、He Var=2/n_in(ReLU 砍半故加倍补偿)、正交奇异值≈1(RNN/深层);大模型常在残差分支乘 1/√N 或固定 0.02 控累加方差。
  • 逐层方差 probe:打印 layer{10,20} 的激活 var:He 下三层同量级,默认 init 则逐层漂移(过小→消失、过大→爆炸);用 nn.init.kaiming_normal_ 实现。
  • 先查初始化再调 lr(坑):「深层训不动」第一步检查初始化方差是否守恒,而不是无脑调 lr;打印逐层激活方差只需十几行,常省几轮盲调。
全零失败与方差守恒对称权重无法打破Var(y)=n_in·σ_w²·σ_x²前向 / 反向守恒推导打印逐层激活方差先查初始化再调 lr
学习路径
  1. 读 2.2:理解全零初始化为何失败(对称无法打破),推导 Var(y)=n_in·σ_w²·σ_x²
  2. 跑全零 vs 随机初始化对比,观察梯度是否卡死不更新
  3. 对深层网络做方差守恒检查,先定位初始化问题再调学习率
  4. 完成 2.3 自测:解释 RNN 为何偏好正交初始化
✔ 能推导方差守恒并复现全零初始化失败,把梯度消失归因到初始化而非 lr
核心知识点详解
  • 全零初始化必然失败:对称权重→对称梯度→无法打破对称,网络退化为一个神经元;这是唯一绝对不能做的初始化。复现:n=all zeros 下梯度卡死不更新。
  • 方差守恒推导:Var(y)=n_in·σ_w²·σ_x²;令输出=输入方差得 σ_w²=1/n_in(Xavier 折中 2/(n_in+n_out)、He 因 ReLU 砍半取 2/n_in);复现中 2/d 下 out_var≈输入量级,1/d 则信号消失、1.0 爆炸。
  • 归一化+lr 是持续的调节(坑):初始化只解决「第一步不崩」;训练中 BatchNorm/RMSNorm、lr、残差仍持续调方差。别把后续梯度消失全甩给初始化——稳定靠三者配合。
动手自测要点推导 σ_w² 表达式probe 逐层方差RNN 偏好正交
学习路径
  1. 过一遍 2.3 自测,推导 σ_w² 并打印 probe 逐层方差
✔ 自测命中判据,方差 probe 与推导能亲手完成
核心知识点详解
  • σ_w² 推导题:Var(y)=n_in·σ_w²·σ_x²→令其=σ_x² 得 σ_w²=1/n_in;Xavier 取 2/(n_in+n_out)、He 取 2/n_in。写全三步推导才算过。
  • probe 判读:He 下各层激活 var 同量级、默认 init 逐层漂移;单调衰减→换 He/Xavier/正交,或加归一化层。
  • RNN 为何正交(坑):正交矩阵奇异值≈1,反复相乘不衰减不放大,故循环结构用正交而非 Xavier/He;只答「梯度更稳」不点奇异值会被追问。
学习路径

2.1 Xavier / He / 正交初始化

初始化方差建议适配激活直觉
Xavier / GlorotVar(W)=2/(n_in+n_out)tanh / sigmoid / 对称激活让前向方差与反向梯度方差都≈守恒
He / KaimingVar(W)=2/n_inReLU 系(ReLU/LeakyReLU)ReLU 砍掉一半神经元,故方差要加倍补偿
正交 OrthogonalW 为随机正交矩阵RNN / 深层 / 循环保持奇异值≈1,避免反复相乘后信号衰减/放大
缩放初始化(GPT 系)按残差层数缩放,如 1/√NPre-LN Transformer深层残差累加需控方差,防止每层叠加爆掉
pythonimport torch, torch.nn as nn

def init_weights(m):
    if isinstance(m, nn.Linear):
        # ReLU 系用 Kaiming;tanh/sigmoid 用 Xavier
        nn.init.kaiming_normal_(m.weight, mode="fan_in", nonlinearity="relu")
        if m.bias is not None:
            nn.init.zeros_(m.bias)             # 偏置通常置 0 即可
    elif isinstance(m, nn.Conv2d):
        nn.init.kaiming_normal_(m.weight, mode="fan_in", nonlinearity="relu")

# 正交初始化:用于 RNN / 深层,保持传播稳定
nn.init.orthogonal_(m.weight)

# 大模型常用:把残差分支的最后一层缩放(如 *0.02),避免初始阶段残差累加炸掉
#   LLaMA 类做法:residual = x + scale * Attn/MLP(dropout(Norm(x)))

现代 LLM(如 LLaMA / Qwen3 / DeepSeek-V4)常在残差分支的输出上乘一个小缩放系数(如 1/√N 或固定 0.02),再叠加到主路——这是对「深层累加方差」的工程补偿,和 He 初始化的思想同源。

pythonimport torch, torch.nn as nn
# 不同初始化下,各层激活方差是否守恒(用同一个输入跑 20 层)
def probe(init_fn, depth=20, dim=256):
    torch.manual_seed(0)
    net = nn.Sequential(*[nn.Linear(dim, dim) for _ in range(depth)])
    net.apply(init_fn)
    x = torch.randn(512, dim)
    with torch.no_grad():
        for i, l in enumerate(net):
            x = torch.relu(l(x))
            if i in (0, 10, depth - 1):
                print(f"  layer{i:2d} var={x.var().item():.4f}")

print("He (kaiming):")
probe(lambda m: nn.init.kaiming_normal_(m.weight) if isinstance(m, nn.Linear) else None)
print("default pytorch init:")
probe(lambda m: None)

# 预期:He 下三层 var 大致同量级(ReLU 砍半后趋于稳定);
# 默认 init 在深网络里 var 逐层漂移(过小→信号消失,过大→爆炸)

经验取值:LLaMA 系常把残差分支的输出做 1/√(2N) 缩放(N 为层数),或在残差投影上乘一个固定小系数(如 0.02),使初始阶段深层累加不发散。这类「按深度缩放」的初始化是 Pre-LN 深层网络能稳定训练的重要一环,和方差守恒推导一脉相承。

2.2 为什么全零初始化失败,以及方差守恒推导

全零(或全等常数)初始化会让所有神经元学到完全相同的东西:对称权重 → 对称梯度 → 永远无法打破对称,网络退化为「一个神经元」。这是唯一绝对不能做的初始化。

text方差守恒推导(前向,单层线性 + 激活前):
设输入 x 各维独立、均值 0、方差 σ_x²;权重 W_ij ~ i.i.d. 均值 0、方差 σ_w²;bias=0。
   y_j = Σ_i W_ij x_i
   E[y_j] = 0
   Var(y_j) = Σ_i E[W_ij²] E[x_i²] = n_in · σ_w² · σ_x²     (独立性)
要「输出方差 = 输入方差」,即 Var(y_j) = σ_x²,需:
   n_in · σ_w² = 1   →   σ_w² = 1 / n_in        (Xavier forward 视角)
反向时同理,希望梯度方差守恒,需 σ_w² = 1 / n_out。
折中(Xavier):σ_w² = 2 / (n_in + n_out)。
ReLU 下 E[ReLU(z)²] = ½ E[z²],故 He 改成 σ_w² = 2 / n_in。
→ 结论:合理的初始化让「每层输出的方差大致守恒」,既不爆炸也不消失。
⚠
初始化只是起点:即使初始化完美,训练中的 BatchNorm / RMSNorm、学习率、残差连接仍在持续调节方差。初始化解决的是「第一步不崩」;稳定训练靠的是归一化 + 合理学习率 + 残差。三者缺一个,深层网络都难训。
pythonimport torch
torch.manual_seed(0)
d = 512
x = torch.randn(256, d)
print("in_var = %.4f" % x.var().item())
# 对比三种缩放下的输出方差(用 ReLU 激活)
for scale, name in [(1.0 / d, "1/d  (Xavier 偏小)"),
                    (2.0 / d, "2/d  (He)"),
                    (1.0,     "1.0  (未缩放)")]:
    W = torch.randn(d, d) * (scale ** 0.5)      # 方差 = scale
    y = torch.relu(x @ W.T)
    print(f"{name:18s} out_var = {y.var().item():.4f}")

# 预期输出(示意):
#   in_var = 1.0000
#   1/d  (Xavier 偏小) out_var = 0.0019   <- 信号几乎消失
#   2/d  (He)          out_var = 0.9837   <- 与输入同量级,守恒
#   1.0  (未缩放)      out_var = 251.3    <- 爆炸
# 结论:让 n_in·σ_w² ≈ 常数,是「每层输出方差守恒」的工程落地

记住这条推导的用途:任何「深层网络训不动」的诊断,第一步都可以先检查初始化方差是否守恒——打印各层激活方差,若逐层单调衰减或放大,问题往往就在这里,而不是学习率。这个检查只需十几行代码,却常能省下几轮盲调。

2.3 动手练习与自测

✔
动手练习与自测(第 2 章):推导题写出关键式子,代码题看各层方差数值。
  1. 推导前向方差守恒,给出 σ_w² 的表达式。答案:Var(y)=n_in·σ_w²·σ_x²;令其为 σ_x² 得 σ_w²=1/n_in;Xavier 取 2/(n_in+n_out),He 取 2/n_in。
  2. 解释为什么全零初始化必然失败。答案:对称权重→对称梯度,无法打破对称,网络退化为单神经元。
  3. 跑 probe() 对比 He 与默认 init 的逐层方差。判据:He 各层同量级,默认 init 逐层漂移。
  4. 说明残差输出缩放的动机。答案:深层残差累加会使方差随层数增长,按 1/√N 缩放可保持稳定。
  5. 给出「激活方差逐层单调衰减」的排查与修复动作。答案:换 He/Xavier/正交初始化,或加归一化层。
  6. 说出 RNN 为何偏好正交初始化。答案:正交矩阵奇异值≈1,反复相乘不衰减/放大。

3. 激活函数:非线性从哪来

知识结构图 · 激活函数
激活函数3 大知识域 · 14 个知识点
ReLU 系ReLU 死亡神经元LeakyReLU α≈0.01PReLU / ELUGELU / SiLU 平滑死亡比例 >0.7 异常
学习路径
  1. 读 3.1:区分 ReLU / LeakyReLU(α≈0.01)/ GELU / SiLU,记死亡神经元成因
  2. 统计每层 ReLU 死亡比例,判断是否 >0.7 需换激活或降 lr
  3. 完成 3.3 自测:说出死亡神经元三种缓解手段
✔ 能打印逐层死亡比例,超标时给出换 SiLU/降 lr/加归一化等对的动作
核心知识点详解
  • ReLU 死亡神经元:x<0 时梯度恒 0,参数永久不更新=「死掉」。三次缓解:降 lr、换 LeakyReLU/GELU/SiLU 给负区梯度、加 BatchNorm 把激活推离危险区。
  • 死亡比例判据:统计每层 ReLU 输出为 0 的占比((h<=0).float().mean());健康 <0.5,长期 >0.7 即大片死亡,需换激活/降 lr/加归一化。
  • 2026 平滑激活成主流:CNN 用 SiLU、LLM 用 GELU/SiLU——负区有非零梯度不会同类 ReLU 永久沉默,且平滑使混合精度/二阶优化更稳;代价是少量 sigmoid/tanh 计算。
GLU 与 SwiGLUGLU 逐元素门控SwiGLU hidden=8d/3hidden=4d 多 50% 参数FFN 占 2/3 参数LLaMA / Qwen / DeepSeek 标配
学习路径
  1. 读 3.2:默写 GLU 门控公式,记 SwiGLU 等价参数量需 hidden=8d/3
  2. 跑参数量对比代码,验证标准 MLP(8d²) 与 SwiGLU(4d 为 12d²) 差异
  3. 完成 3.3 自测:手算 d=4096 时标准 MLP 与 hidden=4d 的参数量
  4. 对接 M7:确认训练管线里 FFN 采用 SwiGLU hidden=8d/3,参数/显存估算与论文对齐
✔ 能手算 SwiGLU 参数量并用 hidden=8d/3 保持与 MLP 等价,交代 FFN 占比 2/3
核心知识点详解
  • GLU 门控公式:y=W3(σ(W1x) ⊙ W2x),用一路信号闸控另一路,表达力更强、训练更稳;SwiGLU 把 sigmoid 换成 SiLU,成为 LLaMA/Qwen3/DeepSeek 的 FFN 标配。
  • SwiGLU 参数陷阱:标准 4d MLP 参数 8d²;SwiGLU 有 3 个矩阵,hidden=4d 时是 12d²(多 50%),故等价必须 hidden=8d/3≈2.67d 才能回到 8d²(d=4096 时 hidden=4d 多约 1670 万参数)。
  • FFN 占比约 2/3(坑):现代 LLM 的 FFN 参数量约占总参数 2/3(注意力因 GQA/KV 共享占比下降),故 hidden 取多少直接决定模型总规模和显存估算——读配置要逐项核对 hidden,别想当然。
动手自测要点死亡神经元三缓解SwiGLU 参数量计算 8d²平滑激活更稳原因
学习路径
  1. 过一遍 3.3 自测,手算 SwiGLU 参数量并解释 GLU 门控优势
✔ 自测命中判据,SwiGLU 参数与平滑激活稳定性能说清
核心知识点详解
  • d=4096 手算题:标准 MLP=2·(d·4d)=8d²≈1.34e8;hidden=4d 的 SwiGLU=12d²≈2.01e8(多 50%);会写出 3·(d·h) 展开才算过。
  • 等价那笔账:3·(d·h)=8d² → h=8d/3;用 hidden=8d/3 替换 4d MLP 参数量一致、显存估算才对得上论文。
  • 平滑更稳的原因(坑):GELU/SiLU 负区有梯度避免 ReLU 式永久死亡,且平滑避免硬截断在低精度下的梯度稀疏与数值跳变;别只说「更稳」不给机制。
学习路径

3.1 ReLU 系与死亡神经元问题

激活公式问题 / 优点
ReLUmax(0, x)便宜;但「死亡神经元」:x<0 时梯度恒 0,过大学习率会整片神经元永久沉默
LeakyReLUmax(αx, x), α≈0.01给负区一个小斜率,缓解死亡
PReLU负区斜率可学习更灵活,但多参数、易过拟合小数据
ELUx<0 时 α(e^x−1)负区平滑趋于 −α,均值更接近 0
GELUx·Φ(x) ≈ 0.5x(1+tanh(√2/π·x))平滑、类 ReLU 但带概率门控,Transformer 主流
SiLU / Swishx·σ(βx)自门控、平滑,LLM 与视觉都常用

ReLU 的「死亡神经元」:当某神经元输入长期 < 0,其梯度恒为 0,参数不再更新,等于这个神经元“死掉”且不可复活。缓解:① 降学习率;② 用 LeakyReLU / GELU / SiLU 给负区留梯度;③ 用 BatchNorm 把激活推离危险区。2026 年新模型基本不用裸 ReLU,CNN 用 SiLU、LLM 用 GELU/SiLU。

pythonimport torch, torch.nn as nn
# 统计每层 ReLU 的「死亡比例」:输出为 0 的神经元占比
net = nn.Sequential(nn.Linear(128, 256), nn.ReLU(),
                    nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 10))
x = torch.randn(512, 128)
with torch.no_grad():
    h = x
    for m in net[:-1]:
        h = m(h) if not isinstance(m, nn.ReLU) else m(h)
        if isinstance(m, nn.ReLU):
            print("zero fraction = %.3f" % (h <= 0).float().mean().item())

# 经验判据:健康值 < 0.5(ReLU 理论上一半为负);
# 若长期 > 0.7,说明「大片死亡」,应换 GELU/SiLU、降学习率、或加归一化

为什么 2026 的激活偏好平滑函数:GELU / SiLU 在负区有非零梯度,不会像 ReLU 那样「一旦进入负区就永久沉默」;同时它们的平滑性让二阶优化与混合精度下的数值更稳。代价是多一点计算(sigmoid / tanh 近似),但在 Tensor Core 上这点开销远小于稳定性收益。

3.2 GLU / SwiGLU:为什么 2026 的 LLM 都用 SwiGLU

GLU(Gated Linear Unit)用「一路信号去闸控另一路」,比普通 MLP 表达力更强且训练更稳。SwiGLU 是 GELU 化的门控,已成为 LLaMA / Qwen3 / DeepSeek 等现代 LLM 的 FFN 标准。

pythonimport torch, torch.nn as nn, torch.nn.functional as F

class GLU(nn.Module):
    def __init__(self, d, h):
        super().__init__()
        self.w1 = nn.Linear(d, h, bias=False)   # 主路
        self.w2 = nn.Linear(d, h, bias=False)   # 门控
        self.w3 = nn.Linear(h, d, bias=False)
    def forward(self, x):
        return self.w3(self.w1(x) * self.w2(x))               # 逐元素门控

class SwiGLU(nn.Module):
    def __init__(self, d, hidden=None):
        super().__init__()
        # 关键:普通 4d FFN 参数量 = 2·(d·4d) = 8d²;
        # SwiGLU 有 3 个矩阵(w_gate, w_up, w_down),要“等价参数量”需 hidden = 8d/3
        hidden = hidden or int(d * 8 / 3)        # ≈ 2.67d,而非 4d
        self.w_gate = nn.Linear(d, hidden, bias=False)
        self.w_up   = nn.Linear(d, hidden, bias=False)
        self.w_down = nn.Linear(hidden, d, bias=False)
    def forward(self, x):
        return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x))
FFN 类型参数量(以 d 为基准)备注
标准 MLP (4d)2 · (d·4d) = 8d²无门控
SwiGLU (hidden=4d)3 · (d·4d) = 12d²参数量变大,常缩 hidden
SwiGLU (hidden=8d/3)3 · (d·8d/3) = 8d²与标准 MLP 等价参数量,主流取法
★
SwiGLU 的参数比例陷阱:很多人在实现 LLM 时直接把 SwiGLU 的 hidden 设成 4d,结果 FFN 参数量从 8d² 暴涨到 12d²(多了 50%),总参数与论文对不上、显存估算全错。要等价替换标准 4d MLP,必须 hidden = 8d/3(≈2.67d)。这是读 LLaMA / Qwen 配置时最容易踩的坑。
pythonimport torch.nn as nn
d = 4096
cnt = lambda m: sum(p.numel() for p in m.parameters())

mlp  = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d))
swi  = nn.Sequential(nn.Linear(d, int(8 * d / 3), bias=False),
                     nn.Linear(d, int(8 * d / 3), bias=False),
                     nn.Linear(int(8 * d / 3), d, bias=False))
swi4 = nn.Sequential(nn.Linear(d, 4 * d, bias=False),
                     nn.Linear(d, 4 * d, bias=False),
                     nn.Linear(4 * d, d, bias=False))

print("标准 MLP (4d)      :", cnt(mlp))       # 约 3.36e7(8d^2)
print("SwiGLU (hidden=8d/3):", cnt(swi))       # 约 3.36e7(8d^2,等价)
print("SwiGLU (hidden=4d)  :", cnt(swi4))      # 约 5.03e7(12d^2,多 50%)

# 结论:d=4096 时,hidden=4d 的 SwiGLU 比等价配置多约 1670 万参数,
# 直接导致总参数、显存估算、训练成本全部对不上论文——务必用 hidden=8d/3

顺带记一个 2026 的组成部分:现代 LLM 的 FFN 参数量约占总参数的 2/3(注意力层因 GQA 与 KV 共享占比下降)。所以 FFN 的 hidden 取多少,直接决定模型总规模——这也是为什么各家的「7B」在 FFN 比例上略有差异,读配置时必须逐项核对。

3.3 动手练习与自测

✔
动手练习与自测(第 3 章):推导与计算题给出结论,代码题看输出数值。
  1. 解释 ReLU 死亡神经元的成因与三种缓解手段。答案:负区梯度恒 0;缓解=降 lr、换 Leaky/GELU/SiLU、加归一化。
  2. 给出「死亡比例」的健康阈值与超标时的动作。判据:>0.7 长期维持即异常,需换激活或降 lr。
  3. 写出 GLU 的门控公式并说明比普通 MLP 强在哪。答案:y=W3(σ(W1x)⊙W2x);门控让信息选择性通过,表达力更强、更稳。
  4. 计算 d=4096 时标准 MLP 与 hidden=4d 的 SwiGLU 参数量。答案:分别约 8d² 与 12d²,后者多 50%。
  5. 说明为什么等价替换要用 hidden=8d/3。答案:SwiGLU 有 3 个矩阵,3·(d·h)=8d² 时 h=8d/3。
  6. 解释 GELU/SiLU 比 ReLU 在混合精度下更稳的原因。答案:平滑、负区有梯度,避免硬截断带来的梯度稀疏与数值跳变。

4. 归一化:训练稳定的命脉

知识结构图 · 归一化
归一化3 大知识域 · 15 个知识点
四种归一化BatchNorm running_mean/varLayerNorm 特征维RMSNorm 只除 RMSGroupNorm 小 batch忘记 eval() 后果RMSNorm 快约 20%
学习路径
  1. 读 4.1:区分 BatchNorm/LayerNorm/RMSNorm/GroupNorm 的统计维度与训练推理差异
  2. 跑 LN vs RMSNorm 耗时对比,记住 eval 期 running_mean/var 必须切对模式
  3. 完成 4.3 自测:说明各归一化统计维度及 RMSNorm 快约 20% 的原因
✔ 能写出各归一化公式与统计维度,实测 RMSNorm 更快并解释 LLM 为何选它
核心知识点详解
  • 四种归一化的统计维度:BatchNorm 沿 batch 维(训练用批统计、推理用 running_mean/var);LayerNorm 沿特征维(与 batch 无关);RMSNorm 只除 RMS 不中心化;GroupNorm 通道分组。小 batch/长序列用 LN 系。
  • RMSNorm 快在省减均值:x·rsqrt(x.pow(2).mean(-1)+eps)·w,比 LayerNorm 少一次「减均值」广播减法,且对 BF16 友好——实测约快 20%(如 0.046ms→0.037ms)。
  • BatchNorm 的坑:batch 太小时批统计噪声大;推理必须 model.eval() 切到 running 统计量,否则输出随 batch 抖动——这是最经典 bug 源。Generator/RNN 逐样本场景用 LN 而非 BN。
Pre-LN vs Post-LNPre-LN 梯度直通Post-LN 需长 warmupwarmup 占 1–3%QK-Norm 抑 loss spikelogit 软上限 tanh
学习路径
  1. 读 4.2:理解 Pre-LN 梯度直通与 Post-LN 需长 warmup 的原因
  2. 对比 Pre-LN vs Post-LN 在深模型上是否需 warmup,验证收敛差异
  3. 完成 4.3 自测:给出 warmup 占步数 1–3% 的经验值与 QK-Norm 抑制 loss spike 作用
  4. 对接 M7:确认训练管线用 Pre-LN + RMSNorm + warmup 标配(约 1–3% 步数)
✔ 能说明 Pre-LN 为何免长 warmup、RMSNorm+Pre-LN 为何是 2026 训练标配
核心知识点详解
  • Pre vs Post 的梯度差异:Post-LN 的 LN 在残差「里面」,梯度被其缩放、深层不稳需暖启动;Pre-LN 的恒等捷径不受 LN 扰动,梯度直达第 1 层,warmup 可显著减小。Post-LN 常 warmup 2000 步/峰值 lr 1e-4,Pre-LN 200 步/3e-4。
  • warmup 经验值:warmup 占总步数 1–3%(Pre-LN 下常见 200–2000 步),峰值 lr 预训练 1e-4~6e-4、微调为其 1/10;配 cos 衰减。
  • QK-Norm 与 logits 软上限(坑):对 query/key 归一化 + tanh 软上限 logits(如 Gemma 2),主要解决训练后期注意力 logits 过大导致的 loss spike;读配置时 norm: "rmsnorm"+norm_type: "pre" 就是这套经验。
动手自测要点统计维度对比LN vs RMSNorm 耗时warmup 比例经验值
学习路径
  1. 过一遍 4.3 自测,实测 LN vs RMSNorm 耗时并解释 warmup 经验值
✔ 自测命中判据,能说清四种归一化差异与 warmup 经验
核心知识点详解
  • 维度对比题:BN 依赖 batch 维(训练)、LN/RMSNorm 走特征维、GN 通道分组;能不能逐条说清决定你过不过。
  • LN vs RMSNorm 耗时:实测 RMSNorm 约快 10–30%(省一次减均值广播);LLM 在 BF16 预训练里这点节省会累积成可观算力。
  • warmup 比例别忘(坑):warmup 约占总步数 1–3%;只背「1–3%」却不解释 Pre-LN 为何能更短,会被追问到机制。
学习路径

4.1 BatchNorm / LayerNorm / RMSNorm / GroupNorm

归一化统计维度训练 / 推理差异适用
BatchNormbatch 维(同通道跨样本)训练用 mini-batch 统计量;推理用滑动平均的全局统计量(running_mean/var)CNN(大 batch)
LayerNorm特征维(单样本内跨通道)与 batch 无关,训练推理一致Transformer / RNN / 小 batch
RMSNorm特征维,但只除 RMS、不做均值中心化训练推理一致,更省算力2026 LLM 主流(LLaMA/Qwen)
GroupNorm通道分组后组内统计与 batch 无关小 batch 视觉 / 检测
pythonimport torch, torch.nn as nn, torch.nn.functional as F

# RMSNorm:比 LayerNorm 更省算力(去掉均值中心化,只按均方根缩放)
class RMSNorm(nn.Module):
    def __init__(self, dim, eps=1e-6):
        super().__init__()
        self.eps, self.w = eps, nn.Parameter(torch.ones(dim))
    def forward(self, x):
        dtype = x.dtype
        x = x.float()
        x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
        return self.w * x.to(dtype)           # 可学习的逐通道缩放

# BatchNorm 的训练/推理差异:model.train() 用 batch 统计并更新 running;
# model.eval() 用 running_mean/var,因此「忘记 eval()」会让推理结果随 batch 抖动
bn = nn.BatchNorm2d(64)
bn.train();  out_train = bn(x)     # 用本批均值方差
bn.eval();   out_eval  = bn(x)     # 用累计 running 统计量
pythonimport torch, torch.nn as nn, time
# 对比 LayerNorm 与 RMSNorm 的耗时(同一 shape,H100 示意)
x = torch.randn(4096, 4096, device="cuda")
ln = nn.LayerNorm(4096).cuda()
rn = RMSNorm(4096).cuda()            # 见上方的 RMSNorm 实现

def bench(mod, n=200):
    for _ in range(10): mod(x)
    torch.cuda.synchronize(); t0 = time.time()
    for _ in range(n): mod(x)
    torch.cuda.synchronize(); return (time.time() - t0) / n * 1e3

print("LayerNorm %.3f ms   RMSNorm %.3f ms" % (bench(ln), bench(rn)))
# 典型:LayerNorm 0.046 ms,RMSNorm 0.037 ms(约快 20%)
# 差异来源:RMSNorm 少一次「减均值」的广播减法,且对 BF16 更友好
# 在几十层 × 上万步的预训练里,这点节省会累积成可观的算力
归一化是否减均值是否依赖 batchBF16 友好度
LayerNorm是否好
RMSNorm否(只除 RMS)否更好
BatchNorm是是(训练)一般(需同步)
GroupNorm是否好

4.2 Pre-LN vs Post-LN:训练稳定性的分水岭

Transformer 里归一化放哪,决定梯度能不能稳定传到浅层:

结构公式(每个子层)梯度行为现状
Post-LNx = x + Sublayer(LN(x))深层梯度经 LN 缩放易不稳,需暖启动 / 小心学习率早期 Transformer(2017)用的,现已少
Pre-LNx = x + Sublayer(x) 后再 LN,或 LN(x + Sublayer(x))残差路径上梯度不被 LN 缩放,更易训深、warmup 可更小2020 后主流,2026 LLM 默认
textPost-LN:   y = x + Attention(LayerNorm(x))        # LN 在残差“里面”,梯度过 LN 被缩放
Pre-LN:    y = x + Attention(x);  y = LayerNorm(y) # 或更常见 y = x + Attention(LayerNorm(x))
# 关键:Pre-LN 让「恒等捷径 x」在反向时不受 LN 扰动,梯度能直达第 1 层,
# 因此能稳定训上百层,且对学习率更鲁棒(warmup 步数可显著减少)。
★
为什么 LLM 用 RMSNorm + Pre-LN:两个决策叠加:① Pre-LN 提供稳定的梯度通道,让上百层可训;② RMSNorm 比 LayerNorm 更省算力且对 BF16 友好(去掉均值中心化)。二者结合是 LLaMA / Qwen3 / DeepSeek 的标配——你读它们配置时的 norm: "rmsnorm" 与 norm_type: "pre" 就是这条工程经验的最终形态。
python# Pre-LN 与 Post-LN 的 warmup / 峰值 lr 需求差异(大模型训练经验值)
configs = {
    "Post-LN": dict(warmup_steps=2000, peak_lr=1e-4, note="需小心初始化与 warmup,否则早期发散"),
    "Pre-LN":  dict(warmup_steps=200,  peak_lr=3e-4, note="梯度通道稳,warmup 与 lr 可放宽"),
}
for name, c in configs.items():
    print(f"{name:8s} warmup={c['warmup_steps']:<5d} peak_lr={c['peak_lr']:.0e}  {c['note']}")

# 预期输出:
#   Post-LN warmup=2000  peak_lr=1e-04  需小心初始化与 warmup,否则早期发散
#   Pre-LN  warmup=200  peak_lr=3e-04  梯度通道稳,warmup 与 lr 可放宽
# 经验:warmup 占总步数 1–3%;Pre-LN 下常见 warmup 200–2000 步、峰值 lr 1e-4 ~ 6e-4

另一条 2026 常见的稳定化技巧是QK-Norm(对注意力的 query/key 做归一化)与logit 软上限(tanh 缩放 logits,如 Gemma 2 与部分开源模型采用),它们主要解决「训练后期注意力 logits 过大导致 loss spike」的问题。配合 Pre-LN + RMSNorm,能让几十亿到上千亿参数的训练更少出现中途崩盘。

4.3 动手练习与自测

✔
动手练习与自测(第 4 章):理论题写结论,代码题看耗时与数值。
  1. 对比 BatchNorm / LayerNorm / RMSNorm / GroupNorm 的统计维度与 batch 依赖。答案:BN 依赖 batch 维;LN/RMSNorm 在特征维;GN 通道分组。
  2. 解释 BatchNorm 训练 / 推理差异,以及忘记 eval 的后果。答案:训练用 batch 统计并更新 running;推理用 running;忘记 eval 会让输出随 batch 抖动。
  3. 跑 LN 与 RMSNorm 的耗时对比,给出量级差。判据:RMSNorm 约快 10–30%,因省去减均值。
  4. 写出 Pre-LN 与 Post-LN 的子层公式并说明梯度差异。答案:Pre-LN 的恒等捷径不受 LN 扰动,梯度直达浅层。
  5. 给出 warmup 占总步数的经验比例。答案:1–3%。
  6. 解释 QK-Norm 与 logits 软上限要解决的问题。答案:抑制训练后期注意力 logits 过大导致的 loss spike。

5. PyTorch 核心与工程实践

知识结构图 · PyTorch 核心与工程实践
PyTorch 核心与工程实践5 大知识域 · 26 个知识点
训练循环与自动微分model.train / evaltorch.no_grad()zero_grad(set_to_none=True)grad_fn 动态计算图detach 截断梯度gradcheck 校验反向
学习路径
  1. 读 5.1:理解 model.train/eval、no_grad、zero_grad(set_to_none) 与动态计算图
  2. 手写完整训练循环(AMP + 累积 + 裁剪),跑通并观察 val_acc 曲线
  3. 用 gradcheck 校验自定义算子的反向实现,确认误差 <1e-6
  4. 对接 M7:用此循环实现 PyTorch 训练管线(AMP、梯度累积、断点续训)
✔ 能手写训练循环跑通,gradcheck 通过,且 no_grad/eval/zero_grad 三开关各司其职
核心知识点详解
  • 三个开关各管一件事:model.train()/eval() 管 Dropout/BatchNorm;torch.no_grad() 管是否建计算图(省显存提速);zero_grad(set_to_none=True) 释放内存更省。混用是 PyTorch bug 头号来源。
  • 五个常见错误:验证忘 no_grad()→显存爆;忘 zero_grad()→梯度累积;忘 model.eval()→指标抖动;累积忘除 accum→等效 lr 变大;.to(device) 与 pin_memory 错配。
  • gradcheck 校验反向:gradcheck(fn, (x,), eps=1e-6, atol=1e-4) 用数值梯度验证解析梯度;返回 False=backward 写错(典型如 autograd.Function 差一项)。必须用 float64,float32 会误报。
Dataset / DataLoadernum_workers = 物理核 / GPUpin_memory + non_blockingpersistent_workersprefetch_factorGPU 利用率锯齿诊断data-only 吞吐对比
学习路径
  1. 读 5.2:理解 num_workers / pin_memory / persistent_workers / prefetch_factor
  2. 跑 data-only 吞吐对比数据管线,判断 num_workers 是否够
  3. 用 nvidia-smi 观察 GPU 利用率,定位是否周期性掉 0(数据瓶颈)
  4. 对接 M7:为训练管线配好 DataLoader 参数,确保 GPU 利用率稳定高位
✔ 能让 GPU 利用率稳定不周期性掉 0,并用 data-only 吞吐量化数据管线是否瓶颈
核心知识点详解
  • DataLoader 参数清单:num_workers≈物理核/GPU、pin_memory=True+non_blocking=True 走快路径、persistent_workers=True 省 epoch 间重建、prefetch_factor=2 预取。别拍 32。
  • 锯齿=数据瓶颈:nvidia-smi 的 GPU 利用率周期性掉到 0 的锯齿几乎一定是数据管线(num_workers 不足或预处理太重);稳定高位但吞吐低则是算力/算子受限。前者优化数据、后者上 torch.compile。
  • data-only 吞吐诊断:只跑 loader 不算模型测吞吐;data-only < 训练吞吐 → 数据是瓶颈。修复顺序:升 num_workers→预处理放进 __getitem__→减增广重操作→pin_memory。num_workers 0→4 常把利用率从 ~30% 拉到 80%+。
AMP 与 torch.compileBF16 免 loss scalingGradScaler 仅 FP16autocast 只包前向+losstorch.compile 算子融合重编译陷阱显存降 55–65%
学习路径
  1. 读 5.3:理解 BF16 免 loss scaling、GradScaler 仅 FP16、autocast 只包前向+loss
  2. 把训练切到 autocast(bf16) + torch.compile,测量吞吐/显存提升
  3. 排查重编译陷阱,确认显存降 55–65%
  4. 对接 M7:训练管线默认开 BF16 AMP 与 torch.compile,记录速度显存数据
✔ 能开启 BF16/compile 并量化显存降幅与吞吐提升,避开重编译陷阱
核心知识点详解
  • autocast 的范围:只包「前向 + loss」,反向由 autograd 按对应精度走;optimizer.step 必须在 autocast 外(用 FP32 更新)。BF16 下loss.backward() 正常,不需要 GradScaler(FP16 才要)。
  • torch.compile 与重编译:torch.compile(model, mode="reduce-overhead") 做算子融合(LN+残差+激活合成少 kernel),大模型训练常提速 20–50%;坑:首次有编译开销、动态形状触发重编译,出诡异报错先关掉它定位。
  • 显存收益量级:BF16/FP16 峰值显存约为 FP32 的 55–65%、吞吐提升 1.5–2×;小模型/小 batch 可能因编译开销更慢,先跑几步 warmup 再计时再下结论。
断点续训与种子model / opt / sched / step / RNG / scaler原子保存 os.replacecudnn.deterministic续训后 loss 抖原因
学习路径
  1. 读 5.4:理解断点需保存 model/opt/sched/step/RNG/scaler 六类状态
  2. 实现带 os.replace 原子保存的 checkpoint,并在中断后精确续训
  3. 固定随机种子并验证续训后 loss 曲线连续
  4. 对接 M7:完成 checkpoint 续训与种子固定的可复现闭环
✔ 中断后从 checkpoint 续训 loss 连续、两跑指标 <1e-3,六类状态全部落盘
核心知识点详解
  • 断点必须存全 6 类:model / optimizer / scheduler / step / RNG / AMP scaler(+分布式 sampler epoch);少存 scheduler→lr 曲线跳变、少存 RNG→增强序列不一致,几乎所有「续训后 loss 抖」都是状态没存全。
  • 保留随机种子实现复现:set_seed 固定 Python/NumPy/Torch + cudnn.deterministic=True+benchmark=False,同环境两跑指标可 <1e-3;跨 GPU 型号/驱动仍可能有微小差,对比要在同环境。
  • 原子保存防半写(坑):先写 path.tmp 再 os.replace(tmp, path)(同文件系统原子操作),避免中断留下损坏 ckpt;加载后恢复 global_step 与 RNG 让数据顺序衔接。
动手自测要点手写训练循环autocast 范围六类状态清单
学习路径
  1. 过一遍 5.5 自测,手写训练循环并核对 autocast 范围与六类 checkpoint 状态
✔ 自测命中判据,能独立完成断电续训与复现验证
核心知识点详解
  • 手写循环判据:train()/eval()、no_grad()、zero_grad(set_to_none) 三处行为切换都正确、验证不建图;能写出含 autocast+累积+裁剪的完整 train() 才算过。
  • autocast 范围题:只包前向+loss,optimizer.step 放外面(FP32 更稳);这也是 FP16/FP8 反复翻车的常见点。
  • set_to_none vs 置 0(坑):set_to_none=True 释放内存、更省显存且略快,覆盖是把梯度写一遍 0;只会背「更快」不给内存原理会被追问。
学习路径

5.1 完整训练循环与自动微分

pythonimport torch, torch.nn as nn
from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
    def __init__(self, X, y): self.X, self.y = X, y
    def __len__(self): return len(self.X)
    def __getitem__(self, i): return self.X[i], self.y[i]

def train(model, loader, val_loader, epochs=5, lr=3e-4, device="cuda", accum=1):
    model.to(device)
    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs * len(loader))
    scaler = torch.amp.GradScaler("cuda")                        # 混合精度(FP16 时才需要)
    lossf = nn.CrossEntropyLoss(label_smoothing=0.1)

    for ep in range(epochs):
        model.train()
        for step, (x, y) in enumerate(loader):
            x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True)
            with torch.autocast("cuda", dtype=torch.bfloat16):
                loss = lossf(model(x), y) / accum                   # 梯度累积要除
            scaler.scale(loss).backward()
            if (step + 1) % accum == 0:
                scaler.unscale_(opt)
                torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)  # 防爆炸
                scaler.step(opt); scaler.update(); opt.zero_grad(set_to_none=True)
            sched.step()

        model.eval()
        with torch.no_grad():
            correct = total = 0
            for x, y in val_loader:
                p = model(x.to(device)).argmax(-1)
                correct += (p == y.to(device)).sum().item(); total += len(y)
        print(f"epoch {ep}  val_acc={correct/total:.4f}")

# 三个开关分别控制三件事:
#   model.train()/eval() -> Dropout、BatchNorm 行为
#   torch.no_grad()      -> 是否建计算图(省显存、提速)
#   set_to_none=True     -> 梯度清零更省内存
⚠
五个最容易犯的 PyTorch 错误:① 验证时忘记 no_grad() 导致显存爆;② 忘记 zero_grad() 导致梯度累积;③ 忘记 model.eval() 导致指标抖动(BN/Dropout);④ 梯度累积时忘记除 accum,等效学习率变大;⑤ 忘记 .to(device) 或用 non_blocking 却被开 pin_memory。
pythonimport torch
from torch.autograd import gradcheck
# 用数值梯度校验自定义算子 / 函数的反向实现(写新算子时的必备自测)
def sq(x):
    return x ** 2
x = torch.randn(5, 5, dtype=torch.double, requires_grad=True)
print("gradcheck:", gradcheck(sq, (x,), eps=1e-6, atol=1e-4))

# 预期输出:gradcheck: True   (解析梯度与数值梯度在容差内一致)
# 返回 False 说明反向写错了——典型如手写 autograd.Function 时 backward 少乘一项
# 注意:gradcheck 需 float64,float32 下容差会误报

自动微分的机制值得记牢:PyTorch 构建动态计算图,每个张量的 grad_fn 记录它是怎么来的;backward() 沿图反向做链式法则。因此 ① 每次前向都是一张新图,控制流(if / for)可随数据变化;② 图里任何一步用了 detach() 或 no_grad() 就会把梯度在那里截断(这是「参数没更新」类 bug 的头号来源);③ 不 backward 的图会被释放,所以要保留中间量就用 retain_graph=True。

5.2 Dataset / DataLoader:num_workers 与 pin_memory

设置作用坑
num_workers用多进程并行做数据预处理,避免 GPU 等数据过大→CPU/内存打满;Windows 下注意 fork 行为;进程里别持有 CUDA 张量
pin_memory=True把数据页锁定到锁页内存,host→device 拷贝更快(与 non_blocking 配合)仅 host→device 拷贝受益;占用锁页内存
persistent_workers=Trueworker 进程在 epoch 间不销毁,省重建开销worker 内若有状态要注意清理
prefetch_factor每 worker 提前加载的 batch 数太大占内存
drop_last丢弃最后不足 batch 的尾批分布式下常用,避免各卡步数不一致
pythonloader = DataLoader(
    ds, batch_size=64, shuffle=True,
    num_workers=4,                 # 一般 = CPU 物理核数 / GPU 数,别拍 32
    pin_memory=True,               # host→device 拷贝走快路径
    persistent_workers=True,
    prefetch_factor=2,
)
for x, y in loader:
    # 配合 pin_memory,这里用 non_blocking=True 异步拷贝
    x = x.to("cuda", non_blocking=True)
✔
DataLoader 的性能经验值:让 GPU 利用率(nvidia-smi 里的 %)稳定在高位而非频繁掉到 0,是数据管线合格的标准。若 GPU 利用率周期性掉到 0,多半是 num_workers 不够或预处理太重——把预处理尽量放到 Dataset 的 __getitem__ 里并行,而不是在主进程里串行做。
pythonimport time, torch
# 只跑数据、不算模型,测「数据管线吞吐」,判断它是否成为瓶颈
t0 = time.time(); n = 0
for x, y in loader:
    n += x.size(0)
    if n >= 4096: break
dt = time.time() - t0
print(f"data-only throughput = {n / dt:.0f} samples/s")

# 判据:data-only 吞吐 < 训练吞吐(样本/s)→ 数据管线是瓶颈
# 修复顺序:① 升 num_workers(到 CPU 核数级)② 预处理放进 __getitem__ 并行
#           ③ 减少在线增广的重操作 ④ pin_memory + non_blocking 异步拷贝
# 经验:num_workers 从 0 升到 4,GPU 利用率常从 30% 拉到 80% 以上
✔
一句话诊断数据瓶颈:看 nvidia-smi 的 GPU 利用率:若呈「周期性掉到 0」的锯齿,几乎一定是数据管线(num_workers 不足或预处理太重);若稳定高位但吞吐仍低,则是模型 / 算子在算力上受限。这个区分能把「优化方向」从瞎猜变成确定:前者优化数据,后者优化模型或上 torch.compile。

5.3 AMP 正确用法与 torch.compile

混合精度(AMP)在支持 Tensor Core 的 GPU 上用低精度做矩阵乘,吞吐翻倍、显存减半。2026 默认用 BF16(几乎不需 loss scaling),FP16 才需要 GradScaler。torch.compile 则把计算图编译优化(算子融合),常再提速 20–50%。

pythonimport torch

# BF16 混合精度(推荐,无需 loss scaling)
model = model.to("cuda")
model = torch.compile(model, mode="reduce-overhead")     # 一行接入编译器
opt = torch.optim.AdamW(model.parameters(), lr=3e-4)

for x, y in loader:
    x, y = x.to("cuda"), y.to("cuda")
    with torch.autocast("cuda", dtype=torch.bfloat16):
        loss = lossf(model(x), y)
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    opt.step(); opt.zero_grad(set_to_none=True)

# FP16 才需要 GradScaler(BF16 通常不需要,但加上也无害):
scaler = torch.amp.GradScaler("cuda")
with torch.autocast("cuda", dtype=torch.float16):
    loss = lossf(model(x), y)
scaler.scale(loss).backward()
scaler.unscale_(opt); clip_grad_norm_(...); scaler.step(opt); scaler.update()
pythonimport torch, torch.nn as nn
model = nn.Sequential(*[nn.Linear(2048, 2048) for _ in range(8)]).cuda()
x = torch.randn(64, 2048).cuda()

def peak_mb(dtype):
    torch.cuda.reset_peak_memory_stats()
    with torch.autocast("cuda", dtype=dtype):
        for _ in range(50):
            model(x).sum().backward()
    return torch.cuda.max_memory_allocated() / 2 ** 20

print("bf16 peak MB = %.0f" % peak_mb(torch.bfloat16))
print("fp16 peak MB = %.0f" % peak_mb(torch.float16))

# 典型:BF16/FP16 峰值显存约为 FP32 的 55–65%,吞吐提升 1.5–2×
# 注意:autocast 只包前向与 loss;optimizer.step 必须留在 autocast 之外(用 FP32 更新)
# 为什么能省:矩阵乘输入被转为低精度,中间激活与算子输出也走低精度

torch.compile 的收益要分清场景:大模型训练多在 20–50% 提速,小模型 / 小 batch 上可能因为编译与重编译开销而变慢。判断方法是先跑几步 warmup 再计时。它的主要贡献是算子融合(把 LayerNorm + 残差 + 激活合成更少 kernel)与减少 Python 侧调度开销,对显存也有小幅帮助。

5.4 断点续训与随机种子固定

pythonimport os, torch, random, numpy as np

def set_seed(seed=0):
    random.seed(seed); np.random.seed(seed)
    torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)
    # 以下两项让卷积 / 注意力结果在 GPU 上也可复现(代价是速度略降)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

def save_ckpt(path, model, opt, sched, step, scaler=None):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ckpt = {"model": model.state_dict(), "opt": opt.state_dict(),
            "sched": sched.state_dict(), "step": step,
            "rng": torch.get_rng_state()}      # 随机状态也要存,保证可复现
    if scaler: ckpt["scaler"] = scaler.state_dict()
    torch.save(ckpt, path)

def load_ckpt(path, model, opt=None, sched=None, scaler=None):
    ckpt = torch.load(path, map_location="cpu")
    model.load_state_dict(ckpt["model"])
    if opt:   opt.load_state_dict(ckpt["opt"])
    if sched: sched.load_state_dict(ckpt["sched"])
    if scaler and "scaler" in ckpt: scaler.load_state_dict(ckpt["scaler"])
    torch.set_rng_state(ckpt["rng"])
    return ckpt.get("step", 0)
⚠
可复现性的残酷真相:固定 Python/NumPy/Torch 种子 + 关 cudnn.benchmark,能让同硬件同版本结果高度可复现,但跨 GPU 型号、跨 CUDA 版本、跨节点仍可能有微小差异(浮点求和顺序不同)。所以实验对比要在「同一环境」下做,并报告均值±标准差而不是单次。这点直接决定你的消融实验结论是否站得住。
pythonimport os, torch
# 原子保存:先写临时文件再 os.replace,避免训练中断留下「半写」的损坏 ckpt
def atomic_save(ckpt, path):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    tmp = path + ".tmp"
    torch.save(ckpt, tmp)
    os.replace(tmp, path)            # 同一文件系统内是原子操作

ckpt = torch.load("ckpt/last.pt", map_location="cpu")
assert ckpt["step"] > 0, "checkpoint 未保存 step,无法正确续训"
print("resume from step", ckpt["step"])

# 断点必须存全:model / opt / sched / step / RNG / scaler / (分布式) sampler 的 epoch
# 少存 scheduler 状态 → 续训后 lr 曲线跳变;少存 RNG → 数据增强序列不一致
# 经验:几乎所有「续训后 loss 抖一下」的诡异现象,都是状态没存全
要保存的状态不保存的后果频率
model.state_dict()无法恢复,全废每个 eval 周期 / 固定步数
optimizer.state_dict()Adam 动量丢失,loss 抖一下同上
scheduler.state_dict()lr 曲线跳变,warmup 重来同上
RNG(torch/numpy/random)增强 / dropout 序列不一致,结果不可复现同上
global_step / epoch数据顺序错位,重复或漏样本同上
AMP scaler / 分布式 samplerFP16 缩放丢失、多卡数据不均同上

5.5 动手练习与自测

✔
动手练习与自测(第 5 章):这章都建议动手写,训练循环是后面所有阶段的基础设施。
  1. 不查文档手写一个含 model.train/eval、no_grad、zero_grad(set_to_none) 的训练循环。判据:三处行为切换都正确,验证不建图。
  2. 用 gradcheck 验证一个自定义函数,并说明返回 False 意味着什么。答案:反向实现与数值梯度不一致,即反向写错。
  3. 设计一个实验判断数据管线是否瓶颈,给出修复顺序。答案:测 data-only 吞吐对比训练吞吐;升 num_workers → 预处理入 Dataset → 异步拷贝。
  4. 说明 autocast 应包住哪些代码,为什么 optimizer.step 要在外面。答案:只包前向+loss;参数更新需 FP32 精度以保证数值稳定。
  5. 写出断点必须保存的六类状态,并各举一个不保存的后果。判据:model/opt/sched/step/RNG/scaler 齐全。
  6. 解释 set_to_none=True 与把梯度置零为 0 的性能差异。答案:前者释放内存、更省显存且略快,后者需写一遍零。

6. 训练工程:在有限资源下把事做成

知识结构图 · 训练工程
训练工程5 大知识域 · 25 个知识点
混合精度FP32 1/8/23FP16 上限 65504 需缩放BF16 范围同 FP32FP8 E4M3 前向 / E5M2 梯度2026 默认 BF16 套餐
学习路径
  1. 读 6.1:背下 FP32/FP16/BF16/FP8 的动态范围与位宽,理解 2026 默认 BF16
  2. 跑 FP16 vs BF16 溢出对比(70000 在 FP16 变 inf),确认 BF16 免缩放
  3. 对接 M7:训练管线默认 BF16,必要时再评估 FP8 前置条件
✔ 能说清 FP16 需 loss scaling 而 BF16 不需(范围同 FP32),并执行正确混合精度
核心知识点详解
  • FP16 vs BF16 的核心差别:FP16 是 1/5/10、最大 65504(70000 溢出成 inf),必须 GradScaler 防溢出;BF16 是 1/8/7、动态范围同 FP32(约 3e38),几乎免缩放,这就是它成为 2026 默认的原因。
  • FP8 的分工:E4M3(±448)用于前向/权重、E5M2(±57344)用于梯度,由 Transformer Engine/torchao 动态维护逐层缩放;需 Hopper+ 硬件与数值纪律,新手先吃透 BF16 再碰 FP8。
  • 求稳不求极致(坑):混合精度不是越低越好——低精度放大未归一化 softmax/极端激活的不稳;老硬件(无 BF16)才用 FP16+GradScaler。2026 默认套餐=BF16+RMSNorm+Pre-LN+SwiGLU+AdamW。
省显存三件套梯度累积 loss 除 accum梯度裁剪 clip_grad_norm梯度检查点时间换空间sched 按等效步调SyncBatchNorm 或 RMSNorm
学习路径
  1. 读 6.2:理解梯度累积(loss 除 accum)、梯度裁剪、梯度检查点三件套
  2. 实现等效大 batch 的梯度累积,注意 sched 按等效步调、loss 除 accum
  3. 开梯度检查点测显存峰值降幅(约 1/4),代价是慢 25–40%
  4. 对接 M7:训练管线集成累积+裁剪+检查点,解决显存不够的大 batch 需求
✔ 能写出正确梯度累积与裁剪,并量化梯度检查点的显存-时间权衡
核心知识点详解
  • 梯度累积的两个要点:loss 要除 accum(等效 batch=微批×accum,否则等效 lr 变大);sched.step() 按等效步走否则 warmup 被放大 accum 倍。BN 统计基于小估,多卡大 effective batch 用 SyncBatchNorm 或 RMSNorm。
  • 梯度裁剪防爆炸:clip_grad_norm_(params, 1.0) 超界则整体缩放,默认防梯度爆炸;阈值过大会失去作用、过小会扭曲更新——战斗经验 0.5–5 起步。
  • 梯度检查点的权衡(坑):反向时重算中间激活、只存边界——激活显存约降到 1/3–1/4(如 1.8GB→0.4GB),但代价是反向多算、整体慢 25–40%;用 torch.utils.checkpoint.checkpoint 或 model.gradient_checkpointing_enable()。
显存估算参数 + 梯度 + 优化器 + 激活AdamW 12B / 参数7B 全量 112 GB激活 ≈ 2·S·B·L·h·kKV Cache 显存公式
学习路径
  1. 读 6.3:背下训练显存 = 参数+梯度+优化器(AdamW 12B)+激活,激活≈2·S·B·L·h·k
  2. 用公式估 7B 全量训练(约 112 GB)并对照实际小时曲线
  3. 对接 M7:为训练管线估好显存,让 batch/accum/检查点选择有的放矢
✔ 能分项推导 7B 显存 ≈ 112 GB 的来源,并对任意模型给出显存预算
核心知识点详解
  • 训练显存五项:训练显存 = 参数 + 梯度 + 优化器状态 + 激活;参数/梯度各 2B(BF16)、AdamW 优化器 12B/参数(FP32 主副本+m+v),前三项与规模线性、激活随 batch×seq 长。
  • 必须记住的 112 GB:7B BF16 全量训练基础 = 7e9×(2+2+12)/1e9 ≈ 112 GB(参数 14 + 梯度 14 + Adam 84),未计激活就超单张 80GB——所以 7B 全量只能 FSDP/ZeRO-3 或多卡,单卡只能 LoRA/QLoRA。
  • KV Cache 公式与长上下文:KV = 2·L·n_kv_head·head_dim·seq·batch·bytes:LLaMA-3-8B(32 层×8 KV 头)每请求 32K 上下文约 4GB、128K 约 16GB;GQA 共享 8 头比 MHA 省 4×——长上下文瓶颈常是 KV 而非权重。
分布式训练DP vs DDP all-reduceFSDP / ZeRO-1/2/3张量并行需 NVLink流水线并行 bubbleZeRO-3 8 卡 14 GB / 卡
学习路径
  1. 读 6.4:区分 DP / DDP(all-reduce) / FSDP / ZeRO-1/2/3 各切什么
  2. 用 FSDP 切 7B 微调,观察每卡显存下降与通信开销
  3. 估算 ZeRO-3 8 卡每卡约 14 GB(实为更小),对照单卡全量
  4. 对接 M7:为单个 GPU 放不下的模型选对并行方案并记录显存数据
✔ 能说清四种并行各切参数/梯度/优化器哪块,并亲手估出多卡显存分摊
核心知识点详解
  • 各并行切什么:DDP每卡全副本只 all-reduce 梯度;ZeRO-1 切优化器状态、ZeRO-2 +梯度、ZeRO-3/FSDP +参数(用时 all-gather);张量并行切层内权重矩阵;流水线按层切 stage。
  • 选型一句话:能 DDP 就 DDP;放不下用 FSDP/ZeRO-3;单层放不下用张量并行(要 NVLink);跨节点极深用流水线。2026 默认栈:FSDP2 + torch.compile + BF16。
  • 多卡显存分摊(坑):7B 基础 112GB 按卡数线性下降:8 卡 FSDP/ZeRO-3 → 每卡约 14GB 基础;all-reduce 带宽 ∝ 2(N−1)/N·P,张量并行走 PCIe 会比 NVLink 慢数倍——先把账算清再选并行。
动手自测要点FP16 为何需缩放7B 显存推导32K KV 约 4 GB
学习路径
  1. 过一遍 6.5 自测,动手推导 7B 显存并算 32K KV Cache
✔ 自测命中判据,显存分解推导能独立完成
核心知识点详解
  • FP16 为何要缩放:FP16 指数位仅 5、上限 65504,梯度平方累加极易溢出;BF16 指数位 8、范围同 FP32 故免缩放。写好「指数位决定可表示范围」这句即得分。
  • 7B 显存推导:7e9×(2参数+2梯度+12Adam)/1e9 ≈ 112GB,再加激活超 80GB——只有 FSDP/ZeRO-3 或 LoRA 能落地。能逐项写出而非背结论是关键。
  • 32K KV 估算(坑):LLaMA 8B(GQA 8 头)32K 上下文每请求约 4GB;别漏 2×(K/V)+层数+KV头数 的系数组合,漏乘往往差 4×。
学习路径

6.1 混合精度:FP16 / BF16 / FP8 的动态范围与取舍

格式位宽(符号/指数/尾数)动态范围取舍
FP321/8/23±3.4e38基准精度,优化器状态常用
FP161/5/10最大 ~65504省显存但易溢出,需 loss scaling
BF161/8/7±3.4e38(同 FP32)范围大不溢出、几乎免缩放;尾数少精度略低;2026 LLM 默认
FP8 (E4M3)1/4/3±4482× 吞吐与显存;范围小,需 TE 缩放,喂 forward/权重
FP8 (E5M2)1/5/2±57344范围大,用于梯度(Transformer Engine 自动分配)

FP8 在 H100 / H200 / B200 上已是 2026 前沿训练格式:E4M3 用于前向与权重(精度优先)、E5M2 用于梯度(范围优先),由 NVIDIA Transformer Engine 或 torchao 自动管理逐层缩放因子(delayed scaling)。它的代价是精度余量更小,对数值不稳定(如未归一化的 softmax、极端激活)更敏感,通常需要配合 RMSNorm + 稳定初始化才敢上。

python# 用 torchao / Transformer Engine 开启 FP8(需 Hopper+ 架构)
# 概念示意:前向用 E4M3、梯度用 E5M2,缩放因子由引擎动态维护
from torchao.float8 import Float8LinearConfig   # 或 import transformer_engine as te

# 更常用的「省心」路径:BF16 全量混合精度(绝大多数 7B 级训练足够)
with torch.autocast("cuda", dtype=torch.bfloat16):
    loss = model(x, y).loss
# 经验:先把 BF16 跑稳,再考虑 FP8 榨性能——FP8 是有经验的团队的“最后一公里”。
★
2026 默认配方:BF16 训练 + RMSNorm + Pre-LN + SwiGLU + AdamW 解耦权重衰减 + warmup+余弦,是 2026 主流 LLM 预训练的「标准套餐」。FP8 是进一步省算力的选项,但需要 Hopper 级硬件与数值纪律,新手先吃透 BF16。
pythonimport torch
# 一句话看清 FP16 与 BF16 的根本区别:指数位决定「能表示多大」
print(torch.tensor([70000.0], dtype=torch.float16))    # tensor([inf]) —— 溢出
print(torch.tensor([70000.0], dtype=torch.bfloat16))   # tensor([70000.]) —— 正常

# FP16: 1/5/10 → 最大 65504,故必须 loss scaling 防溢出
# BF16: 1/8/7  → 范围同 FP32(约 3.4e38),几乎不需缩放
# FP8 : E4M3 范围 ±448(前向/权重);E5M2 范围 ±57344(梯度)
# 2026 前沿:H100/H200/B200 上用 FP8 前向 + BF16 主副本,吞吐再翻倍,
# 但需逐层缩放(delayed scaling)与稳定初始化,属「有经验团队的最后一公里」

选择口诀:求稳先 BF16,求快再试 FP8,只有在老硬件(无 BF16)才用 FP16 + GradScaler。混合精度不是「越低越好」——低精度会放大数值不稳(未归一化的 softmax、极端激活、小 batch 的 BN),所以上 FP8 前先确保 BF16 已经跑得又稳又复现。

6.2 梯度累积、梯度裁剪、梯度检查点

手段公式 / 机制省的是哪块代价
梯度累积等效 batch = batch × accum;每 accum 步才 step降低激活峰值(无大 batch 的显存)训练变慢 accum 倍;BN 统计失真
梯度裁剪clip_grad_norm_(p, max_norm),超界则整体缩放防梯度爆炸(不稳训练)阈值要调,过大会失效
梯度检查点反向时重算中间激活,只存部分激活值(最大变量)时间换空间,约慢 20%–40%
ZeRO / FSDP参数/梯度/优化器状态分片到多卡四类显存全降通信开销
python# 梯度累积:等效大 batch 但显存按小 batch 走
accum, max_norm = 4, 1.0
for i, (x, y) in enumerate(loader):
    with torch.autocast("cuda", dtype=torch.bfloat16):
        loss = lossf(model(x), y) / accum          # 关键:loss 要除以 accum
    loss.backward()
    if (i + 1) % accum == 0:
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
        opt.step(); opt.zero_grad(set_to_none=True)

# 梯度检查点:用算力换显存(只存边界激活,反向重算中间)
model.gradient_checkpointing_enable()           # HF 模型一行开启
# 或手动:
def forward(self, x):
    return torch.utils.checkpoint.checkpoint(self.block, x)
✔
累积的隐藏坑:梯度累积时学习率调度也要按「等效步」走——sched.step() 应每 accum 步调一次,而不是每 micro-batch。否则 warmup 长度被悄悄放大 accum 倍,学习率曲线全错。同样,BatchNorm 的统计量在累积下基于小 batch 估计,多卡 + 大 effective batch 时建议用 SyncBatchNorm 或换 RMSNorm(又一个用 RMSNorm 的理由)。
pythonimport torch, torch.nn as nn
blk = nn.Sequential(nn.Linear(1024, 1024), nn.ReLU(), nn.Linear(1024, 1024)).cuda()

def peak_mb(use_ckpt, depth=20, bs=64, seq=256):
    torch.cuda.reset_peak_memory_stats()
    x = torch.randn(bs, seq, 1024, requires_grad=True).cuda()
    for _ in range(depth):
        x = torch.utils.checkpoint.checkpoint(blk, x) if use_ckpt else blk(x)
    x.sum().backward()
    return torch.cuda.max_memory_allocated() / 2 ** 20

print("no-checkpoint %6.0f MB" % peak_mb(False))
print("grad-ckpt      %6.0f MB" % peak_mb(True))
# 典型:激活显存从 ~1.8 GB 降到 ~0.4 GB(约 1/4),代价是反向多算,整体慢 ~25–40%
# 机制:只保存每段边界激活,反向时重算段内中间量——典型的「时间换空间」
优化手段省的是哪块显存代价 / 量级
梯度累积激活峰值(等效大 batch)时间 × accum
梯度检查点中间激活(最大变量)时间 +25–40%
BF16 / FP8参数、梯度、激活、部分优化器数值精度余量
FSDP / ZeRO-3参数 + 梯度 + 优化器状态通信开销
FlashAttention-3注意力矩阵 O(T²) → O(T)需 Hopper+(FP8 更快)

6.3 显存估算:算清每一块

训练显存 = 参数 + 梯度 + 优化器状态 + 激活值。前三项与模型规模线性相关,激活值随 batch × 序列长度增长,往往是大头。

组成部分估算(每参数)说明
参数 Parameters2 B(BF16)/ 4 B(FP32)推理只需这一项
梯度 Gradients2 B(BF16)反向必存
优化器状态(AdamW)12 BFP32 主副本 4B + m 4B + v 4B
激活值 Activations≈ 2·S·B·L·h·k(k≈10–12,BF16)最大变量;梯度检查点可砍
pythondef train_memory_gb(P, batch=8, seq=2048, L=32, h=4096, bits=2):
    """P = 参数量(如 7e9)。粗略估算全量训练显存(不含激活)。"""
    param = P * bits / 1e9                 # 参数(BF16=2B)
    grad  = P * bits / 1e9                 # 梯度
    optim = P * 12 / 1e9                   # AdamW: fp32 m+v+master = 12B/param
    base  = param + grad + optim
    # 激活(简化):与 batch*seq*L*h 成正比,k≈10
    act   = 2 * batch * seq * L * h * 10 * bits / 1e9
    return dict(param_gb=round(param,1), grad_gb=round(grad,1),
                optim_gb=round(optim,1), act_gb=round(act,1), total_gb=round(base+act,1))

print(train_memory_gb(7e9))
# 典型结果:param≈14GB, grad≈14GB, optim≈84GB → 基础≈112GB(不含激活)
# 结论:单张 80GB A100 放不下 7B 全量微调 → 必须 ZeRO-3 / FSDP 或 LoRA
# LoRA 只训 rank=8 的少量参数 → 优化器状态只针对那几 MB → 显存骤降,可在单卡跑
★
一个必须记住的数字:7B 模型 BF16 全量训练约需 112 GB(仅参数+梯度+Adam 状态,未计激活)。所以「在单张 80GB 卡上全量微调 7B」是不可能的——要么上 2×80GB + ZeRO-3/FSDP,要么用 LoRA/QLoRA 把优化器状态压到可忽略。理解这个公式,你就不会在采购 / 排期时拍错脑袋。
python# KV Cache 显存公式(推理长上下文的头号开销)
def kv_cache_gb(n_layer, n_kv_head, head_dim, seq, batch=1, bytes_per=2):
    # 2 (K 和 V) × L × n_kv_head × head_dim × seq × batch × dtype_bytes
    return 2 * n_layer * n_kv_head * head_dim * seq * batch * bytes_per / 2 ** 30

# LLaMA-3-8B 用 GQA:32 层,8 个 KV 头,head_dim=128,BF16
for ctx in [8192, 32768, 131072]:
    print(f"ctx={ctx:7d}  每请求 KV≈{kv_cache_gb(32, 8, 128, ctx):5.2f} GB")

# 预期输出:
#   ctx=   8192  每请求 KV≈ 1.00 GB
#   ctx=  32768  每请求 KV≈ 4.00 GB
#   ctx= 131072  每请求 KV≈16.00 GB
# 关键:KV 与上下文长度线性增长;GQA 把 8 个 KV 头(而非 32 个 Q 头)共享,
#       相比 MHA 直接省 4×;这就是长上下文服务显存的主要来源与优化重点

两条 2026 的实用结论:① 全量微调 7B 的 112 GB 是「训练」账,与推理的显存需求完全是两码事——推理 7B(BF16 权重 14 GB)+ 一份 KV 就够,量化到 INT4 权重只要约 4 GB;② 长上下文服务的瓶颈往往不是权重而是 KV Cache,所以 GQA / MQA、KV 量化(FP8 KV)、PagedAttention(vLLM)才是长上下文的关键技术,而不是单纯加显存。

6.4 分布式训练:DP / DDP / FSDP / ZeRO 各切什么

策略切分对象通信 / 带宽适用
DP(DataParallel)每卡全量副本,仅梯度汇到主卡求和主卡瓶颈,Python 开销大玩具 / 单机调试,已不推荐
DDP每卡全量副本,反向后 all-reduce 梯度(平均)all-reduce 带宽 ∝ 2(N−1)/N·P单机多卡 / 多机,最常用
FSDP / ZeRO-1切分优化器状态同 DDP + 状态分片通信显存略省
ZeRO-2切分优化器状态 + 梯度多一截梯度 all-gather省更多显存
ZeRO-3 / FSDP切分优化器 + 梯度 + 参数(用时 all-gather)前向/反向各需参数聚合超大数据 / 单卡放不下
张量并行 TP单层权重矩阵按列/行切到多卡层内高带宽(需 NVLink)单层都放不下的巨模型
流水线并行 PP按层切成若干 stage,微批流水stage 间点对点,有 bubble跨节点、极深模型
pythonimport torch
from torch.distributed import init_process_group
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import CPUOffload, MixedPrecision

# FSDP2(2026 推荐,逐参数分片,比旧 FSDP 更省显存更灵活)
model = FSDP(model, use_orig_params=True)         # 包装即分片,训练代码几乎不变

# DDP(更轻量,每卡全副本,仅梯度 all-reduce)
init_process_group("nccl")
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[rank])

# 多机通信的关键:带宽。all-reduce 总传输量约 2(N−1)/N · 参数大小;
# 张量并行若走的是 PCIe 而非 NVLink,会慢数倍 → 巨模型必须 NVLink 互联节点内。
✔
选型一句话:能 DDP 就 DDP(每卡放得下副本时最简单最快);放不下用 FSDP / ZeRO-3(分片参数);单层都放不下用 张量并行(要 NVLink);跨节点极深用 流水线并行补位。2026 的 FSDP2 已能覆盖绝大多数 7B–70B 训练,配合 torch.compile 与 BF16 是默认栈。
python# ZeRO 分片后「每卡基础显存」随卡数下降(以 7B BF16 全量训练为例)
P = 7e9
base_gb = P * (2 + 2 + 12) / 1e9        # 参数 2B + 梯度 2B + Adam 12B = 112 GB
print("未分片基础显存 ≈ %.0f GB" % base_gb)
for N in [1, 2, 4, 8, 16]:
    print(f"ZeRO-3 / {N:2d} 卡 → 每卡基础 ≈ {base_gb / N:6.1f} GB")

# 预期输出:
#   未分片基础显存 ≈ 112 GB
#   ZeRO-3 /  1 卡 → 每卡基础 ≈  112.0 GB   <- OOM
#   ZeRO-3 /  2 卡 → 每卡基础 ≈   56.0 GB
#   ZeRO-3 /  4 卡 → 每卡基础 ≈   28.0 GB
#   ZeRO-3 /  8 卡 → 每卡基础 ≈   14.0 GB   <- 8×80GB 有充足余量放激活
# 结论:7B 全量微调,8 卡 FSDP/ZeRO-3 是现实起点;单卡只能 LoRA/QLoRA
并行 / 分片切分对象通信代价现实起点
DDP不分片,梯度 all-reduce低(每次 2(N−1)/N·P)每卡放得下副本
ZeRO-1/2优化器状态 / +梯度中省显存但参数仍全量
FSDP / ZeRO-3参数 + 梯度 + 优化器高(前反向各 all-gather)7B–70B 全量微调
张量并行 TP层内权重矩阵极高(需 NVLink)单层放不下
流水线并行 PP按层切 stage点对点,有 bubble跨节点极深模型
★
7B 全量微调的现实路径:把两个公式连起来看:训练基础显存 = (2+2+12) B/参数 ≈ 16 B/参数(BF16 参数与梯度 + FP32 Adam),加上激活后 7B 全量需 ≳120 GB。落地路径按显存从紧到松依次是:QLoRA(单卡 24–48 GB)→ LoRA(单卡 80 GB)→ 8×80GB + FSDP2 → 更大集群。先算清账再选路径,比 OOM 到怀疑人生高效得多。

6.5 动手练习与自测

✔
动手练习与自测(第 6 章):这章是「显存与性能」的硬功夫,务必自己算一遍。
  1. 解释 FP16 为何需要 loss scaling 而 BF16 不需要。答案:FP16 指数位 5,最大约 65504,梯度易溢出;BF16 指数位 8,范围同 FP32。
  2. 写出 FP8 E4M3 与 E5M2 的分工与范围。答案:E4M3(±448)用于前向/权重;E5M2(±57344)用于梯度。
  3. 梯度检查点省多少显存、代价多少时间。答案:激活显存可降到约 1/3–1/4,反向多花 25–40% 时间。
  4. 推导 7B 全量训练的基础显存,并说明为何单卡 80GB 不够。答案:16 B/参数 ≈ 112 GB,再加激活更超 80GB。
  5. 用 KV Cache 公式估算 8B 模型(GQA 8 头)在 32K 上下文下的每请求显存。答案:约 4 GB。
  6. 列举三种省显存手段并说明各自代价。判据:累积(时间×accum)、检查点(时间+25–40%)、FSDP(通信)。

7. 卷积与归纳偏置:为 ViT 铺垫

知识结构图 · 卷积与归纳偏置
卷积与归纳偏置3 大知识域 · 13 个知识点
卷积归纳偏置局部性 + 平移等变权值共享省参数感受野递推 R_l经典架构演进
学习路径
  1. 读 7.1:理解局部性 + 平移等变性 + 权值共享,背下感受野递推 R_l 公式
  2. 手算 3 层 3×3 卷积感受野(1→3→5→7),跑卷积栈验形状与参数量
  3. 完成 7.2 自测:解释权值共享为何让参数量与输入分辨率无关
✔ 能手算感受野并跑出卷积栈 shape,说出局部性/平移等变的参数效率来源
核心知识点详解
  • 两大归纳偏置:局部性(有意义的模式是局部的)+ 平移等变性(位置移动后仍是同一东西)+ 权值共享 让参数量远小于同能力全连接:224×224×3 接 128 的全连接一层就要 ~2.3e7 参数,而卷积栈仅 8 万多。
  • 感受野递推:R_l=R_{l-1}+(k_l−1)·j_{l-1}、j_l=j_{l-1}·s_l;3 层 3×3 步长 1 得 R=1→3→5→7——第 3 层一个神经元「看到」原图 7×7,深层覆盖更大区域是层级特征的基础。
  • shape 与参数量(坑):权重是 (out,in,k,k),Conv2d(3,64,7,s=2,p=3) 后 224→112;参数量与输入分辨率无关(只取决于核/通道),这是和全连接的质变。
迁移学习与配方冻结主干 / 部分 / 全量微调ConvNeXt 对标 ViTRandAugment / Mixup / CutMix课程学习加速收敛小数据 CNN 胜 ViT
学习路径
  1. 读 7.1:区分冻结主干 / 部分 / 全量微调,及 RandAugment/Mixup/CutMix 强度
  2. 用预训练主干跑三种迁移方案,记录小数据下的精度差异
  3. 完成 7.2 自测:解释课程学习为何加速收敛、小数据为何 CNN 胜 ViT
  4. 完成 7.2 自测:补上手算感受野与卷积栈形状
✔ 能按数据量选对迁移深度并跑出对比,说清小数据下 CNN 归纳偏置优势
核心知识点详解
  • 迁移三分法:按数据量×与预训练域相似度决定微到哪层:冻结主干(小数据/域接近)、部分微调(解冻后几层+更小 lr)、全量微调(大数据/域差大+极小 lr+早停);几乎总是优于从头训练。
  • 2026 视觉配方:强数据增强(RandAugment/Mixup/CutMix 常 +2–5 点)+ 迁移学习 + 混合精度;课程学习先低分辨率/简单样本逐步提高可显著缩短收敛步数。
  • 小数据 CNN 胜 ViT(坑):现代视觉 ViT 主导(大数据超越 CNN),但数据极少时 CNN 的归纳偏置仍是优势(Val 方差小);别在小数据上硬训 ViT——这就是阶段 11「小数据用 CNN、大数据用 ViT」的伏笔。
动手自测要点3 层 3×3 感受野 7×7卷积栈输出 shape增强强度参数
学习路径
  1. 过一遍 7.2 自测,手算感受野/shape 并确认增强强度
✔ 自测命中判据,感受野与卷积栈 shape 能独立求出
核心知识点详解
  • 感受野手算:3 层 3×3 步长 1:R=1→3→5→7,完全靠 R_l=R_{l-1}+(k−1)·j_{l-1} 递推;给够公式不给过程会被扣分。
  • shape 别算错:(输入−k+2p)/s+1:224→112→56;跑出 [1,64,112,112]/[1,128,56,56]、参数量 83136 与之核对。
  • 增强强度与迁移(坑):Mixup α≈0.2/CutMix≈1.0 常用;数据量决定迁移深度、不要所有场景都全量微调——小数据全量微调比冻结主干更易过拟合。
学习路径

7.1 局部性、平移等变性与感受野计算

CNN 的两大先验是局部性与平移等变性:图像中有意义的模式(边缘、纹理、部件)都是局部的,且在位置移动后仍是同一东西。这使 CNN 参数量远小于同能力的全连接网络,也自带「权值共享」的参数效率。

text感受野(receptive field)计算(卷积 + 步长链):
设第 l 层感受野 R_l、跳跃 j_l(输入像素到该层的步距)、核 k_l、步长 s_l:
  j_l   = j_{l-1} · s_l
  R_l   = R_{l-1} + (k_l − 1) · j_{l-1}
例:3 层 3×3 卷积、步长全 1、起始 R_0=1, j_0=1:
  l1: j=1, R=1+(3−1)·1=3
  l2: j=1, R=3+(3−1)·1=5
  l3: j=1, R=5+2=7   → 第 3 层一个神经元“看到”原图 7×7
→ 深层神经元覆盖更大区域,这是层级特征的基础。
python# 迁移学习三分法:数据量 × 与预训练域的相似度,决定微调到哪一层
import torch, torch.nn as nn
from torchvision import models

backbone = models.convnext_tiny(weights="IMAGENET1K_V1")
for p in backbone.parameters():
    p.requires_grad = False                      # 1) 冻结主干,只训分类头(小数据)
head = nn.Sequential(nn.LayerNorm(768), nn.Linear(768, num_classes))
opt = torch.optim.AdamW(head.parameters(), lr=1e-3)
# 2) 部分微调:解冻后几层 + 更小学习率(中等数据)
# 3) 全量微调:全解冻 + 极小学习率 + 早停(大数据 / 域差异大)
pythonimport torch, torch.nn as nn
# 卷积栈的形状与参数量:权值共享让参数量与图像大小无关
conv = nn.Sequential(nn.Conv2d(3, 64, 7, stride=2, padding=3), nn.ReLU(),
                     nn.Conv2d(64, 128, 3, stride=2, padding=1))
x = torch.randn(1, 3, 224, 224)
print([tuple(t.shape) for t in [conv[0](x), conv(x)]])
print("params:", sum(p.numel() for p in conv.parameters()))

# 预期输出:
#   [(1, 64, 112, 112), (1, 128, 56, 56)]
#   params: 9408 + 73728 = 83136
# 对比:同样把 224×224×3 接到 128 维的全连接,光一层就要 ~2.3e7 参数
# 这就是「局部 + 权值共享」的归纳偏置带来的参数量级优势

2026 视觉训练的默认配方是「强数据增强 + 迁移学习 + 混合精度」:RandAugment / Mixup / CutMix 组合常带来 2–5 个点提升;主干用预训练(timm / CLIP 视觉塔)后小数据也能 fine-tune;再加课程学习(先用低分辨率或简单样本,逐步增加分辨率 / 难度)可显著缩短收敛步数。若数据极少,CNN 的归纳偏置仍可能胜过 ViT——这正是阶段 11 多模态里「小数据为何用 CNN、大数据为何用 ViT」的伏笔。

7.2 动手练习与自测

✔
动手练习与自测(第 7 章):计算题给出数值,实践题写结论。
  1. 手算 3 层 3×3、步长 1 卷积的感受野。答案:1→3→5→7,第 3 层感受野 7×7。
  2. 解释 CNN 的两大归纳偏置及各自带来的好处。答案:局部性(参数量小)、平移等变性(位移不变、权值共享)。
  3. 算出给定卷积栈的输出 shape 与参数量。答案:输入 224 → 112 → 56;参数量与输入分辨率无关。
  4. 说明「冻结主干 / 部分微调 / 全量微调」的适用条件。答案:按数据量与域相似度决定,数据少则冻结。
  5. 列出三种 2026 常用视觉增强及其典型强度。答案:RandAugment、Mixup(α≈0.2)、CutMix(≈1.0)。
  6. 解释课程学习为什么能加速收敛。答案:先易后难,早期梯度方向更稳定、避免一开始就被难例带偏。

8. RNN → Attention:一个必然的演进

知识结构图 · RNN → Attention
RNN → Attention3 大知识域 · 13 个知识点
RNN 的三硬伤串行难并行长距梯度消失固定容量瓶颈LSTM / GRU 门控缓解
学习路径
  1. 读 8.1:背下 RNN 三硬伤(串行/长距梯度消失/固定容量),记住 LSTM 只缓解第二点
  2. 跑 seq2seq+Attention 最小实现,对比 LSTM 与 Attention 的耗时与可并行性
✔ 能列出 RNN 三硬伤并说清为何 Attention 以路径长 1 + 可并行取代它
核心知识点详解
  • RNN 三硬伤:①串行难并行(按时间步递推,训练慢);②长距梯度消失(记不住久远信息);③固定容量瓶颈(所有历史塞进一个向量)。LSTM/GRU 门控只缓解②,没解决①③。
  • 注意力为何取代它:注意力不做压缩、直接让每个位置查看所有位置并加权——天然可并行,且任意两位置路径长都是 1,长距离依赖不再是问题。这是 Transformer 取代 RNN 的根本原因。
  • 递归 vs 检索(坑):RNN 用递归压缩历史(容量有限、难并行),注意力用检索访问历史(路径最短、可并行);只背「可并行」不点「路径长 1/检索」会被追问机制。
注意力与演进注意力并行 + 路径最短Bahdanau 加性注意力注意力 O(T²) 显存FlashAttention-3 在线 softmax流匹配 4–8 步采样
学习路径
  1. 读 8.1:理解注意力并行 + 路径最短 + O(T²) 显存,认识 FlashAttention 在线 softmax
  2. 完成自测:对比 LSTM 与 Attention 在长程任务上的耗时与梯度径
  3. 完成自测:说明「递归压缩」与「检索式访问」的本质差别
✔ 能说清注意力为何路径最短且可并行,并给出 O(T²) 复杂度的解法方向
核心知识点详解
  • 注意力复杂度代价:注意力需 O(T²) 的分数矩阵闪烁空间(T=序列长),RNN 只要 O(T) 状态;同期序列下靠矩阵乘并行,Attention 比串行 LSTM 快数倍(如 0.90ms vs 3.10ms)。
  • FlashAttention 解法:.softmax 分块 + 在线 softmax(不落全矩阵),把注意力显存从 O(T²) 降到 O(T);H100 FP8 下比朴素快 2–4×,是长上下文训练的事实标准。
  • 后 Transformer 方向(坑):后续架构(稀疏/线性注意力、Mamba、FlashAttention)本质都在降注意力二次代价;流匹配/rectified flow 用「学连续速度场」把生成采样从数十步降到 4–8 步——别把它们当完全无关的新东西。
动手自测要点RNN 三硬伤清单LSTM vs Attention 耗时递归 vs 检索差别
学习路径
  1. 过一遍 8.2 自测,重点对比 LSTM vs Attention 与递归 vs 检索
✔ 自测命中判据,能说清注意力取代 RNN 的必然性
核心知识点详解
  • 三硬伤清单:串行难并行、长距梯度消失、固定容量;LSTM/GRU 只解决第二点——这点最容易答错/漏。
  • 耗时对比判读:同期序列下 Attention 靠矩阵乘并行,比串行 LSTM 快若干倍;代价换显存 O(T²)。能报出量级比方向更可信。
  • 递归 vs 检索一句话(坑):前者压缩历史、容量有限;后者检索历史、路径最长 1;只背「压缩/检索」不配机制会被追问。
学习路径

8.1 序列建模的困境与注意力的诞生

RNN 的设想是「用一个隐藏状态记住历史」,但它有三个硬伤:串行难以并行(训练慢)、长距离依赖梯度消失(记不住久远信息)、固定容量瓶颈(所有历史塞进一个向量)。LSTM / GRU 用门控缓解了第二点,但没有解决第一点和第三点。

注意力机制的思路完全不同:不做压缩,直接让每个位置去「查看」所有位置并加权汇总。它天然可并行,且任意两个位置的路径长度都是 1,长距离依赖不再是问题。这就是 Transformer 取代 RNN 的根本原因——也是阶段 10 的起点。

python# seq2seq + Attention 的最小实现:理解「注意力在做什么」
import torch, torch.nn as nn, torch.nn.functional as F

class BahdanauAttention(nn.Module):
    def __init__(self, hidden):
        super().__init__()
        self.W_q = nn.Linear(hidden, hidden, bias=False)
        self.W_k = nn.Linear(hidden, hidden, bias=False)
        self.v   = nn.Linear(hidden, 1, bias=False)
    def forward(self, dec_h, enc_out, mask=None):
        score = self.v(torch.tanh(self.W_q(dec_h).unsqueeze(1) + self.W_k(enc_out)))
        score = score.squeeze(-1)                                    # (B, T)
        if mask is not None:
            score = score.masked_fill(~mask, -1e9)                   # 屏蔽 padding
        w = F.softmax(score, dim=-1)                                 # 注意力权重
        return (w.unsqueeze(1) @ enc_out).squeeze(1), w              # 加权求和 = 上下文
★
从 RNN 到 Transformer,一句话总结:RNN 用递归压缩历史,注意力用检索访问历史。前者容量有限且难并行,后者可并行且路径最短。理解这一点,你就能理解为什么后续所有架构演化(FlashAttention、稀疏注意力、线性注意力、Mamba)本质上都是在降低注意力的二次复杂度代价。
pythonimport torch, torch.nn as nn, time
B, T, D = 8, 512, 128
rnn  = nn.LSTM(D, D, batch_first=True).cuda()
attn = nn.MultiheadAttention(D, 8, batch_first=True).cuda()
x = torch.randn(B, T, D).cuda()

def bench(fn, n=20):
    for _ in range(5): fn()
    torch.cuda.synchronize(); t0 = time.time()
    for _ in range(n): fn()
    torch.cuda.synchronize(); return (time.time() - t0) / n * 1e3

print("LSTM (串行) %.2f ms" % bench(lambda: rnn(x)))
print("Attention(并行) %.2f ms" % bench(lambda: attn(x, x, x)))

# 预期输出(示意):同序列长度下注意力靠矩阵乘并行,比串行 RNN 快数倍
#   LSTM (串行) 3.10 ms
#   Attention(并行) 0.90 ms
# 代价:注意力需要 O(T²) 的分数矩阵(显存),RNN 只需 O(T) 状态
# 2026 优化:FlashAttention-3 用分块 + 在线 softmax 把注意力的显存降到 O(T) 线性,
#   并在 H100 上以 FP8 跑到比朴素实现快 2–4×,是长上下文训练的事实标准

序列(与生成)范式的 2026 版图值得一提:自回归 Transformer(GPT / DeepSeek 系)主导语言;扩散模型(DDPM → DiT)与流匹配(flow matching / rectified flow)主导图像视频生成,后者用「学一个从噪声到数据的连续速度场」取代多步去噪,训练更稳、采样步数更少(常从数十步降到 4–8 步,如 SD3 / Flux 类)。它们的共同点是:把「生成长序列」变成可并行训练的回归问题——这正是本阶段要建立的思维。

8.2 动手练习与自测

✔
动手练习与自测(第 8 章):写结论为主,注意给出机制与量级。
  1. 列出 RNN 的三个硬伤,LSTM / GRU 解决了哪一个。答案:串行难并行、长距梯度消失、定容瓶颈;门控缓解第二个。
  2. 解释注意力为什么能并行而 RNN 不能。答案:注意力对整段做矩阵乘,各位置独立;RNN 需按时间步递推。
  3. 给出注意力相对 RNN 的复杂度代价。答案:时间/显存 O(T²),RNN 为 O(T)。
  4. 说明 FlashAttention-3 解决了什么、提速多少。答案:分块 + 在线 softmax 把显存降到 O(T),H100 FP8 下快 2–4×。
  5. 描述流匹配与扩散的关系与优势。答案:学连续速度场替代多步去噪,训练更稳、采样步数更少。
  6. 一句话总结「RNN 递归 vs 注意力检索」的差别。答案:前者压缩历史、容量有限;后者检索历史、路径最短。

9. 训练调试清单:先小后大

知识结构图 · 训练调试清单
训练调试清单3 大知识域 · 14 个知识点
症状→病因排查loss 不降NaN / Inf 定位train 降 val 升验证波动大GPU 利用率周期掉 0forward hook 定位 NaN
学习路径
  1. 读 9.1:背下五类症状→优先怀疑→动作表,记住「先小后大」两个铁律
  2. 制造一次 NaN,用 forward hook 逐层定位首个非有限层
  3. 完成 9.3 自测①:过拟合一个 batch(8 条)把 loss 压到 <0.05
✔ 能复现 NaN 并用 hook 定位到首个非有限层,养成先小后大的排查顺序
核心知识点详解
  • 两个铁律:①先过拟合一个 batch(2–8 条使劲训,loss 压到近 0 = 前反向优化都通);②先小后大(1% 数据+少量步数先把管线跑通再上全量/多卡)——省下大量排队与 GPU 开销。
  • NaN 用 forward hook 定位:数值错误常被后续算子掩盖(某层 Inf、下一层乘 0 又变有限,直到 softmax(log(0)) 冒 NaN);用 register_forward_hook 逐层打印 torch.isfinite,首个非有限层通常比报错行早 2–5 层。
  • 数值稳定兜底:softmax 前减 max、log 输入加 eps=1e-8、sqrt 内加 eps;BF16 指数 8 位可到 ~3e38 几乎不溢出,FP16 上限仅 65504——这就是 2026 默认 BF16 而非 FP16 的核心原因。
调试实操过拟合一个 batch梯度范数监控clip_grad_norm 传 1e9 只读LR range test峰值 lr 1e-4~6e-4
学习路径
  1. 读 9.2:写过拟合一个 batch、梯度范数监控(clip_grad_norm 传 1e9 只读)、LR range test
  2. 跑 LR range test 画 loss-lr 曲线,取谷底 1/10 作初始 lr
  3. 完成 9.3 自测②④:写出只读梯度范数原理并估出初始 lr
✔ 能画出 loss-lr 三段曲线并定出初始 lr,能解释 clip_grad_norm 传 1e9 是只读不裁
核心知识点详解
  • 过拟合 batch 与梯度范数:8 条样本使劲训到 loss→0(=前反向优化通);监控 clip_grad_norm_(params, 1e9) 的返回值——传 1e9 是「只读不裁」(>正常范数故不裁剪),能拿到真实全局 L2 范数;grad_norm 长期为 0 是红灯(梯度消失/lr 过小/参数没被接管)。
  • LR range test:指数扫 lr(lr=lr_min·γ^i)画 loss-lr 曲线,取斜率最负处的 1/10 作初始 lr;曲线形如「水平→下降谷→上翘」三段,谷底左侧更安全。
  • 峰值 lr 经验值(坑):Transformer 预训练峰值 lr 常用 1e-4~6e-4、微调为预训练的 1/10,配 1–3% 步数 warmup+cos;lr 过大笑 20 步就发散、过小 300 步曲线不动——都能在 10 分钟内暴露。
动手自测要点8 条样本训到 loss<0.05NaN 首个非有限层grad_norm 长期为 0 原因
学习路径
  1. 完成 9.3 全部自测(过拟合 batch / 梯度范数 / NaN 定位 / LR range test)
✔ 判断力与实操四题全过,能在 10 分钟内独立完成一轮故障制造与修复
核心知识点详解
  • loss 卡在 0.6 的三步排查:先确认标签与输入对齐(打印 x[0]/y[0])→再确认容量够(换浅网络)→最后确认 lr/优化器(换 SGD lr=0.1);任一环节把 loss 压到 <0.05 即算通过。
  • clip_grad_norm 传 1e9 的原因:只读不裁——1e9 远大于正常范数不会裁剪,返回值是裁剪前的全局 L2 范数可直接记录;传 1.0 会真裁剪而污染读数。
  • grad_norm 长期为 0 的三因(坑):①梯度消失(深层无残差/归一化);②lr 过小;③参数未被 optimizer 接管(漏传 parameters()/错 param_group)——三选一作答才算全面。
学习路径

9.1 症状 → 病因排查顺序

症状优先怀疑常见根因动作
loss 完全不降数据/标签对齐、学习率过小样本 shuffle 错、标签错位、输入未归一化、lr 太小先过拟合一个 batch(见 9.2),再调 lr / 归一化
loss 变 NaN / Inf数值溢出未用混合精度时 FP16 溢出、初始化方差过大、log(0)、除 0降 lr、加裁剪、换 BF16、检查 softmax/log 输入
train 降、val 升过拟合 / 泄漏容量过大、数据太少、切分泄漏加数据/正则/早停、查泄漏(阶段 6)
验证集波动大验证集太小 / 切分不稳验证样本少、未分层、batch 统计抖动扩大验证、重复 K 折、用分层
GPU 利用率周期掉 0数据 pipeline 瓶颈num_workers 不足、预处理在主进程升 num_workers、预处理进 Dataset
⚠
两个铁律:① 先过拟合一个 batch:用极小的数据子集(如 2–8 条)训练,loss 必须能降到接近 0。降不下去 = 模型容量/实现有 bug(标签错位、前向写错、梯度没流)。这是「先证明能记得,再谈泛化」的最低成本排查。② 先小后大:用 1% 数据 + 少量步数先把管线跑通、曲线平滑、梯度范数正常,再上全量与多卡——能省下大量排队与 GPU 开销。
python# 症状:第 1 个 step 的 loss 就是 NaN。用 forward hook 逐层定位病灶
def probe(name):
    def fn(m, inp, out):
        if torch.is_tensor(out) and not torch.isfinite(out).all():
            print("NaN/Inf 首次出现于:", name)
            raise SystemExit
    return fn

for name, m in model.named_modules():
    if len(list(m.children())) == 0:          # 只挂叶子模块,避免重复计数
        m.register_forward_hook(probe(name))

try:
    model(torch.randn(2, 3, 224, 224).to("cuda"))
except SystemExit:
    pass
# 预期:打印出第一个产生非有限值的模块名(如 blocks.3.norm1),
# 而不是等到最后的 loss.backward() 才在 traceback 里大海捞针。

为什么 hook 定位比读 traceback 快:数值错误常在前向里被后续算子掩盖——某层输出 Inf,下一层乘 0 又变回有限值,直到 softmax(log(0)) 或除以方差时 NaN 才冒出来。经验上 NaN 最早出现的位置通常比报错行早 2–5 层。数值稳定的三条兜底:softmax 前减最大值、log 输入加 eps=1e-8、分母 sqrt 内加 eps。BF16 的 8 位指数可表示到约 3e38,几乎不会溢出;FP16 上限仅 65504,梯度平方累加极易越界——这就是 2026 默认用 BF16 而非 FP16 的核心原因。

9.2 过拟合一个 batch 与梯度范数监控

python# ① 过拟合一个 batch:能记住 = 前向/反向/优化都通;记不住 = 实现 bug
tiny = Subset(train_ds, range(8))
tl = DataLoader(tiny, batch_size=8)
for x, y in tl:                                  # 反复训同一批
    loss = model(x.to("cuda"), y.to("cuda")).loss
    loss.backward(); opt.step(); opt.zero_grad()
    print(loss.item())                           # 应稳定降到 ~0

# ② 监控梯度范数:异常是最早的红灯
for x, y in loader:
    loss = model(x, y).loss; loss.backward()
    g = torch.nn.utils.clip_grad_norm_(model.parameters(), 1e9)  # 不裁剪,只取范数
    if not torch.isfinite(g): print("NaN grad! 检查初始化/精度/输入")
    else: print("grad_norm=%.3f" % g)

梯度范数突然飙到 1e3 以上往往早于 loss 爆炸,是最早的预警信号;而梯度范数长时间为 0 则意味着「梯度消失 / 学习率过小 / 参数没更新」。把 grad_norm 接进实验追踪(第 10 节),是训练可观测的最低门槛。

python# LR range test(Leslie Smith):指数扫描学习率,找下降最陡的区间
def lr_range_test(model, loader, lr_min=1e-6, lr_max=1.0, steps=300):
    opt = torch.optim.AdamW(model.parameters(), lr=lr_min)
    gamma = (lr_max / lr_min) ** (1 / steps)     # 每步等比放大
    lrs, losses = [], []
    for i, (x, y) in enumerate(loader):
        if i >= steps: break
        lr = lr_min * gamma ** i
        for g in opt.param_groups: g["lr"] = lr
        loss = model(x, y).loss
        loss.backward(); opt.step(); opt.zero_grad()
        lrs.append(lr); losses.append(loss.item())
    return lrs, losses

# 判据:把 loss-lr 曲线画成对数横轴,取斜率最负处 lr 的 1/10 作初始 lr。
# 预期形态:lr < 1e-5 曲线几乎水平(不动),lr > 1e-2 迅速上翘(发散),
#           中间有一段平滑下降谷;初始 lr 落在谷底左侧更安全。

机制:以等比步长扫 lr 等价于在 log(lr) 轴上匀速前进,使每个数量级分到相同预算;谷底处对应「梯度尺度 × lr」最匹配参数曲率的位置。经验值:Transformer 类模型常用峰值 lr 为 1e-4~6e-4(预训练)或 2e-5~5e-5(微调,常为预训练的 1/10),配合总计 1–3% 步数的线性 warmup 与之后余弦衰减。lr 过大时 loss 前 20 step 就上翘发散,过小时 300 step 曲线几乎不动——两者都能在 10 分钟内暴露。

9.3 动手练习与自测

✔
自测判据(先做再对照):这一节的练习都能在单卡 10 分钟内跑完。关键不是背结论,而是亲手制造故障再修好——排错能力只能靠见过足够多的『病』来获得。
  1. ① 过拟合一个 batch:取 8 条样本反复训 200 步,loss 应降到接近 0。若停在 0.6 附近不下,写出你的三步排查顺序。
    判据:先确认标签与输入对齐(打印 x[0]、y[0]),再确认容量够(换极浅网络再试),最后确认 lr 与优化器(换 SGD lr=0.1)。任一环节把 loss 压到 <0.05 即算通过。
  2. ② 梯度范数监控:写出用 clip_grad_norm_(params, 1e9) 取范数的代码,并解释为什么传 1e9 而非 1.0。
    判据:1e9 远大于正常范数,等于「只读不裁」;传 1.0 会真裁剪从而污染读数。返回值是裁剪前的全局 L2 范数,可直接记录。
  3. ③ NaN 定位:注入一个 bug 让某层权重为 inf,用 forward hook 找出它。记录首个报错模块名与最终 loss 报错行的层数差。
    判据:能稳定复现「首个非有限层比事后怀疑层更靠前」,说明 hook 比读 traceback 有效。
  4. ④ LR range test:跑一次 300 步指数扫描,画 loss-lr 曲线并估出可用初始 lr。
    判据:曲线呈「水平 → 下降谷 → 上翘」三段;所选初始 lr 应落在谷底左侧约 1 个数量级处。把它与默认 3e-4 对比,写出偏差原因。
  5. ⑤ 稳定性量级:解释为什么 grad_norm 长期为 0 是红灯。
    判据:答出三种可能——梯度消失(深层无残差/归一化)、lr 过小、参数未被 optimizer 接管(漏传 parameters 或用错 param_group)。

10. 实验追踪:让结论可信(里程碑 M7)

知识结构图 · 实验追踪
实验追踪3 大知识域 · 14 个知识点
记录要素W&B / MLflow / TensorBoard超参 lr / batch / seed曲线 loss / grad_norm / tokens_per_sec产物 checkpoint / 配置副本代码版本 git commit环境 torch / CUDA
学习路径
  1. 读 10.1:理解 W&B/MLflow/TensorBoard 该记录的超参/曲线/产物/代码版本/环境
  2. 用 W&B(或 MLflow)跑一次实验,记录 lr/batch/种子/loss/grad_norm/tokens_per_sec
  3. 对接 M7:训练管线接实验追踪,自动记录数据版本哈希+配置+指标
✔ 任何一次实验能凭记录还原数据版本、配置与 commit,指标可自动对比
核心知识点详解
  • 五类记录要素:超参(lr/batch/wd/seed)、曲线(loss/lr/grad_norm/吞吐 tokens_per_sec)、产物(checkpoint/配置副本)、代码版本(git commit+依赖锁哈希)、环境(torch/CUDA/GPU)。缺 commit→无法归因、缺 seed→无法复现。
  • M7 的正确交付:训练管线+实验追踪的可复现闭环:脚本配置化(YAML/dataclass)、W&B/MLflow 自动记录 git commit、断点续训+种子固定;这套闭环复用至阶段 10–11 预训练与阶段 14 Agent 训练。
  • 记录别只记 loss(坑):grad_norm 是训练红灯,吞吐 tokens/s 是性能基准;只记 loss 会漏掉「梯度爆炸/数据瓶颈」的早期信号——两者都该进曲线。
可复现与前沿YAML / dataclass 配置化A/B 固定变量只变优化器Muon 优化器Newton-Schulz 正交化Muon lr 0.02 vs AdamW 3e-4
学习路径
  1. 读 10.1:理解 YAML/dataclass 配置化与 A/B 固定变量只变研究项
  2. 做一个 AdamW vs Muon 的公平 A/B 对照(固定数据/模型/步数/种子),记录 val_loss 与吞吐
  3. 对接 M7:在训练管线里对比 AdamW 与 Muon,说明 Muon 的应用边界
✔ 能设计一个有效的 A/B 实验并定量判断哪个优化器更优(含收敛与吞吐)
核心知识点详解
  • A/B 固定变量只变研究项:对比 AdamW(3e-4) vs Muon(2e-2) 必须固定数据、模型、步数、评估集、种子,只改变优化器与其各自调的 lr;否则一次共用 seed 就说「更好」是无效结论。
  • Muon 的机制与边界:对 2D 权重矩阵的更新先做 Newton-Schulz 正交化(把动量矩阵奇异值压到约 1 使更新「等谱」,抑制 Adam 各方向尺度失衡);只用于隐藏层 2D 权重,embedding/归一化/bias 仍交 AdamW。
  • Muon 的 lr 与代价(坑):Muon 的 lr 常比 AdamW 大 1–2 个数量级(0.02 vs 3e-4)、对 weight decay 更敏感(正交化后尺度不同,不能直接复用 AdamW 的 wd 数值);每步多一次正交化、吞吐略降(如 158k→151k tokens/s),小模型收益不明显、不值得无脑替换。
动手自测要点六类可复现要素AdamW vs Muon 对照曲线回升诊断
学习路径
  1. 对照 10.2 判据完成复现性自检,输出 seed/commit/数据指纹,两跑 <1e-3
  2. 给出 val_loss 第 12k 步回升的过拟合诊断与两个动作
✔ 能复现同指标 <1e-3,能诊断回升曲线(过拟合/早停取最优 checkpoint)
核心知识点详解
  • 六类可复现要素:超参、随机种子、代码版本、环境版本、数据版本、指标曲线;缺种子→无法复现、缺 commit→无法归因、缺环境→跨机漂移无从解释。实验启动时打印并落盘 git rev-parse HEAD 与 torch.version.cuda。
  • A/B 设计的守恒:固定数据/模型/步数/评估集/种子、只变优化器+各自调 lr;同一实验两跑指标差应 <1e-3,超过即复现性有问题。
  • 回升曲线诊断(坑):val_loss 于 12k 步回升而 train_loss 仍降=过拟合;动作为加数据/正则(wd、dropout)、早停取 12k 步的最优 checkpoint(而非最后一步)。
学习路径

10.1 W&B / MLflow / TensorBoard 的记录要素

要素记录什么为什么
超参lr、batch、wd、模型结构、seed可复现与对比的前提
曲线loss / lr / grad_norm / 各评估指标 / 吞吐(tokens/s)诊断训练动态,grad_norm 是红灯
产物checkpoint、配置副本、样本预测、混淆矩阵图出问题能回放,能 diff
代码版本git commit、依赖锁文件哈希「哪次改动导致变化」的唯一答案
环境torch/CUDA/driver 版本、GPU 型号跨环境结果差异的溯源
pythonimport wandb, torch
wandb.init(project="hamauls-orion-dl", config=dict(lr=3e-4, batch=64, wd=0.01, seed=0))

for ep in range(epochs):
    for x, y in loader:
        loss = step(x, y)
        wandb.log({"loss": loss.item()})                 # 曲线
    val = evaluate(model, val_loader)
    gn = grad_norm(model)
    wandb.log({"val_acc": val, "grad_norm": gn,
               "tokens_per_sec": throughput})
    # 产物:保存配置副本与 checkpoint,wandb 自动记录 git commit
    torch.save(model.state_dict(), f"ckpt/ep{ep}.pt")
    wandb.save(f"ckpt/ep{ep}.pt")

# MLflow 等价:mlflow.log_param / log_metric / log_artifact
# TensorBoard:SummaryWriter.add_scalar,本地零依赖,但协作/对比弱于前两者
★
M7 的正确交付:里程碑 M7 不是「跑通一个训练脚本」,而是训练管线 + 实验追踪的可复现闭环:脚本配置化(YAML/dataclass)、用 W&B/MLflow 记录超参/曲线/产物/代码版本、每次实验自动产出可对比表、断点续训与种子固定到位。这套闭环会一路复用到阶段 10–11 的预训练与阶段 14 的 Agent 训练——你现在搭一次,后面 10 个阶段都在用。
python# 把多次实验聚合成可比表:同任务下 AdamW vs Muon(2026 前沿优化器)
runs = [
    dict(opt="AdamW", lr=3e-4, val_loss=2.41, tok_s=158000),
    dict(opt="Muon",  lr=2e-2, val_loss=2.29, tok_s=151000),
]
print("opt      lr      val_loss   tok/s")
for r in runs:
    print("%-8s %-7.4g %-10.3f %d" % (r["opt"], r["lr"], r["val_loss"], r["tok_s"]))

# 预期输出(同 20000 步、同数据、同评估集):
# opt      lr      val_loss   tok/s
# AdamW    0.0003  2.410      158000
# Muon     0.0200  2.290      151000
# 结论:Muon val_loss 低约 0.12,但吞吐略降(每步多一次矩阵正交化)。

2026 年优化器最值得关注的变化是 Muon:它对 2D 权重矩阵的更新先做 Newton-Schulz 正交化(近似求 U·Vᵀ,把动量矩阵奇异值压到约 1),使每步更新「等谱」,天然抑制 Adam 里各方向尺度失衡的问题。经验规律:Muon 用于隐藏层的 2D 权重,embedding / 归一化 / bias 仍交回 AdamW;Muon 的 lr 常比 AdamW 大 1–2 个数量级(常见 0.02 对 3e-4),且对 weight decay 更敏感。Kimi K2 及部分开源大模型已用 Muon 系(含 MuonClip 变体)替代 AdamW 做预训练,同等 token 预算下收敛更快、val loss 更低约 0.05–0.15。代价是每步多出正交化的计算与显存,小模型/小 batch 收益不明显,不值得无脑替换。

10.2 动手练习与自测

✔
自测判据(先做再对照):实验追踪的验收标准只有一条:别人(或三个月后的你)能否只看记录就复现结论。下面每题都对应一个可交付物,做完即里程碑 M7 的验收清单。
  1. ① 最小可复现要素:列出一次实验必须记录的 6 类信息,并说明缺哪类会导致什么后果。
    判据:超参、随机种子、代码版本、环境版本、数据版本、指标曲线。缺种子→无法复现;缺 commit→无法归因;缺环境→跨机漂移无从解释。
  2. ② A/B 对照设计:要在「AdamW vs Muon」间做公平对比,写出你必须固定与必须变化的变量。
    判据:固定数据、模型、步数、评估集、初始化种子;只变优化器与其 lr(各自调优)。若只跑一次、共用 seed 却报「Muon 更好」即为无效结论。
  3. ③ 复现性自检:写一段代码在实验开始时打印并落盘 seed、torch/CUDA 版本、git commit、数据指纹。
    判据:能输出 git rev-parse HEAD 与 torch.version.cuda;同一实验跑两次指标差异应 <1e-3。
  4. ④ 曲线读取:给定一条 val_loss 在第 12k 步后开始回升、train_loss 仍降的曲线,写出诊断与 2 个动作。
    判据:诊断为过拟合;动作为加数据/正则(wd、dropout)、早停取 12k 步的 checkpoint。
  5. ⑤ 前沿判断:为什么 Muon 的 weight decay 不能直接复用 AdamW 的数值?
    判据:因 Muon 更新已被正交化、尺度与 AdamW 不同,wd 需按更新尺度重新标定;直接套用常会过大导致欠拟合或过小导致过拟合。

项目里程碑

贯穿项目 · Hamauls Orion
M7 PyTorch 训练管线与实验追踪 第 33–38 周

把基线迁移到 PyTorch,并搭建一套可复用的训练管线:混合精度、梯度累积、checkpoint 续训、随机种子固定、实验追踪(wandb 或本地 MLflow)。这套管线在第 9 阶段微调时会被直接复用。

本阶段产出(直接进入项目仓库)
验收标准:训练中断后能从 checkpoint 精确续训(loss 曲线连续);任意一次实验都能凭记录还原出数据版本、配置与代码 commit。

阶段练习项目

PROJECT 1
手写训练框架
不用 Trainer,自己实现一个训练脚本:配置化、AMP(BF16) + 梯度累积、梯度裁剪、余弦调度、验证指标、断点续训、TensorBoard/W&B 日志。这是后面所有阶段的基础设施。
要达成的效果
  • 不依赖 Trainer,手写一个含配置化 + BF16 AMP + 梯度累积/裁剪 + 余弦调度 + 断点续训的 train()
  • gradcheck 通过、断点续训后 loss 曲线连续、同环境两跑指标差 <1e-3
  • 验证集指标曲线平滑可读,grad_norm 与吞吐进日志
功能需求
  • 配置用 dataclass 或 YAML 加载,lr/batch/seed/accum/wd 全部参数化
  • 用 torch.autocast("cuda", dtype=bfloat16) + 梯度累积(loss 除 accum)+ clip_grad_norm_
  • 断点至少保存 model/opt/sched/step/RNG(含 os.replace 原子写)
  • 用 TensorBoard 或 W&B 记录 loss/lr/grad_norm/tokens_per_sec
交付物
  • train.py + 配置样例
  • 一次可复现实验的运行日志与曲线截图
  • README(怎么跑、如何续训、关键开关)
边界 · 不做

不做分布式与多卡;不做 RLHF/Agent 特殊逻辑(留给后续阶段)。

PROJECT 2
训练诊断对照实验
在一个小任务上分别制造 5 种病症:学习率过大、过小、无归一化、无残差、初始化方差错误。记录并对比 loss 曲线与梯度范数,形成一份「症状 → 病因」的排错表。
要达成的效果
  • 在小任务上稳定制造 5 种病症并各自跑出可复现的 loss/梯度范数特征
  • 产出一份逐条对照的「症状→病因→修复动作」排查表,能举一反三
  • 每种病症给出对应的 1–2 个修复动作并验证有效
功能需求
  • 用同一基座模型只改一个变量制造病症(lr×10、lr÷10、去 LN、去残差、劣化初始化)
  • 记录每症的 loss 曲线与 clip_grad_norm_(...,1e9) 梯度范数趋势
  • 先过拟合一个 batch 确认基座本身通,再逐个注入 bug
  • 用 forward hook 至少演示一次 NaN/非有限层定位
交付物
  • scripts/debug_ablation.py(可切换开关)
  • 「症状→病因→动作」排查表(MD)
  • 各病症的 loss/梯度范数对比图
边界 · 不做

不做真实业务模型调优;聚焦排错方法与可复现故障注入。

PROJECT 3
小样本视觉迁移
用 200–500 张图做一个多分类任务,对比「冻结主干」「部分微调」「全量微调」「从头训练」四种方案,写出结论与解释。
要达成的效果
  • 在 200–500 张小数据多分类上完成四方案对比,报告 val 指标与训练成本
  • 定量说明四种方案在小数据下的排名与差距(期望冻结/部分微调显著优于从头训练)
  • 给出为什么小数据下 CNN 归纳偏置(或冻结主干)优势的机制解释
功能需求
  • 用 timm/torchvision 预训练主干(如 resnet/convnext tiny)+ 替换分类头
  • 四方案共享同一数据划分与评估集,指标可比
  • 配 RandAugment/Mixup/CutMix 增强与分层学习率
  • 记录各方案的 val_acc/loss、训练耗时、显存
交付物
  • scripts/transfer_learning.py + 运行记录
  • 四方案对比表与结论(Markdown)
  • README(如何在别的数据集复现)
边界 · 不做

不做 ViT 与大数据训练;不做多模态;仅聚焦迁移学习对比方法。

PROJECT 4
PyTorch 训练管线与实验追踪(里程碑 M7)
基于手写训练框架,落地一条可复现管线:Dataset/DataLoader(num_workers+pin_memory)、torch.compile、BF16 混合精度、梯度累积/裁剪/检查点、固定随机种子与代码版本记录;用 W&B 或 MLflow 记录超参、loss/lr/grad_norm/吞吐曲线与 checkpoint 产物,每次实验自动输出可对比表。把它作为后续预训练与 Agent 训练的通用底座。
要达成的效果
  • 落地一条可复现训练管线,torch.compile+BF16 下吞吐/显存相对基线量级可查
  • W&B/MLflow 自动记录超参/曲线/各次实验 checkpoint 产物与 git commit,两次实验指标可自动对比
  • 断点续训 + 种子固定,同环境两跑最终指标差 <1e-3
功能需求
  • Dataset+DataLoader(num_workers/pin_memory/non_blocking)配好,GPU 利用率稳定不周期掉 0
  • BF16 AMP + torch.compile + 梯度累积/裁剪/检查点集成进 train()
  • 用 W&B/MLflow 记录 lr/batch/seed、loss/grad_norm/tokens_per_sec、checkpoint 与配置副本
  • 启动时落盘 seed、git commit、torch/CUDA 版本、数据指纹
  • 每次实验自动产出可对比表(AdamW vs 可选优化器)
交付物
  • src/train_pipeline.py + DataLoader 配置
  • W&B/MLflow 运行链接或导出记录(含曲线与产物)
  • README(作为 10–11/14 阶段通用底座的使用方式)
边界 · 不做

不做多卡/分布式(可留接口);不做模型的业务层逻辑;只交付训练底座。

常见误区

面试高频问题速答

为什么需要残差连接?

深层网络的恒等映射很难学,梯度在多层反向传播中会衰减。残差让 F(x)+x 中的恒等项提供一条梯度直达通道,使优化目标退化为「学残差」而非「学完整映射」。没有残差就没有上百层的可训练网络,Transformer 的每个子层都依赖它。

BatchNorm 和 LayerNorm 的区别,为什么 LLM 用后者?

BN 在 batch 维度统计均值方差,依赖 batch 内样本的相关性与足够大的 batch;LN 在特征维度对单个样本做归一化,与 batch 无关。LLM 的序列长度可变、batch 常很小、且需要推理时逐 token 生成,BN 的批统计量不适用,因此用 LN / RMSNorm。

Pre-LN 和 Post-LN 差在哪,为什么 2026 LLM 默认 Pre-LN + RMSNorm?

Post-LN 把归一化放在子层内部,残差路径上的梯度要过一次 LN 被缩放,深层易不稳,需小心 warmup;Pre-LN 让恒等捷径不受 LN 扰动,梯度能直达浅层,训练更稳、warmup 可更小。再叠加 RMSNorm(比 LN 省算力、对 BF16 友好),就成了 LLaMA/Qwen/DeepSeek 的标配。

怎么诊断梯度消失 / 爆炸?

记录每层梯度范数(hook 或 clip_grad_norm_ 取范数),若逐层指数衰减或放大即为该问题。手段:换合适初始化(Kaiming/Xavier/正交)、加归一化、用残差、换激活(GELU/SwiGLU 优于 sigmoid/tanh)、梯度裁剪、用 BF16 而非 FP16 避免溢出。

混合精度训练为什么能加速?BF16 和 FP16 怎么选?

在支持 Tensor Core 的 GPU 上用低精度做矩阵乘,吞吐显著提升、显存减半。FP16 动态范围小(最大约 65504),容易溢出,需要 loss scaling;BF16 指数位与 FP32 相同,动态范围大,通常无需缩放,训练更稳但有精度损失。现代 LLM 训练基本默认 BF16,FP8 是 Hopper 上的进一步榨取。

7B 模型全量微调要多少显存?为什么单卡 80GB 放不下?

BF16 下:参数 14GB + 梯度 14GB + AdamW 优化器状态(FP32 主副本+m+v 共 12B/参数)84GB ≈ 112GB,还不计激活。单张 80GB A100 连前三项都放不下,必须 ZeRO-3/FSDP 分片或 LoRA/QLoRA(只训少量参数,优化器状态骤降)。显存公式比模型结构更决定部署形态。

FSDP / ZeRO-1/2/3 各切什么?

ZeRO-1 只分片优化器状态;ZeRO-2 再加梯度分片;ZeRO-3(FSDP)把参数也分片,用时 all-gather。切得越细越省显存但通信越多。每卡放得下副本时用 DDP 最简单最快,放不下才上 FSDP/ZeRO-3。

RNN 的瓶颈是什么?注意力如何解决?

RNN 串行执行无法并行、长距离梯度消失遗忘远端、隐藏状态固定维度成信息瓶颈。注意力让每个位置直接对所有位置加权检索,路径长度 1 且完全可并行;代价 O(n²),催生 FlashAttention 与稀疏/线性注意力。

学习资源

PyTorch 官方教程文档 pytorch.org/tutorials/ 从张量到分布式训练,体系完整。重点:60-minute blitz、Autograd mechanics、FSDP 教程。 Dive into Deep Learning(李沐 · 动手学深度学习)教材 d2l.ai/ 理论与代码并重,中文版质量极高,最适合本阶段按章推进。 李沐 · 深度学习论文精读(B 站)视频 space.bilibili.com/1567748478 跟读经典论文的最佳中文入口,讲 ResNet / Transformer / SwiGLU 背景。 Google · Deep Learning Tuning PlaybookGitHub github.com/google-research/tuning_playbook 工业级调参与训练调试手册,含大量“先做这个再做那个”的实用顺序。 A Recipe for Training Neural Networks(Karpathy)长文 karpathy.github.io/2019/04/25/recipe/ 经典长文,讲的全是训练失败的真实原因与排查顺序(先过拟合一个 batch)。 CS231n · 卷积神经网络视觉识别课程 cs231n.stanford.edu/ 视觉方向的经典课程,CNN 与反向传播讲得最透彻。 torchvision / timm 模型库GitHub github.com/huggingface/pytorch-image-models timm 收录大量预训练视觉模型,迁移学习直接用。 PyTorch FSDP 文档(含 ZeRO-3)文档 pytorch.org/docs/stable/fsdp.html FSDP2 与逐参数分片的官方说明,分布式训练必读。 The Annotated Transformer教程 nlp.seas.harvard.edu/annotated-transformer/ 逐行注释的 Transformer 实现,是本阶段与阶段 10 之间的最佳桥梁。
★
2026 形势提示:2026 年的 JD 已经在明确要求「分布式训练原理」「显存与性能优化」「千卡级并行经验」。这些能力的入门就在本阶段的训练工程部分——BF16 + SwiGLU + RMSNorm + Pre-LN + FSDP2 + torch.compile 是当下 LLM 训练的默认配方,而显存估算与调试清单决定了你能在有限预算下把模型训出来。这项能力在后训练与 Agentic RL 时代反而更值钱,因为 RL 训练的 rollout 成本很高,省显存就是省预算。