核心直觉: 模型量化是在更少的离散数值中近似表示权重和激活,用可控的数值误差换取更低的存储、显存、带宽和计算成本。
什么是模型量化
神经网络训练完成后,权重通常使用 FP32、FP16 或 BF16 保存。模型量化(Model Quantization)则用更低位宽的数据类型近似表示权重、激活或中间计算结果。
仅考虑权重时,不同数据类型的理论存储成本如下:
| 数据类型 | 每个参数的位数 | 70 亿参数的理论权重大小 |
|---|---|---|
| FP32 | 32 bit | 约 28 GB |
| FP16 / BF16 | 16 bit | 约 14 GB |
| INT8 | 8 bit | 约 7 GB |
| INT4 | 4 bit | 约 3.5 GB |
因此,将 FP16 权重转成 INT4 后,理论权重大小会缩小到四分之一。实际模型还需要保存 scale、zero point、分组索引等元数据,所以压缩比通常略低于理论值。
量化也不只是“把小数改成整数”。它需要回答三个问题:
- 如何建立浮点数与低精度编码之间的映射;
- 如何控制舍入、截断和异常值造成的误差;
- 如何让部署硬件和推理框架真正利用低精度数据。
数据类型,数值范围和表示精度
浮点数如何组成
对于规格化浮点数,数值可以抽象为:
$$ x=(-1)^s\times 2^E\times m, $$其中符号位 $s$ 决定正负,实际指数 $E$ 决定整体数量级,尾数 $m$ 决定这个数量级上的具体比例。实际编码中保存的是带偏置的指数域,解码后才得到这里的 $E$。
关键在于:$m$ 不是任意实数,而是由有限个尾数位拼成的离散值。正因为 $m$ 只能取有限个值,同一个指数区间内才会存在固定的相邻浮点数间隔。
尾数是如何组成的
以具有 23 位尾数域的规格化 FP32 为例,其有效尾数可写成:
$$ m=(1.f)_2 =1+b_1 2^{-1}+b_2 2^{-2}+\cdots+b_{23}2^{-23}, \qquad b_i\in\{0,1\}. $$前面的 1 称为隐含位或隐藏位。规格化数的最高有效位一定是 1,因此编码时不必保存它;FP32 虽然只存储 23 个尾数位,却具有 24 位二进制有效精度。FP16 同理:存储 10 个尾数位,加上隐含的最高位 1,共有 11 位二进制有效精度。
下标 2 表示这个数采用二进制表示,并不是“乘以 2”。例如:
$$ \begin{aligned} (1.101)_2 &=1\times2^0+1\times2^{-1}+0\times2^{-2}+1\times2^{-3}\\ &=1+\frac{1}{2}+\frac{1}{8}=1.625. \end{aligned} $$因此,尾数的最后一位决定了同一指数区间内能够调整的最小步长。对具有 $p$ 个显式尾数位的规格化浮点格式,这个最小尾数增量是 $2^{-p}$。
| 类型 | 符号位 | 指数位 | 尾数位 |
|---|---|---|---|
| FP32 | 1 | 8 | 23 |
| FP16 | 1 | 5 | 10 |
| BF16 | 1 | 8 | 7 |
为什么 FP16 在 1 附近的间隔是 $2^{-10}$
FP16 和 BF16 都占 16 bit,却有不同取舍:FP16 把更多位留给尾数,BF16 把更多位留给指数。
在 $[1,2)$ 区间内,实际指数 $E=0$。FP16 的规格化数可以写成:
$$ x=2^0\times \left( 1+b_1 2^{-1}+b_2 2^{-2}+\cdots+b_{10}2^{-10} \right). $$尾数域有 10 位,最后一位的权重是 $2^{-10}$。所以 1 后面的下一个 FP16 数为:
$$ 1+2^{-10}=1.0009765625, $$相邻间隔就是:
$$ \Delta_{\mathrm{FP16}} =2^{-10} =0.0009765625. $$这里使用 $-10$ 是因为正在计算尾数最低位的权重,不是因为指数域有多少位。FP16 的 5 位指数域决定可以编码哪些数量级,并不直接决定同一数量级内的相邻间隔。若误用 $2^{-5}=0.03125$,相当于把指数位数误当成了尾数精度。
同理,BF16 有 7 个尾数位,因此在 1 附近:
$$ \Delta_{\mathrm{BF16}}=2^{-7}=0.0078125. $$浮点数的间隔也不是处处相同。对于具有 $p$ 个显式尾数位的规格化数,在 $[2^E,2^{E+1})$ 内:
$$ \operatorname{ULP}=2^{E-p}. $$所以 FP16 在 $[2,4)$ 内的间隔是 $2^{1-10}=2^{-9}$,在 $[1024,2048)$ 内的间隔则是 $2^{10-10}=1$。数值越大,相邻可表示数通常也离得越远。这个公式只描述规格化数;靠近 0 的非规格化数还需要单独讨论。
数值范围对比
| 数据类型 | 最大有限正数 | 最小正规正数 | 最小非正规正数 |
|---|---|---|---|
| FP32 | 约 $3.40\times10^{38}$ | 约 $1.18\times10^{-38}$ | 约 $1.40\times10^{-45}$ |
| FP16 | $65504$ | 约 $6.10\times10^{-5}$ | 约 $5.96\times10^{-8}$ |
| BF16 | 约 $3.39\times10^{38}$ | 约 $1.18\times10^{-38}$ | 约 $9.18\times10^{-41}$ |
| INT8 | $[-128,127]$ | — | — |
| INT4 | $[-8,7]$ | — | — |
FP32
FP32 有:
- 1 位符号;
- 8 位指数;
- 23 位尾数。
它大约能保留 7 位十进制有效数字。例如 1.234567 可以较准确地表示,而 1.23456789 通常会发生舍入。
在 1 附近,FP32 相邻两个数之间的间隔大约是:
$$ 2^{-23}\approx1.19\times10^{-7}. $$例如,1.0000000 和 1.000000119... 大致是相邻的两个 FP32 数。
FP32 的特点是:
- 范围大;
- 精度高;
- 存储和计算开销大;
- 常用于训练中的高精度累加、归一化和优化器状态。
FP16
FP16 有:
- 1 位符号;
- 5 位指数;
- 10 位尾数。
它大约能保留 3~4 位十进制有效数字。在 1 附近,相邻数的间隔为:
$$ 2^{-10}=0.0009765625. $$也就是说,FP16 在 1 附近大致能够区分:
1.0000
1.0009765625
1.001953125
但不能准确表示它们之间的所有数。
FP16 最大有限值只有:
$$ 65504. $$因此 FP16 的主要问题是动态范围比较小,训练时比较容易:
- 上溢出为 infinity;
- 下溢出为 0;
- 丢失小梯度。
BF16
BF16 是 Brain Floating Point 16。它有:
- 1 位符号;
- 8 位指数;
- 7 位尾数。
BF16 的指数位和 FP32 一样,所以它的数值范围与 FP32 基本相同,但精度低得多。在 1 附近,BF16 的间隔为:
$$ 2^{-7}=0.0078125. $$也就是说,相邻值大致是:
1.0000000
1.0078125
1.0156250
BF16 大约只能保留 2~3 位十进制有效数字。
FP16 和 BF16 的核心区别是:
| 对比项 | FP16 | BF16 |
|---|---|---|
| 尾数精度 | 较高 | 较低 |
| 数值范围 | 较小 | 很大,接近 FP32 |
| 训练稳定性 | 较容易溢出 | 通常更稳定 |
| 1 附近的间隔 | 约 $0.00098$ | 约 $0.00781$ |
可以理解为:FP16 用更多位表示“精细程度”,BF16 用更多位表示“数值大小范围”。
INT8
有符号 INT8 的原生范围是:
$$ [-128,127]. $$共有:
$$ 2^8=256 $$个整数编码。INT8 对整数是精确表示的,相邻整数之间固定相差 1。
但模型量化中并不是说原始权重只能位于 $[-128,127]$,而是通过 scale 把一个浮点范围映射到这 256 个整数上。例如:
$$ s=0.01. $$那么:
q = 1 → 0.01
q = 2 → 0.02
q = 100 → 1.00
此时量化后的浮点表示范围大约是:
$$ [-1.28,1.27], $$相邻浮点量化值之间的间隔是 0.01。
工程中的对称 INT8 量化经常只使用:
$$ [-127,127], $$而不使用 $-128$,这样正负两侧可以保持完全对称。
INT4
有符号 INT4 的原生范围是:
$$ [-8,7]. $$共有:
$$ 2^4=16 $$个整数编码。对称量化中也经常使用 $[-7,7]$,即 15 个对称取值。
假设 scale 是:
$$ s=0.1, $$那么:
q = -7 → -0.7
q = -1 → -0.1
q = 0 → 0
q = 1 → 0.1
q = 7 → 0.7
INT4 只有 16 个编码,因此在相同浮点范围下,它的量化步长远大于 INT8,精度也更低。
量化的基本数学原理
浮点数到整数的映射
假设原始浮点数为 $x$,量化后的整数为 $q$。最常见的线性量化公式是:
$$ q=\operatorname{clamp}\left( \operatorname{round}\left(\frac{x}{s}\right)+z,\, q_{\min},q_{\max} \right). $$其中:
- $s$:scale,缩放因子;
- $z$:zero point,零点;
- $q_{\min}$、$q_{\max}$:整数类型能够表示的最小值和最大值;
- $\operatorname{round}$:把结果舍入到整数;
- $\operatorname{clamp}$:把结果限制在整数可表示范围内。
反量化公式是:
$$ \hat{x}=s(q-z). $$其中 $\hat{x}$ 是从量化整数恢复出的近似浮点数。需要注意:
$$ \hat{x}\ne x. $$两者之间的差值就是量化误差:
$$ e=x-\hat{x}. $$为什么要有反量化
量化后的 $q$ 不是原始物理数值,而更像是一个离散编号。例如:
$$ q=5. $$单独看这个 5,并不知道它代表 $5.0$、$0.5$、$0.05$ 还是 $50$。只有结合 scale 和 zero point,才能知道它代表的浮点值。反量化公式仍然是:
$$ \hat{x}=s(q-z). $$例如:
$$ q=5,\qquad s=0.1,\qquad z=0, $$则:
$$ \hat{x}=0.1\times(5-0)=0.5. $$因此,这里的整数编码 $q=5$,实际代表的近似浮点值是 $0.5$。
为什么计算中需要反量化
假设权重被保存成 INT4,但激活仍然是 FP16,原始矩阵乘法为:
$$ Y=XW, $$其中:
- $X$:FP16 激活;
- $W$:INT4 量化权重。
INT4 权重实际表示的是:
$$ W\approx s_wQ_w. $$所以计算变成:
$$ Y\approx X(s_wQ_w). $$硬件需要处理 scale,才能把整数权重解释为正确的数值。实际的高性能 kernel 通常不会先在显存中生成一份完整的 FP16 权重,而是执行:
读取打包的 INT4 权重
↓
解包 INT4
↓
读取对应的 scale
↓
在寄存器或共享内存中恢复近似值
↓
执行矩阵乘法
这也属于广义的反量化过程。
对于 W8A8 整数矩阵乘法,可以先执行:
$$ Q_xQ_w, $$得到 INT32 累加结果,然后在输出阶段乘以:
$$ s_xs_w, $$恢复到实际数值尺度。
scale 和 zero point 分别是什么
Scale:决定整数每一格代表多大的浮点范围。
假设需要用 INT8 表示浮点范围:
$$ [-1.27,1.27]. $$INT8 的有符号整数范围是:
$$ [-128,127]. $$可以设置:
$$ s=0.01. $$这意味着整数每增加 1,对应浮点数增加 0.01:
| 整数 $q$ | 反量化结果 |
|---|---|
| $-100$ | $-1.00$ |
| $-50$ | $-0.50$ |
| $0$ | $0.00$ |
| $50$ | $0.50$ |
| $100$ | $1.00$ |
例如原始数据为:
$$ x=0.537. $$量化为:
$$ q=\operatorname{round}\left(\frac{0.537}{0.01}\right)=54. $$反量化结果为:
$$ \hat{x}=54\times0.01=0.54. $$量化误差为:
$$ e=0.537-0.54=-0.003. $$因此,scale 决定了量化的分辨率。scale 越小,整数的一格越细,精度越高;但同时可覆盖的浮点范围会越小,更容易发生溢出或截断。
Zero point:指定浮点数 0 对应哪个整数。
在一些量化方式中,浮点数 0 不一定映射到整数 0,因此需要 zero point。例如使用无符号 INT8:
$$ q\in[0,255], $$而浮点数范围为:
$$ x\in[-1,3]. $$因为整数只能表示非负数,所以需要把整个区间平移。缩放因子为:
$$ s=\frac{x_{\max}-x_{\min}}{q_{\max}-q_{\min}} =\frac{3-(-1)}{255} =\frac{4}{255}. $$zero point 可以近似计算为:
$$ z=q_{\min}-\frac{x_{\min}}{s} =0-\frac{-1}{4/255} =63.75 \approx64. $$此时:
| 浮点数 | 整数编码 |
|---|---|
| $-1$ | $0$ |
| $0$ | $64$ |
| $3$ | $255$ |
因此,zero point 的作用是平移量化区间,使浮点数 0 能够被准确表示。
量化步长
量化步长通常就是 scale:
$$ \Delta=s. $$它表示相邻两个反量化值之间的距离。假设:
$$ s=0.1, $$那么可表示的值是:
... -0.3、-0.2、-0.1、0、0.1、0.2、0.3 ...
相邻值相差 0.1,所以量化步长是 0.1。
对称量化和非对称量化
根据 zero point 是否为 0,线性量化通常分为两类。
对称量化
对称量化将浮点数范围设置成关于 0 对称的区间:
$$ [-\alpha,\alpha]. $$通常令:
$$ z=0. $$量化公式简化为:
$$ q=\operatorname{clamp}\left( \operatorname{round}\left(\frac{x}{s}\right),q_{\min},q_{\max} \right). $$例如,对称 INT8 量化常使用:
$$ q\in[-127,127]. $$scale 为:
$$ s=\frac{\max(|x_{\min}|,|x_{\max}|)}{127}. $$假设一组权重范围是:
$$ [-1.0,0.6]. $$为了保证所有数据都能表示,需要将范围扩展成:
$$ [-1.0,1.0]. $$此时正数部分的 $0.6\sim1.0$ 区间实际上没有被利用。
对称量化的优势是:
- 计算简单;
- zero point 为 0;
- 更适合矩阵乘法;
- 硬件实现效率通常更高;
- 常用于权重量化。
缺点是:当数据分布明显不对称时,可能浪费整数表示范围。
非对称量化
非对称量化直接覆盖实际范围:
$$ [x_{\min},x_{\max}]. $$scale 为:
$$ s=\frac{x_{\max}-x_{\min}}{q_{\max}-q_{\min}}. $$zero point 通常不为 0。例如数据范围是:
$$ [-1.0,0.6]. $$非对称量化会完整利用整数空间来表示这个范围,而不必扩展到 $[-1,1]$。
优势是:
- 对偏斜的数据分布利用率更高;
- 通常可以获得更小的量化误差;
- 常用于激活值量化。
缺点是:
- 计算中需要处理 zero point;
- 矩阵乘法实现更复杂;
- 某些硬件上的执行效率不如对称量化。
“权重用对称量化、激活用非对称量化”是常见经验,不是普遍定律。实际选择还取决于数据分布、算子实现和硬件后端。
量化如何用于矩阵乘法
神经网络的大部分计算可以归结为:
$$ Y=XW. $$其中 $X$ 是输入或激活值,$W$ 是模型权重,$Y$ 是输出。
假设原始激活矩阵是:
$$ X= \begin{bmatrix} 0.1&0.3\\ -0.5&0.8 \end{bmatrix}. $$对它执行对称 INT8 量化。如果选择:
$$ s_x=0.01, $$那么:
$$ Q_x=\operatorname{round}\left(\frac{X}{s_x}\right)= \begin{bmatrix} 10&30\\ -50&80 \end{bmatrix}. $$于是:
$$ X\approx s_xQ_x \approx0.01 \begin{bmatrix} 10&30\\ -50&80 \end{bmatrix}. $$对称量化后,激活和权重可近似写成:
$$ X\approx s_xQ_x, \qquad W\approx s_wQ_w. $$代入矩阵乘法:
$$ Y\approx(s_xQ_x)(s_wQ_w) =s_xs_w(Q_xQ_w). $$$Q_xQ_w$ 可以由低精度整数矩阵乘法完成。INT8 乘法通常使用 INT32 累加,因为单个最大正数乘积已经达到:
$$ 127\times127=16129, $$矩阵乘法还要累加大量乘积,INT8 无法容纳结果。典型 W8A8 数据流可以概括为:
FP16 / BF16 激活
↓ 量化
INT8 激活 × INT8 权重
↓
INT32 累加
↓ 乘输出 scale
FP16 / BF16 或量化输出
权重 INT4、激活 FP16 的 weight-only 方案则通常在高性能 kernel 内解包权重、读取分组 scale,并在寄存器或共享内存中完成反量化与乘法融合,而不是先在显存中生成完整的 FP16 权重副本。
量化本质
量化本质上是一个离散化过程。
从几何角度理解,量化相当于在数轴上设置一系列固定的离散点。例如 scale 为 0.1 时,可表示的值包括:
... -0.3、-0.2、-0.1、0、0.1、0.2、0.3 ...
原始浮点数会被映射到距离最近的离散点:
0.14 → 0.1
0.17 → 0.2
0.26 → 0.3
这相当于把连续空间切分为多个区间:
[0.05, 0.15) → 0.1
[0.15, 0.25) → 0.2
[0.25, 0.35) → 0.3
每个整数值代表一个量化桶,也可以称为 quantization bin。位宽越低,可使用的桶越少:
- INT8:约 256 个取值;
- INT4:16 个取值;
- INT2:4 个取值。
因此,位宽越低,量化越激进,同一个桶需要覆盖更大的浮点范围,量化误差通常也越大。
一个完整的 INT8 量化示例
假设权重为:
$$ W=[-1.0,-0.5,0,0.3,0.8]. $$最大绝对值 $\alpha=1.0$,因此:
确定量化范围
最大绝对值为:
$$ \alpha=\max(|W|)=1.0. $$INT8 使用:
$$ [-127,127]. $$因此:
$$ s=\frac{1.0}{127}\approx0.007874. $$执行量化
使用:
$$ q=\operatorname{round}\left(\frac{x}{s}\right). $$量化和反量化结果为:
| 原始值 $x$ | 量化值 $q$ | 反量化值 $\hat{x}$ | $x-\hat{x}$ |
|---|---|---|---|
| -1.0 | -127 | -1.0000 | 0.0000 |
| -0.5 | -64 | -0.5039 | 0.0039 |
| 0 | 0 | 0 | 0 |
| 0.3 | 38 | 0.2992 | 0.0008 |
| 0.8 | 102 | 0.8031 | -0.0031 |
量化后的整数权重为:
$$ Q_W=[-127,-64,0,38,102]. $$反量化
使用:
$$ \hat{x}=sq, $$得到:
$$ \hat{W}\approx[-1.0000,-0.5039,0,0.2992,0.8031]. $$下面的 Python 代码复现了这个过程:
from __future__ import annotations
import numpy as np
def symmetric_int8_quantize(
values: np.ndarray,
) -> tuple[np.ndarray, float]:
values = values.astype(np.float32)
max_abs = float(np.max(np.abs(values)))
if max_abs == 0.0:
return np.zeros_like(values, dtype=np.int8), 1.0
scale = max_abs / 127.0
quantized = np.clip(
np.rint(values / scale),
-127,
127,
).astype(np.int8)
return quantized, scale
weights = np.array([-1.0, -0.5, 0.0, 0.3, 0.8])
quantized, scale = symmetric_int8_quantize(weights)
restored = quantized.astype(np.float32) * scale
print(f"scale: {scale:.8f}")
print("quantized:", quantized.tolist())
print("restored:", np.round(restored, 4).tolist())
print("error:", np.round(weights - restored, 4).tolist())
这里使用 np.rint 表示舍入到最近整数。具体后端可能采用 ties-to-even 等明确的舍入规则,比较不同框架时需要确认这一细节。
量化误差从何而来
舍入误差
原始值不能恰好落在量化网格上。例如可表示值为:
0.0、0.1、0.2、0.3
而原始值是 0.17,只能近似映射为 0.2。误差为:
$$ 0.17-0.2=-0.03. $$截断误差
如果原始值超出了量化范围,会被 clamp 到边界。例如量化范围是 $[-1,1]$,但原始值为 1.5,则只能量化为最大值对应的 1.0:
$$ 1.5\longrightarrow1.0. $$误差达到 0.5。这种情况也被称为 clipping 或 saturation。
异常值导致量化范围过大
假设大多数激活值位于 $[-1,1]$,但存在少数异常值 $10$ 和 $-12$。如果按照绝对最大值确定量化范围,就需要覆盖:
$$ [-12,12]. $$对于 INT8,量化步长约为:
$$ s=\frac{12}{127}\approx0.0945. $$这意味着大量位于 $[-1,1]$ 的普通数值只能使用很少的整数档位表示,精度明显下降。
如果忽略异常值,将量化范围限制在 $[-1,1]$,普通数值的精度会提高,但异常值会被截断。
因此,量化范围选择本质上是在平衡两种误差:
- 范围过大:舍入误差增大;
- 范围过小:截断误差增大。
误差在网络层间传播
单个权重的量化误差可能很小,但神经网络包含很多层。例如:
$$ \begin{aligned} h_1&=f(W_1x),\\ h_2&=f(W_2h_1),\\ h_3&=f(W_3h_2), \end{aligned} $$当 $W_1$ 和 $h_1$ 发生量化误差后,误差会进入下一层,并可能逐步传播或放大。
因此,评估量化不能只比较单个权重的误差,还要评估:
- 单层输出误差;
- 隐藏状态差异;
- logits 差异;
- 最终任务精度;
- 生成文本质量。
低位宽量化通常还需要更细的粒度,例如 per-channel、per-group 或 per-block scale,避免一个异常值控制过大的张量范围。
为什么量化能加速推理
降低显存占用
以 FP16 转 INT4 为例:
$$ 16\ \mathrm{bit}\longrightarrow4\ \mathrm{bit}. $$理论权重大小缩小到四分之一。这意味着:
- 更大的模型可以放入单卡;
- 可以增加并发数;
- 可以增加 KV Cache 容量;
- 可以减少多卡切分。
降低显存带宽压力
大模型推理,尤其是 decode 阶段,经常是 memory-bound,而不是 compute-bound。每生成一个 token,都需要从显存读取大量模型权重。
一个 FP16 权重需要读取 2 字节,而 INT4 权重只需要读取 0.5 字节。理论上,权重读取量下降到四分之一。因此,即使需要额外进行反量化,减少的显存访问成本仍可能带来明显加速。
使用低精度硬件计算单元
现代 GPU、NPU 和 CPU 通常具有针对低精度数据的专用计算单元,例如:
- INT8 Tensor Core;
- INT4 Tensor Core;
- FP8 Tensor Core;
- CPU VNNI、AMX;
- NPU 低精度矩阵乘单元。
相同面积和功耗下,低位宽计算通常可以实现更高吞吐。
但要注意:模型变小,不代表推理一定会按位宽比例加速。例如 FP16 转 INT4 后,模型大小理论上缩小 4 倍,但实际性能不一定提升 4 倍,因为还存在:
- 反量化开销;
- scale 读取开销;
- 数据解包开销;
- 算子启动开销;
- 非矩阵乘法算子;
- KV Cache 和激活开销;
- 硬件是否原生支持 INT4;
- batch size 和序列长度的影响。
静态量化和动态量化
静态量化会提前确定量化参数,例如 scale 和 zero point。通常需要准备一批校准数据,运行模型并统计激活范围:
校准数据
↓
收集各层激活分布
↓
确定 scale / zero point
↓
保存量化模型
推理时直接使用预先计算好的量化参数。
优势:
- 推理额外开销较小;
- 更容易使用高性能量化算子;
- 量化参数固定。
缺点:
- 强依赖校准数据;
- 校准数据和线上数据分布不一致时,精度可能下降;
- 难以处理输入相关的动态异常值。
为什么需要校准数据
模型权重是固定的,因此可以直接观察权重范围。但激活值依赖输入。例如同一层,在不同输入下可能出现:
输入 A:激活范围 [-1, 1]
输入 B:激活范围 [-4, 5]
输入 C:激活范围 [-20, 18]
在实际推理前,并不知道典型激活范围是多少。静态量化又要求推理前提前确定 scale,因此需要用一批代表性输入运行模型,观察激活分布。这批输入就是校准数据。
校准过程
基本过程是:
准备代表性数据
↓
用浮点模型执行前向传播
↓
记录每一层激活
↓
统计 min、max、直方图、百分位数
↓
选择 clipping 范围
↓
计算 scale 和 zero point
例如某一层采集到的激活范围是:
$$ [-8.2,7.5]. $$最简单的方法是使用:
$$ a_{\max}=8.2. $$那么对称 INT8 scale 为:
$$ s=\frac{8.2}{127}\approx0.0646. $$推理时,这一层就固定使用这个 scale。
动态量化
动态量化在推理过程中,根据当前输入实时计算 scale。例如,对每个 token 的激活向量动态计算:
$$ s=\frac{\max(|x|)}{127}. $$优势:
- 能适应不同输入;
- 对激活分布变化更稳健;
- 通常不需要复杂校准。
缺点:
- 需要实时计算最大值;
- 需要执行归约操作;
- 引入额外 kernel 和同步开销;
- 小 batch 场景下开销可能更明显。
权重量化通常是静态的,因为模型权重固定;激活量化则可以是静态或动态的。
如何衡量量化效果的好坏
量化评估不能只看一个指标,至少应该分为五个层次:
| 层次 | 关注内容 | 常见指标 |
|---|---|---|
| 张量数值 | 量化前后的近似程度 | MSE、MAE、Relative L2、SQNR |
| 层输出 | 误差在哪些层被放大 | Output MSE、Cosine Similarity |
| 输出分布 | token 决策是否改变 | Logits MSE、KL 散度、Top-k 重合率 |
| 模型能力 | 最终任务是否退化 | PPL、Accuracy、F1、pass@1 |
| 系统收益 | 部署是否真正获益 | 显存、TTFT、TPOT、吞吐、功耗 |
第一层:张量数值误差
均方误差 MSE 为:
$$ \operatorname{MSE} =\frac{1}{N}\sum_{i=1}^{N}(x_i-\hat{x}_i)^2, $$MSE 越小,说明恢复值与原值越接近,但它对大数值误差比较敏感。
平均绝对误差 MAE 为:
$$ \operatorname{MAE} =\frac{1}{N}\sum_{i=1}^{N}|x_i-\hat{x}_i|. $$MAE 表示平均绝对误差,相比 MSE 不会过度放大少量大误差。
相对 L2 误差为:
$$ \operatorname{RelativeL2} =\frac{\|X-\hat{X}\|_2}{\|X\|_2}. $$相比直接看 MSE,它考虑了张量自身的数值尺度。
余弦相似度为:
$$ \cos(X,\hat{X}) =\frac{X\cdot\hat{X}}{\|X\|_2\|\hat{X}\|_2}. $$它主要衡量方向是否一致。对于隐藏状态,余弦相似度通常比绝对值误差更直观。
信号量化噪声比 SQNR 为:
$$ \operatorname{SQNR} =10\log_{10}\left( \frac{\sum_i x_i^2} {\sum_i(x_i-\hat{x}_i)^2} \right). $$单位是 dB。SQNR 越高,说明量化噪声相对于原始信号越小。
第二层:层输出误差
仅比较权重通常不够,还应比较每一层输出。原始层输出与量化层输出分别为:
$$ Y=XW, \qquad \hat{Y}=X\hat{W}. $$可以测量:
- Layer output MSE;
- Relative L2;
- Cosine similarity;
- 每层最大误差;
- 不同 token 的误差分布。
这样可以定位量化敏感层。例如:
第 1~10 层 cosine > 0.999
第 11 层 cosine = 0.993
第 12 层 cosine = 0.94
第 12 层很可能需要:
- 更高位宽;
- 更小 group size;
- 更好的 clipping;
- 保留 FP16。
第三层:logits 和概率分布误差
对于语言模型,可以比较 FP16 模型和量化模型输出的 logits。
Logits MSE:
$$ \operatorname{MSE}(L,\hat{L}). $$KL 散度: 假设原模型概率分布为 $P$,量化模型概率分布为 $Q$:
$$ D_{\mathrm{KL}}(P\|Q) =\sum_i P_i\log\frac{P_i}{Q_i}. $$KL 越小,表示两个 token 概率分布越接近。
Top-1 一致率: 统计原模型和量化模型 argmax token 相同的比例。例如:
FP16 top-1 token:北京
INT4 top-1 token:上海
即使两个 logits 差值不大,也可能改变贪心解码结果。还可以观察:
- Top-5 overlap;
- token rank correlation;
- greedy token agreement。
第四层:模型任务效果
这是最重要的一层。
语言模型常使用困惑度 PPL:
$$ \operatorname{PPL} =\exp\left( -\frac{1}{N}\sum_{i=1}^{N}\log p(x_i\mid x_{\lt i}) \right). $$一般来说,PPL 越低越好。例如:
| 模型 | PPL |
|---|---|
| FP16 | 6.20 |
| INT8 | 6.23 |
| INT4 | 6.58 |
不要只看绝对值,也要看相对变化:
$$ \Delta\operatorname{PPL} =\operatorname{PPL}_{\mathrm{quant}} -\operatorname{PPL}_{\mathrm{fp16}}. $$下游任务
根据模型用途评估:
- 分类:Accuracy、F1;
- 阅读理解:EM、F1;
- 翻译:BLEU、COMET;
- 摘要:ROUGE;
- 代码:pass@1;
- 数学:最终答案准确率;
- 大模型综合能力:MMLU、CMMLU、C-Eval、GSM8K 等。
量化模型应与原始模型使用相同的:
- prompt;
- tokenizer;
- generation 参数;
- 数据集;
- 评测脚本。
生成质量评估
开放式生成还需要评估:
- 指令遵循;
- 事实性;
- 连贯性;
- 重复;
- 格式遵循;
- 长文本稳定性;
- 代码可执行性;
- 数学推理正确性。
可以采用:
- 人工 pairwise 比较;
- 规则评测;
- LLM-as-a-judge;
- 原模型与量化模型输出一致率。
不过 LLM-as-a-judge 本身存在偏差,不能单独使用。
第五层:系统性能
量化不是只为了精度,还要真正获得系统收益。
模型体积
需要比较:
FP16 权重大小
INT8 权重大小
INT4 权重大小
注意 scale、zero point、索引等元数据也占空间,因此实际压缩比小于理论值。
显存占用
需要分别测量:
- 模型权重显存;
- KV Cache;
- 临时 workspace;
- 峰值显存;
- 不同 batch 下的显存。
延迟
大模型推理一般关注:
- TTFT:首 token 延迟;
- TPOT:每个输出 token 的时间;
- ITL:token 间延迟;
- 端到端 latency。
吞吐
常见指标包括:
- output tokens/s;
- total tokens/s;
- requests/s;
- 不同并发下的吞吐。
性能收益是否真实
一个 INT4 模型如果显存下降 70%,但吞吐只提升 3%,可能说明:
- kernel 没有优化;
- 反量化开销过高;
- 当前场景不是 memory-bound;
- 非量化算子占比高;
- batch 或矩阵形状不适合 kernel。
PTQ 和 QAT
PTQ:训练后量化
PTQ 即 Post-Training Quantization。流程是:
训练好的浮点模型
↓
收集校准数据
↓
分析权重和激活分布
↓
计算量化参数
↓
生成量化模型
不需要重新进行完整训练。
优势:
- 成本低;
- 使用方便;
- 适合已有大模型;
- 是当前大模型量化的主要路线。
缺点:
- 低位宽下精度损失可能较明显;
- 对异常值和敏感层处理要求较高。
常见的大模型 PTQ 方法包括权重重构、激活平滑、异常值保护和误差补偿等。
QAT:量化感知训练
QAT 即 Quantization-Aware Training。训练过程中模拟量化和反量化:
$$ x\longrightarrow Q(x)\longrightarrow D(Q(x))\longrightarrow\hat{x}. $$前向传播中,模型看到的是带有量化误差的数值;反向传播则更新浮点权重,使模型逐渐适应量化误差。
常使用伪量化:
FP32 权重
↓ 模拟量化
INT8 数值
↓ 立即反量化
FP32 近似值
↓
继续训练
这里通常并不会真的使用整数执行训练,而是在浮点计算中模拟量化行为。
优势:
- 模型能够主动适应量化误差;
- 低位宽下通常比 PTQ 精度更好;
- 更适合 INT4、INT2 等激进量化。
缺点:
- 需要训练数据;
- 训练成本较高;
- 大模型训练资源要求高;
- 训练流程和分布式系统更复杂。
线性量化并不是唯一方法
前文属于均匀线性量化。它要求相邻整数之间对应固定的浮点间隔:
$$ \hat{x}_{q+1}-\hat{x}_q=s. $$除此之外,还有非均匀量化。例如构造一个码本:
Code 0 → -1.0
Code 1 → -0.4
Code 2 → -0.1
Code 3 → 0.0
Code 4 → 0.1
Code 5 → 0.4
Code 6 → 1.0
数值映射到距离最近的码本元素。
这种方法适合数据分布不均匀的情况。例如权重大量集中在 0 附近,就可以在 0 附近设置更密集的离散值。常见思想包括:
- codebook quantization;
- k-means quantization;
- NormalFloat;
- logarithmic quantization;
- vector quantization。
但其硬件实现通常比线性整数矩阵乘法更复杂。
常见误区
- INT4 模型一定比 FP16 快 4 倍。 位宽只决定理论数据量,真实速度还受硬件、kernel、解包和工作负载限制。
- 反量化会还原原始值。 反量化只能恢复量化网格上的近似值,舍入和截断造成的信息已经丢失。
- 量化只影响权重。 激活、KV Cache 和中间累加也可以采用不同精度;W4A16、W8A8 等命名正是在区分权重与激活位宽。
- 量化误差小,模型质量就一定不变。 小的局部误差可能跨层放大,也可能改变 logits 排名和最终生成结果。
- 低精度数据都属于整数线性量化。 FP8、NormalFloat、码本与向量量化有不同的表示和计算机制,不能套用同一组公式。
总结
模型量化的本质是离散化:用有限编码近似连续浮点值。scale 决定网格间距,zero point 决定网格原点;对称量化实现简单,非对称量化更充分地利用偏斜范围。进入矩阵乘法后,低精度整数乘法通常配合更高精度累加,并在输出阶段恢复数值尺度。
量化是否成功,最终取决于两个条件:模型质量损失是否可接受,以及目标硬件上的显存、延迟和吞吐是否真的改善。只有把数值误差、模型能力和系统性能放在一起评估,才有意义。