☰
深度学习数值格式终极指南:FP32、FP8、INT8与量化实战
2026/9/29 5:52:02 网站建设 项目流程

我还在想,怎么把这么硬核的东西讲得让人不犯困。结果发现自己越写越起劲——因为数值格式这事儿,真的是越抠越有意思。从FP32一路卷到FP4和INT8,表面上是一堆规格表,背后其实是整个深度学习硬件和算法摊牌的过程。

先说个扎心的事实:大部分人用浮点数,其实只知道"精度越高越好"。但真到了部署大模型、抠性能的时候,才会发现FP32算不动、FP16存不下,市场上给出的选择又乱成一团。E4M3、E5M2、E2M1、INT8这些缩写背后,是硬件厂商和算法工程师联手做的"妥协艺术"。

这篇我不打算给你贴一堆废话说"如何使用",而是直接从格式的本质开始拆:每一位是怎么分配的、为什么会这样分、训练和推理各自该用什么、真机跑起来是什么速度。最后我还会把ONNX量化INT8的实操经验掏出来,这些都是我实际跑项目时踩过的坑。

1. 从FP32到FP8:精度不够用之后我们到底做了些什么

1.1 为什么FP32会成为负担

FP32是单精度浮点数,按照IEEE 754标准,1位符号位、8位指数位、23位尾数位,一共32位。它能表示的数值范围约从1.2×10⁻³⁸到3.4×10³⁸,在CPU时代这是绝对的标配。

但深度学习的计算密度越来越大,问题就出现了:数据和模型参数都是以十亿计的,每个参数占4字节,一亿参数就是400MB。现在动辄几十亿几百亿参数的大模型,光是用FP32存一遍权重,就需要几十GB甚至上百GB显存。训练时还需要存梯度、优化器状态,那更是雪上加霜。这就是为什么几乎所有AI硬件的算力规格里,FP32永远是垫底的——因为硬件厂商早就默认你不会拿它来干重活。

1.2 从FP16到BF16:训练时我们牺牲了尾数还是牺牲了范围

为了压缩数据量,FP16出现了——1位符号、5位指数、10位尾数,只有FP32一半大小。当时做训练混精度(Mixed Precision)的思路很简单:数值计算用FP32做累加和主权重,但前向和反向计算用FP16,存下来的主要是中间激活值和部分梯度。

FP16有个明显的弱点:动态范围太窄。5位指数的偏置是15,能表示的最大值约65504。做反向传播时梯度值常常小到1e-5以下,FP16的精度很快就不够用了,会下溢成0。这个现象相信做过混精度训练的同行都有体会。

BF16就是冲着这个来的。它把指数位恢复到8位,和FP32一模一样,所以动态范围和FP32几乎相同,下溢问题大幅缓解。但代价是尾数只剩7位,精度号称约3位十进制有效数字。你可以理解为BF16是一个"范围优先"的格式,适合做大数和小数混杂的梯度场景。现在大多数AI加速卡训练都主推BF16混精度,就是因为它在范围上足够"安全"。

1.3 推理侧的压缩需求:格式越攒越多

训练侧搞定以后,推理侧的胃口又上来了。边端设备内存小、带宽有限、推理时延要求严格,于是INT8量化、FP8甚至FP4就进入视野。请注意一个核心趋势——从FP32到BF16是"范围优先"的妥协,从FP16到INT8则是"整数均匀量化"路线,而FP8的E4M3和E5M2则试图在浮点减半的路上继续降低成本。

这就是为什么你会在YAML配置文件、TensorRT引擎规格、ONNX部署文档里,越来越多地看到E4M3、E5M2、E2M1、INT8同时出现。每一种格式都对应不同的使用阶段和硬件能力。

2. E4M3与E5M2的指数尾数博弈:FP8的两种活法

2.1 E4M3的规格和它能干什么

FP8不像FP16只有一个标准。它有两种主流的位分配方式:E4M3和E5M2。

E4M3的意思是1位符号、4位指数、3位尾数(实际上尾数+1位隐式精度,能表示的有效精度大约相当于4位二进制)。指数位少意味着动态范围小,尾数位多一点则意味着精度相对好一点。它的最大有限值是448左右,最小正常值是2⁻⁶。没有独立的Inf和NaN编码,解释器需要用特殊值来做容错。

这种格式主要用于前向传播,也就是激活值(Activations)和权重(Weights)。原因是前向传播的数值范围通常比较可控,尤其是经过归一化(LayerNorm/RMSNorm)之后,激活值的分布往往集中在某个相对稳定的区间。E4M3的精度可以保证这些数值的尾数细节被尽可能保留。

我在实际测试中说句实在话,如果用E4M3做权重的PTQ(训练后量化),配合好的校准方法,精度的掉落比想象中小很多,因为网络已经训练到一定程度,参数分布相对稳定。

2.2 E5M2的规格和它服务的场景

E5M2则是1位符号、5位指数、2位尾数。最大有限值约57344,最小正常值约2⁻¹⁴,动态范围比E4M3大了几个量级,但尾数精度很低(只有约2位二进制有效精度)。

这就很明白了吧——E5M2是给反向传播和梯度用的。梯度数值在一层一层回传过程中会经历数量级的剧烈变化,有的层梯度大,有的层梯度极小,如果动态范围不够,小梯度的信息就全部被砸成0了。所以反向传播宁可牺牲精度,也要换来足够的数值范围。

除此之外,E5M2还保留了Inf和NaN编码支持,这在计算梯度时特别重要。因为梯度链式求导中偶尔会出现除零或者极端值的情况,硬件能够原样表示这些特殊值,比E4M3只能出错后由上层兜底要自然得多。

2.3 两种FP8典型应用与硬件支持情况

现在很多AI加速芯片(比如Grace-Hopper架构、部分NPU和推理卡)都支持FP8两种模式,但关键是最通过融合算子(Fused Kernel)同时利用E4M3和E5M2的出:前向用E4M3,反向用E5M2。如果你只是用FP8存权重做推理,E4M3足够;如果涉及在线训练或微调,务必确认你的框架和硬件同时支持E5M2。

不同硬件对FP8的支持细节也不一样。有些芯片在整数计算单元上模拟FP8,并没有原生的浮点硬件,这会导致精度表现和速度都有差异。

需要强调的一点:FP8不是所有算子的万能解药。像Softmax这类涉及指数和分式的算子,FP8精度不够,容易出现结果漂移。通常量化方案中,这类层会保持高精度,或者用混合精度策略切回FP32/FP16。

3. E2M1和INT8:把数字压缩到极限的量化艺术

3.1 E2M1的“数值可数性”

E2M1:1位符号、2位指数、1位尾数,一共4位。这个格式能表示的数值非常有限——用手指头数得过来,如果没有隐式前导1,也就只剩下几个离散值。

你可能要问了,这么粗的格式拿来干嘛?答案是权重极端压缩和某些特定硬件的FP4推理。举个例子,有的模型经过特殊的训练和量化感知训练(QAT),可以让权重分布集中到E2M1能够表达的几个值附近,推理时从4字节降到0.5字节,内存占用减少8倍,对边缘部署极有吸引力。

但注意,E2M1不适合直接用来做通用张量存储,更不适合做激活值。它的误差是非线性的,没有办法通过简单的scale校准来修复大范围分布的误差。基本上,FP4是一种"已经做好心理准备"的极限压缩。我看到很多项目用E2M1跑小模型演示效果不错,但遇到大模型还是得回到FP8或者INT8。

3.2 INT8的均匀量化和scale/zero-point机制

INT8是8位整数,从-128到127(有符号),每个间隔是均匀的。这是它和FP8最大的区别:浮点格式在不同数量级下间隔不同,而INT8在全范围内是等间隔的。均匀间隔的数学性质让它非常适合做线性映射——也就是用一个scale和一个zero-point,把浮点分布映射到整数区间:

int8_val = clamp(round(fp_val / scale) + zero_point, -128, 127)

这个逻辑看着简单,但里面的门道很多。scale决定了区间的大小,选得太大精度流失严重,选得太小会溢出。zero-point则负责对齐零点,否则输入中的0在整数域中可能无处安放,导致后续的padding(比如卷积中全零边界)出现偏差。

INT8的推理之所以快,是因为硬件对INT8的矩阵乘做了专门优化。很多AI芯片上,INT8的单位算子吞吐量是FP16的2倍,是FP32的4倍。这个速度优势是大规模推理落地的重要基础。

3.3 头还是尾:什么场景用FN系列,什么场景坚持INT8

我的经验是:训练和微调阶段FP8(E4M3/E5M2)是更自然的选择,因为梯度性质决定了浮点更合适;而纯推理部署场景INT8更成熟、工具链更完善。

推理时选择INT8还是FP8,主要看硬件。如果你的推理引擎跑在只支持INT8的NPU/CPU上,量化到E4M3也是白搭,算子不认。反之,如果你的GPU支持FP8的tensor core,整条链路可以做得更顺滑,因为FP8的前向精度天然好于INT8,特别是在处理分布宽动态的激活值上。

在我们实测的几个Transformer模型里,FP8 E4M3权重的精度损失比INT8均匀量化小一半以上,尤其是在最后几层(logits附近),INT8量化经常需要特殊校准才能压住误差。

4. 同一段算子,FP16/BF16/INT8/FP8谁跑得快

4.1 算力规格表的透明算账

说到速度,就得看硬件规格表。很多AI加速卡的算力是这么标的:

FP32算力有一个值,FP16/BF16的Tensor Core算力约等于它的2倍,INT8约等于它的4倍(部分架构是8倍),FP8在H100这类芯片上能到FP16的2倍甚至更多。这意味着一个简单的事实:同样一次矩阵乘,用INT8比用FP32能快4-8倍,用FP8也比FP32快4-8倍,具体取决于架构。

但这里有个陷阱:峰值算力是理论数据,实际推起来会被内存带宽、算子融合程度、量化/反量化开销拖累。INT8计算再快,如果每次都要把fp32的激活值现转成int8,转换本身的代价可能吃掉一部分速度优势。所以部署时做算子融合(把量化/反量化融合进前一个/后一个算子)是基本操作。

4.2 实测里看到的真实速度差异

我在实际项目中对比过onnxruntime的FP32和INT8动态量化模型,在小batch(1-8)下,推理速度提升通常在1.5-2.5倍之间,并没有达到理论4倍。原因是batch小的时候算子切换和内存搬运占比很大,整数算子的优势没有完全发挥出来。而把batch提到32、64以后,INT8的优势会明显放大。

FP8在GPU上的表现类似。E4M3模式下,由于不需要频繁做double-round,前向计算噪声很小,推理画面感和INT8差不多——在小batch时提升有限,大batch才放得开。

如果同时涉及BF16,它的速度一般和FP16接近,但精度比FP16稳定。我做训练的时候更愿意把主流程放在BF16上,而推理则根据算子量化敏感度来决定是INT8还是FP8。

4.3 算力需求怎么算:别用位数直接推

很多人在对比FP16、BF16、INT8、FP32、FP64时,只盯着 "位数减半速度就翻倍" 这个直觉,却忘了算力需求还涉及两个维度:计算量(FLOPs)和内存带宽(Bytes)。

模型体积减半(比如FP32到FP16),最直接节约的是内存带宽和显存占用,真正的FLOPS还得看硬件单元是FP32的还是Tensor Core的。硬件的FP32单元不会因为你的数据变成FP16就自动翻倍吞吐——当然,很多GPU的FP32和FP16共用一部分单元,如果你的算子跑在CUDA Core而非Tensor Core上,速度差异并不会太悬殊。

所以我的判断顺序是:先看框架有没有把算子调成tensor core/int8/fp8路径,再看数据格式。格式只是前提,路径才是性能关键。

5. ONNX Runtime INT8量化流程实战笔记

5.1 动态量化和静态量化的分岔路

搜索热词里正好有.onnx量化int8,这个我太熟。ONNX Runtime的量化主要有两条路线。

动态量化(Dynamic Quantization):只在推理时对激活值做动态统计并量化,权重量化离线完成。好处是无需校准数据,部署简单;坏处是激活值的scale是运行时候统计的,会引入额外开销。对于模型权重占比大的场景(比如LLM),纯动态量化提升明显;但对卷积网络这种激活值占大头的情况,就比较一般了。

静态量化(Static Quantization):离线用一小部分校准数据预先统计激活值的min/max或分布,生成scale和zero-point,推理时直接使用。这样运行时没有统计开销,速度更快,但你需要准备具有代表性的校准集。

我推荐一个判断方法:如果模型以线性/Embedding为主,可以先试动态量化,实现成本低、效果立竿见影;如果模型卷几层卷积、Residual结构多、激活分布广,静态量化更值得投入。

5.2 校准数据的选取和校准方法

静态量化的核心是校准。校准集不能太大,也不能太小,通常几百张有代表性的样本就够了。关键是覆盖面:要涵盖实际部署时会遇到的亮度、对比度、噪声、类别比例等差异,否则scale就会偏。

ONNX Runtime里常见校准算法有MinMax、Percentile和Entropy。MinMax最简单,取绝对最大值;Percentile排出极端离群点;Entropy基于信息论让量化前后的KL散度最小。我自己的经验:默认先跑MinMax,速度快,精度尚可;如果某一层误差特别大,就针对性改成Percentile,把个别的尖峰过滤掉,效果往往立竿见影。

每个关键对照表:

校准方法原理适用场景我的建议
MinMax取绝对最大分布集中先跑这个
Percentile剔除极值有离群点针对性微调
EntropyKL散度最小宽分布激活精度优先时用

5.3 量化敏感层的处理

我在实操中发现,就算整体精度还能看,总有少数层偷偷掉点。常见问题集中在:

  • 最后一层全连接或卷积(输出直接对应logits),对量化误差敏感
  • BatchNorm折叠不彻底的残差分支
  • LayerNorm/Sigmoid/GELU这类非线性激活层

解法通常有三个:把敏感层保留FP32(混合精度);对敏感层单独使用更大的量化粒度(per-channel);或者在QAT训练阶段做量化感知训练,让权重适应量化误差。

最优化的做法其实是设计模型时就考虑量化友好,比如避免极端动态范围的激活函数,把输出的分布压窄一点。如果模型已训练完毕,退而求其次就是精度敏感层不量化。

5.4 实际跑onnxruntime量化时的几个坑

  • 模型里如果有无用的Identity节点,量化前建议先简化/清理图结构,否则量化后图结构啰嗦,推理延迟下不来
  • 动态量化模式下,Gather和MatMul内的float输入会被转为int8,但Embedding查表依然是浮点,对大词表模型提升有限
  • 静态量化需要校准器(Calibrater)在CPU/GPU上跑一遍,这步别省,否则scale完全是乱猜
  • 不同onnxruntime版本对量化算子支持度差异大,要确认所部署环境的ONNX Runtime版本和量化算子完整度

6. 从精度需求倒推格式选型:我的决策框架

6.1 按阶段选:训练、微调、推理是三张牌

我自己在项目里的决策逻辑很直白——先看阶段。

训练阶段主推BF16或FP16混精度,梯度用BF16保范围。如果显卡不支持BF16,兜底FP16但要小心下溢,必要时加loss scaling。

微调阶段如果你打算全参数微调,还是BF16/FP32为主,LoRA这类参数高效微调可以肆无忌惮地用FP8/E4M3,因为只更新少量低秩矩阵,误差可控。

推理阶段按部署端能力走:CPU上INT8是唯一的性价比之王;支持FP8的GPU上,权重用E4M3,激活能量化就量化,激活敏感到不行的就混合精度;至于E2M1/FP4,只建议在内存极度紧张且对精度有充分预期的场景使用。

按阶段选格式,核心不是比哪个格式"更好",而是比哪个格式在这个阶段的代价最小。

6.2 按算子类型选:宽动态与窄动态

数值分布是选格式的另一个视角。

权重矩阵经过训练后,分布通常集中在0附近,动态范围窄、尾部小,适合INT8、E4M3这种"高精度窄范围"格式。

激活值经过激活函数后,在每一层分布差异大。如果是ReLU系,输出全是非负,范围可控;但如果用了GELU、SiLU,负半轴也有值,分布宽一点,容易在量化边缘出问题。这种情况下你要么选FP8的E4M3增强动态范围,要么在INT8静态量化时花心思校准。

梯度呢?动态范围极不稳定,差量级的跨度可以到1e-8~1e-1,E4M3基本不可能,FP16也有风险,必须FP32或BF16/E5M2。这是反向传播的硬约束。

6.3 误差评估的正确姿势

换格式之前,先定误差基线。我通常会用三个维度:

  • 端到端的精度指标(accuracy/rouge/BLEU等)
  • 中间层输出的余弦相似度或L2差距
  • 最终logits分布的KL散度

一和二能帮你定位是哪一层出了问题,三能看整体分布漂移。三者并行观察,量化调参会快得多。

不要只盯着单个模型跑分。如果你部署的是多个模型服务,还得考虑量化对全部模型的一致影响,有的模型对格式转换非常敏感,有的却像铁打的。敏感度不一致是正常的,接受这一点,才会老老实实给不同模型做单独的量化方案。

6.4 一句话总结选型心得

如果只让我留一句话给刚入门的读者,那就是:训练保范围用BF16,推理保速度用INT8,想在推理上再抠一点、硬件又支持的话,就上FP8的E4M3。

E2M1的FP4永远是压箱底的大招,不是常规武器。而且你要明白一点——无论选哪个格式,真正的功夫不在于认识那几个字母,而在于你知道自己的数值在哪儿、误差从哪儿来、硬件吃哪一套。搞清楚了这些,格式表在你手里就不是天书了。

最后说个实操中的细节:我每次做量化方案之前,都会花半小时把自己模型的每个权重分布直方图打出来看一眼。这笔时间永远值得。因为数据分布长什么样,直接决定了你该信任MinMax还是Percentile,该用per-tensor还是per-channel,甚至能提醒你哪个层不适合量化。格式选型,本质上就是对分布的理解和妥协。

还有一个小技巧想分享给跑ONNX的同行:做INT8量化前,先把模型的Node Fusion和常量折叠跑完,再初始化量化器。很多表面上"量化后精度崩了"的案例,其实是因为图里残留了大量冗余Casts和Transposes,精度根本没崩,纯粹是结构太乱影响了calibrater的统计质量。把这些边角清干净,事半功倍。

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

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

立即咨询