深度学习基础 Deep Learning
这是从「调库」变成「理解」的阶段。重点不是记住多少网络结构,而是掌握训练这件事的工程化方法:怎么诊断不收敛、怎么判断过拟合、怎么在有限显存下训更大的模型、怎么让实验可复现。2026 年的 LLM 训练配方(RMSNorm + Pre-LN + SwiGLU + BF16 + FSDP2 + torch.compile)全都是这里概念的沿伸——把地基打牢,阶段 10–11 的 Transformer 与预训练你会理解得飞快。
阶段总览
- 理解 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)
| 周次 | 主题 | 交付物 |
|---|---|---|
| 第 1 周 | MLP / 初始化 / 激活 / 归一化原理 | 手写各组件并对照消融 |
| 第 2 周 | PyTorch 核心与训练循环 + Dataset/DataLoader | 不查文档手写完整训练循环 |
| 第 3 周 | 训练工程(BF16/FP8/累积/裁剪/检查点)+ 显存估算 | 在 8–24GB 显存上训练一个小模型并算清显存 |
| 第 4 周 | CNN 与视觉 + 迁移学习 | 微调一个预训练视觉模型做分类 |
| 第 5 周 | 分布式训练原理(DDP / FSDP / ZeRO) | 单卡模拟 + 多卡脚本骨架 |
| 第 6 周 | 训练调试清单 + 实验追踪 + 综合项目(M7) | PyTorch 训练管线 + W&B/MLflow 追踪的可复现实验 |
1. 从线性到 MLP:网络为什么能学到东西
学习路径
- 读 1.1:理解万能逼近定理的「能拟合≠学得高效」,背下 parity 浅网需指数宽度
- 跑 parity 数据集,观察 n 增大时深度 2 所需宽度的指数爆炸
- 完成 1.3 自测:解释「深而窄为何优于浅而宽」及换归纳偏置的价值
核心知识点详解
- 万能逼近的三个局限:①只保证存在一个宽网络,没说梯度下降找得到;②所需宽度随输入维/精度指数增长;③只谈表达力,泛化靠归纳偏置与正则。故现代模型走「深而窄」而非「浅而巨宽」。
- 深>宽的量化直觉:parity:深度 2 网络拟合 n 位奇偶需 ~2^n 宽度(n=12→4096),深网用 O(n) 宽度 + O(n) 层即可;固定参数量下深而窄的验证误差常明显更低。
- 层级复用与组合泛化:第 1 层学边缘、第 2 层学部件、第 3 层学物体,同一组原语被反复复用→参数效率高;这正是 CNN/Transformer 多层次堆叠而非单层超宽的根本原因。
学习路径
- 读 1.2:理解组合泛化、残差恒等捷径与 CNN/Attention 的归纳偏置
- 跑 first_layer_grad(True/False),对比有无残差时的最浅层梯度范数量级
- 完成 1.3 自测:说明残差为何等价于「学残差」,弱/强偏置各举一例
核心知识点详解
- 残差 = 学残差:
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 复现:
depth=40下h=h+blk(h)使最浅层梯度 ~1e-1,去掉残差则指数衰减到 1e-6 以下;能复现这个量级差即过关。 - 学残差的等价性:残差把恒等映射变成梯度的恒等捷径,目标退化为 F(x)=H(x)−x,从而深层无须苦学恒等;这是 deep 网络能可靠训练的关键。
- 偏置举例别空谈(坑):弱偏置+大数据=纯 Transformer 大规模预训练、强偏置+小数据=CNN 小图像集;光背「弱胜强」不举具体对比例子会被判偏题。
1.1 万能逼近定理的直觉与局限
1989 年 Cybenko 证明:一个隐藏层、足够宽、用非线性的 MLP,能以任意精度逼近任意紧集上的连续函数。这是「神经网络能拟合一切」的理论根源——但「能拟合」≠「学得高效」。
| 维度 | 万能逼近的代价 | 现实含义 |
|---|---|---|
| 宽度换能力 | 宽度随精度 / 输入维指数增长 | 单层网络要“巨宽”才能表达复杂函数,参数量爆炸 |
| 深度换能力 | 同样表达能力,深度网络所需宽度小得多 | 深层用层级组合,参数效率高 |
关键局限:① 定理只保证「存在」一个宽网络,没说梯度下降找不找得到;② 需要的宽度随输入维/精度指数增长,单层在高位感知任务上不实用;③ 它解释「表达力」,不解释「泛化」——泛化靠的是归纳偏置与正则,不是逼近定理。所以现代模型普遍走深而窄而非「浅而巨宽」。
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 深度、组合泛化与层级特征
- 组合泛化:深层把简单原语组合成复杂概念(词→短语→句→段落→语义),每层只学「局部可复用」的变换,参数量远小于一次性学全局映射。
- 梯度路径:深度带来更长的反向路径,这是残差连接(第 4 节)存在的根本理由——没有恒等通道,深层梯度会消失。
- 与 CNN / Transformer 的统一:卷积是「局部 + 权值共享」的归纳偏置,Transformer 是「全局注意力 + 位置编码」,本质都是用特定结构把「深度」用在合适的地方。
- 实践推论:在数据有限时,加深度不如加正确的归纳偏置(如图像用 CNN、序列用注意力);数据海量大时,深度 + 弱偏置(纯 Transformer)反而赢。
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 动手练习与自测
- 用 parity 数据集说明为什么「浅而宽」需要指数宽度。答案:n 位 parity 用深度 2 拟合需 ~2^n 个线性区,宽度指数增长;深网 O(n) 宽度即可。
- 跑 first_layer_grad(True) 与 (False),解释两者量级差异。判据:无残差时梯度随深度指数衰减(可达 1e-6 以下)。
- 说明残差连接为什么等价于「学残差而非学完整映射」。答案:恒等项提供梯度直达通道,目标退化为 F(x)=H(x)−x。
- 解释固定参数量下「深而窄」为何常优于「浅而宽」。答案:层级组合让原语被复用,参数效率更高。
- 描述 Mixture-of-Depths 的核心思想,并说明它省的是什么。答案:router 决定 token 是否经过某层,省的是「不必要的深度算力」。
- 举出「弱归纳偏置 + 大数据」胜出与「强偏置 + 小数据」胜出各一例。答案:前者=纯 Transformer 大规模预训练;后者=CNN 小图像数据集。
2. 初始化:信号不爆炸也不消失
学习路径
- 读 2.1:背下 Xavier 2/(n_in+n_out)、He 2/n_in、正交奇异值≈1 的方差建议
- 用随机网络打印逐层激活方差,验证 He 对 ReLU 的补偿来自 2/n_in
- 完成 2.3 自测:推导 σ_w² 表达式并说明先查初始化再调 lr
核心知识点详解
- 三种初始化方差:Xavier
Var=2/(n_in+n_out)(tanh/sigmoid)、HeVar=2/n_in(ReLU 砍半故加倍补偿)、正交奇异值≈1(RNN/深层);大模型常在残差分支乘1/√N或固定0.02控累加方差。 - 逐层方差 probe:打印
layer{10,20}的激活 var:He 下三层同量级,默认 init 则逐层漂移(过小→消失、过大→爆炸);用nn.init.kaiming_normal_实现。 - 先查初始化再调 lr(坑):「深层训不动」第一步检查初始化方差是否守恒,而不是无脑调 lr;打印逐层激活方差只需十几行,常省几轮盲调。
学习路径
- 读 2.2:理解全零初始化为何失败(对称无法打破),推导 Var(y)=n_in·σ_w²·σ_x²
- 跑全零 vs 随机初始化对比,观察梯度是否卡死不更新
- 对深层网络做方差守恒检查,先定位初始化问题再调学习率
- 完成 2.3 自测:解释 RNN 为何偏好正交初始化
核心知识点详解
- 全零初始化必然失败:对称权重→对称梯度→无法打破对称,网络退化为一个神经元;这是唯一绝对不能做的初始化。复现:
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² 推导题:
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 / Glorot | Var(W)=2/(n_in+n_out) | tanh / sigmoid / 对称激活 | 让前向方差与反向梯度方差都≈守恒 |
| He / Kaiming | Var(W)=2/n_in | ReLU 系(ReLU/LeakyReLU) | ReLU 砍掉一半神经元,故方差要加倍补偿 |
| 正交 Orthogonal | W 为随机正交矩阵 | RNN / 深层 / 循环 | 保持奇异值≈1,避免反复相乘后信号衰减/放大 |
| 缩放初始化(GPT 系) | 按残差层数缩放,如 1/√N | Pre-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。
→ 结论:合理的初始化让「每层输出的方差大致守恒」,既不爆炸也不消失。
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 动手练习与自测
- 推导前向方差守恒,给出 σ_w² 的表达式。答案:Var(y)=n_in·σ_w²·σ_x²;令其为 σ_x² 得 σ_w²=1/n_in;Xavier 取 2/(n_in+n_out),He 取 2/n_in。
- 解释为什么全零初始化必然失败。答案:对称权重→对称梯度,无法打破对称,网络退化为单神经元。
- 跑 probe() 对比 He 与默认 init 的逐层方差。判据:He 各层同量级,默认 init 逐层漂移。
- 说明残差输出缩放的动机。答案:深层残差累加会使方差随层数增长,按 1/√N 缩放可保持稳定。
- 给出「激活方差逐层单调衰减」的排查与修复动作。答案:换 He/Xavier/正交初始化,或加归一化层。
- 说出 RNN 为何偏好正交初始化。答案:正交矩阵奇异值≈1,反复相乘不衰减/放大。
3. 激活函数:非线性从哪来
学习路径
- 读 3.1:区分 ReLU / LeakyReLU(α≈0.01)/ GELU / SiLU,记死亡神经元成因
- 统计每层 ReLU 死亡比例,判断是否 >0.7 需换激活或降 lr
- 完成 3.3 自测:说出死亡神经元三种缓解手段
核心知识点详解
- 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 计算。
学习路径
- 读 3.2:默写 GLU 门控公式,记 SwiGLU 等价参数量需 hidden=8d/3
- 跑参数量对比代码,验证标准 MLP(8d²) 与 SwiGLU(4d 为 12d²) 差异
- 完成 3.3 自测:手算 d=4096 时标准 MLP 与 hidden=4d 的参数量
- 对接 M7:确认训练管线里 FFN 采用 SwiGLU hidden=8d/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,别想当然。
核心知识点详解
- 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 系与死亡神经元问题
| 激活 | 公式 | 问题 / 优点 |
|---|---|---|
| ReLU | max(0, x) | 便宜;但「死亡神经元」:x<0 时梯度恒 0,过大学习率会整片神经元永久沉默 |
| LeakyReLU | max(αx, x), α≈0.01 | 给负区一个小斜率,缓解死亡 |
| PReLU | 负区斜率可学习 | 更灵活,但多参数、易过拟合小数据 |
| ELU | x<0 时 α(e^x−1) | 负区平滑趋于 −α,均值更接近 0 |
| GELU | x·Φ(x) ≈ 0.5x(1+tanh(√2/π·x)) | 平滑、类 ReLU 但带概率门控,Transformer 主流 |
| SiLU / Swish | x·σ(β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 等价参数量,主流取法 |
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 动手练习与自测
- 解释 ReLU 死亡神经元的成因与三种缓解手段。答案:负区梯度恒 0;缓解=降 lr、换 Leaky/GELU/SiLU、加归一化。
- 给出「死亡比例」的健康阈值与超标时的动作。判据:>0.7 长期维持即异常,需换激活或降 lr。
- 写出 GLU 的门控公式并说明比普通 MLP 强在哪。答案:y=W3(σ(W1x)⊙W2x);门控让信息选择性通过,表达力更强、更稳。
- 计算 d=4096 时标准 MLP 与 hidden=4d 的 SwiGLU 参数量。答案:分别约 8d² 与 12d²,后者多 50%。
- 说明为什么等价替换要用 hidden=8d/3。答案:SwiGLU 有 3 个矩阵,3·(d·h)=8d² 时 h=8d/3。
- 解释 GELU/SiLU 比 ReLU 在混合精度下更稳的原因。答案:平滑、负区有梯度,避免硬截断带来的梯度稀疏与数值跳变。
4. 归一化:训练稳定的命脉
学习路径
- 读 4.1:区分 BatchNorm/LayerNorm/RMSNorm/GroupNorm 的统计维度与训练推理差异
- 跑 LN vs RMSNorm 耗时对比,记住 eval 期 running_mean/var 必须切对模式
- 完成 4.3 自测:说明各归一化统计维度及 RMSNorm 快约 20% 的原因
核心知识点详解
- 四种归一化的统计维度: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。
学习路径
- 读 4.2:理解 Pre-LN 梯度直通与 Post-LN 需长 warmup 的原因
- 对比 Pre-LN vs Post-LN 在深模型上是否需 warmup,验证收敛差异
- 完成 4.3 自测:给出 warmup 占步数 1–3% 的经验值与 QK-Norm 抑制 loss spike 作用
- 对接 M7:确认训练管线用 Pre-LN + RMSNorm + warmup 标配(约 1–3% 步数)
核心知识点详解
- 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"就是这套经验。
核心知识点详解
- 维度对比题: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
| 归一化 | 统计维度 | 训练 / 推理差异 | 适用 |
|---|---|---|---|
| BatchNorm | batch 维(同通道跨样本) | 训练用 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 统计量
- BatchNorm 对 batch size 敏感:batch 太小时 batch 统计量噪声大,训练不稳;batch=1(如逐样本生成)时 BN 几乎不可用。这就是 RNNT/Transformer 推理用 LN 的原因。
- BatchNorm 的推理差异是经典 bug 源:训练时记录 running_mean/var,推理时用它们。若训练不充分或忘了 model.eval(),推理输出会漂移。
- RMSNorm 为什么省算力:LayerNorm 要先算均值再减(中心化),RMSNorm 只算平方均值再除(无减均值),少一次广播减法,且对低精度更友好——LLM 在 BF16 下尤需这种省算力的细节。
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 更友好
# 在几十层 × 上万步的预训练里,这点节省会累积成可观的算力
| 归一化 | 是否减均值 | 是否依赖 batch | BF16 友好度 |
|---|---|---|---|
| LayerNorm | 是 | 否 | 好 |
| RMSNorm | 否(只除 RMS) | 否 | 更好 |
| BatchNorm | 是 | 是(训练) | 一般(需同步) |
| GroupNorm | 是 | 否 | 好 |
4.2 Pre-LN vs Post-LN:训练稳定性的分水岭
Transformer 里归一化放哪,决定梯度能不能稳定传到浅层:
| 结构 | 公式(每个子层) | 梯度行为 | 现状 |
|---|---|---|---|
| Post-LN | x = x + Sublayer(LN(x)) | 深层梯度经 LN 缩放易不稳,需暖启动 / 小心学习率 | 早期 Transformer(2017)用的,现已少 |
| Pre-LN | x = 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 步数可显著减少)。
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 动手练习与自测
- 对比 BatchNorm / LayerNorm / RMSNorm / GroupNorm 的统计维度与 batch 依赖。答案:BN 依赖 batch 维;LN/RMSNorm 在特征维;GN 通道分组。
- 解释 BatchNorm 训练 / 推理差异,以及忘记 eval 的后果。答案:训练用 batch 统计并更新 running;推理用 running;忘记 eval 会让输出随 batch 抖动。
- 跑 LN 与 RMSNorm 的耗时对比,给出量级差。判据:RMSNorm 约快 10–30%,因省去减均值。
- 写出 Pre-LN 与 Post-LN 的子层公式并说明梯度差异。答案:Pre-LN 的恒等捷径不受 LN 扰动,梯度直达浅层。
- 给出 warmup 占总步数的经验比例。答案:1–3%。
- 解释 QK-Norm 与 logits 软上限要解决的问题。答案:抑制训练后期注意力 logits 过大导致的 loss spike。
5. PyTorch 核心与工程实践
学习路径
- 读 5.1:理解 model.train/eval、no_grad、zero_grad(set_to_none) 与动态计算图
- 手写完整训练循环(AMP + 累积 + 裁剪),跑通并观察 val_acc 曲线
- 用 gradcheck 校验自定义算子的反向实现,确认误差 <1e-6
- 对接 M7:用此循环实现 PyTorch 训练管线(AMP、梯度累积、断点续训)
核心知识点详解
- 三个开关各管一件事:
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 会误报。
学习路径
- 读 5.2:理解 num_workers / pin_memory / persistent_workers / prefetch_factor
- 跑 data-only 吞吐对比数据管线,判断 num_workers 是否够
- 用 nvidia-smi 观察 GPU 利用率,定位是否周期性掉 0(数据瓶颈)
- 对接 M7:为训练管线配好 DataLoader 参数,确保 GPU 利用率稳定高位
核心知识点详解
- 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%+。
学习路径
- 读 5.3:理解 BF16 免 loss scaling、GradScaler 仅 FP16、autocast 只包前向+loss
- 把训练切到 autocast(bf16) + torch.compile,测量吞吐/显存提升
- 排查重编译陷阱,确认显存降 55–65%
- 对接 M7:训练管线默认开 BF16 AMP 与 torch.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 再计时再下结论。
学习路径
- 读 5.4:理解断点需保存 model/opt/sched/step/RNG/scaler 六类状态
- 实现带 os.replace 原子保存的 checkpoint,并在中断后精确续训
- 固定随机种子并验证续训后 loss 曲线连续
- 对接 M7:完成 checkpoint 续训与种子固定的可复现闭环
核心知识点详解
- 断点必须存全 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 让数据顺序衔接。
核心知识点详解
- 手写循环判据:
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 -> 梯度清零更省内存
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=True | worker 进程在 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)
__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()
- torch.compile 的坑:第一次运行有编译开销(几秒到几十秒,图越大越久);动态控制流 / 大量 Python 标量张量会触发重编译(recompilation),应在热路径里保持张量形状稳定。2026 的 torch.compile 已相当成熟,但遇到诡异报错先关掉它定位问题。
- autocast 的范围:只包「前向 + loss」,反向由 autograd 自动按对应精度走;不要把 optimizer.step 放进 autocast。
- BF16 下也可不用 GradScaler:BF16 动态范围与 FP32 同,几乎不会溢出,这也是它取代 FP16 成为 LLM 训练默认的原因。
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)
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 / 分布式 sampler | FP16 缩放丢失、多卡数据不均 | 同上 |
5.5 动手练习与自测
- 不查文档手写一个含 model.train/eval、no_grad、zero_grad(set_to_none) 的训练循环。判据:三处行为切换都正确,验证不建图。
- 用 gradcheck 验证一个自定义函数,并说明返回 False 意味着什么。答案:反向实现与数值梯度不一致,即反向写错。
- 设计一个实验判断数据管线是否瓶颈,给出修复顺序。答案:测 data-only 吞吐对比训练吞吐;升 num_workers → 预处理入 Dataset → 异步拷贝。
- 说明 autocast 应包住哪些代码,为什么 optimizer.step 要在外面。答案:只包前向+loss;参数更新需 FP32 精度以保证数值稳定。
- 写出断点必须保存的六类状态,并各举一个不保存的后果。判据:model/opt/sched/step/RNG/scaler 齐全。
- 解释 set_to_none=True 与把梯度置零为 0 的性能差异。答案:前者释放内存、更省显存且略快,后者需写一遍零。
6. 训练工程:在有限资源下把事做成
学习路径
- 读 6.1:背下 FP32/FP16/BF16/FP8 的动态范围与位宽,理解 2026 默认 BF16
- 跑 FP16 vs BF16 溢出对比(70000 在 FP16 变 inf),确认 BF16 免缩放
- 对接 M7:训练管线默认 BF16,必要时再评估 FP8 前置条件
核心知识点详解
- 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。
学习路径
- 读 6.2:理解梯度累积(loss 除 accum)、梯度裁剪、梯度检查点三件套
- 实现等效大 batch 的梯度累积,注意 sched 按等效步调、loss 除 accum
- 开梯度检查点测显存峰值降幅(约 1/4),代价是慢 25–40%
- 对接 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()。
学习路径
- 读 6.3:背下训练显存 = 参数+梯度+优化器(AdamW 12B)+激活,激活≈2·S·B·L·h·k
- 用公式估 7B 全量训练(约 112 GB)并对照实际小时曲线
- 对接 M7:为训练管线估好显存,让 batch/accum/检查点选择有的放矢
核心知识点详解
- 训练显存五项:训练显存 = 参数 + 梯度 + 优化器状态 + 激活;参数/梯度各 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 而非权重。
学习路径
- 读 6.4:区分 DP / DDP(all-reduce) / FSDP / ZeRO-1/2/3 各切什么
- 用 FSDP 切 7B 微调,观察每卡显存下降与通信开销
- 估算 ZeRO-3 8 卡每卡约 14 GB(实为更小),对照单卡全量
- 对接 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 为何要缩放: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 的动态范围与取舍
| 格式 | 位宽(符号/指数/尾数) | 动态范围 | 取舍 |
|---|---|---|---|
| FP32 | 1/8/23 | ±3.4e38 | 基准精度,优化器状态常用 |
| FP16 | 1/5/10 | 最大 ~65504 | 省显存但易溢出,需 loss scaling |
| BF16 | 1/8/7 | ±3.4e38(同 FP32) | 范围大不溢出、几乎免缩放;尾数少精度略低;2026 LLM 默认 |
| FP8 (E4M3) | 1/4/3 | ±448 | 2× 吞吐与显存;范围小,需 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 是有经验的团队的“最后一公里”。
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)
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 × 序列长度增长,往往是大头。
| 组成部分 | 估算(每参数) | 说明 |
|---|---|---|
| 参数 Parameters | 2 B(BF16)/ 4 B(FP32) | 推理只需这一项 |
| 梯度 Gradients | 2 B(BF16) | 反向必存 |
| 优化器状态(AdamW) | 12 B | FP32 主副本 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 → 显存骤降,可在单卡跑
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 互联节点内。
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 | 跨节点极深模型 |
6.5 动手练习与自测
- 解释 FP16 为何需要 loss scaling 而 BF16 不需要。答案:FP16 指数位 5,最大约 65504,梯度易溢出;BF16 指数位 8,范围同 FP32。
- 写出 FP8 E4M3 与 E5M2 的分工与范围。答案:E4M3(±448)用于前向/权重;E5M2(±57344)用于梯度。
- 梯度检查点省多少显存、代价多少时间。答案:激活显存可降到约 1/3–1/4,反向多花 25–40% 时间。
- 推导 7B 全量训练的基础显存,并说明为何单卡 80GB 不够。答案:16 B/参数 ≈ 112 GB,再加激活更超 80GB。
- 用 KV Cache 公式估算 8B 模型(GQA 8 头)在 32K 上下文下的每请求显存。答案:约 4 GB。
- 列举三种省显存手段并说明各自代价。判据:累积(时间×accum)、检查点(时间+25–40%)、FSDP(通信)。
7. 卷积与归纳偏置:为 ViT 铺垫
学习路径
- 读 7.1:理解局部性 + 平移等变性 + 权值共享,背下感受野递推 R_l 公式
- 手算 3 层 3×3 卷积感受野(1→3→5→7),跑卷积栈验形状与参数量
- 完成 7.2 自测:解释权值共享为何让参数量与输入分辨率无关
核心知识点详解
- 两大归纳偏置:局部性(有意义的模式是局部的)+ 平移等变性(位置移动后仍是同一东西)+ 权值共享 让参数量远小于同能力全连接: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;参数量与输入分辨率无关(只取决于核/通道),这是和全连接的质变。
学习路径
- 读 7.1:区分冻结主干 / 部分 / 全量微调,及 RandAugment/Mixup/CutMix 强度
- 用预训练主干跑三种迁移方案,记录小数据下的精度差异
- 完成 7.2 自测:解释课程学习为何加速收敛、小数据为何 CNN 胜 ViT
- 完成 7.2 自测:补上手算感受野与卷积栈形状
核心知识点详解
- 迁移三分法:按数据量×与预训练域相似度决定微到哪层:冻结主干(小数据/域接近)、部分微调(解冻后几层+更小 lr)、全量微调(大数据/域差大+极小 lr+早停);几乎总是优于从头训练。
- 2026 视觉配方:强数据增强(RandAugment/Mixup/CutMix 常 +2–5 点)+ 迁移学习 + 混合精度;课程学习先低分辨率/简单样本逐步提高可显著缩短收敛步数。
- 小数据 CNN 胜 ViT(坑):现代视觉 ViT 主导(大数据超越 CNN),但数据极少时 CNN 的归纳偏置仍是优势(Val 方差小);别在小数据上硬训 ViT——这就是阶段 11「小数据用 CNN、大数据用 ViT」的伏笔。
核心知识点详解
- 感受野手算: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
→ 深层神经元覆盖更大区域,这是层级特征的基础。
- 经典架构演进:LeNet → AlexNet → VGG(堆深)→ ResNet(残差解决退化)→ EfficientNet(复合缩放)→ ConvNeXt(把 CNN 现代化以对标 ViT)。
- 现代视觉是 ViT 主导:Vision Transformer 把图像切块当 token,大数据下超过 CNN;但小数据下 CNN 的归纳偏置仍是优势(这是阶段 11 多模态 / 视觉的重点铺垫)。
- 迁移学习是默认动作:预训练主干 + 替换分类头 + 分层学习率(主干小、头部大)几乎总是比从头训练强。
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 动手练习与自测
- 手算 3 层 3×3、步长 1 卷积的感受野。答案:1→3→5→7,第 3 层感受野 7×7。
- 解释 CNN 的两大归纳偏置及各自带来的好处。答案:局部性(参数量小)、平移等变性(位移不变、权值共享)。
- 算出给定卷积栈的输出 shape 与参数量。答案:输入 224 → 112 → 56;参数量与输入分辨率无关。
- 说明「冻结主干 / 部分微调 / 全量微调」的适用条件。答案:按数据量与域相似度决定,数据少则冻结。
- 列出三种 2026 常用视觉增强及其典型强度。答案:RandAugment、Mixup(α≈0.2)、CutMix(≈1.0)。
- 解释课程学习为什么能加速收敛。答案:先易后难,早期梯度方向更稳定、避免一开始就被难例带偏。
8. RNN → Attention:一个必然的演进
学习路径
- 读 8.1:背下 RNN 三硬伤(串行/长距梯度消失/固定容量),记住 LSTM 只缓解第二点
- 跑 seq2seq+Attention 最小实现,对比 LSTM 与 Attention 的耗时与可并行性
核心知识点详解
- RNN 三硬伤:①串行难并行(按时间步递推,训练慢);②长距梯度消失(记不住久远信息);③固定容量瓶颈(所有历史塞进一个向量)。LSTM/GRU 门控只缓解②,没解决①③。
- 注意力为何取代它:注意力不做压缩、直接让每个位置查看所有位置并加权——天然可并行,且任意两位置路径长都是 1,长距离依赖不再是问题。这是 Transformer 取代 RNN 的根本原因。
- 递归 vs 检索(坑):RNN 用递归压缩历史(容量有限、难并行),注意力用检索访问历史(路径最短、可并行);只背「可并行」不点「路径长 1/检索」会被追问机制。
学习路径
- 读 8.1:理解注意力并行 + 路径最短 + O(T²) 显存,认识 FlashAttention 在线 softmax
- 完成自测:对比 LSTM 与 Attention 在长程任务上的耗时与梯度径
- 完成自测:说明「递归压缩」与「检索式访问」的本质差别
核心知识点详解
- 注意力复杂度代价:注意力需 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 步——别把它们当完全无关的新东西。
核心知识点详解
- 三硬伤清单:串行难并行、长距梯度消失、固定容量;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 # 加权求和 = 上下文
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 动手练习与自测
- 列出 RNN 的三个硬伤,LSTM / GRU 解决了哪一个。答案:串行难并行、长距梯度消失、定容瓶颈;门控缓解第二个。
- 解释注意力为什么能并行而 RNN 不能。答案:注意力对整段做矩阵乘,各位置独立;RNN 需按时间步递推。
- 给出注意力相对 RNN 的复杂度代价。答案:时间/显存 O(T²),RNN 为 O(T)。
- 说明 FlashAttention-3 解决了什么、提速多少。答案:分块 + 在线 softmax 把显存降到 O(T),H100 FP8 下快 2–4×。
- 描述流匹配与扩散的关系与优势。答案:学连续速度场替代多步去噪,训练更稳、采样步数更少。
- 一句话总结「RNN 递归 vs 注意力检索」的差别。答案:前者压缩历史、容量有限;后者检索历史、路径最短。
9. 训练调试清单:先小后大
学习路径
- 读 9.1:背下五类症状→优先怀疑→动作表,记住「先小后大」两个铁律
- 制造一次 NaN,用 forward hook 逐层定位首个非有限层
- 完成 9.3 自测①:过拟合一个 batch(8 条)把 loss 压到 <0.05
核心知识点详解
- 两个铁律:①先过拟合一个 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 的核心原因。
学习路径
- 读 9.2:写过拟合一个 batch、梯度范数监控(clip_grad_norm 传 1e9 只读)、LR range test
- 跑 LR range test 画 loss-lr 曲线,取谷底 1/10 作初始 lr
- 完成 9.3 自测②④:写出只读梯度范数原理并估出初始 lr
核心知识点详解
- 过拟合 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 分钟内暴露。
核心知识点详解
- 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 |
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 动手练习与自测
- ① 过拟合一个 batch:取 8 条样本反复训 200 步,loss 应降到接近 0。若停在 0.6 附近不下,写出你的三步排查顺序。
判据:先确认标签与输入对齐(打印 x[0]、y[0]),再确认容量够(换极浅网络再试),最后确认 lr 与优化器(换 SGD lr=0.1)。任一环节把 loss 压到 <0.05 即算通过。 - ② 梯度范数监控:写出用
clip_grad_norm_(params, 1e9)取范数的代码,并解释为什么传 1e9 而非 1.0。
判据:1e9 远大于正常范数,等于「只读不裁」;传 1.0 会真裁剪从而污染读数。返回值是裁剪前的全局 L2 范数,可直接记录。 - ③ NaN 定位:注入一个 bug 让某层权重为 inf,用 forward hook 找出它。记录首个报错模块名与最终 loss 报错行的层数差。
判据:能稳定复现「首个非有限层比事后怀疑层更靠前」,说明 hook 比读 traceback 有效。 - ④ LR range test:跑一次 300 步指数扫描,画 loss-lr 曲线并估出可用初始 lr。
判据:曲线呈「水平 → 下降谷 → 上翘」三段;所选初始 lr 应落在谷底左侧约 1 个数量级处。把它与默认 3e-4 对比,写出偏差原因。 - ⑤ 稳定性量级:解释为什么
grad_norm长期为 0 是红灯。
判据:答出三种可能——梯度消失(深层无残差/归一化)、lr 过小、参数未被 optimizer 接管(漏传 parameters 或用错 param_group)。
10. 实验追踪:让结论可信(里程碑 M7)
学习路径
- 读 10.1:理解 W&B/MLflow/TensorBoard 该记录的超参/曲线/产物/代码版本/环境
- 用 W&B(或 MLflow)跑一次实验,记录 lr/batch/种子/loss/grad_norm/tokens_per_sec
- 对接 M7:训练管线接实验追踪,自动记录数据版本哈希+配置+指标
核心知识点详解
- 五类记录要素:超参(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 会漏掉「梯度爆炸/数据瓶颈」的早期信号——两者都该进曲线。
学习路径
- 读 10.1:理解 YAML/dataclass 配置化与 A/B 固定变量只变研究项
- 做一个 AdamW vs Muon 的公平 A/B 对照(固定数据/模型/步数/种子),记录 val_loss 与吞吐
- 对接 M7:在训练管线里对比 AdamW 与 Muon,说明 Muon 的应用边界
核心知识点详解
- 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),小模型收益不明显、不值得无脑替换。
学习路径
核心知识点详解
- 六类可复现要素:超参、随机种子、代码版本、环境版本、数据版本、指标曲线;缺种子→无法复现、缺 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,本地零依赖,但协作/对比弱于前两者
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 动手练习与自测
- ① 最小可复现要素:列出一次实验必须记录的 6 类信息,并说明缺哪类会导致什么后果。
判据:超参、随机种子、代码版本、环境版本、数据版本、指标曲线。缺种子→无法复现;缺 commit→无法归因;缺环境→跨机漂移无从解释。 - ② A/B 对照设计:要在「AdamW vs Muon」间做公平对比,写出你必须固定与必须变化的变量。
判据:固定数据、模型、步数、评估集、初始化种子;只变优化器与其 lr(各自调优)。若只跑一次、共用 seed 却报「Muon 更好」即为无效结论。 - ③ 复现性自检:写一段代码在实验开始时打印并落盘 seed、torch/CUDA 版本、git commit、数据指纹。
判据:能输出git rev-parse HEAD与torch.version.cuda;同一实验跑两次指标差异应 <1e-3。 - ④ 曲线读取:给定一条 val_loss 在第 12k 步后开始回升、train_loss 仍降的曲线,写出诊断与 2 个动作。
判据:诊断为过拟合;动作为加数据/正则(wd、dropout)、早停取 12k 步的 checkpoint。 - ⑤ 前沿判断:为什么 Muon 的 weight decay 不能直接复用 AdamW 的数值?
判据:因 Muon 更新已被正交化、尺度与 AdamW 不同,wd 需按更新尺度重新标定;直接套用常会过大导致欠拟合或过小导致过拟合。
项目里程碑
把基线迁移到 PyTorch,并搭建一套可复用的训练管线:混合精度、梯度累积、checkpoint 续训、随机种子固定、实验追踪(wandb 或本地 MLflow)。这套管线在第 9 阶段微调时会被直接复用。
本阶段产出(直接进入项目仓库)hamauls_orion/train/loop.py:通用训练循环(AMP、梯度累积、梯度裁剪、断点续训、早停)hamauls_orion/train/tracking.py:实验追踪封装,记录数据版本哈希 + 配置 + 指标configs/train/*.yaml:Hydra 风格配置,命令行可覆盖docs/exp/pytorch-baseline.md:与 M6 基线对比,确认指标不退化- 一个真实的 bug 复盘:记录你遇到的 shape/显存/数值稳定性问题及定位过程
阶段练习项目
- 不依赖 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 特殊逻辑(留给后续阶段)。
- 在小任务上稳定制造 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/梯度范数对比图
不做真实业务模型调优;聚焦排错方法与可复现故障注入。
- 在 200–500 张小数据多分类上完成四方案对比,报告 val 指标与训练成本
- 定量说明四种方案在小数据下的排名与差距(期望冻结/部分微调显著优于从头训练)
- 给出为什么小数据下 CNN 归纳偏置(或冻结主干)优势的机制解释
- 用 timm/torchvision 预训练主干(如 resnet/convnext tiny)+ 替换分类头
- 四方案共享同一数据划分与评估集,指标可比
- 配
RandAugment/Mixup/CutMix增强与分层学习率 - 记录各方案的 val_acc/loss、训练耗时、显存
scripts/transfer_learning.py+ 运行记录- 四方案对比表与结论(Markdown)
- README(如何在别的数据集复现)
不做 ViT 与大数据训练;不做多模态;仅聚焦迁移学习对比方法。
- 落地一条可复现训练管线,
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 阶段通用底座的使用方式)
不做多卡/分布式(可留接口);不做模型的业务层逻辑;只交付训练底座。
常见误区
- 验证时忘记 no_grad / eval,导致显存爆或指标不可复现。
- 梯度累积时忘记缩放 loss,等效学习率被放大,训练发散却找不到原因。
- 只跑一次就下结论,不固定种子、不做对照,实验结论无法复现。
- 过早优化架构,却不检查数据与标签是否对齐(数据集 bug 比模型 bug 常见得多)。
- 在小数据上硬训 ViT,忽略 CNN 的归纳偏置优势;或在大数据上死守 CNN,效率落后。
- 训练几小时的脚本没有断点续训,一次掉线全部重来。
- SwiGLU 的 hidden 直接设 4d,导致 FFN 参数量从 8d² 暴涨到 12d²,与论文配置对不上、显存估算全错(应 hidden=8d/3)。
- 在单张 80GB 卡上试图全量微调 7B,忘了 112GB 的显存公式,反复 OOM 却没想到 ZeRO-3 / LoRA。
- 把 FP16 当 BF16 用却没开 loss scaling,梯度溢出成 NaN;或不该用 FP8 时强行上 FP8 导致数值不稳。
- 忘记 model.eval() 就让 BatchNorm 在推理时用了 batch 统计量,结果随 batch 抖动。
面试高频问题速答
为什么需要残差连接?
深层网络的恒等映射很难学,梯度在多层反向传播中会衰减。残差让 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 与稀疏/线性注意力。