2. 浮点数基础:FP16、BF16、FP32、TF32 的数据格式与精度对比
大伙儿做深度学习训练,每天都在跟浮点数打交道。但说真的,很多人对 FP32、FP16、BF16、TF32 这些格式的理解,也就停留在“位数不同”的层面。我之前在调一个超大模型时,就因为 FP16 的精度范围踩了个大坑——模型直接炸了。后来我才彻底搞明白,这些格式的差异,远不只是几个二进制位那么简单。
这一节,咱们就掰开揉碎了看。我会从位结构讲起,再对比它们的数值范围、精度、适用场景。你理解透之后,再做混合精度训练或者 GPU 调优,心里就有底了。
2.1 为什么要有多种浮点格式?
先回答一个基础问题:为什么不用统一的 FP32?
原因很简单:快和省。FP16 的带宽和计算吞吐比 FP32 高一倍,BF16 在保持大动态范围的同时也省显存。但天下没有免费的午餐——低位宽必然损失精度或者范围。你想想看,我们用混合精度训练,本质就是在精度和性能之间做权衡。
我在早期做 ResNet 训练时,觉得 FP16 省显存就无脑用,结果 loss 降不下去。后来才发现是 FP16 的指数位数太少,导致梯度下溢。这个坑,你们一定要避开。
所以,理解每种格式的位结构,才能知道它适合做什么。
2.2 四种格式的位排布对比
这里我直接给出一张结构对比表。注意看指数的位数——它决定了能表示的数值范围;尾数的位数决定了精度(相对误差)。
| 格式 | 总位数 | 符号位 (bit) | 指数位 (bit) | 尾数位 (bit) | 偏置值 | 最大可表示值 | 最小正规值 |
|---|---|---|---|---|---|---|---|
| FP32 | 32 | 1 | 8 | 23 | 127 | ~3.4e38 | ~1.18e-38 |
| FP16 | 16 | 1 | 5 | 10 | 15 | 65504 | ~6.1e-5 |
| BF16 | 16 | 1 | 8 | 7 | 127 | ~3.4e38 | ~1.18e-38 |
| TF32 | 19 | 1 | 8 | 10 | 127 | ~3.4e38 | ~1.18e-38 |
你看,FP16 的指数只有 5 位,所以最大只能到 65504,而 BF16 保留了 FP32 的 8 位指数,范围跟 FP32 一样大。但 BF16 的尾数只有 7 位,精度比 FP16 的 10 位尾数还低。所以 BF16 适合那些对数值范围敏感、但对精度容忍度高的任务(比如梯度累积)。
TF32 是 NVIDIA Ampere 架构推出的“混合”格式。它内部用 19 位存储(8 位指数 + 10 位尾数),但计算时自动处理。我的实测经验是:TF32 的精度跟 FP32 几乎没差别,但速度能快 1.5–2 倍。说白了,它是 NVIDIA 给不想改代码又想白嫖加速的人准备的。
2.3 一张图看懂四种格式的位结构
下面这张 SVG 把四种格式的位宽分布画出来了。注意看指数和尾数的长度对比——这是精度的根本区别。
从图上能很直观地看到:FP16 的指数段最短,所以能表示的最大值最小;BF16 的指数段长度跟 FP32 一样,但是尾数砍掉了一大截,精度损失严重。TF32 则是在 FP32 基础上压缩了尾数,保留了完整的指数范围。
2.4 精度与范围的数学对比
接下来我用量化的方式对比一下。你注意看 FP16 的最小正规值——只有 6.1e-5,也就是说比这个还小的正数都会变成零(下溢)。我之前训练一个 Transformer 模型,梯度里有很多元素在 1e-6 量级,FP16 直接把它们清零了。你想想看,这模型还能收敛才怪。
BF16 的最小正规值跟 FP32 一样,都是 1.18e-38,所以它对梯度下溢的容忍度极高。但它的相对精度(尾数的有效位数)只有约 2–3 位有效十进制数字。我建议在不需要高精度中间结果的地方用 BF16,比如 LayerNorm 的输入输出。
我习惯用下面这个表格来快速决定选哪种格式:
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 主权重存储 | FP32 | 避免累积误差 |
| 前向传播(大多数层) | FP16 / BF16 | 加速计算 |
| 梯度 | FP32 副本 + 混合精度 | 防止下溢 |
| 损失函数 / 归一化层 | FP32 | 精度敏感 |
| 大批量训练(显存吃紧) | BF16 | 大动态范围 + 省显存 |
| NVIDIA Ampere 以上 GPU | TF32 (默认开启) | 无需改代码即可加速 |
torch.set_autocast_dtype(torch.bfloat16),它会自动在精度敏感层使用 FP32,其他地方用 BF16。我在训练 70B 模型时,用这个策略省了 30% 显存,收敛曲线跟全 FP32 几乎一致。
2.5 实际使用中的注意事项
- FP16 的最大值只有 65504。如果你的激活值或权重超过这个数,就会变成 inf。我在做对比学习(SimCLR)时,温度系数设得太小导致 logits 很大,结果 FP16 全炸了。解决方法是做梯度裁剪或者换 BF16。
- BF16 精度低但不丢范围。它特别适合梯度通信的场景。因为梯度通常很小但需要范围覆盖,用 BF16 做 allreduce 可以减少一半带宽,且几乎不影响最终精度。
- TF32 只在 Ampere/Ada 架构上生效。你在 RTX 3090、A100、H100 上默认就能用。老架构(Volta、Turing)不支持,会回退到 FP32。可以用
torch.backends.cuda.matmul.allow_tf32 = True确保开启。 - 混合精度训练时,必须维护一份 FP32 的权重副本。大多数框架(AMP / bfloat16 autocast)自动帮你做了。但我见过有人自己写手撸训练循环,忘了做这一步,结果精度一路下跌。切记:
master_weights必须保留 FP32。
# PyTorch 混合精度训练示例(自动混合精度 AMP)
import torch
from torch.cuda.amp import autocast, GradScaler
model = MyModel().cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
scaler = GradScaler() # 动态 loss scaling,防止 FP16 下溢
for data, target in dataloader:
optimizer.zero_grad()
with autocast(dtype=torch.float16): # 或 torch.bfloat16
output = model(data)
loss = loss_fn(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
- FP32:通用标准,精度高,速度慢。
- FP16:速度快,但范围窄、精度低,容易下溢/上溢。
- BF16:范围与 FP32 一致,精度略低于 FP16,适合梯度通信和大模型训练。
- TF32:Ampere 架构专属,零代码改动加速,精度几乎无损。
我个人建议:如果你用的是新 GPU(A100/H100 或同等),优先尝试 BF16+TF32 的组合。BF16 做前向和反向,TF32 做矩阵乘法,效果非常棒。如果模型较大且梯度容易消失,一定要用动态 loss scaling 或者直接上 BF16。