论文: 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() 的数据流向:

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 精确计算。

具体流程:

  1. 识别异常维度: 设定一个阈值(如 6.0),找出 magnitude 超过阈值的特征维度
  2. 拆分矩阵: 将输入按维度拆成两部分——异常部分和正常部分
  3. 双路计算:
    • 异常维度(约 0.1%)→ FP16 矩阵乘法
    • 正常维度(约 99.9%)→ INT8 矩阵乘法
  4. 合并结果: 将两路结果相加,得到最终的 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() 重要

  1. 首次证明大模型可以无损 INT8 推理: 在 175B 规模上零精度损失,这在之前被认为不可能
  2. 发现了 Emergent Features: 异常特征是理解大模型量化困难的关键洞察,后续几乎所有 LLM 量化工作(SmoothQuant、AWQ、GPTQ)都建立在这个发现之上
  3. 实用价值: 直接催生了 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 量化。