想象你出门办事,要把一天的现金带在身上。FP32 是能装下所有面额、还能精确保管的“重型保险箱”,FP16 是轻便的日常钱包,BF16 像是厚厚一叠大面额钞票夹,FP8 则是巴掌大的小零钱包。GPU 训练模型时,成百亿个数字在内存与计算单元之间搬来搬去,每个数字都要装进这四种“钱包”中的某一种。钱包越小,携带越轻、跑得越快,但也可能装不下大额数字、找不准零钱。低精度训练的全部智慧,就是学会给每个数字挑一个“够用又省钱”的钱包。 Tensor Core 是 GPU 上专门做矩阵乘法的计算核心,它同时支持 FP32、FP16、BF16、FP8 等数值格式。理解这些格式的“容量”与“精细度”,是 GPU 编程工程师优化训练吞吐、压低显存占用的基本功。本文用“钱包”作比喻,带你从零看懂低精度训练。
掌握低精度格式的精度-范围权衡,你就能在大模型训练与推理中把显存占用和计算延迟大幅压低,这是 AI 基础设施岗位的核心技能之一。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
数值格式基础
想象你出门办事,随身带着四种现金收纳工具:FP32 是能装下所有面额、还能精确保管的“重型保险箱”;FP16 是轻便的日常钱包;BF16 像是厚厚一叠大面额钞票夹;FP8 则是巴掌大的小零钱包。GPU 训练模型时,成百亿个数字在内存与计算单元之间搬来搬去,每个数字都必须装进这四种“钱包”中的某一种。钱包越小,携带越轻、跑得越快,但可能装不下或找不准。
计算机里浮点数的格式,就是钱包的内部分格。最通用的 FP32 用 32 位二进制表示一个数字;FP16 和 BF16 用 16 位;FP8 用 8 位。不管哪种格式,都像同一个抽屉分成三格:1 位符号、若干位指数、若干位尾数。指数决定数字能“大到什么程度”,尾数决定数字能“精确到多少位”。
了解了抽屉格局,你就会发现:低精度训练的全部艺术,在于为每个数字挑选最合适的钱包。选大了浪费空间,选小了丢精度甚至爆掉。
- 符号位用来表示正负,0 代表正数,1 代表负数。
- 指数项指数 e 减去偏移量 bias 后作为 2 的幂,决定了数字的量级大小。
- 尾数项小数部分 f 与隐藏的 1 组成有效数字,决定相对精度。
FP16 动态范围
FP16 就是那只轻便钱包:确实轻快,但你不能把面额太大的支票塞进去。FP16 指数只有 5 位,偏移量是 15,它能表示的最大有限正常数大约是 65504。一旦某个权重或中间计算超过这个值,它不会“找回零钱”,而是直接变成 Infinity,整次训练就会崩掉。
GPU 训练中,梯度突然冲到 65504 并不是小概率事件——模型最后一层或损失函数梯度很陡的时候特别容易出现。于是工程师设计了“损失缩放”,把梯度先放大或缩小再塞进钱包,避免溢出或下溢。
明白 FP16 这个钱包的容量上限,你就能理解:当数值经常接近 65504,就该换用更大面额的钱包,也就是下一节的 BF16。
- 尾数部分FP16 尾数 10 位全为 1 时,得到二进制 1.1111111111,即十进制 2 - 2^{-10}。
- 指数部分指数 5 位,偏差 15,最大正常指数为 30,指数项为 2^{15} = 32768。
- 相乘1.9990 × 32768 ≈ 65504,这就是 FP16 能装下的最大金额。
BF16 指数扩展
BF16 是那种“只做大额钞票夹”。它把 FP16 的指数位从 5 位扩到 8 位,也就是模仿 FP32 的量级范围;代价是尾数从 10 位削减到 7 位。好比钱包原本有收纳零钱的小格子,现在全被大面额夹层替换了。
8 位指数配上偏移量 127,BF16 的最大正常数范围与 FP32 几乎一致(约 3.4×10^38),所以大梯度不再是问题;但 7 位尾数意味着只有大约 2.4 位有效十进制数字。换句话说,数值范围很够,但只能记住两三位小数。
当你的梯度经常到达 FP16 装不下的量级,BF16 的大面额夹层就派上了用场,代价只是零头被抹掉。
- 隐藏位尾数 M 位之外还有隐藏的 1,所以有效二进制位是 M+1。
- 十进制换算每个二进制位大约带来 0.3010 个十进制有效位。
- 结果对 BF16 而言,8 × 0.3010 ≈ 2.41 位有效十进制数字。
FP8 训练推理
FP8 像超轻零钱包,只有 8 位,GPU 一次能同时搬运更多数字。它有几种变体,最常见的是 E4M3(4 指数 + 3 尾数)和 E5M2(5 指数 + 2 尾数)。E4M3 优先保证精度,最大约 448;E5M2 优先保证范围,最大约 57344。
训练中,FP8 通常不会单独承担所有工作:主权重仍保存在 FP32“保险箱”里,只在矩阵乘法等重计算算子中临时使用 FP8,再配合损失缩放。推理时,由于不需要反向传播和梯度,FP8 用起来更安全,常被用来压低延迟、提升吞吐。
零钱包虽轻,但装不下大额交易;FP8 训练必须搭配 FP32 主账本与损失缩放,才能既快又不丢“钱”。
- 尾数贡献3 位尾数中最大合法步长是 6/8,因为尾数 7 搭配指数 8 会超过最大范围,实际被饱和回 448。
- 指数贡献E4M3 正常指数最大到 8,带来 256 倍放大。
- 乘积1.75 × 256 = 448,这就是 E4M3 的最大有限值。
与FP32精度对比
如果上一节是四个钱包的容量对比,这一节是它们的“找零能力”对比:相对误差。FP32 的 23 位尾数,最小相对间隔约 5.96×10^{-8};FP16 的 10 位尾数约 4.88×10^{-4};BF16 的 7 位尾数约 3.91×10^{-3};FP8 的 3 位尾数约 6.25×10^{-2}。
相对误差可由尾数与隐藏位共同决定。尾数位数越多,相邻两个可表示数之间的距离就越小,意味着找零更精确。
精度低的钱包找零极粗,但只要账目不需要细到分,FP8 的误差完全可以接受,而且速度快得多。
- 隐藏位实际有效尾数是 M+1 位,因为左侧总有一个隐藏的 1。
- 最小相对间隔相邻两个可表示数之间大约相差 2^{-(M+1)}。
- 代入对比M=23 时 ε≈5.96×10^{-8},M=10 时 ε≈4.88×10^{-4},M=7 时 ε≈3.91×10^{-3},M=3 时 ε≈6.25×10^{-2}。
混合精度策略
成熟的会计不会把所有现金随身带:主账本锁在 FP32 保险箱,随身只带 FP16 或 BF16 零钱包,每笔交易都配一个“放大镜”——损失缩放。这就是混合精度训练。
具体流程是:先将损失 L 乘以缩放因子 s,再做反向传播;梯度在低精度计算时因被放大而不会下溢成 0;更新前再除以 s,恢复到真实尺度。这样既享受了低精度的速度,又保住了小梯度的“零钱”。
保险箱里的主账本从未离开,小钱包只负责跑腿和记账,混合精度才能让训练又稳又快。
- 损失缩放把损失放大 s 倍,让那些特别小的梯度不至于在 FP16 中变成 0。
- 反向传播在低精度计算中得到缩放后的梯度 \tilde{g}。
- 更新前还原除以 s 后,再用 FP32 主权重做更新,从而保持训练稳定。
格式选择指南
出门选钱包,取决于你一天要经手多大面额、多细的零钱。训练也是如此:如果你的梯度或激活值经常冲到 65504 附近,说明 FP16 钱包容量可能不足,优先考虑 BF16;如果模型对精度非常敏感,FP16 在大多数小数值区间更精细;如果追求最大吞吐且愿意做缩放,FP8 零钱包能带来最大加速;遇到收敛不稳定,FP32 账本就是最终兜底。
工程上可以写成最小化“误差 + 时间成本”的选择模型:
四种钱包没有绝对的好坏,只有合不合适。低精度训练,就是学会给每个数字找到最合适、最省钱的钱包组合。
- 误差项ε 越小,越接近 FP32 训练结果。
- 成本项B 是存储字节,S 是吞吐,比值越小越快。
- 系数α 和 β 根据你的任务调节,精度优先时 α 调大,速度优先时 β 调大。
AI 应用场景
大语言模型预训练
阶段:模型训练
解决的问题:LLM 参数量动辄几十亿,全 FP32 训练显存爆炸;传统 FP16 又容易梯度溢出。用 BF16/FP8 混合精度可将显存降低近一半,同时通过 FP32 主权重与损失缩放保持稳定收敛。
具体案例:在 H100 GPU 上训练 7B 参数模型时,默认使用 BF16 混合精度:主权重存 FP32,AdamW 优化器状态存 FP32,前向/反向矩阵乘用 BF16。显存占用下降约 40%,训练吞吐提升近 2 倍,收敛曲线与全 FP32 几乎一致。
大模型推理加速
阶段:模型推理 / 部署
解决的问题:在线推理对延迟极其敏感,FP32 计算慢且带宽浪费大。FP8 没有反向传播,误差可控,特别适合矩阵乘,可将吞吐提升 30%-60%。
具体案例:在 TensorRT 中将 LLM 的 Linear 层权重量化到 FP8(E4M3),激活也使用 FP8,配合理量化校准。最终 batch-serving 吞吐提升约 50%,精度损失小于 1%。
目标检测任务混合精度训练
阶段:模型训练
解决的问题:YOLO 等检测模型使用 FP16 训练时,损失在 0.1-500 之间剧烈波动,小梯度容易下溢为 0,导致不收敛。
具体案例:训练 YOLOv8 时启用自动混合精度(AMP),开启动态损失缩放。缩放因子在 128-8192 之间自动调整,小梯度被放大后仍然可表示,最终收敛速度和显存占用均优于全 FP16 方案。
Python 演示
import mathimport numpy as np def to_bfloat16(x): # 模拟 BF16:先舍入再保留高 16 位 bits = x.astype(np.float32).view(np.uint32) rounded = (bits + 0x00008000) & 0xFFFF0000 return rounded.view(np.float32) def to_fp8_e4m3(x, scale=1.0): # 极简模拟 FP8(E4M3),把标量量化到 FP32 可读的近似值 v = float(x) / scale sign = 1.0 if v >= 0 else -1.0 a = abs(v) if a == 0.0: return 0.0 e = math.floor(math.log2(a)) e = max(-6, min(8, e)) m = round((a / (2 ** e) - 1.0) * 8) m = max(0, min(7, m)) out = sign * (1 + m / 8) * (2 ** e) * scale return max(-448.0, min(448.0, out)) def main(): weights = np.array([0.001, 0.1, 1.5, 3.14159, 12.8, 1e5], dtype=np.float32) fp16 = weights.astype(np.float16).astype(np.float32) bf16 = to_bfloat16(weights) fp8 = np.array([to_fp8_e4m3(w) for w in weights], dtype=np.float32) print("原始 FP32:", weights) print("FP16 :", fp16) print("BF16 :", bf16) print("FP8(E4M3):", fp8) err16 = np.max(np.abs(fp16 - weights) / np.maximum(np.abs(weights), 1e-30)) errbf = np.max(np.abs(bf16 - weights) / np.maximum(np.abs(weights), 1e-30)) errfp8 = np.max(np.abs(fp8 - weights) / np.maximum(np.abs(weights), 1e-30)) print("最大相对误差 FP16:", err16) print("最大相对误差 BF16:", errbf) print("最大相对误差 FP8 :", errfp8) if __name__ == "__main__": main()
rounded = (bits + 0x00008000) & 0xFFFF0000模拟 BF16 的舍入行为,把 FP32 低位抹掉后保留高 16 位。fp16 = weights.astype(np.float16).astype(np.float32)演示 1e5 在 FP16 中变成 inf,即“钱包撑爆”。e = max(-6, min(8, e))把指数限制在 E4M3 能表示的范围内,防止量化时越界。m = round((a / (2 ** e) - 1.0) * 8)把小数部分量化到 3 位尾数,相当于“抹零到 1/8”。return max(-448.0, min(448.0, out))模拟 E4M3 的最大有限值饱和,超出 448 就截断。errbf = np.max(np.abs(bf16 - weights) / np.maximum(np.abs(weights), 1e-30))计算最大相对误差,衡量每个钱包找零到底差多少。
常见误区
误区FP16 速度更快,所以把权重、梯度、优化器状态全部改成 FP16,模型应该跑得飞快。
正解如果所有东西都改成 FP16,主权重和梯度的小数值很容易溢出或下溢,训练很快就会发散。正确做法是混合精度:主权重保持 FP32,前向/反向计算用低精度,梯度缩放后更新回 FP32。
为何常见:因为直觉上“低精度 = 更省更快”,忽略了数值范围和误差积累的约束。
误区BF16 和 FP16 都是 16 位,只是封装不同,效果差不多,随便选一个就行。
正解FP16 指数 5 位尾数 10 位,精度高但范围小;BF16 指数 8 位尾数 7 位,范围与 FP32 接近但精度低。面对大范围梯度,FP16 可能溢出为 Infinity,BF16 反而安全。
为何常见:两者位宽相同,容易忽略内部指数/尾数分配的巨大差异。
误区既然 FP8 在 Tensor Core 上很快,那我直接把它作为默认格式,训练应该没有问题。
正解FP8 的 E4M3 最大约 448,梯度稍大就可能饱和;训练仍需要 FP32 主权重、损失缩放,并把敏感算子留在 FP16/FP32。推理场景没有反向传播,FP8 用起来才更安全。
为何常见:只看到 FP8 吞吐高,忽略了动态范围小、误差会被多次迭代放大的问题。
误区梯度太大导致 NaN,所以要用损失缩放,它是防爆炸的。
正解损失缩放主要是防止“下溢”,即过小的梯度在 FP16/BF16/FP8 中变成 0。方法是在反向传播前把损失乘一个放大因子,让梯度落在可表示范围,再在更新时除回来。
为何常见:直觉上缩放会把数值变大,容易联想到爆炸而不是下溢。
误区我只要把模型所有层全部强制成 FP16,就是混合精度了。
正解真正的混合精度会智能地让 LayerNorm、Softmax、损失计算等敏感算子保留在 FP32,只有矩阵乘等重计算算子用低精度,必要时还配合动态损失缩放。
为何常见:把“混合”简单理解为“统一替换”,忽略了不同算子对数值误差的敏感程度不同。
面试题
Q1. FP16 能表示的最大有限数值大约是多少?
- A.65504← 正确答案
- B.3.4e38
- C.448
- D.2^24
答案:A。FP16 指数 5 位、尾数 10 位,最大有限值约为 65504。3.4e38 是 FP32/BF16 的范围,448 是 FP8 E4M3 的最大值,2^24 与浮点格式无关。
Q2. BF16 相比 FP16 最大的区别是什么?
- A.指数位从5位扩展到8位← 正确答案
- B.尾数位从10位扩大到23位
- C.占用内存从16位降到8位
- D.符号位从1位增加到2位
答案:A。BF16 把指数位从 5 位扩到 8 位,动态范围接近 FP32;尾数反而从 10 位减到 7 位,因此精度低于 FP16。内存仍是 16 位,符号位仍是 1 位。
Q3. 混合精度训练通常会做哪些事情?
- A.主权重保存为FP32← 正确答案
- B.前向反向使用低精度← 正确答案
- C.使用损失缩放规避下溢← 正确答案
- D.所有算子都改用FP8
答案:ABC。混合精度的核心是主权重 FP32、前反向低精度、损失缩放防下溢。并不是所有算子都改用 FP8,像 LayerNorm 等敏感算子通常保留在 FP32。
Q4. FP8 的 E4M3 格式,最大有限值约为多少?
- A.448← 正确答案
- B.65504
- C.3.4e38
- D.57344
答案:A。E4M3 用 4 位指数、3 位尾数,最大有限值约 448。57344 是 E5M2 的最大值,65504 是 FP16 的最大值,3.4e38 是 FP32/BF16 的范围。
Q5. BF16 相对误差较大的根本原因是什么?
- A.尾数位只有7位← 正确答案
- B.指数位达到8位
- C.符号位占1个bit
- D.内存带宽过大
答案:A。尾数位只有 7 位意味着可区分的相邻数较少,相对误差约为 2^{-8} ≈ 3.9×10^{-3}。指数位多带来的是范围优势,符号位与误差无关,带宽大不会直接导致误差。
Q6. 损失缩放主要解决什么问题?
- A.梯度下溢导致全零← 正确答案
- B.梯度爆炸导致NaN
- C.权重过大导致过拟合
- D.激活值饱和导致失活
答案:A。低精度格式能表示的最小正常数有限,极小的梯度会变成 0,称为下溢。损失缩放先把损失乘一个大因子,让梯度落在可表示范围内,更新时再除回来。
Q7. 当梯度和激活值经常接近 65504 附近时,更适合选用哪种格式?
- A.BF16保住范围← 正确答案
- B.FP16精度更高
- C.FP8速度最快
- D.FP32全部保留
答案:A。FP16 最大有限值就是 65504,接近它很容易溢出。BF16 指数位扩展到 8 位,范围接近 FP32,能显著降低溢出概率,是这类场景的首选低精度格式。
Q8. FP8 在训练中的最常见用法是什么?
- A.全精度模型参数存储
- B.前向反向矩阵乘计算← 正确答案
- C.只替换损失函数计算
- D.替换优化器的动量项
答案:B。FP8 常用于矩阵乘法等重计算算子,以提升吞吐;主权重、优化器状态和损失计算仍留在 FP32,避免误差积累。
Q9. 关于混合精度的说法,哪些是正确的?
- A.主权重仍保存在FP32← 正确答案
- B.损失缩放利于提升小梯度← 正确答案
- C.低精度算子的累加器是FP8
- D.梯度更新通常在FP32进行← 正确答案
答案:ABD。混合精度保留了 FP32 主权重和 FP32 梯度更新,损失缩放提升小梯度在低精度下的可表示性。累加器在 Tensor Core 中通常仍是 FP32,而不是 FP8,所以 C 错误。
Q10. 在 Tensor Core 上,BF16 相比 FP16 的独特优势是什么?
- A.降低溢出概率且保持范围← 正确答案
- B.提高有效十进制位数精度
- C.减少显存占用为原来一半
- D.免去损失缩放的所有步骤
答案:A。BF16 与 FP32 的动态范围一致,梯度大时更不容易溢出,这是它区别于 FP16 的独特优势。BF16 精度比 FP16 低,显存占用同样是 16 位,训练仍可能需要损失缩放。