浮点数基础:从IEEE 754到混合精度

做深度学习这几年,我越来越觉得浮点数是个被低估的话题。很多人上来就开混合精度训练,结果模型不收敛,或者精度掉得莫名其妙。说白了,你不了解FP16的脾气,它就会给你颜色看。

今天我们就从最底层的浮点数讲起。别担心,我不会搬出一堆枯燥的公式。我会结合我踩过的坑,帮你把这些概念串起来。

IEEE 754标准回顾

IEEE 754是浮点数的国际标准。它定义了浮点数在计算机里怎么存、怎么算。几乎所有现代CPU和GPU都遵循这个标准。

一个浮点数由三部分组成:

  • 符号位:1位,0表示正数,1表示负数
  • 指数位:决定数值的范围
  • 尾数位:决定数值的精度

公式很简单:(-1)^符号 × 2^(指数-偏置) × 1.尾数

嗯,这里要注意。指数是有偏置的。FP32的偏置是127,FP16的偏置是15。为什么要偏置?为了能表示很小的数。这个细节我当年面试时被问过,答不上来挺尴尬的。

FP32与FP16的位宽与精度对比

先看一张对比表,一目了然:

类型 总位宽 符号位 指数位 尾数位 最大正数 最小正数
FP32 32位 1 8 23 ~3.4×10³⁸ ~1.4×10⁻⁴⁵
FP16 16位 1 5 10 65504 ~6.1×10⁻⁵

你看,FP32的尾数有23位,FP16只有10位。这意味着什么?FP32能精确表示大约7位十进制小数,FP16只能表示大约3位。精度差了整整一个数量级。

我在项目中遇到过一个问题:用FP16训练一个BERT模型,loss死活降不下去。后来发现是梯度太小,直接被FP16截断成0了。这就是精度不够的典型表现。

FP16的表示范围与精度限制

FP16的最大值是65504。你想想看,一个激活值如果超过65504,直接变成inf。这在训练中很常见,尤其是大模型。

FP16的最小正数是6.1×10⁻⁵。听起来很小对吧?但梯度更新时,很多参数的变化量比这个还小。一旦小于这个值,就变成0了。这就是所谓的「下溢」问题。

⚠️ 避坑指南
我曾经在训练一个ResNet-50时,发现某些层的梯度全是0。排查了半天,原来是FP16的表示范围不够,梯度下溢了。后来加了loss scaling才解决。所以,FP16训练必须配合梯度缩放,这是铁律。

还有一个坑:FP16的精度在数值接近0时特别差。因为尾数只有10位,很多小数根本表示不了。比如0.1这个数,在FP16里其实是个近似值。多次累加后误差会越来越大。

BF16与TF32简介

为了解决FP16的问题,Google和NVIDIA分别推出了自己的方案。

BF16(Brain Floating Point)

BF16是Google为TPU设计的。它保留了FP32的8位指数,只把尾数砍到7位。这样表示范围和FP32一样大,但精度更低。

类型 总位宽 指数位 尾数位 最大正数
BF16 16位 8 7 ~3.4×10³⁸

我个人习惯在训练大模型时优先用BF16。为什么?因为它的范围和FP32一样,不会出现inf或下溢的问题。虽然精度低一点,但深度学习对精度的容忍度其实很高。

💡 小技巧
如果你的GPU支持BF16(比如A100、H100),建议优先使用。BF16训练比FP16更稳定,几乎不需要调loss scaling。

TF32(TensorFloat-32)

TF32是NVIDIA在Ampere架构上推出的。它用19位来存储:1位符号、8位指数、10位尾数。说白了,它是个「缩水版」的FP32。

TF32的好处是:输入输出都是FP32格式,但内部计算用TF32加速。你不需要改任何代码,NVIDIA的库会自动处理。我在A100上跑过实验,TF32比纯FP32快了大约8倍,精度几乎没损失。

📌 核心要点
  • FP32:精度高,速度慢,适合需要高精度的场景
  • FP16:速度快,但范围小、精度低,需要loss scaling
  • BF16:范围大,精度低,适合大模型训练
  • TF32:自动加速,无需改代码,适合Ampere及以上架构

下面这张图展示了不同浮点格式的位宽分布,帮你直观理解它们的区别:

不同浮点格式位宽对比 FP32 (32位) S 指数 (8位) 尾数 (23位) FP16 (16位) S 指数 (5位) 尾数 (10位) BF16 (16位) S 指数 (8位) 尾数 (7位) TF32 (19位) S 指数 (8位) 尾数 (10位) 符号位 指数位 尾数位

从这张图可以清楚看到:FP16和BF16虽然都是16位,但指数位不同。BF16的指数位和FP32一样多,所以范围大。FP16的尾数位更多,所以精度稍高。这是个取舍问题。

我个人建议:如果你的模型参数少于1B,用FP16配合loss scaling就够了。如果模型更大,或者你不想折腾loss scaling,直接上BF16。至于TF32,它是个「无痛升级」方案,适合不想改代码又想提速的场景。

好了,浮点数基础就讲到这里。记住一句话:没有最好的格式,只有最合适的格式。下一节我们会深入混合精度训练的具体实现,到时候这些知识都会用上。

📝 课后思考
为什么FP16训练时,loss scaling的初始值通常设为2的幂次(如128、256、512)?这和浮点数的表示有什么关系?想明白这个问题,你对FP16的理解就更深一层了。

想要更多资料,联系可可