大语言模型量化技术(五):FP8
FP8 是什么
FP8 是 8 bit 的浮点格式,和 FP16、BF16 一样由"符号位 + 指数位 + 尾数位"组成,只是位数更少。8 bit 放不下完整的 IEEE 754 格式,Open Compute Project(OCP)在 2022 年定义了两个实用格式:E4M3 和 E5M2,E 指指数位(Exponent)的位数,M 指尾数位(Mantissa)的位数。
| 格式 | 符号 | 指数 | 尾数 | 最大值 | 最小正规数 | 最小非正规数 |
|---|---|---|---|---|---|---|
| FP16 | 1 | 5 | 10 | 65504 | ||
| BF16 | 1 | 8 | 7 | |||
| E4M3 | 1 | 4 | 3 | 448 | ||
| E5M2 | 1 | 5 | 2 | 57344 |
两个格式是动态范围和精度的两种取舍:
- E4M3 尾数有 3 bit,精度更细,但动态范围只有 。推理中的权重和前向激活一般用它。它没有无穷大,舍入时超出的值会饱和到 。
- E5M2 指数有 5 bit,动态范围扩大到 ,但尾数只有 2 bit,精度更粗。它主要用于训练中的梯度,因为梯度的动态范围通常比激活更大。
FP8 作为浮点格式,最关键的性质是量化步长是相对的。以 E4M3 为例,尾数 3 bit 意味着每个指数区间内被均匀切成 份,相邻两个可表示数值之间的间隔等于当前指数对应的 倍。也就是说:
- 数值在 区间时,步长是 ;
- 数值在 区间时,步长是 ;
- 数值在 区间时,步长是 。
绝对步长随数值大小按比例变化,相对误差则被限制在一个常数范围内(E4M3 约 )。对比之下,INT8 的量化网格是均匀的:步长由 scale 决定后处处相同,数值越接近零,相对误差越大。这个差异会让 FP8 和 INT8 在实际数据上表现不同。
什么是 FP8 量化
量化通常被理解为:把浮点数除以一个步长 scale,映射到整型的量化网格上,所以 INT8、INT4 这类整型量化很直观。FP8 本身就是浮点数,乍看不太像量化。但量化的本质只是把数值映射到一个有限的可表示集合,FP8 的集合就是 E4M3 或 E5M2 能表示的那些数值。
以 E4M3 为例,量化网格就是该格式可表示的数值集合。每个数值都可以表示为
其中 是符号位, 是指数位, 是尾数位,取值范围分别为 ,,。
FP8 量化就是将浮点数映射到这个有限的、可表示的数值集合中。以 E4M3 为例,步骤如下:
- 根据待量化数据的范围确定 scale ,使缩放后的数值落在 E4M3 的可表示范围内。对称量化时,常用 。
- 将数据除以 scale,得到 。
- 将每个结果舍入到最近的 E4M3 可表示值;超出范围的值饱和到 或 ,得到量化结果 。
- 保存 和 scale 。使用时通过 还原近似值。
所以 FP8 量化和 INT8 量化没有本质区别,只有网格形状不同:FP8 是非均匀的浮点集合,INT8 是均匀的整数集合。
FP8 与 INT8 量化精度对比
图中对比了 INT8 和 FP8 的量化网格。INT8 是均匀的整数集合,FP8 是非均匀的浮点集合:数值大时步长更大,数值小时步长更小。
虽然上面使用的 scale 是一个确定的值,但我们考虑量化步长是应该是考虑相邻的两个可表示数值之间的间隔,也就是相对步长。在 INT8 中,量化步长是固定的,等于 scale。而在 FP8 中,因为浮点数的表示是非均匀的,两个相邻的可表示数值之间的间隔随数值大小而变化。
比如 E4M3 中连续两个可表示数值之间的差异在 区间是 ,在 区间是 ,在 区间是 。假如 scale 为 0.1,那么量化后的步长在这些区间分别是 、 和 。
假设某个 tensor 中,99% 的数值落在 ,还有一个离群值是 10,下面是使用 INT8 和 FP8 量化的对比。
- INT8:步长 。数值 除以步长约 0.64,舍入到 1,还原为 ,误差约 ;数值 约 1.27 个步长,同样舍入到 1,误差约 。也就是说,占绝大多数的 区间,整个只能使用 -1、0、1 三个量化等级。
- FP8 E4M3:scale 。数值 ,落在 区间,步长 0.25,量化为 2.25,误差约 ;数值 ,落在 区间,步长 0.5,量化为 4.5,误差约 。同样的 区间,FP8 有 100 多个量化等级可用。
INT8 的步长被离群值撑大后,主体小值被压到很少几个等级;FP8 的相对步长让小值和大值各自保住分辨率。LLM 激活大多贴近零、偶有大幅值,正好是 FP8 擅长处理的形状。
所以 W8A8 FP8 通常不需要 SmoothQuant 那样的平滑变换,靠 FP8 的相对步长就能覆盖激活的动态范围。
为什么需要 FP8 量化
前面几篇介绍了 GPTQ、AWQ 等 4 bit 权重量化方法。8 bit 量化在 LLM 推理中同样是常见选择,最典型的形态是 W8A8:权重和激活都量化到 8 bit,直接调用硬件的 8 bit 矩阵乘法指令计算。
在 Ampere 架构(A100)上,8 bit 矩阵乘法指令只有 INT8,所以早期的 W8A8 方案基本围绕 INT8 展开。LLM.int8() 用混合精度分解处理了激活中的离群值,SmoothQuant 又通过等价缩放变换,把激活的量化难度"迁移"一部分到权重,让 W8A8 INT8 量化变得可用。
从 Hopper 架构(H100、H800、H20 等)开始,NVIDIA GPU 的 Tensor Core 原生支持 FP8(8 bit 浮点)矩阵乘法,其峰值吞吐量约为 BF16 的两倍,与 INT8 持平。此后,越来越多的模型厂商开始直接发布 FP8 权重的官方版本,比如 DeepSeek-V3、DeepSeek-R1 的官方权重就是 FP8。
使用 FP8 量化,可以在保证数值精度的前提下,充分利用 Hopper 及后续架构的 FP8 矩阵乘法指令,在保证数值精度的前提下充分利用硬件的高吞吐量。
能否直接把权重转成 FP8
模型权重大多分布在零附近,似乎天然落在 FP8 的表示范围内,那能不能直接把 FP16 权重转成 FP8?严格来说,逐元素转本身就是一种量化,等价于 scale 的 Per-Tensor 量化。这样做简单,但精度不一定高。一是 FP16 权重未必落在 E4M3 的范围内:E4M3 只到 ,而 FP16 到 ,直接转会超出范围或下溢为零。二是 E4M3 只有 3 bit 尾数,FP16 有 10 bit,舍入误差不小,权重信息可能丢失。
引入一个缩放因子 ,先把 FP16 权重除以 ,再转成 FP8,就能避免超范围和下溢。但 FP8 尾数少,不溢出也仍有舍入误差。减小误差的办法是让每个量化范围覆盖更少的数据,也就是用更细的粒度,对每组权重分别计算 scale。
权重量化:128×128 分块 FP8
以线性层的权重矩阵 ( 个输出通道, 个输入通道)为例。把 沿行和列各按 128 切分,得到 个块,记第 个块为 。每个块的量化过程是:
- 统计块内最大绝对值:
- 计算反量化用的 scale:
这样块内最大的权重恰好映射到 E4M3 的最大值 448。
- 逐元素量化:
每个元素先除以 ,再舍入到最近的 E4M3 可表示数值(平局时取偶),超出 的会被饱和。
- 存储 (每元素 8 bit)和 (通常用 FP32,32 bit)。
推理时反量化:
存储开销可以算一笔账:每个 个权重组多存 1 个 FP32 scale,即 4 字节,相对权重本身的字节数约为 ,可以忽略。
在 checkpoint 里,这些 scale 通常是一个名为 weight_scale_inv 的独立张量,形状为 。这个名字沿袭自历史命名,存的就是反量化用的缩放系数。权重张量本身的 dtype 则是 F8_E4M3,和 BF16 张量混在同一个 safetensors 文件里。
激活动态量化
权重是静态参数,scale 可以在量化时算好、随 checkpoint 一起发布。激活不同:同一个线性层的输入,每个 token 不一样,每层也不一样,无法预先算好固定的 scale。通用的做法是动态量化(dynamic activation scheme):推理时,矩阵乘之前用当前输入现算 scale。
取某个 token 的输入 (上一层输出的隐藏状态),先算:
再量化:
有的实现每个 token 只用一个 scale,有的会像权重那样把 维再按 128 分组、每组一个 scale,粒度更细。计算 只是对 个元素做一次归约,相比矩阵乘可以忽略,所以动态量化的成本很低。
这样,完整的矩阵乘变成:
其中 是权重沿 维的第 个 128 列块, 是对应的输入片段。每个括号内的乘积由 FP8 Tensor Core 完成,外层的 和 在累加时乘回去。
FP8 矩阵乘
这里以 DeepSeek 开源的 DeepGEMM 为例,说明 FP8 矩阵乘在 GPU 上的执行过程。
在 DeepGEMM 中,FP8 矩阵乘的执行流程大致如下:
- 将权重离线按照 128×128 分块,每个块量化为 FP8,并存储对应的 scale;
- 将输入激活按 token 维度以 128 分组动态量化为 FP8,每个 token 的每组都得到一个 scale;
- 调用 FP8 Tensor Core 计算每个块的矩阵乘,FP32 累加;
- 在尾声阶段,将各部分累加结果乘以对应的 scale,得到最终输出。
需要注意的是,这个加速只在支持 FP8 Tensor Core 的 GPU 上成立(Hopper、Ada、Blackwell 等)。在 A100 这类只支持 INT8 的卡上,推理框架仍然可以加载 FP8 权重,但会在计算前反量化回 BF16/FP16。此时 FP8 只省了 checkpoint 的下载和磁盘空间,推理显存和速度都与 BF16 相当。所以部署 Qwen3.8-27B-FP8 这类模型时,建议用 H100、H800、H20 或更新的卡,搭配 vLLM、SGLang 这类框架,才能同时拿到存储和速度两方面的收益。
实例:Qwen3.8-27B-FP8
下面结合一个例子来看在真实的模型中,FP8 量化是如何配置和使用的,下面是 Qwen3.8-27B-FP8 的 config.json 中 quantization_config 字段:
{
"quant_method": "fp8",
"fmt": "e4m3",
"activation_scheme": "dynamic",
"weight_block_size": [128, 128],
"modules_to_not_convert": ["lm_head", "model.embed_tokens", "..."]
}
quant_method: fp8:量化方法是 FP8;fmt: e4m3:权重以 E4M3 存储;weight_block_size: [128, 128]: 分块量化,每块一个 scale;activation_scheme: dynamic:激活在运行时动态量化,因此整个发布不需要任何校准数据;modules_to_not_convert:保持 BF16 的模块列表,即不需要量化的模块。
FP8 与其他量化方案的对比
| 方案 | 权重精度 | 激活精度 | 是否需要校准 | 典型硬件 | 定位 |
|---|---|---|---|---|---|
| W4A16(GPTQ/AWQ) | INT4 | BF16 | 需要 | 任意 | 极限压缩显存 |
| W8A8 INT8(SmoothQuant) | INT8 | INT8(静态) | 需要 | Ampere 及以后 | 8 bit 计算加速 |
| W8A8 FP8(本文) | FP8 | FP8(动态) | 不需要 | Hopper/Ada 及以后 | 官方 8 bit 发布格式 |
FP8 W8A8 的特点是:不需要校准数据和平滑变换,在 Hopper 及更新的 GPU 上直接获得 8 bit 计算加速,所以成为模型厂商发布 8 bit 官方权重的通用格式。它的边界也很清楚:位宽停在 8 bit,如果显存预算还需要进一步压缩(单卡跑更大模型),仍要用 INT4 或 FP4 一类的低位宽方案。
总结
FP8 用的是非均匀的浮点量化网格。可表示数值之间的间距随数值大小按比例变化,小值和大值各有各的分辨率。LLM 激活大多贴近零、少数异常大,这个形状正好被浮点网格接住,所以 FP8 不需要 SmoothQuant 那类平滑变换。
权重和激活的处理方式不同。权重按块静态缩放,缩放因子随 checkpoint 一起保存;激活在推理时现算 scale。两者都不依赖校准数据,这也是厂商愿意直接发布 FP8 权重的原因。
最后是硬件前提。只有原生支持 FP8 矩阵乘的 Hopper 及更新架构,才能把 8 bit 的存储优势延伸到计算吞吐;在更早的 GPU 上,FP8 只是一层压缩存储,实际计算仍需回到 BF16。