第 20 讲 · 模型量化(二):GPTQ · Model Quantization II: GPTQ

Large Language Model Systems · Lecture 20。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 20 · Mon 3/30 · Model Quantization II(slides:llmsys-20-quantization2.pdf,38 页)。 阅读材料:Frantar et al. (2023) GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers(ICLR);Hassibi & Stork (1993) Optimal Brain Surgeon;Frantar & Alistarh (2022) Optimal Brain Compression(OBQ);Xiao et al. (2023) SmoothQuant;Lin et al. (2024) AWQ。 依据说明:slides 覆盖:基础量化方法的回顾、GPTQ 的目标与两个关键想法、算法概览(Hessian 逆的 Cholesky 分解、逐块量化与补偿、惰性更新)、OBS、OBQ(逐行分解、高效求逆、固定顺序的列更新、任意顺序的观察)、惰性批量更新、Cholesky 预计算、实验(校准数据、推理加速、与 RTN 的比较、量化耗时、2 位、小模型上与精确方法的比较)、GPTQ-for-LLaMa 的代码走读(初始化、Hessian 的累加、惰性批量更新、Cholesky 重写)、AutoGPTQ 的用法、总结与局限。SmoothQuant 与 AWQ 在本课的课程大纲里列为比较对象,slides 没有展开,取自原论文。本讲义的从零实现(RTN、OBS、OBQ、朴素与真正的 GPTQ)、所有在城市语言模型上的实验、两个权重的几何例子、阻尼与校准数据的实验、简化版 AWQ、INT4 打包与 GEMV,都是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:code/lecture20/(5 个脚本 + gptq.py;复用第 19 讲的模型;每个 1–45 秒) 预计阅读时间:3 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 把逐层目标 ‖WX − ŴX‖² 写成每行一个二次型 eᵀHe/2,解释 H = 2XXᵀ 的含义,以及为什么”就近取整”不是最优的。
  2. 推导 OBS 的补偿公式,说清楚 OBQ 怎样把它用于量化、为什么它慢。
  3. 说出 GPTQ 的三个改进——固定顺序、惰性批量更新、Cholesky 分解——各自解决了什么,为什么它们不改变结果。
  4. 从零实现 GPTQ,在一个真正训练过的模型上与 RTN、第 19 讲的方法比较;理解分组、act-order、阻尼、校准数据的作用。
  5. 比较 GPTQ、AWQ、SmoothQuant 的思路;解释量化后的模型为什么能变快、为什么必须有融合的内核。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
量化的网格、粒度、RTN、逐层目标、离群特征(第 19 讲)量化的 KV 缓存、推理服务里的量化模型(第 22–27 讲)
线性代数:二次型、矩阵的逆、Cholesky 分解QLoRA(4 位基座 + LoRA,第 23 讲)
解码是访存受限的(第 4 讲、第 9 讲)剪枝(SparseGPT 用同一套数学)

0.3 本讲地图 Map

§1  逐层目标与 Hessian:为什么就近取整不是最优                                    ← 代码 01
§2  OBS 与 OBQ:一次动一个权重、补偿其余的;顺序重要吗                              ← 代码 02
§3  GPTQ:固定顺序、惰性批量更新、Cholesky;阻尼与校准数据                          ← 代码 03
§4  量化整个模型:与 RTN、与第 19 讲的方法比较;分组与 act-order                   ← 代码 04
§5  GPTQ 之外:SmoothQuant、AWQ                                                    ← 代码 05
§6  量化之后的推理:打包、反量化、为什么能变快                                       ← 代码 05
§7 小结   §8 常见误区   §9 练习   §10 延伸阅读   §11 下一讲预告

1. 逐层目标与 Hessian · The layer-wise objective

1.1 拆成每行一个二次型

第 19 讲的逐层目标(slides 第 6 页):对一个线性层,找量化后的 Ŵ 使

∥WX−W^X∥22\| WX - \hat{W}X \|_2^2

最小(X 是这一层在校准数据上的输入)。看 Ŵ 的一行 ŵ(一个输出通道),记误差 e = w − ŵ,这一行的输出误差平方和是 Σ_t (x_t · e)² = eᵀ(XXᵀ)e。所以

∥WX−W^X∥22=∑行12e⊤He,H=2XX⊤\| WX - \hat{W}X \|_2^2 = \sum_{\text{行}} \frac{1}{2} e^\top H e,\qquad H = 2XX^\top

01 号脚本第 1 部分在数值上核对了这个等式(两边都是 51.7014775723)。两个重要的推论:

  • 一个层拆成了”输出数”个互不相关的小问题——每行各管各的;
  • 它们共用同一个 H——H 只取决于输入 X,与权重无关。这是 OBQ、GPTQ 能高效的根本原因。

RTN 只看 eᵀe(每个权重离网格多近),等于假设 H 是单位矩阵:每个输入维一样大、互不相关。真实的输入远不是这样。

1.2 两个权重的例子

01 号脚本第 2 部分:两个权重 w = (0.30, 0.30),两个输入的相关系数 0.9,网格间距 0.5。

做法量化后欧氏距离 ‖w − q‖输出误差 eᵀHe/2
RTN:各自就近取整(0.5, 0.5)0.2830.1487
先量化 w₁、补偿 w₂(0.30 → 0.119)再取整(0.5, 0.0)0.3610.0213
在网格上穷举的最优(0.5, 0.0)0.3610.0213

RTN 选了离原权重最近的格点,输出误差却是最优解的 7 倍。原因是 H 的形状:两个输入高度相关,H = [[1.97, 1.76], [1.76, 1.94]],它的等高线是沿 (1, −1) 方向拉长的椭圆。RTN 的两个误差同号(都是 −0.2),正好落在最”贵”的 (1, 1) 方向;最优解让误差一正一负——x₁e₁ + x₂e₂ 里两项相互抵消。RTN 用圆形的欧氏距离去量一个椭圆形的目标。

第二行是本讲的核心思想:量化一个权重之后,把它的误差按 H 的方向分摊给还没量化的权重,再量化下一个。一步补偿就找到了最优解。

1.3 真实的 H

01 号脚本第 3 部分看城市语言模型几个层的 H(128 条校准序列):

层输入维X 的秩diag(H) 最大 / 最小RTN(INT3)GPTQ(INT3)
blocks.0.attn.q1288041.6260.212
blocks.0.ffn.w112812897.2960.684
blocks.0.ffn.w23843841,791,650281.040.8
blocks.3.ffn.w2384384115,493,17687312773
  • diag(H) 是各输入维平方的平均(乘 2):ffn.w2 的输入维之间差了上百万倍(第 19 讲:w2 的输入大到几百)。对应列的权重误差,有的几乎不影响输出,有的被放大上万倍。
  • 第 1 层 q 的输入秩只有 80:它是 80 个 token 嵌入经过 RMSNorm 的结果,最多 80 种不同的向量。H 是奇异的,H⁻¹ 不存在——要加阻尼(§3.5)。奇异也意味着有些方向上怎么改权重都不影响输出,正好用来吸收误差。
  • GPTQ(同样的 3 位网格)把这些层的逐层误差降低了 3.1–10.7 倍。

2. OBS 与 OBQ · Optimal Brain Surgeon & Quantizer

2.1 OBS:剪掉一个权重

最优脑外科(Hassibi & Stork 1993,slides 第 15 页)回答的是剪枝的问题:在一行权重里剪掉一个(置 0),使目标增加最少,其余权重怎样补偿?对二次的目标,答案是闭式的:

q=arg⁡min⁡qwq2[H−1]qq,δ=−wq[H−1]qq⋅H:,q−1q = \arg\min_q \frac{w_q^2}{[H^{-1}]_{qq}},\qquad \delta = -\frac{w_q}{[H^{-1}]_{qq}} \cdot H^{-1}_{:,q}

误差的增加量正好是 w_q² / (2[H⁻¹]_qq)。02 号脚本第 1 部分(12 个权重、相关的输入):OBS 的选择、误差(0.004750)与”每次去掉一个、其余用最小二乘重新拟合”的穷举完全一致;按幅度剪掉最小的权重且不补偿,误差 0.026961,大了 6 倍——一个权重小不等于它不重要(它的输入可能很大),而且其余权重本可以调整来弥补。

量化是同一个问题:只是”把 w_q 变成 0”换成”把 w_q 变成最近的格点”(slides 第 16 页):

q=arg⁡min⁡q(quant(wq)−wq)2[H−1]qq,δF=−wq−quant(wq)[H−1]qq⋅(HF−1):,qq = \arg\min_q \frac{(\text{quant}(w_q) - w_q)^2}{[H^{-1}]_{qq}},\qquad \delta_F = -\frac{w_q - \text{quant}(w_q)}{[H^{-1}]_{qq}} \cdot (H_F^{-1})_{:,q}

F 是还没量化的权重。量化一个、补偿其余的,再从 H⁻¹ 里”去掉”这个已经固定的权重,重复。

2.2 去掉一个变量:一步消元

每量化一个权重,就要得到”删掉它之后”的 H_F 的逆。直接重新求逆是 O(d³);OBQ 论文用了一个引理:

H−q−1=H−1−1[H−1]qqH:,q−1Hq,:−1H_{-q}^{-1} = H^{-1} - \frac{1}{[H^{-1}]_{qq}} H^{-1}_{:,q} H^{-1}_{q,:}

——高斯消元的一步,O(d²)。02 号脚本第 2 部分:与直接求逆差 1e-15,消元后第 q 行列变成 0。

2.3 OBQ:逐行、贪心

OBQ(Frantar & Alistarh 2022,slides 第 16–17 页)对每一行(行与行之间没有 Hessian 的相互作用,§1.1):每一步在剩下的权重里挑”量化之后误差增加最少”的那个,量化它、补偿其余、消元,直到全部量化。每行 d_col 步、每步 O(d_col²),整层 O(d_row · d_col³)——对 ResNet、BERT 这样的模型可以,对几十亿参数不行。

2.4 顺序重要吗

slides 第 19 页(GPTQ 论文的观察):按贪心顺序相对任意固定顺序的改进通常很小——先量化的、误差大的权重会被后面剩下的权重补偿。02 号脚本第 3 部分在第 2 层 attn.o 上测(逐层误差):

位数RTNOBQ(贪心)固定顺序(GPTQ)随机的固定顺序act-order
INT40.53660.07410.02580.04570.0193
INT32.44270.34090.12300.22230.0876
INT215.042.431.181.390.545
  • 主要的收益来自”量化一个、补偿其余”:INT3 从 2.44 降到 0.12(20 倍)。
  • 在这一层上贪心顺序反而更差(0.34)。贪心每一步只挑”自己的取整误差最小”的权重,把难取整的留到最后——那时剩下能帮它补偿的权重已经很少了。(我们核对过实现:同一份 OBQ 代码改成固定顺序,结果与 GPTQ 差 1.5e-16。)GPTQ 论文说贪心的改进”通常很小”,在这里甚至是负的。
  • 顺序也不是完全无关:随机顺序比自然顺序差;act-order(先量化输入最大的列,即 diag(H) 从大到小)最好——最重要的列先量化,后面还有最多的权重可以帮它补偿。官方实现把它作为选项(desc_act)。

放弃贪心的回报是速度:固定顺序时所有行在同一列上共用同一个 H⁻¹ 的更新——每列只消元一次,而不是每个权重一次(slides 第 18 页),复杂度从 O(d_row·d_col³) 降到 O(max(d_row·d_col², d_col³))。02 号脚本第 4 部分(⏱):128 × 384 的 ffn.w2,OBQ 约 39 秒,GPTQ 0.04 秒(约 1000 倍);外推到 GPT-3 的 FFN 层(49152 × 12288),复杂度相差 12288 倍。


3. GPTQ 算法 · The GPTQ algorithm

3.1 朴素的固定顺序版本

固定列顺序 0, 1, 2, …,对每一列 j(所有行一起):

  1. 量化这一列:q = quant(W[:, j]);
  2. 误差 e = (W[:, j] − q) / [H⁻¹]_jj;
  3. 补偿后面所有列:W[:, j:] −= e ⊗ H⁻¹_{j, j:};
  4. 从 H⁻¹ 里消去 j。

这已经是 GPTQ 的数学了(gptq.gptq_naive)。剩下两个改进是工程上的,但对大模型至关重要。

3.2 惰性批量更新

朴素版本每处理一列都要更新后面所有列——读写整个 W,计算 / 访存比极低,GPU 用不满(slides 第 20 页)。关键观察:第 j 列的取整只受”已经作用到它身上的更新”影响,后面列的更新此刻无关。所以可以按 B 列(默认 128)一块:

  • 块内:逐列量化,补偿只立刻作用在这一块剩下的列上(小矩阵,留在缓存里);
  • 块外:整块做完后,用一次矩阵乘法 W[:, i2:] −= Err · H⁻¹[i1:i2, i2:] 一起补偿(slides 第 12 页)。

补偿是线性的,早加晚加结果一样。03 号脚本第 1 部分:B = 1、8、32、128、384 与朴素版本逐位相同;算法里累计的 Σ(w − q)²/(2[U]_jj²) 正好等于最终的逐层误差(26.572064;RTN 是 209.77)——每一步的增量就是 OBS 公式预测的误差增加,对二次目标是精确的。

3.3 Cholesky 预计算

朴素版本每列还要对整个 H⁻¹ 消元一次。可是第 j 步真正用到的只是那时 H⁻¹ 的第 j 行从对角线往右的部分(slides 第 21 页)。03 号脚本第 2 部分验证:这些行恰好是 H⁻¹ 的上三角 Cholesky 因子 U 的行(除以 √对角元)——高斯消元和 Cholesky 分解本来就是同一件事。所以一开始算一次:

H = torch.linalg.cholesky(H)          # H = L Lᵀ
H = torch.cholesky_inverse(H)         # H⁻¹
Hinv = torch.linalg.cholesky(H, upper=True)   # H⁻¹ = Uᵀ U,取上三角 U

之后不再更新逆矩阵。好处不只是快:大模型里反复消元的舍入误差会累积,H⁻¹ 甚至会变得不正定(slides 第 21 页”数值不准确会成为大问题”),Cholesky 稳定得多。

3.4 完整的算法

slides 第 7、13 页的算法(gptq.gptq,结构与官方 gptq.py / AutoGPTQ 的 fasterquant 一致):

H += λ·I                                        # 阻尼
Hinv = cholesky(cholesky_inverse(cholesky(H)), upper=True)
for i1 in range(0, d_col, B):                   # 每块 B 列
    W1 = W[:, i1:i2]; Err1 = zeros
    for i in range(i2 - i1):
        w = W1[:, i]; d = Hinv1[i, i]
        q = quant(w)                            # 1. 量化这一列(所有行)
        err = (w - q) / d                       # 2. 误差
        W1[:, i:] -= outer(err, Hinv1[i, i:])   # 3. 块内立刻补偿
        Err1[:, i] = err
    W[:, i2:] -= Err1 @ Hinv[i1:i2, i2:]        # 4. 块外惰性补偿(一次矩阵乘法)

⏱ 03 号脚本第 3 部分(1024 × 1024 的随机层):朴素版本约 3.5 秒;Cholesky + B = 1 约 0.4 秒;B = 32 约 0.14 秒、B = 128 约 0.23 秒。GPU 上的差别更大——GPTQ 论文在一块 A100 上约 4 小时量化完 OPT-175B / BLOOM-176B。

3.5 阻尼与校准数据

H = 2XXᵀ 可能是奇异的(§1.3)或者病态的。GPTQ 给对角线加 λ = 1% × mean(diag(H))(slides 第 8、35 页的 “+λI”)。03 号脚本第 4 部分在另外 128 条序列上评估逐层误差(INT3):

校准数据λ = 01e-41%10%100%
2 条(64 个 token,秩 64 < 384)失败170.162.458.172.9
8 条(256 个 token)失败53.125.537.865.3
128 条(4096 个 token)82.632.817.736.161.5

(RTN 是 206.9。)token 比输入维少时 XXᵀ 奇异,λ = 0 时 Cholesky 直接失败。阻尼也是正则化:H 只是真实输入分布的一个估计,数据少时完全相信它会”过拟合”校准数据;λ 越大,补偿越保守(λ → ∞ 时 H⁻¹ 趋近对角阵,GPTQ 退化成 RTN)。数据足够时 1% 最好。GPTQ 论文的校准数据是 128 条随机的 2048 token C4 片段(slides 第 23 页)——通用文本,与下游任务无关。


4. 量化整个模型 · Quantizing the whole model

4.1 一块一块地来

slides 第 23 页的实验设置:逐行的非对称最小–最大网格;一个 Transformer 块一个块地量化,每块的校准输入来自上一个已量化块的输出。每一块里:用这一块的全精度权重跑一遍校准数据,收集每个线性层的输入、算 H(slides 第 32 页的 add_batch 就是在累加 H),再逐个量化(gptq.quantize_model_gptq)。一次只需要一块的权重和激活——这是它能在一块 GPU 上量化 175B 模型的原因。

4.2 RTN vs GPTQ

04 号脚本第 1 部分(超出下界;全精度 3.0;⏱ GPTQ 量化整个模型约 0.44 秒):

位数网格RTNGPTQGPTQ + act-order
INT4非对称、逐通道2.93.13.0
INT3非对称、逐通道5.03.43.1
INT3非对称、逐组 323.33.03.0
INT2非对称、逐通道20.18.03.5
INT2非对称、逐组 328.76.53.3
INT2absmax、逐通道(第 19 讲的网格)153.812.13.4
  • 这个模型 4 位时怎么量化都无损(第 19 讲说过它”皮实”);位数越低,GPTQ 的优势越大。slides 第 25–26 页在 OPT、BLOOM 上是同一个方向:4 位时 RTN 在大模型上已经明显掉点,3 位时 RTN 基本失效,GPTQ 仍然可用。
  • act-order 在极低位数时帮助巨大(2 位非对称逐通道 8.0 → 3.5)。
  • 分组也有帮助,代价是存 scale。act-order 和分组一起用时,每组里的列不再相邻,部署时要额外存”每列属于哪一组”(AutoGPTQ 的 g_idx)。slides 第 28 页:GPTQ 配合分组甚至能做到 2 位、三值量化。

一个细节:GPTQ 官方实现的”对称”网格用满 2^b 个值(2 位是 {−2, −1, 0, 1}·s),第 19 讲的 absmax 只用 2^b − 1 个({−1, 0, 1}·s)——2 位时 RTN 分别是 21.4 和 153.8。比较不同方法时要用同一个网格。

4.3 与第 19 讲的方法比较

04 号脚本第 2 部分,同一个网格(INT2、absmax、逐通道):

方法超出下界⏱ 秒
RTN153.80.00
AdaQuant(逐层,梯度下降)19.82.5
ZeroQuant 逐块蒸馏(梯度下降)3.910.9
QAT(整个模型,梯度下降)4.76.8
GPTQ(逐层,二阶补偿)12.10.23
GPTQ + act-order3.40.24
  • GPTQ 和 AdaQuant 的目标完全相同(逐层输出误差):GPTQ 解析地求补偿,比梯度下降更好、快 10 倍。
  • 按自然顺序的 GPTQ 不如逐块蒸馏和 QAT——它们的目标更”大”,各层的误差可以互相配合。但加上 act-order,GPTQ 在这里最好,只用了 0.24 秒,不需要梯度,也不需要把整个块或模型放进反向传播。
  • 规模越大,这个差别越关键:slides 第 27 页,GPTQ 量化 175B 约 4 个 GPU 小时,ZeroQuant-LKD 量化 1.3B 就要 3 小时;QAT 在 175B 上不可行。slides 第 29 页:在小模型上,GPTQ 和 OBQ 这类精确但昂贵的方法几乎一样准,却快得多。

4.4 校准数据与校准输入的来源

04 号脚本第 3 部分(INT2 非对称逐通道):64 个 token 时 9.0,1024–4096 个时 8.0,16384 个 8.3——几千个 token 之后基本饱和。第 4 部分比较每块的校准输入来自”已量化的前几块”还是全精度模型:INT3 3.4 vs 3.5,INT2 8.0 vs 7.6。道理上前者更贴近部署时的真实输入,但在这个模型上差别很小(2 位时后者反而好一点);前者在实现上的显存好处是确定的。

4.5 实际怎么用

slides 第 36 页的 AutoGPTQ 用法:

quantize_config = BaseQuantizeConfig(bits=4, group_size=128, damp_percent=0.01, desc_act=False)
model = AutoGPTQForCausalLM.from_pretrained(model_id, quantize_config)
model.quantize(examples_ids, batch_size=1, use_triton=True)
model.save_quantized(out_dir, use_safetensors=True)

对照本讲:bits / group_size 是网格与分组(§4.2),damp_percent 是阻尼(§3.5),desc_act 是 act-order(§2.4),examples 是校准数据(§4.4)。


5. GPTQ 之外 · SmoothQuant & AWQ

5.1 SmoothQuant:把激活的难度挪进权重

第 19 讲:激活有系统性的离群特征维时,W8A8 崩溃;LLM.int8() 把这些维拿出来用 16 位算。SmoothQuant(Xiao et al. 2023)用一个恒等变换:

Y=XW⊤=(X diag(s)−1)(W diag(s))⊤,sj=max⁡∣Xj∣αmax⁡∣W:,j∣1−αY = XW^\top = \big(X\,\mathrm{diag}(s)^{-1}\big)\big(W\,\mathrm{diag}(s)\big)^\top,\qquad s_j = \frac{\max|X_j|^\alpha}{\max|W_{:,j}|^{1-\alpha}}

按特征维把激活除以 s、权重的对应列乘以 s,函数不变;s 一次性折进前一个归一化层的 γ 和权重里,推理时没有额外开销。α 是迁移强度。05 号脚本第 1 部分(植入 300 倍离群值的模型):

α激活的最大绝对值W8A8 逐张量W8A8 向量级
0(不平滑)594.08366.52054.8
0.51.03.13.0
0.91.03.13.0

所有计算都是 INT8,不用拆成两个矩阵乘法——这比 LLM.int8() 快。代价是要选 α:太小,激活还是难量化;太大,权重变得难量化(论文对多数模型用 0.5)。

5.2 AWQ:保护重要的权重

AWQ(Activation-aware Weight Quantization,Lin et al. 2024)也是只量化权重,但思路比 GPTQ 简单:输入大的那些列最重要(约 1% 的”显著通道”)。把这些列先乘以 s 再量化(它们的相对量化误差变小),输入除以 s。s_j = mean|X_j|^α,在 α ∈ [0, 1] 里搜一个使输出误差最小的。05 号脚本第 2 部分(简化版:每层单独搜 α,没有做截断搜索):

设置RTNAWQ(简化版)GPTQ + act-order
INT3、逐通道5.03.53.1
INT2、逐通道20.19.33.5
INT2、逐组 328.74.03.3

AWQ 只用了 H 的对角线(输入的大小),GPTQ 用了整个 H(包括列与列之间的相关),所以 GPTQ 更准;但 AWQ 量化后的权重仍是普通的 RTN,没有逐列的补偿,内核最简单、量化最快,也不容易对校准数据过拟合。实践中两者常一起用,或者按部署环境选。

方法量化什么核心想法用到的输入统计量
RTN权重就近取整无
GPTQ权重量化一列,按 H⁻¹ 补偿其余列整个 H = 2XXᵀ
AWQ权重放大重要的列再量化diag(H)(每维的大小)
LLM.int8()权重 + 激活离群维用 16 位单独算运行时找 |x| ≥ 6 的维
SmoothQuant权重 + 激活把激活的离群值挪进权重每维的最大值

6. 量化之后的推理 · Inference with quantized weights

6.1 打包与反量化

INT4 的权重两个装一个字节(低 4 位、高 4 位),每行(或每组)再存一个 FP16 的 scale 和零点。矩阵-向量乘法里,反量化可以”推迟”到求和之后:

yi=si(∑jqijxj−zi∑jxj)y_i = s_i \Big( \sum_j q_{ij} x_j - z_i \sum_j x_j \Big)

整数矩阵只需要和 x 乘一次,零点的修正只要 Σx 一个数。05 号脚本第 3 部分:4096 × 4096 的权重打包后 8.02 MiB(FP16 32 MiB),GEMV 与”先反量化再乘”差 2e-6。

6.2 为什么能变快——以及为什么必须融合

⏱ 在本机 CPU 上用几个独立的 PyTorch 操作来做:FP32 GEMV 约 0.35 ms,INT4 解包 + 乘 + 修正约 6 ms——反而慢了 17 倍。解包产生了一个完整大小的中间矩阵,读写的字节比省下的还多。量化要变快,必须有融合的内核:在寄存器里解包、反量化、乘加,从显存只读 4 位的权重。

batch = 1 的解码是访存受限的,速度的上限就是”读的字节少了多少”:

模型FP16INT4(组 128)INT3(组 128)读的字节少
7B14 GB3.6 GB2.7 GB3.88× / 5.12×
OPT-175B350 GB90.2 GB68.4 GB3.88× / 5.12×

slides 第 24 页(GPTQ 论文):用自己写的融合 GEMV 内核,OPT-175B 端到端在 A100 上快 3.25 倍、A6000 上快 4.5 倍(流水线并行测的——FP16 的 175B 模型要好几块卡,3 位的模型一块 80 GB 的 A100 就放得下)。单批推理是 GEMV、访存受限,反量化虽然多了计算,但读的字节少了,整体还是快。

6.3 局限

slides 第 37 页:GPTQ 的理论计算量没有变(只是读得少了,反量化还多了一点计算);它只量化权重,不处理激活。所以批大到计算受限之后收益变小——那时要靠 W8A8 / FP8 这类连激活一起量化、用低精度张量核心计算的方法(SmoothQuant、FP8 训练与推理)。


7. 小结 · Summary

  1. 逐层目标 ‖WX − ŴX‖² = Σ_行 eᵀHe/2,H = 2XXᵀ,所有行共用。RTN 用欧氏距离量一个椭圆形的目标,不是最优的。
  2. OBS:剪掉 / 量化一个权重后,其余权重的最优补偿是 −(w_q − q)/[H⁻¹]qq · H⁻¹{:,q};OBQ 逐行贪心地用它,O(d_row·d_col³),只适合小模型。
  3. GPTQ 的三个改进:固定顺序(所有行共用 H⁻¹ 的更新,主要的加速;顺序的影响小,act-order 最好)、惰性批量更新(块内立刻补偿、块外攒成矩阵乘法,结果逐位相同)、Cholesky 预计算(数值稳定、不再更新逆矩阵)。再加阻尼 λ = 1%。
  4. 在我们的模型上:INT2 RTN 154 → GPTQ 12.1 → 加 act-order 3.4,比第 19 讲的 AdaQuant / 逐块蒸馏 / QAT 都好,0.24 秒;175B 的模型约 4 个 GPU 小时。
  5. SmoothQuant 把激活的离群值挪进权重,让 W8A8 全用 INT8;AWQ 放大输入大的列再量化,简单、快,但只用了 H 的对角线。
  6. 量化的加速来自读得少:必须有融合的反量化内核;单批解码时 4 位约 3.9 倍的上限,GPTQ 在 OPT-175B 上实测 3.25–4.5 倍。

8. 常见误区 · Common pitfalls

  1. “量化误差最小,就是让每个权重离格点最近。” 要紧的是输出误差,由 H 决定(§1.2)。
  2. “H 是每一行各自的。” H = 2XXᵀ 只取决于输入,所有行共用(§1.1)。
  3. “OBQ 的贪心顺序一定比固定顺序好。” 在我们的层上反而更差;GPTQ 放弃贪心换来所有行共用计算(§2.4)。
  4. “惰性批量更新是一种近似。” 结果逐位相同(§3.2)。
  5. “Cholesky 只是为了快。” 更重要的是数值稳定(§3.3)。
  6. “阻尼是可有可无的小技巧。” 校准 token 少于输入维时 H 奇异,λ = 0 直接失败;它也是正则化(§3.5)。
  7. “校准数据越多越好。” 几千个 token 之后基本饱和(§4.4)。
  8. “GPTQ 能让计算变少。” 计算量不变,省的是读权重的字节(§6.3)。
  9. “量化之后自然就快了。” 不融合的反量化反而更慢(§6.2)。
  10. “GPTQ 解决了激活的离群值。” 它只量化权重;激活要靠 LLM.int8() 或 SmoothQuant(§5)。

9. 练习 · Exercises

  1. 证明 ‖WX − ŴX‖_F² = Σ_r e_rᵀ(XXᵀ)e_r。为什么各行之间没有相互作用?
  2. H = [[2, 1.8], [1.8, 2]],w = (0.3, 0.3),网格间距 0.5。算 RTN 和 (0.5, 0) 两个格点的 eᵀHe/2。
  3. 写出 OBS 剪掉第 q 个权重之后的补偿 δ,并验证 δ_q = −w_q。
  4. 用消元公式从 H⁻¹ 里去掉一个变量,复杂度是多少?OBQ 量化一个 d_row × d_col 的层总共要多少次这样的消元?GPTQ 呢?
  5. 为什么 GPTQ 能所有行共用一个 H⁻¹ 的更新,而 OBQ 不能?
  6. 解释惰性批量更新为什么不改变结果。B = 1 和 B = d 分别对应什么?
  7. 为什么消元到第 j 步时 H⁻¹ 的第 j 行(从对角线往右)等于 H⁻¹ 的 Cholesky 因子的第 j 行乘 √[H⁻¹]_jj?
  8. 一个层的输入维是 4096,你只有 2 条 1024 token 的校准序列。不加阻尼会怎样?你会选多大的 λ?
  9. act-order 先量化 diag(H) 大的列。为什么这有道理?它和分组一起用时有什么麻烦?
  10. SmoothQuant 的 α = 0 和 α = 1 各意味着什么?为什么不能直接取 α = 1?
  11. AWQ 和 GPTQ 都只量化权重。各自用了 H 的哪部分信息?什么情况下 AWQ 会和 GPTQ 差不多?
  12. 一个 13B 的模型、INT4(组 128)、A100 带宽 2 TB/s。batch = 1 时每生成一个 token 至少要多久?FP16 呢?为什么实际的加速比这个比值小?

参考答案

  1. (WX − ŴX) 的第 r 行是 e_rᵀX,它的平方和是 e_rᵀXXᵀe_r;对 r 求和就是 Frobenius 范数的平方。每一行的输出只依赖这一行的权重,所以行与行之间没有交叉项。
  2. RTN (0.5, 0.5):e = (−0.2, −0.2),eᵀHe = 0.04·2 + 0.04·2 + 2·1.8·0.04 = 0.304,/2 = 0.152。(0.5, 0):e = (−0.2, 0.3),eᵀHe = 0.08 + 0.18 − 2·1.8·0.06 = 0.044,/2 = 0.022。
  3. δ = −w_q / [H⁻¹]qq · H⁻¹{:,q};第 q 个分量 δ_q = −w_q / [H⁻¹]_qq · [H⁻¹]_qq = −w_q,所以 w_q + δ_q = 0。
  4. 一次 O(d_col²)。OBQ:每行 d_col 次、共 d_row·d_col 次,总 O(d_row·d_col³)。GPTQ:每列一次(所有行共用),共 d_col 次,O(d_col³)——而且用 Cholesky 一次算完。
  5. OBQ 每行的贪心顺序不同,每行的 H_F⁻¹(已量化的变量集合不同)也不同;GPTQ 所有行按同样的顺序,第 j 步时每行”已经量化的变量”都是前 j 列,H_F⁻¹ 相同。
  6. 第 j 列被量化之前,它需要的只是前 j 列的补偿都已经作用到它身上;补偿是线性的、可以累加,块外的列晚一点、一次性地收到所有补偿,结果一样。B = 1 是”每列都对后面所有列做一次外积”;B = d 是”整个矩阵一块”,块外更新不存在(全在块内)。
  7. 消元公式和 Cholesky 分解都是对 H⁻¹ 做对称的高斯消元:第 j 步消元后剩下的子矩阵就是 Schur 补,而 Cholesky 因子的第 j 行正是”消元到第 j 步时第 j 行 ÷ √主元”。
  8. 2048 个 token < 4096 维,XXᵀ 的秩 ≤ 2048,H 奇异,Cholesky 失败。λ 要比默认的 1% 大一些(03 号脚本:64 个 token、384 维时 10% 最好)——数据越少越应该保守。
  9. diag(H) 大的列对应输入大的维,它们的量化误差对输出影响最大;先量化它们,后面还有最多的权重可以帮它补偿。和分组一起用时,组是按”量化顺序”划分的,一组里的列不再相邻,部署时要存每列的组号(g_idx),反量化内核要按它查 scale。
  10. α = 0:s_j = 1/max|W_{:,j}|,权重的每一列被归一化,激活反而乘上了权重的最大值——难度全留在(甚至加重在)激活上;α = 1:s_j = max|X_j|,激活的每一维都被归一化到 1,所有的”幅度”都挪进了权重,权重的列被乘上激活的最大值,权重变得很难量化。α 在中间平衡两者。(05 号脚本表里的”α = 0”一行指的是不做平滑,不是公式里的 α = 0。)
  11. AWQ 用了 diag(H)(每个输入维的大小),GPTQ 用了整个 H(还有维与维之间的相关)。输入维之间几乎不相关(H 接近对角阵)时,两者差不多。
  12. INT4:13B × 4.125/8 ≈ 6.7 GB → 3.4 ms;FP16:26 GB → 13 ms。实际加速小于 3.9 倍:注意力、KV 缓存的读取、嵌入、反量化的计算、内核发射等不随权重位数减少;批变大后会转为计算受限。

10. 延伸阅读 · Further reading

  • Frantar et al. (2023) GPTQ(ICLR):本讲的主要来源;官方代码 IST-DASLab/gptq,社区版 GPTQ-for-LLaMa、AutoGPTQ。
  • Hassibi & Stork (1993) Second Order Derivatives for Network Pruning: Optimal Brain Surgeon;LeCun et al. (1990) Optimal Brain Damage。
  • Frantar & Alistarh (2022) Optimal Brain Compression(OBQ);Frantar & Alistarh (2023) SparseGPT:同一套数学用于一次性剪枝。
  • Xiao et al. (2023) SmoothQuant;Lin et al. (2024) AWQ。
  • Dettmers et al. (2023) QLoRA:4 位的 NF4 基座 + LoRA(第 23 讲)。
  • Frantar et al. (2024) MARLIN:GPTQ 格式的高效 INT4 × FP16 内核。

11. 下一讲预告 · Next lecture

量化让权重读得更少;可是长序列时,注意力本身也是访存受限的——N × N 的分数矩阵要写回显存再读出来。第 21 讲是 Tri Dao 的嘉宾讲座:FlashAttention 1–4。注意力的 FLOPs 一点没少,FlashAttention 为什么能快好几倍?我们会回到第 13 讲的在线 softmax 和分块,看它怎样一步步适配 A100、H100、Blackwell 的硬件特性。

← 返回课程目录