☰
一文讲透深度学习数据类型:FP32、FP16、BF16、TF32、INT8
2026/10/1 2:48:06 网站建设 项目流程

深度学习跑久了,很多人会碰到一个诡异现象:同样的模型,在GPU上训练得好好的,一部署到边缘设备上,或者一开混合精度,Loss突然变成NaN,或者推理结果跟原来差了十万八千里。我见过不止一个项目组排查半天,最后发现是对数据类型(FP32、FP16、BF16、TF32、INT8这些)的理解出了问题。这玩意儿看着不起眼,实际上决定了你的模型能不能训起来、能不能跑得快、能不能在低算力设备上落地。

这篇文章我想把深度学习里主流的数据类型一次性讲透:它们在底层的二进制表示上有什么差异,训练和推理场景分别怎么选,混合精度训练到底在做什么,还有ONNX量化和RKNN部署时那些“精度下降”“数值不动”的坑是怎么来的。不管你是刚入门的学生,还是被部署问题折磨的工程师,读完应该能对“精度”这件事建立起一个完整的直觉。

1. 浮点数据类型的基础差异

1.1 浮点数在计算机里的底层结构

先说一个最基础的问题:计算机是怎么存一个小数的?绝大多数人学过的答案是IEEE 754浮点数标准,这套标准把一个数拆成三部分——符号位、指数位、尾数位。

如果把一个浮点数比作科学计数法,大概长这样:

[ \pm 1.xxx \times 2^{y} ]

符号位决定正负,指数位决定取值范围(相当于科学计数法里的“10的几次方”),尾数位决定精度(相当于有效数字能保留几位)。

  • FP32(单精度浮点):1位符号 + 8位指数 + 23位尾数,总共32位。
  • FP16(半精度浮点):1位符号 + 5位指数 + 10位尾数,总共16位。
  • BF16(Brain Floating Point):1位符号 + 8位指数 + 7位尾数,总共16位。
  • TF32(TensorFloat-32):1位符号 + 8位指数 + 10位尾数,总共19位,主要用于NVIDIA Ampere架构的Tensor Core矩阵运算。

你看FP16和BF16的总位数都是16位,但分配策略完全不同。FP16给了尾数10位,却只给指数5位;BF16反过来,指数给了8位,尾数只留7位。这个差异决定了它们擅长的场景是反过来的。后面我会细说。

先记住一个核心结论:指数位决定你的模型会不会“爆”(溢出),尾数位决定你的模型“准不准”(精度)。很多人只盯着精度看,忽略了范围,这是踩坑的重灾区。

1.2 四种主流浮点类型的范围与精度对比

不同浮点类型的核心差异先看一张表:

数据类型总位数指数位尾数位最大范围(约)最小正正规数相对精度(约)
FP3232823(3.4\times10^{38})(1.18\times10^{-38})(6\times10^{-8})
FP1616510(6.55\times10^{4})(6.10\times10^{-5})(4.9\times10^{-4})
BF161687(3.4\times10^{38})(1.18\times10^{-38})(7.8\times10^{-3})
TF3219810(3.4\times10^{38})(1.18\times10^{-38})(4.9\times10^{-4})

从这张表能读出几个关键信息:

第一,FP16最容易被“范围”卡死。它的最大表示范围只有65504,约(6.55\times10^{4})。什么意思?如果你的梯度或Loss在训练中超过了这个数,直接变成Inf。更麻烦的是FP16的最小正正规数是(6.10\times10^{-5}),比FP32大了好几个数量级,意味着特别小的梯度会被“吸收”成0。梯度变0,参数就不更新了,模型直接“假死”。

第二,BF16的范围和FP32完全一样。因为它指数位同样是8位,所以不存在上溢和下溢问题。代价是尾数从23位缩到7位,精度大幅降低。这就是为什么BF16刚出来的时候很多人说它是“粗制滥造”的精度——好处是深度学习确实不太需要极高的尾数精度,后面讲训练时我会展开。

第三,TF32的精度介于FP16和FP32之间,但它主要出现在NVIDIA的Tensor Core内部。它只有10位尾数,却保留了FP32的指数范围,这决定了它是一个“不用改代码就能白嫖加速”的选项。

第四,别忘了符号位。四个类型都占了1位作符号,所以正负数对称,没有额外的坑。

1.3 尾数位与指数位的工程比喻

我习惯把浮点数想成“相机里的变焦镜头”。指数位是广角端,决定你拍得了多亮多暗的景(范围);尾数位是清晰度,决定你细节还原有多好(精度)。FP16这支镜头,广角端很弱(最大范围小),但放大看细节还行;BF16刚好反过来,广角端和FP32一样广,但细节糊得多;TF32把广角端拿到了FP32级别,细节精度却只有FP16的水平。

这个比喻在实操里很有用:当你决定用哪种类型时,先问自己两个问题——模型里的数值可能遇到极端大的情况吗?需要保留多少位有效数字?前者指向指数位,后者指向尾数位。先回答这两个问题,数据类型选型就成功了一半。

2. 训练模式下到底选什么精度

2.1 混合精度训练(AMP)的核心逻辑

很多人以为混合精度训练就是把所有参数从FP32换成FP16,然后把Loss改成FP16训练,不是的。混合精度的意思是:参数用FP32保存一份“主副本”,计算过程中用FP16加速,每次更新时把FP16算出来的梯度转回FP32更新。这就像你平时记账用精确到分的大账本,但心算的时候用口头估算,算完再誊回账本。

具体流程大概是这样的:

  1. 把模型参数复制一份FP16副本,每次前向传播用FP16跑,速度更快、显存占用更低;
  2. 计算FP16梯度;
  3. 把FP16梯度转回FP32,再乘上学习率,更新FP32主参数;
  4. 循环进入下一步。

为什么不让FP32参数直接更新?因为Tensor Core在FP16上的计算吞吐量通常是FP32的几倍甚至更高,而模型质量主要靠FP32主参数来保证。这种“FP32保存、FP16计算”的分工,就是为了在速度和质量之间找平衡。

PyTorch里实现混合精度非常方便:

import torch from torch.cuda.amp import autocast, GradScaler model = MyModel().cuda() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scaler = GradScaler() for data, target in dataloader: data, target = data.cuda(), target.cuda() optimizer.zero_grad() with autocast(): output = model(data) loss = criterion(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

2.2 损失缩放(Loss Scaling)是怎么回事

刚才说到FP16的下限很高,梯度小了会被冲成0。损失缩放就是为了对付这个问题。

思路很直白:把Loss乘以一个系数(比如1024或2048),让它变大,反传出来的梯度也相应变大,就不会低于FP16的最小表达范围了。梯度更新前再除以这个系数,恢复成正常尺度。这就是GradScaler在PyTorch里干的事。

但要注意:损失缩放并不会解决FP16的“上溢”问题。如果损失缩放太多,反而会让某些梯度超过FP16上限变成Inf。所以GradScaler是动态调整的——一旦发现梯度出现Inf或NaN,就减小缩放因子;连续多次没出问题,就尝试增大一点。这也是为什么用了AMP以后,偶尔会看到训练日志里打印出“GradScaler is decreasing the loss scale”之类的信息,没必要慌,它在自我调节。

实操中我个人体会,FP16混合精度在多数CV模型(ResNet、YOLO、Vision Transformer)上效果都比较稳定,但在一些NLP模型,尤其是带时序依赖的模型上,容易出现梯度下溢。这时候可以考虑换BF16。

2.3 BF16和FP16怎么选

这是当前训练场景下最常被问的问题。

BF16的优势前面说过:指数位和FP32一致,范围和动态范围跟FP32完全对齐,训练中几乎不会因为溢出翻车,也不需要损失缩放那一套机制。这大大简化了训练流程。

但BF16的尾数只有7位,精度比FP16还低。你可能会问:那它凭什么能训模型?

实际情况是:深度学习计算过程中,权重更新依赖的是梯度的“期望”而不是“精确值”。7位二进制尾数,差不多对应十进制2-3位有效数字。对SGD/Adam这类带噪声的优化器来说,这点有效数字其实够用了。相反,FP17那5位指数位反而是硬伤,容易让梯度直接消失或爆炸。

如果是用NVIDIA A100、H100这些新卡训练,我现在的默认选择是:主力用BF16混合精度,除非发现某个模型在BF16下出现了明确的精度问题,再退回TF32或者纯FP32。如果是老卡(V100以下,不支持BF16),就老老实实FP16混合精度,配合损失缩放。

简单总结:

对比维度FP16BF16
指数范围小(易上溢/下溢)与FP32一致
尾数精度较高较低
损失缩放需要不需要
硬件支持广泛(老卡也支持)需要较新硬件
适合场景老设备、精度优先新设备、稳定优先

2.4 TF32:A100用户的隐藏加速开关

TF32这个类型比较特殊,它不是一个“公开的数据存储类型”,而是NVIDIA Ampere架构里Tensor Core做矩阵运算时内部使用的一个精度模式。说白了,Tensor Core在算矩阵乘法时没有乖乖用FP32算,而是把FP32的输入数据截断成19位(10位尾数),然后算完再输出FP32结果。

这样做的收益是:矩阵运算速度可以比纯FP32快很多,接近FP16的吞吐量,而且代码层面你完全不用改,还是用FP32的API调用。损失是尾数精度从23位降到10位。

对多数深度学习模型来说,这个精度的损失几乎不可感知,但确实有个别对数值敏感的任务(比如某些科学计算、强化学习)会受影响。所以NVIDIA提供了手动开关:

# 关闭 TF32,强制使用真正的 FP32 export NVIDIA_TF32_OVERRIDE=0 # 打开 TF32(默认状态也可能是开) export NVIDIA_TF32_OVERRIDE=1

在PyTorch里也可以按需控制:

torch.backends.cuda.matmul.allow_tf32 = True # 矩阵乘开 TF32 torch.backends.cudnn.allow_tf32 = True # 卷积开 TF32

我踩过的坑是:有一段时间跑强化学习任务,模型一直不收敛,折腾了好几天,最后发现是Tensor Core默认走了TF32,把某些数值敏感的运算精度削掉了。关掉之后问题立刻消失。所以我现在的建议是:训练CV、NLP这类主流模型时TF32一般没问题,但如果你的模型训练出现不明原因的抖动、不收敛,先把TF32关了试试,这个排查成本很低。

3. 推理阶段的数据类型与量化

3.1 FP16推理真的够用吗

训练完成之后,模型部署到服务端推理,通常第一个加速操作就是把权重从FP32转成FP16。这一步的效果立竿见影:显存占用直接减半,推理速度在支持FP16 Tensor Core的显卡上也能有明显提升。

而且对绝大多数已经训练好的模型来说,FP16推理几乎是无损的。原因很简单:模型参数的分布范围是训练时确定的,一般不会出现极端大值,而且推理阶段只做前向计算,没有反向传播带来的梯度下溢问题。

不过有两点要注意:

第一,激活值的分布范围比权重广。BN层的running_mean/running_var如果出现极小值或极接近0的数,FP16下可能直接变成0,导致输出异常。我遇到过YOLO系列模型FP16推理时,某些BBox坐标出现NaN的情况,最后定位到是某个归一化层的数值太小,FP16存不了。

第二,某些算子对精度非常敏感。比如Softmax,里面有个指数运算,输入稍微大一点,指数结果就可能溢出。所以很多推理框架在实现FP16推理时,Softmax是算了再转回FP32做的。如果你手写推理引擎,这个细节千万别漏。

3.2 ONNX导出与INT8量化基础

FP16虽然快,但对边缘设备或手机端来说还是“太重”了。INT8量化才是真正把模型压到极致的方案:权重和激活都用8位整数(int8)表示,模型体积降到原来的四分之一,推理速度在支持INT8指令的CPU/NPU上往往还能再快一个量级。

ONNX是最常用的模型中间格式,它的量化流程一般是这样的:

  1. 把训练好的模型导出为ONNX格式;
  2. 准备一批校准数据(calibration dataset),典型几百到几千张样本即可;
  3. 用校准数据跑模型,统计每一层激活数值的动态范围(min/max或直方图);
  4. 根据统计结果计算每个张量的scale和zero_point;
  5. 把FP32/Fp16算子替换为INT8算子(比如MatMul、Conv,浮点输入转换对应QuantizeLinear);
  6. 导出带上量化信息的ONNX模型。

这里最关键的是第2、3步:校准数据怎么选。很多人直接把训练集拿来做校准,这种做法在分类模型上问题不大,但在检测、回归模型上可能会翻车。校准数据的分布必须能覆盖模型在真实场景下会遇到的数值范围。我见过一个项目,用全是白天的图片做了校准,结果模型一到夜间场景输出全部乱掉。这不是量化算法的锅,是校准数据没选对。

ONNX量化分两种:训练后量化(PTQ)和量化感知训练(QAT)。

PTQ是傻瓜式方案,模型训完直接量化,速度快但精度下降不可控。打个比方,PTQ就像你先把一张照片打印出来,然后拿美图软件去压缩画质,原图信息已经定了,压缩后能保留多少细节全看运气。

QAT则是在训练的时候就模拟量化过程:前向传播时把权重人为四舍五入成INT8,反向更新时还是用高精度的梯度。这样模型在训练中就“适应”了量化误差,最终量化效果通常比PTQ高很多。如果PTQ精度掉得太狠,QAT是正解。

3.3 RKNN部署INT8量化的那些坑

这几年国产NPU设备越来越普及,RKNN是Rockchip系列芯片上的神经网络推理框架,网上搜“onnx转rknn int8”能出来一大堆问题。我自己在RK3588、RV1126这类设备上部署模型也有不少经验,这里专门聊聊INT8量化的坑。

第一个坑是算子兼容性。ONNX里很多算子(特别是自定义算子、某些新版本算子)RKNN根本不支持,转换的时候要么直接报错,要么被映射到CPU算子导致速度没有提升。所以你在转RKNN之前,最好先把ONNX模型里的算子梳理一遍,遇到不支持的算子尽量改写成RKNN能支持的组合。

第二个坑是量化精度掉得夸张,甚至数值不动。这是热词里提到的高频问题。你有没有遇到过这种情况:FP32模型在PC上验证没问题,转成RKNN INT8之后,输出结果要么全是0,要么不变,要么偏差巨大?

我总结的排查顺序是:

  1. 看校准数据集和校准方式:RKNN的量化校准默认会给每个输入做统计,但如果你的数据集跟真实数据分布不一致,量化尺度会完全偏掉。可以把RKNN导出时生成的量化参数表(如果框架支持)导出来看,检查每层的scale和zero_point是不是过于离谱。

  2. 看归一化方式:训练时如果用了减均值除方差,导出ONNX时很多时候会忽略这个预处理步骤。结果就是RKNN端拿到的输入和训练分布的输入差了十万八千里,量化误差自然爆炸。这种情况不是量化的错,是预处理的错。

  3. 检查动态范围统计:你有某些层激活值本身就集中在很小的范围内,稍微量化一下信息就全丢了。可以把每一层的输出分布打印出来看看。如果某一层在FP32下输出分布范围就很窄,比如都在0.01到0.03之间,那INT8量化必然出现“数值基本不动”的现象——因为在这个范围内,量化步长可能比信号本身还大。

  4. 试试混合量化:不是所有层都需要INT8。把一些敏感层(通常是最前面几层和最后几层)保留为FP16或FP32,中间大计算量的层用INT8,效果往往立竿见影。

第三个坑是RKNN的量化参数版本问题。RKNN Toolkit和RKNN Runtime版本不对齐时,量化结果可能诡异不同。建议开发、验证、量产用同一套版本,别乱升乱降。

3.4 INT8量化对模型结构的影响

很多人好奇:为什么有的模型量化后精度基本不掉,有的模型一量化就崩?

核心在于模型结构对量化误差的“容忍度”。卷积、全连接这类算子,权重分布如果近似正态分布且比较集中,量化误差相对可控。但像MobileNet这类使用了大量Depthwise卷积的结构,参数分布往往很尖锐(集中在0附近),直方图统计不好做,量化误差就会被放大。

另一个常见隐患是大量BatchNorm层被融合进卷积之后的变化。BN层在训练时是跟着batch统计量跑的,推理时用的是全局统计量。ONNX导出后可以去融合BN,但融合过程会改变数值范围。有些量化工具对融合后的算子统计做得不好,导致量化参数不准。如果你发现某个模型量化后精度暴跌,试着对比一下ONNX在“融合前 vs 融合后”的输出分布,有时候就是这里出了问题。

3.5 INT16的意义

除了INT8,热词里还提到了INT16。你可能觉得INT8都够用,为什么还要INT16?

实际案例是这样的:某些边缘芯片对INT8的加速效果很好,但INT8的动态范围只有256个等级,对于输入范围很大的传感器数据(比如IMU、毫米波雷达数据),或者一些对精度要求高的回归任务,INT8量化后误差可能不可接受。INT16提供了65536个等级,精度损失明显更小,而存储和计算开销又比FP16低。

所以当你在RKNN或其他边缘平台上遇到“INT8精度不够、FP16又太慢”的尴尬时,先看看芯片支不支持INT16,这往往是一个性价比很高的中间档。

4. 前沿数据类型:FP8与更多可能

4.1 FP8是什么:E4M3与E5M2

网络热词里有人问“fp8和bf16, fp16分别是什么意思”,这里我专门展开讲一下FP8。

FP8是近年英文数据格式的前沿焦点,NVIDIA H100、Ada Lovelace架构的GPU,以及部分最新旗舰SoC,都开始支持FP8运算。它只有8位,是比FP16更“激进”的压缩方案。

FP8内部又分两种子格式:

  • E4M3:1位符号 + 4位指数 + 3位尾数。最大表示范围约448,精度相对较高。适合用于前向传播和推理。
  • E5M2:1位符号 + 5位指数 + 2位尾数。最大表示范围可达(5.7\times10^{4}),范围更大但精度更低。适合用于反向传播和梯度累积场景。

FP8和BF16、FP16的关系,可以这样理解:BF16是把FP32的尾数砍成一截,保留范围;FP16是保留较多尾数但牺牲范围;FP8则是范围和精度都很激进地压缩,它更像一种专门给大规模集群训练(动辄成千上万卡)设计的传输和计算格式。在几千卡并行训练时,通信量是瓶颈,FP8能显著降低通信开销,这也是H100上大模型训练喜欢用FP8的原因。

4.2 FP8在实际工程里要不要追

FP8虽然很火,但目前工程上我建议保持“观望+小范围验证”的态度。

原因有三点:

第一,硬件支持还不普及。大多数存量设备根本不支持FP8的Tensor Core加速,代码写了也没法上线。

第二,训练稳定性问题。FP8的尾数太少,直接用于训练时,优化器状态(比如Adam的一阶、二阶矩估计)几乎必须用FP32或BF16保存。也就是说,FP8只能用在矩阵乘法计算上,主权重和优化器状态还得占用高精度存储,实际显存节省没有想象中那么多。

第三,框架支持程度参差不齐。PyTorch的FP8还在快速迭代中,API不稳定。如果不是做前沿研究或者公司有明确的技术预研要求,现阶段花时间在FP8上不如先把FP16/BF16/INT8这几条常用链路打磨好。

个人经验:新技术可以关注,但生产环境先跑在成熟稳定的数据类型上。这不是保守,而是让模型质量有底线的工程选择。

5. 常见问题与排查技巧实录

5.1 “INT8量化后精度下降、数值不动”的排查路径

这个场景实在太经典了,我把排查路径整理成一张速查表:

现象可能原因排查方向
精度轻微下降(1%-3%)校准数据分布不匹配换更多样化的校准集;检查归一化参数
精度明显下降(10%以上)某一敏感层量化损坏逐层对比FP32和INT8输出,定位可疑层
输出全部为0校准集数值范围统计错了/数据预处理错打印量化scale,检查输入预处理
输出数值固定不变激活值分布过窄,量化步长太大检查层输出分布,考虑混合量化
某些输出NaN/Inf算子溢出或量化零点算错了检查zero_point计算,谨慎修改框架默认配置
模型能跑但速度没提升有算子被映射为CPU算子查看RKNN/ONNX Runtime日志,优化算子支持性

还有一个非常有用的调试技巧:做逐层对比。把FP32模型和INT8模型分别跑一遍,把每一层的输出节点都导出来,然后逐层算它们的余弦相似度或平均绝对误差。哪一层从这一层开始偏差突然变大,问题就出在哪一层。这个定位方法在ONNX和RKNN上都能用,只是要花点时间把中间节点都命名好。

5.2 校准数据集怎么准备才靠谱

我再单独强调一遍校准数据。这是PTQ量化流程中最容易被低估的环节。

校准数据的目标是“尽量穷尽模型在真实推理时会遇到的数值范围”。我的经验是:

  • 数据量不求多,200到1000张(或样本)足够,但来源要覆盖真实场景的各种极端情况;
  • 分类任务:每一类尽量都覆盖到,不要全是容易样本;
  • 检测任务:包含小目标、大目标、遮挡、模糊等各种难度;
  • 回归任务:输入范围要覆盖训练时的最小值和最大值,尤其要把边界值包含进来;
  • 如果模型部署在某固定场景(比如固定的工业产线或固定角度的摄像头),就用这个场景的实际数据做校准,不要拿通用数据集代替。

5.3 数值敏感层怎么识别

最后分享一个识别“敏感层”的技巧,这是我踩过几次坑后总结出来的。

所谓数值敏感层,就是量化后误差被放大的层。常见的有:

  • 神经网络的输入层(第一层卷积/全连接):输入数据的动态范围往往很大,直接量化容易丢信息;
  • 输出层(最后的全连接或回归头):输出值范围直接关联任务结果,精度损失会被直接放大;
  • 注意力机制中的Softmax、LayerNorm:对数值稳定性极其敏感;
  • 残差结构中的Add节点:加法会累加量化误差,两个量化后的数相加,误差会叠加。

对于一个模型,先尝试把这几类层设成高精度(FP16/FP32)保留,其他层走INT8。多数情况下这样已经能恢复绝大部分精度损失,而且推理速度损失很小。

5.4 混合精度训练中Loss变成NaN怎么办

训练侧有一个问题也经常被问到:用了AMP之后Loss变成NaN了,怎么办?

排查顺序一般是这样:

  1. 先看是不是学习率太大。AMP虽然有损失缩放,但它不会解决本质上的梯度爆炸问题。
  2. 确认损失缩放有没有在正常运行。可以打印scaler.get_scale(),看看数值是不是在正常范围内。
  3. 检查模型结构里是否有不稳定的算子(比如某些激活函数在FP16下表现不好)。
  4. 尝试把个别模块强制设置成FP32计算(PyTorch用model.module.float()或者装饰器@torch.cuda.amp.custom_fwd(cast_inputs=torch.float32))。
  5. 如果是老设备且模型对精度极敏感,干脆退回BF16或纯FP32,别死磕FP16。

损失缩放本身是动态的,但如果一段时间内梯度频繁出现Inf/NaN,GradScaler会不断调低缩放因子,最终缩到接近1,这时候就等于没用了。所以不要只盯着step看,训练跑一半发现Loss突然全部为NaN,先检查一下缩放因子的值。

6. 我自己的一些习惯总结

玩了这么久深度学习的数据类型,我自己的选择习惯已经稳定成了一套规则,分享出来供你参考:

  • 训练:新卡优先BF16混合精度,老卡用FP16 + GradScaler,出问题先关TF32试试;
  • 推理服务端:FP16起步,显存不够再考虑INT8,尽量先用PTQ测一测精度,掉得多再上QAT;
  • 边缘部署:能用FP16就先用FP16,实在跑不动再上INT8/INT16,优先做混合量化而不是全INT8;
  • 遇到任何数值问题,第一件事是复现FP32结果,确认基线没问题,再一层一层往上叠精度损失。

数据类型这个话题看似基础,但对训练稳定性和部署效果的影响比大多数人想象中大得多。把每一步的数值范围、精度损失来源搞清楚,再看那些负面经验类的热词问题,基本都能顺势推导出解法。踩过几次坑之后,我反而觉得这些“精度问题”是深度学习工程中最有规律可循的一类问题。希望这篇文章能给正在排查的你提供几条清晰的路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询