跳转到主要内容

混合精度训练:bf16、fp32 Master 与 Loss Scaling

混合精度训练里谁用 bf16 跑、谁用 fp32 记:精度地图、master weights、loss scaling 与 fp16/bf16 的本质差别

· 约 4 分钟阅读

本文目录

训练基础 里的 16 B/param 账本默认了「bf16 混合精度 + AdamW」。这一页回答它的前提:一个 step 里到底哪些数据用 bf16 跑、哪些必须用 fp32 记,以及为什么 bf16 不需要 loss scaling 而 fp16 需要。

30 秒复习
  • 一句话:bf16 负责跑(前向反向算子吃带宽和 Tensor Core),fp32 负责记(master 权重、优化器状态、归约与累加);每个 step 开头把 master cast 成 bf16,结尾在 fp32 上更新后写回。
  • 三个判断:GEMM 输入输出可以是 bf16,但内部累加必须 fp32;optimizer 更新必须在 fp32 master 上进行,否则小更新被 bf16 舍入吞掉;bf16 指数位与 fp32 相同(8 位),所以不需要 loss scaling,fp16 指数只有 5 位,必须 scaling。
  • 核心模型:精度分配 = 存储/带宽用 bf16(2 B),精确状态用 fp32(4 B);两者拼出 16 B/param 的静态账。
  • 边界:master weights 不是唯一实现(Kahan summation、直接 bf16 更新都有探索);fp8 训练需要更细的 per-tensor scaling,是另一套合同。

1. 为什么要混合精度

纯 fp32 训练浪费:Tensor Core 的 bf16 吞吐是 fp32 的数倍,且权重、激活的显存和 HBM 流量直接减半。纯 bf16 训练不行:精度不够的地方真的存在——梯度归约、优化器的小步更新、softmax/loss 的归约,都会在低精度下损失到影响收敛。混合精度就是按「谁对误差敏感」分配精度。

2. 精度地图:一个 Step 里谁用什么精度

混合精度训练:一个 Step 的精度地图

数据 / 算子精度原因
fp32 master Θfp32更新的累加基准,必须精确
bf16 权重bf16前向反向实际读取,省一半显存和带宽
前向 GEMMbf16 输入,fp32 累加Tensor Core 内部累加器本来就是 fp32
激活bf16显存大头,对误差不敏感
loss / softmax 归约fp32归约链长,低精度误差累积
梯度 ∇Wbf16 存储,fp32 累积通信与存储 bf16,本地累积 fp32
optimizer 状态 m、vfp32二阶统计量对精度敏感

3. Master Weights:为什么更新必须在 fp32 上

AdamW 的单步更新量级通常是权重的 10−310^{-3} 到 10−410^{-4} 以下。bf16 只有 8 位有效数字(尾数 7 位 + 隐含位),当更新量小于权重的舍入间隔时,Θ+ΔΘ\Theta + \Delta\Theta 会被舍入回 Θ\Theta——权重原地不动,训练悄悄停滞。fp32 有 24 位有效数字,相对精度高约 2162^{16} 倍,更新不会丢失。

所以标准做法是:常驻一份 fp32 master 权重,optimizer 在 fp32 上做更新,每个 step 开头(或前向前)把 master cast 成 bf16 供算子读取。这就是 16 B/param 账里那 4P 的 master 项,也是混合精度里「fp32 负责记」的核心。

4. fp16 的 Loss Scaling,bf16 为什么不用

两种 16 位格式的位分配不同:

格式指数位尾数位动态范围精度
fp16510~±65504,最小正数 ~6e-8高(11 位有效数字)
bf1687与 fp32 同范围低(8 位有效数字)

梯度里有大量小幅值(<2−24< 2^{-24} 不罕见),fp16 的指数范围装不下它们,直接变成 0,梯度消失。fp16 训练的补救是 loss scaling:前向结束前把 loss 乘一个大常数 S,梯度整体放大 S 倍进入 fp16 的安全区,optimizer 更新前再除回 S;GradScaler 还会动态调整 S——出现 inf/nan 就跳步并把 S 减半。

bf16 指数位与 fp32 相同,梯度的幅值范围原样装得下,不需要 loss scaling,训练循环更省心。代价是尾数只有 7 位,权重的相对精度更粗——这正是需要 fp32 master 的另一面原因。现代 LLM 训练因此几乎一律选 bf16。

5. 落地:PyTorch AMP 骨架

scaler = torch.amp.GradScaler()          # 仅 fp16 需要;bf16 可省略

for batch, labels in dataloader:
    optimizer.zero_grad()
    with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
        loss = model(batch, labels=labels).loss
    scaler.scale(loss).backward()        # bf16: loss.backward()
    scaler.step(optimizer)               # bf16: optimizer.step()
    scaler.update()

autocast 决定哪些算子进 bf16(GEMM、卷积)、哪些保持 fp32(softmax、归一化、loss 归约),维护着一张框架级的黑白名单。注意 AMP 默认并不维护 master weights——那是 DeepSpeed / Megatron / torch.distributed.fsdp 混合精度配置做的事;裸 AMP + AdamW 的直接 bf16 更新在小 lr 后期会有可见损失。

相关页面

修改历史1 次提交