深夜食堂的大厨阿明最近遇上了幸福的烦恼:客人越来越多,但厨房里的“精装账本”(FP32)太厚太重,记一页菜谱要翻半天,出菜速度跟不上。于是他想了个主意——用更轻薄的记事本代替账本。可换本子哪有这么简单?轻薄的记事本(FP16)虽然翻得快,但记不住太大的数字,也记不住太小的数字;送菜小哥递来的“宽幅便签”(BF16)倒是又能记大数又轻便,可惜每个数字只能写个大概。阿明还添置了多功能料理机(张量核心)、想出了“先放大再缩回”的称量法(Loss Scaling)、学会了攒齐几批食材再统一下锅(梯度累积),最后连外卖高峰期的“速记笺”(FP8)都用上了。这一整套“又快又省还不翻车”的记账学问,就是大模型训练里的混合精度训练。本文会带你认识各种数值精度如何分工协作,以及它们如何让大模型训练变得又快又稳。

岗位关联

掌握混合精度训练,是 AI 训练工程师的必备技能:在相同显卡上把训练速度提升 2-3 倍、显存占用减半,同时避免精度损失或训练崩溃,直接关系到大模型项目的成本与周期。

目录
  1. 可视化演示
  2. 数值精度基础
  3. FP16与BF16
  4. 张量核心原理
  5. 损失缩放机制
  6. 梯度累积策略
  7. FP8前沿实践
  8. 调试与稳定性
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示混合精度训练核心机制:FP16/BF16/FP8数值格式对比、张量核心加速、Loss Scaling防梯度下溢和梯度累积等效大批次训练。

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

数值精度格式对比 FP16: 5位指数+10位尾数 BF16: 8位指数+7位尾数 FP8: 4-5位指数+2-3位尾数 精度越低显存越省但范围越小

数值精度基础

深夜食堂打烊后,阿明翻开那本厚厚的《配方簿》。每一道菜都占一整页,上面用工整的字迹写着“豆腐 500g、豆瓣酱 15g、花椒粉 0.5g”。这本精装账本什么都好:数字精确、永不爆表,唯一的缺点就是太厚太重,翻页要花时间。深度学习界的“精装账本”就是 FP32——32 位浮点数,它是 PyTorch 里默认的精度,也是所有模型权重最初的“居住地”。阿明每天都要在这本账本上记录几千条数据,翻页的手速渐渐跟不上点菜的速度了。

账本为什么这么重?因为每个数字都要写三栏:一栏写正负号(符号位),一栏写数字的大小量级(指数位),一栏写精确的每一位数字(尾数位)。浮点数在计算机里也是这么存的:符号位决定正负,指数位决定“10 的多少次方”,尾数位决定“具体是多少”。FP32 用 1 位符号、8 位指数、23 位尾数,总共 32 位来记录一个数字,所以它精确、范围大,但也占空间、运算慢。

用数学的语言说,一个 FP32 浮点数是这样拼出来的:

精装账本虽然可靠,但阿明很快发现:高峰期一晚上要记几百条菜谱,账本的重量直接拖慢了他的翻页速度。如果有一种更轻的记账方式就好了。

x=(−1)s×2e−bias×(1+m)x = (-1)^{s} \times 2^{e - \mathrm{bias}} \times (1 + m)
这个公式表示:一个浮点数 = 正负号 × 2 的(指数-偏移)次方 × (1+尾数)。偏移 bias 是为了让指数能表示负数,FP32 中 bias = 127。
  1. 符号位 s决定这个数是正还是负。s=0 表示正数,s=1 表示负数,就像账本里“进账”和“出账”两栏。
    (−1)s(-1)^{s}
  2. 指数位 e决定这个数的量级范围,相当于账本里的“百位、千位、万位”栏。e 是 0 到 255 的整数,减去偏移 bias 后得到真正的指数。
    2e−bias2^{e - \mathrm{bias}}
  3. 尾数位 m决定这个数的精确程度,相当于账本里的小数点后几位。尾数越多,数字写得越精细。
    (1+m)(1 + m)
  4. 组合成最终数值把三栏内容相乘,就得到了完整记录的浮点数。
    x=(−1)s×2e−bias×(1+m)x = (-1)^{s} \times 2^{e - \mathrm{bias}} \times (1 + m)

FP16与BF16

阿明先试了“小记事本”(FP16):同样的厚度能记两倍的条目,翻页飞快。但小记事本有个致命缺陷——它的“量级栏”只有 5 格。记录“豆腐 500g”没问题,可一旦要记录“花椒粉 0.00006g”这种微量食材,直接就写成了 0;如果哪天客人点了 70000g 的巨型炖锅,又直接爆表写成“无穷大”。FP16 正是这样:1 位符号 + 5 位指数 + 10 位尾数,最大只能表示 65504,最小正常数只有约 6.1e-5,范围比 FP32 窄得多。

送菜小哥看到阿明发愁,递来一本“宽幅便签”(BF16):纸的厚度和 FP16 一样薄,但量级栏从 5 格加宽到了 8 格——和精装账本一模一样宽!代价是精确栏从 10 格缩水到 7 格。这意味着 BF16 能记录的数量级范围和 FP32 几乎一样大(最大约 3.4e38,最小约 1e-38),只是每个数字的“精细度”略差。把两种格式写成公式,差异一目了然:

阿明最终选了宽幅便签做主力记录本:训练大模型时,梯度经常在 1e-3 到 1e-8 之间剧烈变化,BF16 宽大的量程让这些微小数字不至于“一记就写成 0”;而 7 位尾数的精度对梯度来说已经足够——毕竟我们只需要知道“往哪走、走多快”,不需要精确到小数点后第八位。选 FP16 还是 BF16,本质是在“范围”和“精度”之间做取舍:像图像分类这种数值相对规整的任务,FP16 也能胜任;而大模型训练这种梯度跨度大的场景,BF16 是更稳的选择。

xFP16=(−1)s×2e−15×(1+m),e∈[0,31]x_{\mathrm{FP16}} = (-1)^{s} \times 2^{e-15} \times (1+m), \quad e \in [0, 31]
xBF16=(−1)s×2e−127×(1+m),e∈[0,255]x_{\mathrm{BF16}} = (-1)^{s} \times 2^{e-127} \times (1+m), \quad e \in [0, 255]
FP16 的指数只有 5 位,偏置 15,所以范围窄;BF16 的指数有 8 位,偏置 127,与 FP32 相同,所以范围宽。范围宽意味着不容易溢出和下溢。
  1. FP16 的位分配1 位符号 + 5 位指数 + 10 位尾数,量级栏太窄是它的短板。
    1+5+10=161 + 5 + 10 = 16
  2. FP16 的指数范围5 位指数最大到 31,减去偏置 15,实际只覆盖 2^-14 到 2^15。
    eFP16∈[0,31],bias=15e_{\mathrm{FP16}} \in [0, 31], \quad \mathrm{bias} = 15
  3. BF16 的位分配1 位符号 + 8 位指数 + 7 位尾数,量级栏与 FP32 看齐。
    1+8+7=161 + 8 + 7 = 16
  4. BF16 的指数范围8 位指数最大到 255,减去偏置 127,与 FP32 的 2^-126 到 2^127 完全一致。
    eBF16∈[0,255],bias=127e_{\mathrm{BF16}} \in [0, 255], \quad \mathrm{bias} = 127

张量核心原理

记事本换轻薄了,可阿明发现出菜慢的真正瓶颈不是“翻本子”,而是“切菜”。一晚上几百道菜,每道菜要切肉、切菜、配调料,刀工再快也赶不上点菜的速度。于是他咬牙买了一台多功能料理机:把豆腐放进去,机器一次性完成“切块 + 称重 + 调汁”三步,出菜速度立刻翻倍。深度学习里的“切菜”就是矩阵乘法——神经网络每一层都在做巨大的矩阵相乘,而张量核心(Tensor Core)就是专门为它设计的“料理机”。

普通计算单元做矩阵乘法需要分步执行:先逐元素相乘,再把结果累加,最后加上偏置。张量核心的厉害之处在于把这三步融合成一步,一次时钟周期就能完成一个 4x4 小矩阵的“乘加融合”运算。在 A100 等现代 GPU 上,张量核心处理 FP16 矩阵乘法的吞吐量是普通 CUDA 核心的数倍。它的数学本质是:

不过料理机也有脾气:食材必须切成标准大小才能快速处理。张量核心同样要求输入数据是 FP16 或 BF16 格式,并且数据要放在显存里、由支持 CUDA 架构的 GPU(如 Volta 及更新架构)执行。这也正是混合精度训练的加速原理——用 16 位精度喂给张量核心,一次能塞进两倍的数据,计算单元全程满负荷运转。阿明的厨房革命,正式开始了。

Di,j=∑k=1KAi,k×Bk,j+Ci,jD_{i,j} = \sum_{k=1}^{K} A_{i,k} \times B_{k,j} + C_{i,j}
这个公式表示:两个矩阵 A 和 B 相乘后,再加上矩阵 C,一次性得到结果矩阵 D。张量核心把“乘”和“加”合成一个硬件指令,省去了中间数据的搬运。
  1. 输入矩阵 A 和 B神经网络中的权重和激活值被切分成小块送入张量核心。
    A∈RM×K,B∈RK×NA \in \mathbb{R}^{M \times K}, \quad B \in \mathbb{R}^{K \times N}
  2. 逐元素乘累加A 的第 i 行第 k 个元素与 B 的第 k 行第 j 个元素相乘,然后对 k 从 1 到 K 求和。
    Ai,k×Bk,jA_{i,k} \times B_{k,j}
  3. 融合偏置 C在同一个硬件指令里把矩阵 C 直接加上,不额外读一遍数据。
    +Ci,j+ C_{i,j}
  4. 得到结果矩阵所有乘加一次性完成,这就是张量核心“又快又省”的来源。
    Di,j=∑k=1KAi,k×Bk,j+Ci,jD_{i,j} = \sum_{k=1}^{K} A_{i,k} \times B_{k,j} + C_{i,j}

损失缩放机制

换用宽幅便签后,阿明又遇到新麻烦:有些菜谱里的“微量食材”实在太少了,比如 0.00002g 的藏红花粉。BF16 虽然范围大,但 7 位尾数的精度记录这种超小数字时还是会丢掉很多有效信息;如果换成 FP16,这种数字干脆就写成 0 了。深度学习训练中,梯度就是这样一种“微量食材”——尤其在训练后期或深层网络中,梯度可能小到 1e-6 以下,FP16 一存就下溢成 0,模型再也学不动了。这就是著名的“梯度下溢”问题。

阿明的解决办法很朴素:既然量杯太小记不下微量粉末,那就先加 1024 倍水把它溶解,记录“稀释后的总量”,用时再按比例浓缩回去。Loss Scaling 就是这个思路:把损失值乘以一个缩放因子 S(比如 1024),再走反向传播。由于梯度是损失对权重的导数,损失放大 S 倍后,梯度也会同步放大 S 倍,原本小到 1e-6 的梯度变成 1e-3,正好落在 FP16 的舒适区。更新权重前,再把梯度除以 S 还原。

缩放因子怎么定?静态缩放是拍一个固定值(比如 1024),简单但不够灵活;动态缩放则会在训练中自动调节:连续几百步都没有出现溢出的 Inf 或 NaN,就把 S 翻倍;一旦检测到溢出,就把 S 减半并跳过这轮更新。这就像阿明根据客人口味自动调整调料浓度——盐多味重就加水,味淡就再加盐。Loss Scaling 让 FP16 也能安全地表达微小梯度,是混合精度训练中不可或缺的“保护剂”。

L′=L×SL' = L \times S
g′=∂L′∂W=S⋅∂L∂W=S⋅gg' = \frac{\partial L'}{\partial W} = S \cdot \frac{\partial L}{\partial W} = S \cdot g
W←W−η⋅g′SW \leftarrow W - \eta \cdot \frac{g'}{S}
第一行把损失放大 S 倍;第二行说明梯度也会随之放大 S 倍;第三行更新权重时才除以 S,把梯度还原,保证数学上等价于原来的训练过程。
  1. 放大损失前向传播算出的损失 L 乘以缩放因子 S,得到缩放后的损失 L'。
    L′=L×SL' = L \times S
  2. 梯度同步放大反向传播时,损失对权重的导数(梯度)也变成了原来的 S 倍。
    g′=S⋅gg' = S \cdot g
  3. 更新前还原先用缩放后的梯度 g' 更新,再除以 S,效果等同于用原始梯度 g 更新。
    W←W−η⋅g′SW \leftarrow W - \eta \cdot \frac{g'}{S}

梯度累积策略

周日晚上是深夜食堂一周最忙的时候,一下子来了 10 桌客人,每桌都点同款招牌菜。阿明的台面只有那么大,10 桌的食材一次摆不下。他灵机一动:分三批备料,先处理好前 3 桌的食材放一边,再处理中间 3 桌,最后处理剩余 4 桌,等所有食材都备齐了,再一起下锅炒。这样一来,台面不够用的难题迎刃而解。

深度学习训练中,“台面面积”就是 GPU 显存。理想情况下,我们希望用大的 batch size 来获得稳定的梯度估计,但大 batch 意味着前向和反向传播需要同时存下更多中间结果,显存常常装不下。梯度累积(Gradient Accumulation)的策略与阿明如出一辙:把一个大的 batch 拆成 N 个小的 micro-batch,依次计算每个 micro-batch 的梯度,但先不更新权重;攒够 N 个梯度后,求出平均值,再用它做一次权重更新。等效 batch size = 单次 micro-batch 大小 × N,数学效果与一次喂入大 batch 非常接近。

值得注意的是,梯度累积期间不要更新权重,否则每一批的梯度都作用在当前权重上,就失去了“攒齐再下锅”的意义。Batch Normalization 之类的层也要额外处理,因为每个 micro-batch 的统计量不等于整个大 batch 的统计量。阿明把这套方法记进菜谱后,10 桌客人同时点菜也不愁了,台面照旧够用,出菜质量一点没差——这正是梯度累积在大模型训练中成为标配的原因:千亿参数模型用单卡可能连 1 条样本都塞不下,但把 batch 拆成 32 个 micro-batch,照样能凑出一个“虚拟大 batch”。

gi=1Bmicro∑j=1Bmicro∇θLjg_i = \frac{1}{B_{\mathrm{micro}}} \sum_{j=1}^{B_{\mathrm{micro}}} \nabla_{\theta} \mathcal{L}_j
gˉ=1Nacc∑i=1Naccgi\bar{g} = \frac{1}{N_{\mathrm{acc}}} \sum_{i=1}^{N_{\mathrm{acc}}} g_i
θ←θ−η⋅gˉ\theta \leftarrow \theta - \eta \cdot \bar{g}
第一个公式是每个 micro-batch 的梯度;第二个公式把 N 个 micro-batch 的梯度求平均;第三个公式用平均梯度更新一次权重。关键是必须除以 N,否则梯度会被放大 N 倍,训练就会震荡甚至发散。
  1. 计算 micro-batch 梯度每个小批次都独立计算一次梯度,像阿明分批处理食材。
    gi=1Bmicro∑j=1Bmicro∇θLjg_i = \frac{1}{B_{\mathrm{micro}}} \sum_{j=1}^{B_{\mathrm{micro}}} \nabla_{\theta} \mathcal{L}_j
  2. 累积并求平均把 N 个梯度加起来除以 N,得到与“一次大 batch”等价的平均梯度。
    gˉ=1Nacc∑i=1Naccgi\bar{g} = \frac{1}{N_{\mathrm{acc}}} \sum_{i=1}^{N_{\mathrm{acc}}} g_i
  3. 更新权重只在累积完成这一步更新权重,期间权重保持不动。
    θ←θ−η⋅gˉ\theta \leftarrow \theta - \eta \cdot \bar{g}

FP8前沿实践

外卖高峰期,阿明根本来不及用宽幅便签抄完整菜名。他发明了一套“速记笺”:菜名只写缩写,用量只写大概,出锅前再对照一次原始菜谱。虽然潦草,但出菜效率立刻又上了一个台阶。FP8 浮点数就是这套速记笺——只用 8 位来记录一个数字,比 FP16 还省一半存储,理论上张量核心的吞吐还能再翻一倍。但速记笺也分两种写法,对应 FP8 的两种格式:E4M3 和 E5M2。

E4M3 用 4 位指数、3 位尾数,偏置为 7;E5M2 用 5 位指数、2 位尾数,偏置为 15。前者尾数多一比特,记录的数字更精细,但指数范围小,容易溢出;后者指数范围大,能覆盖更广的数量级,但尾数只有 2 位,精度较差。阿明的经验是:抄菜名用 E4M3(要写清楚到底是什么菜),记录用量用 E5M2(范围大不容易爆表)。对应到训练中,前向传播的激活值和权重用 E4M3,反向传播的梯度用 E5M2。

FP8 是当前最前沿的低精度训练方向,但阿明很清楚:速记笺只敢在出菜高峰用,后台的精装账本必须一直摊开在桌上,随时准备回查。FP8 同样不能单打独斗——主权重必须保持 FP32 或 BF16,FP8 只负责前向和反向的“临场速记”;而且只有训练进入稳定期(损失曲线平滑下降)后才建议启用,否则精度损失可能让训练直接崩盘。对炼丹师来说,FP8 是锦上添花的“涡轮增压”,而不是随时能踩的地板油。

xE4M3=(−1)s×2e−7×(1+m),e∈[0,15]x_{\mathrm{E4M3}} = (-1)^{s} \times 2^{e-7} \times (1+m), \quad e \in [0, 15]
xE5M2=(−1)s×2e−15×(1+m),e∈[0,31]x_{\mathrm{E5M2}} = (-1)^{s} \times 2^{e-15} \times (1+m), \quad e \in [0, 31]
E4M3 指数只有 4 位,动态范围小但精度好;E5M2 指数有 5 位,动态范围大但精度更差。所以前者适合前向传播,后者适合梯度。
  1. E4M3 的表示4 位指数 + 3 位尾数,偏置 7,最大指数 15,动态范围有限。
    xE4M3=(−1)s×2e−7×(1+m),e∈[0,15]x_{\mathrm{E4M3}} = (-1)^{s} \times 2^{e-7} \times (1+m), \quad e \in [0, 15]
  2. E5M2 的表示5 位指数 + 2 位尾数,偏置 15,最大指数 31,动态范围大得多。
    xE5M2=(−1)s×2e−15×(1+m),e∈[0,31]x_{\mathrm{E5M2}} = (-1)^{s} \times 2^{e-15} \times (1+m), \quad e \in [0, 31]
  3. 范围与精度的取舍E4M3 精度更高,E5M2 范围更大,二者互补。
    DE4M3<DE5M2,PE4M3>PE5M2D_{\mathrm{E4M3}} < D_{\mathrm{E5M2}}, \quad P_{\mathrm{E4M3}} > P_{\mathrm{E5M2}}

调试与稳定性

这天晚上,一位老客人说鱼香肉丝的味道不对劲——太咸了。阿明不慌不忙地开始排查:先看盐罐是不是空了(梯度下溢成 0),再想是不是花椒放太多(梯度溢出成 Inf),最后翻开配方簿逐项对照(监控损失曲线和梯度范数)。混合精度训练中,90% 的“味道不对”都出在这两个地方:一是指数溢出导致权重变成 Inf 或 NaN,二是指数下溢导致梯度变成 0,模型彻底停学。

阿明的排查工具箱里最常用的“仪表”是梯度范数——把所有权重的梯度平方求和再开根号,得到一个代表“整体梯度大小”的数值。训练早期梯度范数应该在正常范围,如果某个时刻突然飙升到 1e10 以上,八成是溢出了;如果突然变成 0,则是下溢了。配合损失曲线一起看,就能定位问题出在前向还是反向。数学上,梯度范数定义为:

如果检测到 Inf 或 NaN,动态 Loss Scaling 会自动把缩放因子减半并跳过这一轮更新,给模型一个“刹车缓冲”:

如果缩放因子减半后仍然持续出现 NaN,阿明还有最后一招“回退降级”:把出问题的层换回 BF16 甚至 FP32 重跑一遍。这就像菜实在太咸救不回来时,果断重新做一份而不是硬着头皮上菜。实际训练中,许多成熟的流程会默认每 N 步打印一次梯度范数和损失值,一旦发现异常曲线,立即从最近的 checkpoint 恢复并调低学习率或缩放因子。阿明的深夜食堂越开越顺,靠的不是运气,而是一整套“记录—监控—回退”的稳定训练哲学。

∥g∥2=∑i=1ngi2\lVert g \rVert_2 = \sqrt{\sum_{i=1}^{n} g_i^2}
S←{S×2,连续Nok步无溢出S÷2,出现Inf/NaNS \leftarrow \begin{cases} S \times 2, & \text{连续} N_{\mathrm{ok}} \text{步无溢出} \\ S \div 2, & \text{出现} \mathrm{Inf / NaN} \end{cases}
第一个公式是监控用的梯度范数;第二个公式是动态缩放因子的调整策略——训练稳定就加倍,出问题就减半并跳过本轮,防止权重被污染。
  1. 计算梯度范数把所有梯度的平方相加再开根号,得到整个梯度向量的大小。
    ∥g∥2=∑i=1ngi2\lVert g \rVert_2 = \sqrt{\sum_{i=1}^{n} g_i^2}
  2. 判稳与增长连续多步没有溢出,说明训练平稳,缩放因子 S 翻倍,进一步放大微小梯度。
    S←S×2S \leftarrow S \times 2
  3. 遇错回退一旦出现 Inf 或 NaN,立即把 S 减半并跳过本轮更新,防止坏梯度污染权重。
    S←S÷2S \leftarrow S \div 2

AI 应用场景

大语言模型全流程预训练

阶段:模型训练加速阶段

解决的问题:7B 参数的大语言模型如果用 FP32 训练,单张 80GB 的 H100 显卡连模型权重加优化器状态都放不下,更别提训练数据;且 FP32 矩阵运算无法利用张量核心,训练速度慢得无法接受。使用 BF16 混合精度 + 梯度累积,显存占用直接减半,张量核心全速运转,训练吞吐提升 2-3 倍。

具体案例:某团队用 32 张 A100 训练 7B 模型,采用 BF16 混合精度:权重、梯度用 BF16 前向/反向,优化器状态保持 FP32;序列过长导致单条样本显存爆满,于是设置 micro-batch = 1、梯度累积 16 步,等效 batch size 达到 16,最终在相同预算下把训练周期从 45 天压缩到 18 天。

计算机视觉检测模型的稳定收敛

阶段:损失优化与稳定性调试阶段

解决的问题:小目标检测任务中,大量背景区域的梯度非常微小,直接用 FP16 训练时这些梯度下溢成 0,导致小目标永远学不出来。动态 Loss Scaling 将微小梯度放大到 FP16 可表示范围,再在权重更新前反缩放,让模型稳定收敛。

具体案例:某安防团队训练 YOLOv8 检测小尺寸行人,开启 AMP 混合精度后训练到第 30 轮 loss 不降。排查发现梯度范数骤降为 0,是 FP16 下溢所致。切换为 BF16 + 动态 Loss Scaling(初始 scale=1024,增长因子 2,回退因子 0.5),第 30 轮后损失继续下降,mAP 最终提升 4 个百分点。

长文本微调的显存优化

阶段:显存资源调度阶段

解决的问题:微调大模型处理长文档时,单条样本可能长达 32K tokens,GPU 显存根本装不下一个大 batch。梯度累积把 batch 拆成多个 micro-batch,每次只算一个 micro-batch 的梯度,显存占用从 batch=8 降到 batch=1,实现“小显存也能微调大模型”。

具体案例:某金融团队用消费级 24GB 显卡微调 13B 模型做研报摘要,序列长度 8192,单卡只能放 1 条样本。设置梯度累积 32 步、等效 batch size 32,配合 BF16 混合精度,成功完成全参数微调,且生成质量与 A100 上大 batch 训练的结果基本一致。

Python 演示

python45 行
import numpy as np  def to_fp16(x: np.ndarray) -> np.ndarray:    """模拟 FP16 表示:先转 float16,再转回 float32。"""    return x.astype(np.float16).astype(np.float32)  def to_bf16(x: np.ndarray) -> np.ndarray:    """模拟 BF16 表示:保留 float32 的指数位,截断尾数到 7 位。"""    u = x.astype(np.float32).view(np.uint32)    u_rounded = (u + np.uint32(0x8000)) & np.uint32(0xFFFF0000)    return u_rounded.view(np.float32)  def precision_table() -> None:    """对比不同数值经过 FP16 / BF16 截断后的表示结果。"""    samples = np.array(        [1.0, 1e-3, 6e-5, 1e-7, 1e-30, 1e-38, 65504.0, 70000.0],        dtype=np.float32,    )    print("== 不同精度的数值表示对比 ==")    print("原始值\tFP16结果\tBF16结果")    for v in samples:        v32 = np.array([v], dtype=np.float32)        print(            f"{v:.6g}\t{to_fp16(v32)[0]:.6g}\t{to_bf16(v32)[0]:.6g}"        )  def loss_scaling_demo() -> None:    """展示 Loss Scaling 如何救回微小梯度。"""    tiny_grad = np.array([2e-5, -3e-5, 1.5e-5], dtype=np.float32)    scale = 1024.0    print("\n== Loss Scaling 演示 ==")    print("原始梯度\tFP16直存\tFP16缩放后")    for g in tiny_grad:        direct = to_fp16(np.array([g]))[0]        scaled_back = to_fp16(np.array([g * scale]))[0] / scale        print(f"{g:+.3e}\t{direct:+.3e}\t{scaled_back:+.3e}")  if __name__ == "__main__":    precision_table()    loss_scaling_demo()
目的:用 NumPy 模拟 FP16 与 BF16 的数值截断效果,直观展示 FP16 下溢、溢出问题,以及 Loss Scaling 如何让微小梯度在 FP16 中存活。
  1. x.astype(np.float16).astype(np.float32)模拟 FP16:先降到半精度再转回,小于约 6e-8 的数会变成 0。
  2. u + np.uint32(0x8000)BF16 模拟中的舍入:加 0x8000 实现四舍五入而非直接截断。
  3. (u + np.uint32(0x8000)) & np.uint32(0xFFFF0000)保留 float32 的符号位和指数位(8 位),只截断尾数到 7 位,构造出 BF16。
  4. scaled_back = to_fp16(np.array([g * scale]))[0] / scale先放大 1024 倍存入 FP16,再除以 1024 还原,微小梯度得以保留。
  5. if __name__ == "__main__":可执行入口,运行后依次打印精度对比表和 Loss Scaling 演示结果。
  6. print(f"{v:.6g}\t...")用科学计数法格式化输出,方便观察下溢和溢出。
  7. 1e-30` 与 `1e-38这两个测试值分别展示 BF16 能表示、而 FP32 边界附近的情形。

常见误区

误区FP16 和 BF16 都是 16 位浮点数,精度差别不大,训练时用哪个都一样,随便选一个能跑就行。

正解两者的差异不在“精度谁高”,而在“动态范围”天差地别。FP16 的指数只有 5 位,最大只能表示 65504,最小正常数约 6e-5;BF16 的指数有 8 位,范围和 FP32 一致,最大约 3e38,最小约 1e-38。大模型训练的梯度经常跨越多个数量级,FP16 极易下溢或溢出,BF16 则安全得多。这就像小记事本和宽幅便签:纸一样厚,但能记的数字量级完全不同。

为何常见:大家只盯着“16 位”这个容量,误以为位数相同就等价,忽略了位数内部“指数位和尾数位怎么分配”才是决定数值范围的关键。

误区混合精度是官方提供的自动加速功能,代码里加上 AMP 开关,训练速度自动翻倍,不用做任何额外配置。

正解混合精度加速的前提是硬件支持张量核心、数据在 GPU 上、且输入格式是 FP16/BF16。在 CPU 上跑或使用老显卡,开 AMP 反而更慢。即便硬件达标,如果模型里有大量逐元素小算子、数据搬运频繁,加速效果也会大打折扣。正确做法是先用 profiler 定位瓶颈,确认矩阵运算占据主要时间后再开启混合精度。

为何常见:厂商宣传大力强调“自动混合精度加速”,让很多人误以为它是不需要条件的万能加速器,忽略了硬件和运算结构的前提。

误区梯度太小会下溢成 0,那缩放因子越大越好,干脆设成 100 万,这样什么微小梯度都跑不掉了。

正解缩放因子过大,梯度放大后会超过 FP16 最大值 65504,直接溢出成 Inf,损失函数变成 NaN,训练当场崩溃。缩放因子需要落在“既不下溢也不溢出”的区间内,因此实践中多用动态缩放:连续多步无溢出就加倍,一出现 Inf 就减半并跳过本轮。这就像放大镜的倍数要适中,倍数太大反而什么都看不清。

为何常见:人们容易只看到“下溢”这一个风险,忽略了缩放是双向的——放大同样会把溢出风险放大,形成顾此失彼的思维盲区。

误区既然等效 batch 变大 N 倍,那我把每个 micro-batch 的 loss 直接除以 N,反向传播出来的梯度就自动平均了。

正解梯度累积的正确做法是:每个 micro-batch 正常计算梯度并累加,N 步之后把累加梯度除以 N 再更新权重。如果简单地把每个 loss 都除以 N,单步梯度看起来变小了,但累积完不除以 N 的话,总梯度依然等于大 batch 的梯度,数值偏大;如果每步都除 N 又不累积,梯度又会被缩小 N 倍。关键是“先攒后平均”,而不是“每步缩小”。

为何常见:人们对“除以 N”的直觉停留在单步计算上,忽略了累积过程本身就是一个求和操作,归一化应该在求和之后做一次而不是每一步都做。

误区FP8 比 BF16 省一半显存又快一倍,那应该从头到尾都用 FP8,训练速度最大化。

正解FP8 的动态范围和精度都极其有限,直接全流程使用会让主权重、优化器状态都遭受严重量化误差,训练很难稳定收敛。实践中 FP8 只用于前向/反向的低精度部分,主权重和优化器状态必须保持 FP32/BF16;且训练进入稳定期后才建议开启,同时要配合动态 Loss Scaling 和梯度范数监控,随时准备回退。这就像外卖高峰才用速记笺,正式菜谱永远写在账本里。

为何常见:看到 FP8 的加速数字后容易忽略它的精度短板,再加上厂商宣传聚焦吞吐提升,导致人们误以为低精度可以无脑代替高精度。

面试题

Q1. FP16 大约能表示的最大数值是多少?

单选题 · easy

  • A.6e-5 左右
  • B.65504 左右← 正确答案
  • C.3.4e38 左右
  • D.1.8e38 左右

答案:B。FP16 指数只有 5 位,最大正规数约 65504。选项 A 是 FP16 的最小正常数量级,选项 C 是 FP32/BF16 的最大值,选项 D 是 FP16 无法达到的量级。记住“小记事本记不了大数字”就能避开混淆。

Q2. BF16 相较 FP16 的核心优势是什么?

单选题 · easy

  • A.尾数位更多更精确
  • B.动态范围与 FP32 接近← 正确答案
  • C.占用显存更少
  • D.训练速度恒定不变

答案:B。BF16 把指数位扩展到 8 位(与 FP32 相同),动态范围极大扩展,不怕梯度下溢。尾数位比 FP16 少一截,所以不是精度更高;显存两者同为 16 位,占用一样;速度取决于硬件,不是恒定的。宽幅便签的优势是能记大数,不是记得更细。

Q3. 混合精度训练中最适合保持 FP32 的项目是什么?

单选题 · easy

  • A.数据增强策略
  • B.数据加载流程
  • C.权重参数更新← 正确答案
  • D.日志打印函数

答案:C。权重更新涉及累加操作,若在 FP16 下执行,微小更新量会被舍入掉,导致训练不收敛,因此主权重和优化器状态必须保持 FP32。数据增强、加载、日志与精度无关。这就像账面底稿必须用精装账本保存,临时便签只用来传话。

Q4. Loss Scaling 的主要作用包括哪些?

多选题 · mid

  • A.防止梯度下溢成 0← 正确答案
  • B.提高模型预测精度
  • C.放大微小梯度到可表示范围← 正确答案
  • D.增加 GPU 显存容量

答案:AC。Loss Scaling 先把损失放大 S 倍,梯度随之放大,避免微小梯度在 FP16 下溢成 0;更新前再缩小还原,并不改变模型本身的表达能力,自然不能“提高预测精度”,也跟显存无关。这就像把微量盐先放大称量再按比例入锅,不会改变菜的味道。

Q5. 梯度累积 N 步后的等效 batch size 是多少?

单选题 · mid

  • A.单步 batch size × N← 正确答案
  • B.单步 batch size ÷ N
  • C.与单步 batch size 无关
  • D.单步 batch size + N

答案:A。梯度累积将 N 个 micro-batch 的梯度求平均后再更新,等效于一次看到 N 倍的数据量,因此等效 batch = 单步 micro-batch × N。类比阿明分三批备料,最终下锅的食材总量是三批之和,而不是三分之一。

Q6. 张量核心高效工作通常需要哪些条件?

多选题 · mid

  • A.输入数据为 FP16/BF16 格式← 正确答案
  • B.数据位于 GPU 显存← 正确答案
  • C.硬件架构支持 Tensor Core← 正确答案
  • D.必须使用 FP32 权重

答案:ABC。张量核心专门为低精度矩阵乘加设计,输入必须是 FP16/BF16 等格式;数据在显存中才能被 GPU 直接读取;架构(如 Volta 及以上)必须提供张量核心硬件。它不需要也不希望权重必须 FP32。料理机只认标准切块的食材。

Q7. FP8 中 E4M3 与 E5M2 的核心区别是什么?

单选题 · mid

  • A.指数位数量不同,范围和精度不同← 正确答案
  • B.符号位数量不同
  • C.尾数位完全相同
  • D.只能用于推理不能训练

答案:A。E4M3 是 4 位指数 + 3 位尾数,精度好范围小;E5M2 是 5 位指数 + 2 位尾数,范围大精度低。两者符号位都是 1 位,尾数位不同;且 FP8 完全可以参与训练的前向/反向。速记笺两种写法各有取舍。

Q8. 动态 Loss Scaling 在检测到梯度出现 Inf 或 NaN 时的正确做法是?

单选题 · hard

  • A.增大缩放因子继续训练
  • B.降低缩放因子并跳过本轮更新← 正确答案
  • C.立即切换成纯 FP32 从头训练
  • D.保持缩放因子不变继续训练

答案:B。溢出说明缩放因子过大,继续训练会让坏梯度污染权重,所以必须减半并跳过本轮,给训练一个缓冲。增大因子会雪上加霜;切换纯 FP32 是最后手段而非第一反应;保持不变则问题持续。这就像菜咸了先加水而不是继续加盐。

Q9. 正确使用梯度累积通常需要做到哪些?

多选题 · hard

  • A.累积结束后将梯度除以累积步数 N← 正确答案
  • B.把学习率同步放大 N 倍
  • C.累积期间不更新权重← 正确答案
  • D.每一步都立刻更新权重

答案:AC。N 步梯度求和后必须除以 N 才等价于大 batch 的梯度;累积期间权重保持不动,最后才统一更新。学习率无需放大,因为平均梯度大小与原训练一致;每一步都更新就完全违背了“累积”的初衷。

Q10. 当梯度数值降至 FP16 最小表示范围之下时,最可能发生什么?

单选题 · extreme

  • A.梯度下溢变成 0← 正确答案
  • B.梯度溢出变成 Inf
  • C.梯度变成负值
  • D.梯度变成 NaN

答案:A。FP16 最小正常数为 6e-5 量级,更小到一定程度就表示不了,直接舍入为 0,这就是梯度下溢。溢出成 Inf 是超过最大值 65504 才会发生;变成负值或 NaN 与下溢无关。微小梯度“记成 0”正是 Loss Scaling 要解决的核心问题。