你知道FP6和BF6的区别吗?一文搞懂AI训练如何选择数值精度
"God made the integers, all else is the work of man." —— Leopold Kronecker
在 Python 里运行这行代码:
>>> 0.1 + 0.2 == 0.3
False
>>> 0.1 + 0.2
0.30000000000000004
大多数人第一次看到这个结果都会觉得是 Python 的 Bug。但实际上这是所有现代编程语言共同的行为,C、Java、JavaScript 一样。
这背后的原因,和大模型为什么从 FP32 切换到 BF16、为什么梯度有时会突然变成 nan、为什么量化会损失精度——都是同一个物理原因。
3.1 浮点数的物理结构(IEEE 754)
计算机只有比特(0 和 1)。要用有限的比特表示无限稠密的实数,必然要做取舍。IEEE 754 标准的设计思路是:用科学计数法的二进制版本。
就像十进制的 ,一个浮点数被拆成三部分存储:
- 符号位(Sign):1 bit,0 正 1 负。
- 指数位(Exponent):决定数值的量级(能表示多大/多小的数)。
- 尾数位(Mantissa):决定数值的精度(有效数字有几位)。
三种格式的结构对比一目了然:

图:三行分别是 FP32、FP16、BF16,色块宽度正比于位数。红色=符号位,蓝色=指数位,绿色=尾数位。右侧标注了各格式的动态范围和十进制精度。
核心结论一眼能看出来:BF16 就是把 FP32 直接截断保留前 16 位——指数位位数不变(都是 8 bit),只砍掉了尾数位的精度。这个设计决策的含义下一节详说。
为什么 0.1 + 0.2 ≠ 0.3?
因为 在二进制里是一个无限循环小数:
就像 在十进制里无法精确表示()一样,FP64 只能存有限位,截断后引入了误差。两次截断误差相加,就出现了 。
动手试试:
import struct, numpy as np # 看 0.1 在内存里实际存了什么 b = struct.pack('f', 0.1) print(bin(int.from_bytes(b, 'little'))) # 0b111101110011001100110011001101 ← 注意尾数不是精确的
浮点数在数轴上的分布是不均匀的
这是理解所有数值问题的关键洞察:

图:上方是 Mini-Float(1+3+2 bit)的数轴分布,蓝线越稀疏代表精度越低;下方柱状图显示相邻浮点数的间隔随数值增大而增大。
数值越大,相邻两个可表示数之间的"间隙"越大。任何落在间隙里的计算结果都会被强制舍入到最近的可表示数。
这直接导致了「大数吃小数」:
import torch
a = torch.tensor(10_000_000.0, dtype=torch.float32)
b = torch.tensor(0.0000001, dtype=torch.float32)
print(a + b) # tensor(1.0000e+07) ← b 消失了
附近 FP32 的间隙约为 ,而 比这个间隙小了 14 个数量级,直接被抹掉。
上溢出与下溢出
两个边界情况:
-
上溢出(Overflow):计算结果超过格式能表示的最大值,变成
inf(无穷大)。- FP16 最大值只有 65504,乘法很容易溢出。
- 训练中出现
loss = inf通常是这个原因。
-
下溢出(Underflow):计算结果太接近 0,比格式能表示的最小正数还小,直接归零。
- FP16 最小正规数约为 。
- 反向传播中梯度经过多层乘法后可能非常小,在 FP16 下直接变成
0.0,参数永远不更新——这就是梯度消失的数值根因之一。
3.2 AI 训练中的精度选择
理解了浮点数的物理结构,现在来看三种格式在 AI 训练中的命运:

图:横轴是最大可表示数值的 log10,越长动态范围越大。注意 FP16 的动态范围远小于 BF16 和 FP32,而 BF16 与 FP32 几乎相同。
为什么 BF16 赢了 FP16?
FP16 的早期混合精度训练(V100 时代)面临一个持续的头痛问题:训练崩溃。
原因很直接——FP16 的指数位只有 5 bit,最大值 65504。而大模型中间层的激活值、梯度、权重更新量,随时可能超过这个上限变成 inf,然后整个训练崩掉。
Google 的工程师提出了 BF16(Brain Float 16),做了一个极其简单的设计:直接把 FP32 后 16 bit 截掉。
- 总位数,FP16:16,BF16:16
- 指数位,FP16:5 bit(最大值 65504),BF16:8 bit(最大值 ~3.4×10³⁸)
- 尾数位,FP16:10 bit(精度高),BF16:7 bit(精度低)
- 溢出风险,FP16:高(需要 Loss Scaling),BF16:几乎没有
- 神经网络适配性,FP16:一般,BF16:好(网络对精度鲁棒,对范围敏感)
神经网络天然对精度(尾数位)有鲁棒性——加一点噪声(量化误差)通常不影响收敛;但对范围(指数位)非常脆弱——一旦溢出就 nan/inf,训练直接挂。BF16 正好满足了这个需求。
科普:BF16 最早由 Google Brain 提出,用于 TPU 训练。随后 NVIDIA 在 Ampere 架构(A100)开始原生支持 BF16 Tensor Core,让 BF16 成为了大模型训练的默认精度。
Loss Scaling(仅 FP16 需要)
如果你用的是老显卡(V100、T4、2080Ti)只支持 FP16,需要手动处理下溢出问题。
核心思路:人为把 Loss 放大,让梯度"显得更大",避免被下溢出归零。
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler() # 自动管理 scale 因子
for x, y in dataloader:
optimizer.zero_grad()
with autocast(): # 前向用 FP16
loss = model(x, y)
scaler.scale(loss).backward() # loss × scale → 反向传播梯度也被放大
scaler.step(optimizer) # 梯度 ÷ scale 还原,再更新参数
scaler.update() # 动态调整 scale(没有 inf/nan 就加倍)
PyTorch 的 GradScaler 还会自动检测梯度里是否出现了 inf/nan:如果出现,跳过这次参数更新,把 scale 因子减半,再试一次。
常见误区:用了 BF16 还需要 Loss Scaling 吗?不需要。BF16 的动态范围和 FP32 一样,不存在 FP16 的溢出问题,
GradScaler在 BF16 下是 no-op(什么都不做)。
3.3 量化初步
BF16 相比 FP32,显存减半、速度翻倍。那能不能继续压缩?
量化(Quantization) 是下一步:把浮点数映射到整数格点。
最常见的线性量化公式:
其中 (scale)是缩放因子,(zero-point)是零点偏移。反量化:。
- FP8 E4M3,位宽:8 bit 浮点(4 指数位,3 尾数位),显存相比 FP32:÷4,适用场景:H100 训练前向、权重/激活
- FP8 E5M2,位宽:8 bit 浮点(5 指数位,2 尾数位),显存相比 FP32:÷4,适用场景:H100 训练反向、梯度
- INT8,位宽:8 bit 整数,显存相比 FP32:÷4,适用场景:推理加速(Tensor Core 支持)
- W4A16,位宽:权重 4 bit,激活 16 bit,显存相比 FP32:÷8(权重),适用场景:大模型推理(AWQ/GPTQ)
- INT4,位宽:4 bit 整数,显存相比 FP32:÷8,适用场景:极限压缩推理
FP8 的两种变体设计有不同取向:
- E4M3(4 指数位,3 尾数位):精度稍高,动态范围略小(最大值 448)。适合前向传播的权重和激活值——数值通常集中在较小范围内,更高精度更重要。
- E5M2(5 指数位,2 尾数位):动态范围更大(最大值 57344),精度稍低。适合反向传播的梯度——梯度分布较宽,动态范围比精度更关键。
H100 原生支持 FP8 Tensor Core,在 Transformer Engine(NVIDIA 的 FP8 训练库)里实现了自动缩放(per-tensor scaling):每个张量动态计算一个缩放因子,把数值映射到 FP8 的最佳表示范围,然后在 Tensor Core 内用 FP8 计算,累加器用 FP32,结果再转回 BF16 存储。
量化有两种主要方式:
- PTQ(Post-Training Quantization,训练后量化):模型训练好之后再量化,简单快速,但精度损失相对大。适合推理部署。
- QAT(Quantization-Aware Training,感知量化训练):训练过程中模拟量化误差(在前向中加入量化-反量化操作),让模型"习惯"量化带来的噪声。精度更好,但训练更复杂。
动手试试:用
bitsandbytes一行代码量化 HuggingFace 模型:from transformers import AutoModelForCausalLM, BitsAndBytesConfig import torch bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16) model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-3-8B", quantization_config=bnb_config, device_map="auto" ) # 原本需要 ~16 GB 显存,量化后只需 ~5 GB
核心结论:浮点数不是精确的实数,它是一把刻度不均匀的尺子——靠近 0 的地方精度高,数值越大精度越低。AI 训练的精度选择本质上是在"动态范围"和"精度"之间做权衡:FP16 精度高但易溢出;BF16 动态范围大但精度低;量化进一步压缩位宽以换取速度和显存。神经网络对范围敏感、对精度鲁棒,这是 BF16 和量化在实践中成功的根本原因。