想象你出门办事,要把一天的现金带在身上。FP32 是能装下所有面额、还能精确保管的“重型保险箱”,FP16 是轻便的日常钱包,BF16 像是厚厚一叠大面额钞票夹,FP8 则是巴掌大的小零钱包。GPU 训练模型时,成百亿个数字在内存与计算单元之间搬来搬去,每个数字都要装进这四种“钱包”中的某一种。钱包越小,携带越轻、跑得越快,但也可能装不下大额数字、找不准零钱。低精度训练的全部智慧,就是学会给每个数字挑一个“够用又省钱”的钱包。 Tensor Core 是 GPU 上专门做矩阵乘法的计算核心,它同时支持 FP32、FP16、BF16、FP8 等数值格式。理解这些格式的“容量”与“精细度”,是 GPU 编程工程师优化训练吞吐、压低显存占用的基本功。本文用“钱包”作比喻,带你从零看懂低精度训练。

岗位关联

掌握低精度格式的精度-范围权衡,你就能在大模型训练与推理中把显存占用和计算延迟大幅压低,这是 AI 基础设施岗位的核心技能之一。

目录
  1. 可视化演示
  2. 数值格式基础
  3. FP16 动态范围
  4. BF16 指数扩展
  5. FP8 训练推理
  6. 与FP32精度对比
  7. 混合精度策略
  8. 格式选择指南
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

数值格式基础

想象你出门办事,随身带着四种现金收纳工具:FP32 是能装下所有面额、还能精确保管的“重型保险箱”;FP16 是轻便的日常钱包;BF16 像是厚厚一叠大面额钞票夹;FP8 则是巴掌大的小零钱包。GPU 训练模型时,成百亿个数字在内存与计算单元之间搬来搬去,每个数字都必须装进这四种“钱包”中的某一种。钱包越小,携带越轻、跑得越快,但可能装不下或找不准。

计算机里浮点数的格式,就是钱包的内部分格。最通用的 FP32 用 32 位二进制表示一个数字;FP16 和 BF16 用 16 位;FP8 用 8 位。不管哪种格式,都像同一个抽屉分成三格:1 位符号、若干位指数、若干位尾数。指数决定数字能“大到什么程度”,尾数决定数字能“精确到多少位”。

了解了抽屉格局,你就会发现:低精度训练的全部艺术,在于为每个数字挑选最合适的钱包。选大了浪费空间,选小了丢精度甚至爆掉。

v=(−1)s×2 e−bias×(1+f)v = (-1)^s \times 2^{\,e - \mathrm{bias}} \times (1 + f)
这个公式把所有浮点格式统一起来:符号管正负,指数管量级,尾数管精度。
  1. 符号位用来表示正负,0 代表正数,1 代表负数。
    s∈{0,1}s \in \{0, 1\}
  2. 指数项指数 e 减去偏移量 bias 后作为 2 的幂,决定了数字的量级大小。
    2 e−bias2^{\,e - \mathrm{bias}}
  3. 尾数项小数部分 f 与隐藏的 1 组成有效数字,决定相对精度。
    1+f1 + f

FP16 动态范围

FP16 就是那只轻便钱包:确实轻快,但你不能把面额太大的支票塞进去。FP16 指数只有 5 位,偏移量是 15,它能表示的最大有限正常数大约是 65504。一旦某个权重或中间计算超过这个值,它不会“找回零钱”,而是直接变成 Infinity,整次训练就会崩掉。

GPU 训练中,梯度突然冲到 65504 并不是小概率事件——模型最后一层或损失函数梯度很陡的时候特别容易出现。于是工程师设计了“损失缩放”,把梯度先放大或缩小再塞进钱包,避免溢出或下溢。

明白 FP16 这个钱包的容量上限,你就能理解:当数值经常接近 65504,就该换用更大面额的钱包,也就是下一节的 BF16。

vmax⁡FP16=(2−2−10)×215≈65504v_{\max}^{\text{FP16}} = (2 - 2^{-10}) \times 2^{15} \approx 65504
FP16 的最大有限值是“最大尾数”乘“最大指数”的乘积,超过就变成正无穷。
  1. 尾数部分FP16 尾数 10 位全为 1 时,得到二进制 1.1111111111,即十进制 2 - 2^{-10}。
    2−2−102 - 2^{-10}
  2. 指数部分指数 5 位,偏差 15,最大正常指数为 30,指数项为 2^{15} = 32768。
    215=327682^{15} = 32768
  3. 相乘1.9990 × 32768 ≈ 65504,这就是 FP16 能装下的最大金额。
    1.9990×32768≈655041.9990 \times 32768 \approx 65504

BF16 指数扩展

BF16 是那种“只做大额钞票夹”。它把 FP16 的指数位从 5 位扩到 8 位,也就是模仿 FP32 的量级范围;代价是尾数从 10 位削减到 7 位。好比钱包原本有收纳零钱的小格子,现在全被大面额夹层替换了。

8 位指数配上偏移量 127,BF16 的最大正常数范围与 FP32 几乎一致(约 3.4×10^38),所以大梯度不再是问题;但 7 位尾数意味着只有大约 2.4 位有效十进制数字。换句话说,数值范围很够,但只能记住两三位小数。

当你的梯度经常到达 FP16 装不下的量级,BF16 的大面额夹层就派上了用场,代价只是零头被抹掉。

D≈(M+1)log⁡102D \approx (M + 1) \log_{10} 2
有效十进制位数由“尾数位 + 隐藏位”决定,BF16 大约只有 2.4 位有效数字。
  1. 隐藏位尾数 M 位之外还有隐藏的 1,所以有效二进制位是 M+1。
    M+1M + 1
  2. 十进制换算每个二进制位大约带来 0.3010 个十进制有效位。
    log⁡102≈0.3010\log_{10} 2 \approx 0.3010
  3. 结果对 BF16 而言,8 × 0.3010 ≈ 2.41 位有效十进制数字。
    8×0.3010≈2.418 \times 0.3010 \approx 2.41

FP8 训练推理

FP8 像超轻零钱包,只有 8 位,GPU 一次能同时搬运更多数字。它有几种变体,最常见的是 E4M3(4 指数 + 3 尾数)和 E5M2(5 指数 + 2 尾数)。E4M3 优先保证精度,最大约 448;E5M2 优先保证范围,最大约 57344。

训练中,FP8 通常不会单独承担所有工作:主权重仍保存在 FP32“保险箱”里,只在矩阵乘法等重计算算子中临时使用 FP8,再配合损失缩放。推理时,由于不需要反向传播和梯度,FP8 用起来更安全,常被用来压低延迟、提升吞吐。

零钱包虽轻,但装不下大额交易;FP8 训练必须搭配 FP32 主账本与损失缩放,才能既快又不丢“钱”。

vmax⁡E4M3=(1+68)×28=448v_{\max}^{\text{E4M3}} = \left(1 + \frac{6}{8}\right) \times 2^{8} = 448
E4M3 的最大值是 1.75 倍尾数乘 2 的 8 次方,恰好是 448。
  1. 尾数贡献3 位尾数中最大合法步长是 6/8,因为尾数 7 搭配指数 8 会超过最大范围,实际被饱和回 448。
    68=0.75\frac{6}{8} = 0.75
  2. 指数贡献E4M3 正常指数最大到 8,带来 256 倍放大。
    28=2562^{8} = 256
  3. 乘积1.75 × 256 = 448,这就是 E4M3 的最大有限值。
    1.75×256=4481.75 \times 256 = 448

与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 的误差完全可以接受,而且速度快得多。

ε≈2−(M+1)\varepsilon \approx 2^{-(M+1)}
相对误差近似为 2 的负有效尾数位次方,尾数每多一位,误差减半。
  1. 隐藏位实际有效尾数是 M+1 位,因为左侧总有一个隐藏的 1。
    M+1M + 1
  2. 最小相对间隔相邻两个可表示数之间大约相差 2^{-(M+1)}。
    2−(M+1)2^{-(M+1)}
  3. 代入对比M=23 时 ε≈5.96×10^{-8},M=10 时 ε≈4.88×10^{-4},M=7 时 ε≈3.91×10^{-3},M=3 时 ε≈6.25×10^{-2}。
    2−24≈6×10−8,2−4=6.25×10−22^{-24} \approx 6 \times 10^{-8}, \quad 2^{-4} = 6.25 \times 10^{-2}

混合精度策略

成熟的会计不会把所有现金随身带:主账本锁在 FP32 保险箱,随身只带 FP16 或 BF16 零钱包,每笔交易都配一个“放大镜”——损失缩放。这就是混合精度训练。

具体流程是:先将损失 L 乘以缩放因子 s,再做反向传播;梯度在低精度计算时因被放大而不会下溢成 0;更新前再除以 s,恢复到真实尺度。这样既享受了低精度的速度,又保住了小梯度的“零钱”。

保险箱里的主账本从未离开,小钱包只负责跑腿和记账,混合精度才能让训练又稳又快。

g~=∂(s⋅L)∂w,w←w−η⋅g~s\tilde{g} = \frac{\partial (s \cdot L)}{\partial w}, \qquad w \leftarrow w - \eta \cdot \frac{\tilde{g}}{s}
训练时先把损失放大,反向传播得到的梯度再缩小回来,最后用 FP32 主权重更新。
  1. 损失缩放把损失放大 s 倍,让那些特别小的梯度不至于在 FP16 中变成 0。
    s⋅Ls \cdot L
  2. 反向传播在低精度计算中得到缩放后的梯度 \tilde{g}。
    g~=∂(s⋅L)∂w\tilde{g} = \frac{\partial (s \cdot L)}{\partial w}
  3. 更新前还原除以 s 后,再用 FP32 主权重做更新,从而保持训练稳定。
    w←w−η⋅g~sw \leftarrow w - \eta \cdot \frac{\tilde{g}}{s}

格式选择指南

出门选钱包,取决于你一天要经手多大面额、多细的零钱。训练也是如此:如果你的梯度或激活值经常冲到 65504 附近,说明 FP16 钱包容量可能不足,优先考虑 BF16;如果模型对精度非常敏感,FP16 在大多数小数值区间更精细;如果追求最大吞吐且愿意做缩放,FP8 零钱包能带来最大加速;遇到收敛不稳定,FP32 账本就是最终兜底。

工程上可以写成最小化“误差 + 时间成本”的选择模型:

四种钱包没有绝对的好坏,只有合不合适。低精度训练,就是学会给每个数字找到最合适、最省钱的钱包组合。

min⁡fmtα⋅εfmt+β⋅BfmtSfmt\min_{\text{fmt}} \quad \alpha \cdot \varepsilon_{\text{fmt}} + \beta \cdot \frac{B_{\text{fmt}}}{S_{\text{fmt}}}
选择格式就是平衡误差和速度:精度敏感时调大 α,速度敏感时调大 β。
  1. 误差项ε 越小,越接近 FP32 训练结果。
    εfmt\varepsilon_{\text{fmt}}
  2. 成本项B 是存储字节,S 是吞吐,比值越小越快。
    BfmtSfmt\frac{B_{\text{fmt}}}{S_{\text{fmt}}}
  3. 系数α 和 β 根据你的任务调节,精度优先时 α 调大,速度优先时 β 调大。
    α,β≥0\alpha, \beta \geq 0

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 演示

python43 行
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()
目的:用 Python 模拟 FP16、BF16、FP8(E4M3) 三种低精度格式,直观对比同一组权重在不同“钱包”里的精度损失与溢出情况。
  1. rounded = (bits + 0x00008000) & 0xFFFF0000模拟 BF16 的舍入行为,把 FP32 低位抹掉后保留高 16 位。
  2. fp16 = weights.astype(np.float16).astype(np.float32)演示 1e5 在 FP16 中变成 inf,即“钱包撑爆”。
  3. e = max(-6, min(8, e))把指数限制在 E4M3 能表示的范围内,防止量化时越界。
  4. m = round((a / (2 ** e) - 1.0) * 8)把小数部分量化到 3 位尾数,相当于“抹零到 1/8”。
  5. return max(-448.0, min(448.0, out))模拟 E4M3 的最大有限值饱和,超出 448 就截断。
  6. 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 能表示的最大有限数值大约是多少?

单选题 · easy

  • 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 最大的区别是什么?

单选题 · mid

  • A.指数位从5位扩展到8位← 正确答案
  • B.尾数位从10位扩大到23位
  • C.占用内存从16位降到8位
  • D.符号位从1位增加到2位

答案:A。BF16 把指数位从 5 位扩到 8 位,动态范围接近 FP32;尾数反而从 10 位减到 7 位,因此精度低于 FP16。内存仍是 16 位,符号位仍是 1 位。

Q3. 混合精度训练通常会做哪些事情?

多选题 · mid

  • A.主权重保存为FP32← 正确答案
  • B.前向反向使用低精度← 正确答案
  • C.使用损失缩放规避下溢← 正确答案
  • D.所有算子都改用FP8

答案:ABC。混合精度的核心是主权重 FP32、前反向低精度、损失缩放防下溢。并不是所有算子都改用 FP8,像 LayerNorm 等敏感算子通常保留在 FP32。

Q4. FP8 的 E4M3 格式,最大有限值约为多少?

单选题 · hard

  • A.448← 正确答案
  • B.65504
  • C.3.4e38
  • D.57344

答案:A。E4M3 用 4 位指数、3 位尾数,最大有限值约 448。57344 是 E5M2 的最大值,65504 是 FP16 的最大值,3.4e38 是 FP32/BF16 的范围。

Q5. BF16 相对误差较大的根本原因是什么?

单选题 · mid

  • A.尾数位只有7位← 正确答案
  • B.指数位达到8位
  • C.符号位占1个bit
  • D.内存带宽过大

答案:A。尾数位只有 7 位意味着可区分的相邻数较少,相对误差约为 2^{-8} ≈ 3.9×10^{-3}。指数位多带来的是范围优势,符号位与误差无关,带宽大不会直接导致误差。

Q6. 损失缩放主要解决什么问题?

单选题 · easy

  • A.梯度下溢导致全零← 正确答案
  • B.梯度爆炸导致NaN
  • C.权重过大导致过拟合
  • D.激活值饱和导致失活

答案:A。低精度格式能表示的最小正常数有限,极小的梯度会变成 0,称为下溢。损失缩放先把损失乘一个大因子,让梯度落在可表示范围内,更新时再除回来。

Q7. 当梯度和激活值经常接近 65504 附近时,更适合选用哪种格式?

单选题 · mid

  • A.BF16保住范围← 正确答案
  • B.FP16精度更高
  • C.FP8速度最快
  • D.FP32全部保留

答案:A。FP16 最大有限值就是 65504,接近它很容易溢出。BF16 指数位扩展到 8 位,范围接近 FP32,能显著降低溢出概率,是这类场景的首选低精度格式。

Q8. FP8 在训练中的最常见用法是什么?

单选题 · mid

  • A.全精度模型参数存储
  • B.前向反向矩阵乘计算← 正确答案
  • C.只替换损失函数计算
  • D.替换优化器的动量项

答案:B。FP8 常用于矩阵乘法等重计算算子,以提升吞吐;主权重、优化器状态和损失计算仍留在 FP32,避免误差积累。

Q9. 关于混合精度的说法,哪些是正确的?

多选题 · hard

  • A.主权重仍保存在FP32← 正确答案
  • B.损失缩放利于提升小梯度← 正确答案
  • C.低精度算子的累加器是FP8
  • D.梯度更新通常在FP32进行← 正确答案

答案:ABD。混合精度保留了 FP32 主权重和 FP32 梯度更新,损失缩放提升小梯度在低精度下的可表示性。累加器在 Tensor Core 中通常仍是 FP32,而不是 FP8,所以 C 错误。

Q10. 在 Tensor Core 上,BF16 相比 FP16 的独特优势是什么?

单选题 · easy

  • A.降低溢出概率且保持范围← 正确答案
  • B.提高有效十进制位数精度
  • C.减少显存占用为原来一半
  • D.免去损失缩放的所有步骤

答案:A。BF16 与 FP32 的动态范围一致,梯度大时更不容易溢出,这是它区别于 FP16 的独特优势。BF16 精度比 FP16 低,显存占用同样是 16 位,训练仍可能需要损失缩放。