训练时最怕什么?不是loss不降,而是莫名其妙跳到NaN。我之前调一版模型时,同样的数据和代码,把精度设置换了一下,一个版本跑得非常顺,另一个版本每隔几百步就崩一次。排查到最后发现不是代码问题,而是浮点格式本身的数值特性在作怪。那一刻我意识到,FP、FP16和BF16这三个词,虽然天天挂在嘴边,但很多人并没有真正理解它们的区别,以及这种区别会在什么时候坑到你。
这篇文章不打算讲那种"FP16是半精度、BF16是脑浮点"的百度百科式概念,而是从二进制位出发,把三种格式的结构差异、数值范围、训练推理中的实际表现一次讲透。无论你是在调训练脚本,还是在给推理服务选型,看完这篇基本就能自己做判断。
1. 先从二进制位说起:三种浮点格式的结构差异
1.1 IEEE 754浮点标准的底层约定
所有现代计算机里的浮点数,基本都遵循同一套规则:用二进制来近似表示实数。核心思想是把一个数字拆成三部分——符号位、指数位和尾数位。符号位决定正负,指数位决定数量级,尾数位决定精度。
这个设计很像科学计数法。比如十进制里的 1.23 × 10^5,1.23是有效数字,10的指数5决定这个数有多大。浮点数里"尾数"就是有效数字,"指数"就是十的几几次方,只不过底数换成了2。
关键在于,三种格式的位宽不同,但指数和尾数的分配比例也不同。这就导致它们不是简单的"精度降一半"的关系,而是"范围和精度各自做出了不同的取舍"。
1.2 三张卡片的位分配对比
以32位单精度浮点(也就是我们经常直接叫FP的格式)为基准,FP16和BF16都是16位,但内部结构差异明显:
| 格式 | 总位宽 | 符号位 | 指数位 | 尾数位 | 指数偏置 |
|---|---|---|---|---|---|
| FP(FP32) | 32 | 1 | 8 | 23 | 127 |
| FP16 | 16 | 1 | 5 | 10 | 15 |
| BF16 | 16 | 1 | 8 | 7 | 127 |
表格里最值得注意的一点是:FP16的指数位只有5位,而BF16保留了8位指数。**BF16的指数位与FP32完全一致,这意味着BF16能表示的数量级范围和FP32一样大。**FP16看似也是16位,但因为指数位被压缩到5位,它的数值范围比FP32和BF16小得多。
反过来看尾数位。FP16有10位尾数,BF16只有7位,FP32有23位。尾数位直接决定了相对精度。所以从纯精度角度说,FP16比BF16要高一点,但这点精度优势在实际训练中常常被FP16的范围缺陷掩盖。
1.3 指数决定"能蹦多高",尾数决定"分得多细"
理解浮点格式,一定要记住这句话:指数位管范围,尾数位管精度。
打个比方,指数位更像是整栋楼的楼层范围,尾数位像是每一层的电梯按钮精度。FP16相当于一栋只有几层的矮楼(指数范围窄),所以它很难同时容纳"很大"和"很小"的数字。BF16则是把楼盖得很高(和FP32一样能容纳巨量级),但电梯按钮只有几个(尾数少),所以相邻两个可表示数字之间的间隔很大。
这个区别在训练场景里会带来完全不同的行为模式。FP16常常因为数值溢出而崩溃,BF16则很少溢出,但会表现出一种缓慢的、不易察觉的精度损失。这些都是后文要展开讲的重点。
2. 数值范围与精度误差:FP16的溢出危机和BF16的精度妥协
2.1 三种格式的极限数值计算
有了位结构,就能算出每种格式真正的极限数值。这里直接给结论,顺便写一下计算方式,方便你自己验证:
- FP32:最大正规值约 3.40×10^38,最小正规值约 1.18×10^(-38)。23位尾数对应的十进制有效位数约为 log10(2^23) ≈ 6.92,也就是大约7位有效数字。
- FP16:最大正规值约 65504,最小正规值约 6.10×10^(-5)。10位尾数对应约 log10(2^10) ≈ 3.01,也就是大约3位有效数字。
- BF16:最大正规值约 3.39×10^38,最小正规值约 1.18×10^(-38),这两个数字和FP32几乎一样。7位尾数对应约 log10(2^7) ≈ 2.10,也就是大约2位有效数字。
看到差距了吗?FP16的最大值才65504,这在神经网络训练里是个非常危险的天花板。比如一个普通的分类任务,logits稍微大一点,或者带有指数项的中间结果一膨胀,瞬间就能冲到几万甚至几十万,直接溢出变成无穷大,后续所有计算全部污染。
BF16虽然尾数少,有效位数只有约2位,但它的范围足够大,几乎不会发生溢出。
2.2 FP16在训练中为什么会爆NaN
很多人第一次遇到FP16训练崩溃,第一反应是"学习率太高了"或者"代码写错了",但其实很可能是FP16的数值范围撑不住了。
神经网络训练的中间过程会产生大量大小悬殊的数字。以Transformer类模型的注意力分数为例,Q和K的点积结果加上位置信息后,数值可能轻松超过几十。如果此时使用FP16存储和计算,一旦某个中间值超过65504,就会变成Inf。Inf参与后续任何加减乘除,轻则出现NaN,重则整轮梯度失效。
更隐蔽的是梯度问题。反向传播中计算出的梯度经常是极小的数值,例如10^(-4)级别甚至更小。FP16的最小正规值是6.10×10^(-5),小于这个值的梯度会被直接舍入为0,或者进入次正规数区域损失精度。这意味着小梯度在FP16下很容易被"抹零",导致参数更新不充分,训练卡在某个位置迟迟不收敛。
2.3 BF16的相对精度问题在实际中的表现
BF16没有溢出问题,但它的精度低得也很有"特色"。7位尾数意味着它的相对精度只有 2^(-8) = 1/256,约0.4%。也就是说,一个数值在BF16里相邻两档之间的间隔,大约是数值本身的0.4%。
这个精度在大部分深度学习场景里是够用的,因为训练本身带有随机性,0.4%的相对误差对于梯度更新来说常常可以接受。但如果你的任务对数值特别敏感,比如某些强化学习中的优势函数估计,或者需要非常精细的数值增量时,BF16的低精度就会显现出来。表现通常是loss下降曲线的底部出现一段"平台期",怎么优化都降不下去,但又没有完全炸掉。
我的经验是:遇到这种"没溢出但效果差一点"的问题,先别急着怪模型结构,换回FP32跑几百步对比一下曲线,如果FP32的效果明显更好,那大概率是BF16的精度不够,而不是训练策略有问题。
3. 训练场景的实战选择:混合精度是怎么工作的
3.1 混合精度的核心思路:主权重保持高精度
既然每种格式都有缺陷,聪明的做法自然是组合使用。这就是混合精度训练的底层逻辑:前向和反向传播用低精度来加速,权重更新用高精度来兜底。
具体做法是,模型里始终存一份FP32的"主权重",每次更新都在这份主权重上进行。FP16或BF16只用于前向计算和反向的梯度计算。算出来的梯度先转换回FP32,再加到FP32主权重上。这样既享受了低精度带来的速度和显存优势,又避免了权重在多次迭代中因为舍入误差被反复磨损。
很多框架的自动混合精度接口做的就是这件事,但很多人只把它当"开关"来用,没搞懂背后的机制。一旦遇到训练出问题,就不知道该从哪个环节调。
3.2 FP16的Loss Scaling机制
FP16训练最核心的技巧是Loss Scaling,也就是把loss值人为放大若干倍。
放大loss有什么用?因为梯度是通过链式法则从loss反传回来的,loss乘以一个scale因子(比如1024),梯度也会同比例放大。原本小到FP16里会变成0的梯度,放大后就落在FP16可以正常表示的范围内了。等梯度返回到FP32主权重时,再统一除以这个scale因子,还原真实梯度。
实际操作中scale因子可以动态调整。比较稳妥的做法是:每过若干训练步检查一次是否有梯度溢出,若出现溢出就减小scale,若长时间没有溢出就适当增大。这套机制在FP16训练中几乎是必须的。如果看到FP16训练早期loss就变成NaN或Inf,先检查loss scaling是否配置正确,尤其是自己手工实现训练循环而没有用框架封装接口的情况,十有八九问题出在这里。
3.3 BF16的"不用缩放"优势与代价
BF16最讨喜的地方就在这里:它不需要loss scaling。
因为BF16的指数范围和FP32一样,能表示的最小正规值和FP32相同,小梯度不会因为超出范围而变成0。从这个角度来说,BF16是一种"更省心"的混合精度格式。
省心的代价是什么呢?回到前面说的0.4%相对精度。不需要缩放,是因为它在计算大数值时不会翻车,但每次计算的舍入误差都不会小。尤其在梯度累计过程中,多次微小的舍入误差叠加,在某些任务里足以让收敛精度明显下降。
我在实际使用中对比过同一模型分别用FP16和BF16训练的结果:大部分任务两者收敛效果几乎一致,BF16训练过程还更稳定;但在一个对数值分辨率要求较高的回归任务中,FP16最终误差比BF16低了约15%。这说明并没有绝对的"谁更好",只有"谁更适合当前任务"。
3.4 不同训练阶段的行为对比
如果系统性地观察两种格式在训练中的行为,差异是比较明显的:
| 观察维度 | FP16 | BF16 |
|---|---|---|
| 梯度溢出风险 | 高,需要loss scaling | 低,基本不需要缩放 |
| 小梯度保留能力 | 弱,易被舍入为0 | 强,范围大能保留 |
| 大数值计算稳定性 | 差,中间值易超过65504 | 好,最大范围同FP32 |
| 稳态精度 | 较高,尾数10位 | 较低,尾数7位 |
| 调参复杂度 | 高,要关注scale和溢出 | 低,配置简单 |
如果你在做一个对收敛稳定性要求很高、但不想花太多时间调缩放参数的项目,BF16几乎是默认选择。如果你的任务需要尽量高的最终精度,并且你熟悉loss scaling的调参套路,FP16在某些场景下还能发挥出细微的精度优势。
4. 推理部署的格式取舍:显存、带宽与加速器特性
4.1 内存占用与带宽收益的定量对比
推理场景和训练场景关注点不一样,训练更在意数值稳定性,推理更在意延迟、吞吐和显存容量。
最直观的收益来自显存减半。同样一个模型,FP32存储需要大约28GB的话,FP16和BF16都只需要约14GB。这个差距在部署大模型时是决定性的。许多在单卡上装不下的模型,换半精度就能塞进去。
带宽方面同样受益。推理中解码阶段通常是访存密集型的,大量时间花在读取模型权重上。从FP32换成16位格式,每个权重占用字节减半,相同显存带宽下能传输的权重数量翻倍。在GPU算力足够但带宽不足的场景里,这一项带来的加速收益往往比想象中更大。
4.2 大模型部署为什么偏爱BF16
目前在生成式模型和各类大模型的推理部署中,BF16是普遍推荐的格式。原因有三点:
第一,BF16的指数范围与FP32一致,部署时如果遇到某些数值较大的中间结果,不容易出现FP16那种溢出。推理服务追求稳定性,没人想半夜收到告警说显存里出了Inf。
第二,BF16在硬件加速器上的支持已经非常普遍。很多新一代加速单元都把BF16作为原生支持的运算格式,算力往往与FP32相当甚至更高。反观FP16,虽然也不错,但在一些设备上只是兼容运行而非完全发挥性能。
第三,BF16转换成本低。因为它的指数位和FP32完全一样,从FP32转成BF16只需要截掉尾数的低16位,硬件实现简单,转换速度很快。FP16转换则要同时调整指数和尾数,步骤更繁琐。
这三点合起来,让BF16成为推理场景中最省心的16位格式。
4.3 与FP8/INT8等更激进方案的衔接
如果BF16还满足不了你的显存需求,下一步自然会往FP8甚至INT8的方向走。但这里要记住一个基本原则:格式越低比特,越要关注模型的动态范围和量化误差。
FP8有两种常见分配,一种偏范围,尾数少,类似BF16的思路;另一种偏精度,尾数多点但指数范围窄,类似FP16的思路。它在BF16的基础上把空间继续压缩一半,但支持的硬件范围和成熟度都不如16位格式。INT8则完全是另一套量化思路,通常需要校准数据和更复杂的量化算法来补偿精度损失。
从我的实践经验来看,部署管线的演进路径通常是这样:先用BF16跑通全流程,确保模型效果无损;如果显存还是紧张,再考虑对特定层做FP8量化;最后才考虑INT8或者更低比特。每一步都需要进行效果验收,不要一上来就上最低比特的格式。
5. 硬件兼容性盘点:不同设备上的支持情况与注意事项
5.1 各厂商设备的基本支持态势
不同硬件对FP16和BF16的支持成熟度并不相同,这也是选型时很容易踩的坑。
从市场整体看,消费级和专业级GPU对FP16的支持时间更早,很多架构从很早开始就为FP16设计了专门的加速单元和算子库。因此如果你手里的设备比较老,FP16往往是更可靠的加速选项。BF16则是后来才逐步推广的格式,新设备基本都支持,但老旧设备上可能出现算子缺失、只能走模拟路径的情况,速度优势打折扣。
云端专用加速芯片的情况则相反,很多面向AI计算的专用处理器很早就把BF16作为主要计算格式,FP16反而未必是优化重点。如果你的训练和部署环境主要跑在云端的专用加速器上,围绕BF16来设计会更顺畅。
5.2 不同硬件形态下的选型差异
除了独立GPU,还有几类常见硬件形态需要单独关注:
- 消费级显卡:FP16支持普遍且成熟,BF16支持要看具体架构,新一代基本没问题。
- 云端加速卡:新一代产品几乎都对BF16做了重点优化,很多官方示例和镜像默认推荐BF16。
- 边缘和端侧设备:市场分化明显,很多移动端和嵌入式平台的神经网络加速单元仍以FP16或INT8为主要格式,BF16支持往往有限。
- CPU处理器:较新的处理器才提供完整的16位浮点指令集。如果你主要靠CPU做推理,需要仔细查一下指令集支持情况,否则可能只能运行在降级路径上,速度反而不如FP32。
这个盘点不是为了给出"买哪家"的建议,而是想提醒你:做技术选型时,不能只看格式本身的数值特性,还要看目标硬件平台对格式的支持深度。强上某种"纸面最优"格式,到了不支持的环境里反而适得其反。
5.3 我自己的选型决策流程
因为经常在训练和部署之间来回切换,我总结了一套简单的决策流程,分享出来供参考:
第一,先确认目标硬件平台对BF16的原生支持情况。如果支持良好,训练和部署默认优先BF16,它能省去大量loss scaling相关的调试时间。
第二,如果平台对BF16支持不好,或者算子覆盖不全,转用FP16,但需要提前规划loss scaling策略,并准备好溢出监控机制。
第三,无论是在训练还是推理阶段,都准备一个FP32的"精度对照组"。模型结构复杂时,先用FP32跑通并记录指标基线,再切换到低精度格式对比效果。这样一旦出现问题,能快速判断是格式选择的问题还是模型本身的问题。
第四,对量化到更低比特(FP8或INT8)的方案保持克制,先在关键任务和小规模样本上验证效果,再决定是否全量推进。
6. 我的排错与验证经验
6.1 三种快速定位精度问题的方法
实践中最怕的就是"似乎有精度问题但说不清在哪"。这里整理三个可以复现的排查手段。
第一个方法是梯度溢出检查。训练过程中定期打印梯度的最大值、最小值和NaN比例。如果梯度最大值接近该格式的最大可表示值,说明有溢出风险;如果NaN比例突然升高,基本可以断定是计算过程中的Inf传染。这一步能帮你第一时间区分"范围问题"和"精度问题"。
第二个方法是FP32基线对比。选定若干固定step数,分别用FP32和低精度格式训练,记录每一步的loss和验证集指标。如果两者曲线在前几十步就明显分化,说明低精度格式对数值干扰过大;如果两条曲线前半段一致、后期才分开,往往说明低精度的舍入误差在缓慢积累。
第三个方法是针对性激活统计。挑几个关键中间张量,打印它们的数值分布,看是否处于低精度格式的"舒适区"。比如用FP16时,如果发现某些中间结果经常在10000以上,就该引起警惕,因为离65504的溢出线已经很近了。这一步通常是定位问题层的最快路径。
6.2 常见误用场景与坑
最后聊聊我在实际项目中反复见到的几个坑。
第一个坑是权重和激活函数隐含地超出格式范围。有些操作一步到位还好,就怕经过多次累加和指数运算后数值快速膨胀。比如softmax前的logits,在长序列和大量类别任务里很容易变得很大。用BF16通常能扛住,用FP16就非常危险。
第二个坑是批量归一化或层归一化中的小数值处理。归一化本身会产生非常接近零的数值,FP16表示这些数值时精度不足,可能导致方差计算失真。一些框架在混合精度实现中会把归一化层保留在FP32计算,目的正是避开这个坑。
第三个坑是混合使用不同格式导致的隐式转换。有些场景下模型权重是FP16,但某个中间张量被框架自动转成了FP32去计算,结果反而因为频繁转换损失了精度。遇到奇怪的精度问题时,建议仔细追踪每个算子的输入输出精度,别默认所有计算都在同一格式下进行。
第四个坑是自定义算子绕过了混合精度策略。如果你用了自定义的CUDA算子或者第三方扩展,这些算子很可能固定以FP32运行,这时即使整体设置了低精度,也不会有任何加速,甚至可能在边界处产生数值不一致。这个坑最难察觉,排查时一定要把自定义算子纳入检查范围。
6.3 最终的心里话
跑过这么多实验之后,我的感受是:FP16和BF16都不是"更好的精度格式",而是"不同的取舍方案"。FP16像一个灵敏度很高但底盘不牢的仪器,调好参数后能做出漂亮的成绩,但随时可能翻车;BF16像一个皮实耐用的工具,没有惊艳的表现,但几乎不会在数值层面给你添乱。
如果你刚接触混合精度训练,我的建议是直接从BF16入手,它的调试成本低得多。等你把整个训练流程跑顺了,理解了每一步为什么这样设计,再去尝试FP16,才会知道该在哪些地方小心。技术选型从来不应该是"谁参数好看选谁",而是"哪个格式让你能安稳睡个好觉"。
这段内容我纠结了很久才写出来,因为它不像前面几节那样有定理可讲,更多的是一种实战手感。希望你在自己的项目里也能找到一个这样的"安稳格式"。