大模型量化系列(一):LLM.int8() — 大模型量化领域的里程碑之作
论文: LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale
作者: Tim Dettmers, Mike Lewis, Younes Belkada, Luke Zettlemoyer
发表: NeurIPS 2022 | arXiv:2208.07339
30 秒速览
朴素的 INT8 量化看似能将显存减半,但在大模型上会直接崩溃。LLM.int8() 首次揭示了原因:涌现异常特征(emergent features)——极少数隐藏维度会出现远超正常范围的激活值,撑爆量化范围。通过向量量化 + 混合精度分解的方案,它在当时的超大模型上实现了零精度损失的 INT8 推理。这项工作奠定了大模型量化领域的基础,后续 SmoothQuant、GPTQ、AWQ 等方法都建立在它发现的核心问题之上。
一、传统量化手段
论文的背景部分介绍了两种基础量化策略,以及它们在粒度上的变体。
1. Absmax 量化(绝对最大值量化)
论文原文称为 Absmax quantization,是最常用的量化方式。核心思想是:找到整个张量的绝对最大值,等比例映射到 INT8 的范围 [-127, 127]。
量化公式:
$$X_{i8} = \left\lfloor \frac{127 \cdot X_{f16}}{\max_{ij}(|X_{f16,ij}|)} \right\rceil$$
其中缩放因子 $s = \frac{127}{|X_{f16}|\infty}$,反量化则是 $X{f16} \approx X_{i8} / s$。
优点: 计算简单,只需一个缩放因子,乘法运算即可还原。实际部署中最常用。
缺点: 只用一个全局缩放因子(tensor-wise),一个异常值就能把整个张量的量化精度拉低。如果数据分布不以零为中心(比如 ReLU 输出全是正数),[-127, 0] 这半段范围就完全浪费了。
2. Zeropoint 量化(零点量化)
论文原文称为 Zeropoint quantization,也就是非对称量化。它在 absmax 的基础上引入了零点偏移(zero-point) 和归一化动态范围(normalized dynamic range):
$$nd_x = \frac{2 \times 127}{\max(X) - \min(X)}$$
$$zp_x = X_{f16} \cdot \min(X)$$
$$X_{i8} = \lfloor nd_x \cdot X_{f16} \rceil$$
优点: 通过仿射变换把数据分布铺满 INT8 的全部 [-127, 127] 范围,对非对称分布的精度更高。
缺点: 矩阵乘法时需要额外处理零点偏移。在没有专用指令(如 multiply_i16)的 GPU/TPU 上,需要展开为四项加法:
$$C_{i32} = A_{i8}B_{i8} + A_{i8} \cdot zp_b + B_{i8} \cdot zp_a + zp_a \cdot zp_b$$
计算开销显著增大。因此 zeropoint 量化虽然精度更高,但在实际中反而不如 absmax 常用。
3. 量化粒度:Tensor-wise vs Row-wise
以上两种策略默认是 tensor-wise——对整个张量用一个缩放因子。论文指出,这会导致一个异常值毁掉整个张量的量化精度。
一种改进是 row-wise 量化(Khudia et al., 2021):对矩阵的每一行使用独立的缩放因子,把异常值的影响限制在单行内。
| 粒度 | 做法 | 优点 | 局限 |
|---|---|---|---|
| Tensor-wise | 整个张量一个缩放因子 | 最简单 | 一个异常值毁掉全局 |
| Row-wise | 每行一个缩放因子 | 隔离异常值影响 | 粒度仍不够细 |
那么问题来了:有没有比 row-wise 更细的粒度,能进一步压低量化误差?
二、LLM.int8() 的核心方法
关键发现:Emergent Features(涌现特征)
论文最重要的发现不是某个量化技巧,而是一个现象:当模型规模超过约 6.7B 参数时,Transformer 的隐藏状态中会出现极少数”异常特征”(outlier features)。
这些异常特征有以下特点:
- 数量极少: 在上千个隐藏维度中,只有大约 6 个维度 出现异常
- 幅度极大: 这些异常值的 magnitude 比普通值大 100 倍以上
- 高度规律: 它们在不同输入、不同层中稳定出现在相同的维度上
这意味着什么?如果用传统的标量量化,缩放因子 $S$ 会被这些异常值”撑大”,导致绝大多数正常值在量化后被压缩到几乎相同的整数——信息就这样丢失了。
解决方案第一部分:向量量化(Vector-wise Quantization)
LLM.int8() 在 row-wise 的基础上更进一步,提出了 vector-wise 量化——不再按行或按列独立量化,而是把矩阵乘法看作一系列独立的内积,每个内积都有自己的缩放因子:
- 对激活矩阵 $X$ 的每一行计算独立的缩放因子 $c_x$
- 对权重矩阵 $W$ 的每一列计算独立的缩放因子 $c_w$
反量化时,对每个输出元素只需将对应的行、列缩放因子相乘:
$$C_{f16} \approx \frac{1}{c_x \otimes c_w} \cdot X_{i8} W_{i8}$$
其中 $c_x \in \mathbb{R}^s$,$c_w \in \mathbb{R}^o$,$\otimes$ 是外积。这比 row-wise 粒度更细——row-wise 只给每行一个缩放因子,而 vector-wise 让每个内积(即输出矩阵的每个元素)都有最优的缩放组合,大幅减少了量化误差。
对于绝大部分”正常”数据,向量量化已经足够好了。
核心方法完整流程(对照原论文示意图)
下面结合原论文的流程图,完整梳理 LLM.int8() 的数据流向:

流程图分为两条并行路径,最终将结果相加得到 FP16 输出。让我们逐部分拆解。
上半部分:8-bit Vector-wise Quantization(虚线框内)
这是处理常规值(Regular values,浅蓝色) 的主路径,包含 4 个步骤:
Step (1) — 查找向量级缩放常量 $C_x$ & $C_w$
- 对激活矩阵 $X$ 的每一行求绝对最大值,得到行缩放因子向量 $C_x$。例如图中的 $C_x$ 第一行为 2,是因为该行最大值是 $\max(|2|,|-1|,|1|) = 2$。
- 对权重矩阵 $W$ 的每一列求绝对最大值,得到列缩放因子向量 $C_w$。例如图中的 $C_w$ 第一列为 1,是因为该列最大值是 $\max(|-1|,|0|,|-2|,|-1|) = 2$,缩放后对应值为 1(这里展示的是缩放因子本身)。
Step (2) — 量化
- 激活值:$X_{I8} = X \cdot (127 / C_x)$ —— 每行除以自己的缩放因子,乘 127,取整到
[-127, 127]的整数 - 权重值:$W_{I8} = W \cdot (127 / C_w)$ —— 每列除以自己的缩放因子,同理取整
Step (3) — INT8 矩阵乘法
- 用量化后的矩阵做乘法:$X_{I8} \cdot W_{I8} = \text{Out}_{I32}$
- 注意:INT8 × INT8 的乘积会溢出 8-bit,所以累积结果存为 INT32
Step (4) — 反量化
- 用外积 $C_x \otimes C_w$ 逐元素缩放 INT32 输出:$\text{Out}{F16} = \frac{\text{Out}{I32} \cdot (C_x \otimes C_w)}{127 \times 127}$
- 这里除以 $127 \times 127$ 是因为量化时乘了两次 127,反量化时需要除回来
下半部分:16-bit Decomposition(虚线框内)
这是处理异常值(Outliers,黄色) 的路径:
Step (1) — 分解异常值
- 从激活矩阵 $X$ 中挑出 magnitude 超过阈值的列(即异常维度)及其对应的权重列 $W$
- 例图中 $X$ 的第 1 列(值 45, 12, 37)和第 3 列(值 17, 63, 83)被识别为异常列
- 这些值远超同矩阵中其他元素,无法用 INT8 表示
Step (2) — FP16 矩阵乘法
- 异常部分直接用 FP16 计算:$X_{F16} \cdot W_{F16} = \text{Out}_{F16}$
- 虽然这部分不能享受量化加速,但因为异常维度占比极小(约 0.1%),额外开销可以忽略
最终融合
两条路径的输出在图中最下方的加号处汇合:
$$\text{Out}{FP16} = \underbrace{\text{Dequantize}(X{I8} \cdot W_{I8})}{\text{INT8 路径}} ;+; \underbrace{X{F16}^{\text{outlier}} \cdot W_{F16}^{\text{outlier}}}_{\text{FP16 路径}}$$
关键设计洞察:
- INT8 路径承担了 99.9% 的计算量,享受量化带来的显存减半和计算加速
- FP16 路径只处理极少量异常值,确保精度不损失
- 两者相加后得到完整的 FP16 输出,对外部调用者来说完全透明——就像从未做过量化一样
解决方案第二部分:混合精度分解(Mixed-Precision Decomposition)
但向量量化仍然搞不定那 6 个异常维度。LLM.int8() 的做法是:把它们单独拎出来,用 FP16 精确计算。
具体流程:
- 识别异常维度: 设定一个阈值(如 6.0),找出 magnitude 超过阈值的特征维度
- 拆分矩阵: 将输入按维度拆成两部分——异常部分和正常部分
- 双路计算:
- 异常维度(约 0.1%)→ FP16 矩阵乘法
- 正常维度(约 99.9%)→ INT8 矩阵乘法
- 合并结果: 将两路结果相加,得到最终的 FP16 输出
用公式表达就是:
$$Y = X_{\text{outlier}} \times W_{\text{outlier}} ;(\text{FP16}) ;+; Q(X_{\text{normal}}) \times Q(W_{\text{normal}}) ;(\text{INT8})$$
这个设计的精妙之处在于:99.9% 的计算走 INT8(省显存、速度快),只有极少数异常走 FP16(保精度)。 两全其美。
三、实验结果
论文在多个规模的模型上进行了验证,覆盖了从 125M 到 175B 参数的模型:
精度表现
- 在 OPT-175B 和 BLOOM-176B 上,LLM.int8() 与 FP16 基线相比零精度损失
- 在 HellaSwag、PIQA、Lambada、Winogrande 等基准测试上,分数差异均在统计误差范围内
- 小模型(<6.7B)本身量化就比较容易,LLM.int8() 同样表现良好
显存节省
- 推理显存需求直接减半(从 16-bit 到 8-bit)
- BLOOM-176B:从需要 8×80GB A100 → 缩减到 4×80GB A100
- OPT-175B:可以在更少的 GPU 上完成推理
速度影响
- 超大模型(≥176B)相比 FP16 慢约 15-23%(主要瓶颈在 INT8 Tensor Core 的利用率)
- 小模型(<6B)延迟增加更明显,但后续版本已优化
对比朴素量化
如果不用混合精度分解,直接做 INT8 量化会怎样?论文给出了对比:朴素 INT8 量化在 6.7B 以上模型会出现严重的性能崩塌,困惑度(perplexity)飙升到不可用的程度。而 LLM.int8() 通过隔离异常维度,完全避免了这个问题。
四、总结与思考
为什么 LLM.int8() 重要
- 首次证明大模型可以无损 INT8 推理: 在 175B 规模上零精度损失,这在之前被认为不可能
- 发现了 Emergent Features: 异常特征是理解大模型量化困难的关键洞察,后续几乎所有 LLM 量化工作(SmoothQuant、AWQ、GPTQ)都建立在这个发现之上
- 实用价值: 直接催生了
bitsandbytes库,通过 Hugging Face 集成让普通开发者也能使用
局限性
- 不支持 CPU 推理,仅限 NVIDIA GPU(需要 INT8 Tensor Core:Turing 及以上架构)
- 无法直接保存和加载 INT8 权重——每次加载都需要从 FP16 重新量化
- 对 6B 以下的小模型加速效果不明显
后续影响
LLM.int8() 打开了大模型量化的潘多拉魔盒。它提出的”异常特征”问题成为了后续研究的核心课题:
- SmoothQuant(2022):不分离异常值,而是把 activation 的异常”平滑”到 weight 中,让两边都更容易量化
- GPTQ(2023):进一步将权重量化到 4-bit 甚至 3-bit
- AWQ(2023):发现只有 1% 的”显著权重”需要保留高精度,其余可以激进压缩
回过头看,LLM.int8() 的意义不仅在于它本身的工程价值,更在于它为大模型量化这个方向指明了问题的本质——搞定 outlier,就搞定了一切。
系列预告: 下一篇我们将介绍 SmoothQuant——它如何用”搬家”的思路,把 activation 的难题转移给 weight,从而实现更优雅的 W8A8 量化。