Skip to content

约 3 分钟阅读

04. 混合精度怎样计算 ​

上一篇的八次加法说明:每一步都用低精度保存,小增量可能逐个消失;留在高精度中累加,最后再转换,结果就能保住。这一篇用一次模型计算,解释混合精度怎样分配这些工作。

先分清计算中的几类数 ​

一个很小的模型可以写成 输出 = w₁ × x₁ + w₂ × x₂ + b。w₁、w₂、b 是训练后保存的参数,常称为权重;x₁、x₂ 是本次输入。多层模型把一层结果送往下一层,这些随输入变化的中间数称为激活。把许多乘积不断加起来的过程称为累加。

于是,“模型使用 BF16”还可以继续拆开问:权重按什么格式保存,乘法读取什么格式,累加器用什么格式,输出最后写成什么格式?这些位置可以采用不同选择。

例如,设备读取 BF16 权重和 BF16 激活,计算乘积,并在 FP32 累加器里保留和,最后把结果写回 BF16。中间的额外精度能保住一部分加法细节;输入已经舍掉的信息,则需要更精确的输入才能恢复。

例如 1.0351 保存为 BF16 后变成 1.03125,再转为 FP32,仍然是 1.03125。扩大容器只让后续计算有更多刻度可用,原来的小数细节需要重新从更精确的原始数据读取。

为什么不把每一步都设成 FP32 ​

70 亿个数从 FP32 换成 BF16,原始参数从约 28 GB 减到 14 GB,搬运相同数量参数所需的字节数也减半。支持低精度矩阵计算的硬件,还可能每秒完成更多乘加。实际收益取决于模型、设备以及实现这项计算的程序,通常称为算子或 kernel。

另一方面,求一大组数的和、把数转成概率、计算训练误差等位置,可能需要更细的刻度或更大范围。混合精度就按这些计算特点分配格式。下面的配置图可以分别改变权重与激活,再单独改变缓存:

BF16 基线;KV BF16。归一化与累加示例均保留 FP32。

图中的 KV 缓存,是文本生成时保存已处理内容的中间状态,后续生成会继续读取;这里先把它看作另一块需要保存数字的空间即可。归一化会按一组数的统计特征调整它们的尺度,是模型里常见的处理。归一化与累加使用 FP32 是图中的示例安排,具体模型应检查各算子的实际执行方式。

训练扩展:小梯度为什么需要缩放

推理使用已有权重计算结果;训练则先用一个数衡量预测与正确答案的偏差,称为损失,再计算参数发生小变化时,损失会以多大速率变化,这个变化率称为梯度。程序据此决定调整方向。FP16 范围较窄,很小的梯度可能在保存时变成零。

损失缩放先把损失的数值乘 1024。它随参数变化的变化量也乘 1024,因此算出的梯度同比放大,能暂时离开容易变成零的区域。比如原本 10^-8 太小,放大后成为 1.024 × 10^-5,就有机会在 FP16 中保留下来。

真正更新参数前,再按相同比例除回去,通常配合 FP32 参数和更新计算完成;若在 FP16 中除回去并保存那个极小值,它仍可能变成零。实际计算还会产生舍入,缩放系数也需要动态调整。

PyTorch 中,autocast 按设备和算子规则选择格式;GradScaler 管理缩放、还原以及非有限梯度检测。BF16 的指数范围较宽,常见 BF16 训练通常无需使用梯度缩放。训练算法和硬件也可能影响状态的保存方式。

已经使用 PyTorch:对应的代码入口

在支持相应格式的 CUDA 环境中,前向计算通常放在以下上下文里。model、inputs 和 targets 由已有训练程序准备;这里只展示精度选择的位置。

python
import torch

with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
    outputs = model(inputs)
    loss = loss_fn(outputs, targets)

采用 FP16 时,常配合 torch.amp.GradScaler("cuda"),由它执行缩放后的反向传播和参数更新。完整训练需要匹配设备支持、模型和优化器;出现 NaN 时应定位第一次产生异常的算子。

敏感位置怎样处理 ​

假设一段语音模型转为低精度后,整体输出接近原版,最终说话人相似度却在阈值附近反复翻转。可以先保持主干计算,单独把相似度和后处理恢复到 FP32,用同一批录音对照。如果问题消失,就得到一条具体线索,再决定是否保留局部提升。

混合精度的核心是给不同计算位置分配数值预算。下一篇FP8、FP4 与整数量化继续缩小保存空间,并解释位数很少时怎样借助缩放因子表示一组数。

别急,先让缓存热一下。