对应 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
学完这一讲,你应该能够:
- 把逐层目标 ‖WX − ŴX‖² 写成每行一个二次型 eᵀHe/2,解释 H = 2XXᵀ 的含义,以及为什么”就近取整”不是最优的。
- 推导 OBS 的补偿公式,说清楚 OBQ 怎样把它用于量化、为什么它慢。
- 说出 GPTQ 的三个改进——固定顺序、惰性批量更新、Cholesky 分解——各自解决了什么,为什么它们不改变结果。
- 从零实现 GPTQ,在一个真正训练过的模型上与 RTN、第 19 讲的方法比较;理解分组、act-order、阻尼、校准数据的作用。
- 比较 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 页):对一个线性层,找量化后的 Ŵ 使
最小(X 是这一层在校准数据上的输入)。看 Ŵ 的一行 ŵ(一个输出通道),记误差 e = w − ŵ,这一行的输出误差平方和是 Σ_t (x_t · e)² = eᵀ(XXᵀ)e。所以
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.283 | 0.1487 |
| 先量化 w₁、补偿 w₂(0.30 → 0.119)再取整 | (0.5, 0.0) | 0.361 | 0.0213 |
| 在网格上穷举的最优 | (0.5, 0.0) | 0.361 | 0.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.q | 128 | 80 | 4 | 1.626 | 0.212 |
| blocks.0.ffn.w1 | 128 | 128 | 9 | 7.296 | 0.684 |
| blocks.0.ffn.w2 | 384 | 384 | 1,791,650 | 281.0 | 40.8 |
| blocks.3.ffn.w2 | 384 | 384 | 115,493,176 | 8731 | 2773 |
- 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),使目标增加最少,其余权重怎样补偿?对二次的目标,答案是闭式的:
误差的增加量正好是 w_q² / (2[H⁻¹]_qq)。02 号脚本第 1 部分(12 个权重、相关的输入):OBS 的选择、误差(0.004750)与”每次去掉一个、其余用最小二乘重新拟合”的穷举完全一致;按幅度剪掉最小的权重且不补偿,误差 0.026961,大了 6 倍——一个权重小不等于它不重要(它的输入可能很大),而且其余权重本可以调整来弥补。
量化是同一个问题:只是”把 w_q 变成 0”换成”把 w_q 变成最近的格点”(slides 第 16 页):
F 是还没量化的权重。量化一个、补偿其余的,再从 H⁻¹ 里”去掉”这个已经固定的权重,重复。
2.2 去掉一个变量:一步消元
每量化一个权重,就要得到”删掉它之后”的 H_F 的逆。直接重新求逆是 O(d³);OBQ 论文用了一个引理:
——高斯消元的一步,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 上测(逐层误差):
| 位数 | RTN | OBQ(贪心) | 固定顺序(GPTQ) | 随机的固定顺序 | act-order |
|---|---|---|---|---|---|
| INT4 | 0.5366 | 0.0741 | 0.0258 | 0.0457 | 0.0193 |
| INT3 | 2.4427 | 0.3409 | 0.1230 | 0.2223 | 0.0876 |
| INT2 | 15.04 | 2.43 | 1.18 | 1.39 | 0.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(所有行一起):
- 量化这一列:q = quant(W[:, j]);
- 误差 e = (W[:, j] − q) / [H⁻¹]_jj;
- 补偿后面所有列:W[:, j:] −= e ⊗ H⁻¹_{j, j:};
- 从 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):
| 校准数据 | λ = 0 | 1e-4 | 1% | 10% | 100% |
|---|---|---|---|---|---|
| 2 条(64 个 token,秩 64 < 384) | 失败 | 170.1 | 62.4 | 58.1 | 72.9 |
| 8 条(256 个 token) | 失败 | 53.1 | 25.5 | 37.8 | 65.3 |
| 128 条(4096 个 token) | 82.6 | 32.8 | 17.7 | 36.1 | 61.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 秒):
| 位数 | 网格 | RTN | GPTQ | GPTQ + act-order |
|---|---|---|---|---|
| INT4 | 非对称、逐通道 | 2.9 | 3.1 | 3.0 |
| INT3 | 非对称、逐通道 | 5.0 | 3.4 | 3.1 |
| INT3 | 非对称、逐组 32 | 3.3 | 3.0 | 3.0 |
| INT2 | 非对称、逐通道 | 20.1 | 8.0 | 3.5 |
| INT2 | 非对称、逐组 32 | 8.7 | 6.5 | 3.3 |
| INT2 | absmax、逐通道(第 19 讲的网格) | 153.8 | 12.1 | 3.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、逐通道):
| 方法 | 超出下界 | ⏱ 秒 |
|---|---|---|
| RTN | 153.8 | 0.00 |
| AdaQuant(逐层,梯度下降) | 19.8 | 2.5 |
| ZeroQuant 逐块蒸馏(梯度下降) | 3.9 | 10.9 |
| QAT(整个模型,梯度下降) | 4.7 | 6.8 |
| GPTQ(逐层,二阶补偿) | 12.1 | 0.23 |
| GPTQ + act-order | 3.4 | 0.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)用一个恒等变换:
按特征维把激活除以 s、权重的对应列乘以 s,函数不变;s 一次性折进前一个归一化层的 γ 和权重里,推理时没有额外开销。α 是迁移强度。05 号脚本第 1 部分(植入 300 倍离群值的模型):
| α | 激活的最大绝对值 | W8A8 逐张量 | W8A8 向量级 |
|---|---|---|---|
| 0(不平滑) | 594.0 | 8366.5 | 2054.8 |
| 0.5 | 1.0 | 3.1 | 3.0 |
| 0.9 | 1.0 | 3.1 | 3.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 部分(简化版:每层单独搜 α,没有做截断搜索):
| 设置 | RTN | AWQ(简化版) | GPTQ + act-order |
|---|---|---|---|
| INT3、逐通道 | 5.0 | 3.5 | 3.1 |
| INT2、逐通道 | 20.1 | 9.3 | 3.5 |
| INT2、逐组 32 | 8.7 | 4.0 | 3.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 和零点。矩阵-向量乘法里,反量化可以”推迟”到求和之后:
整数矩阵只需要和 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 的解码是访存受限的,速度的上限就是”读的字节少了多少”:
| 模型 | FP16 | INT4(组 128) | INT3(组 128) | 读的字节少 |
|---|---|---|---|---|
| 7B | 14 GB | 3.6 GB | 2.7 GB | 3.88× / 5.12× |
| OPT-175B | 350 GB | 90.2 GB | 68.4 GB | 3.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
- 逐层目标 ‖WX − ŴX‖² = Σ_行 eᵀHe/2,H = 2XXᵀ,所有行共用。RTN 用欧氏距离量一个椭圆形的目标,不是最优的。
- OBS:剪掉 / 量化一个权重后,其余权重的最优补偿是 −(w_q − q)/[H⁻¹]qq · H⁻¹{:,q};OBQ 逐行贪心地用它,O(d_row·d_col³),只适合小模型。
- GPTQ 的三个改进:固定顺序(所有行共用 H⁻¹ 的更新,主要的加速;顺序的影响小,act-order 最好)、惰性批量更新(块内立刻补偿、块外攒成矩阵乘法,结果逐位相同)、Cholesky 预计算(数值稳定、不再更新逆矩阵)。再加阻尼 λ = 1%。
- 在我们的模型上:INT2 RTN 154 → GPTQ 12.1 → 加 act-order 3.4,比第 19 讲的 AdaQuant / 逐块蒸馏 / QAT 都好,0.24 秒;175B 的模型约 4 个 GPU 小时。
- SmoothQuant 把激活的离群值挪进权重,让 W8A8 全用 INT8;AWQ 放大输入大的列再量化,简单、快,但只用了 H 的对角线。
- 量化的加速来自读得少:必须有融合的反量化内核;单批解码时 4 位约 3.9 倍的上限,GPTQ 在 OPT-175B 上实测 3.25–4.5 倍。
8. 常见误区 · Common pitfalls
- “量化误差最小,就是让每个权重离格点最近。” 要紧的是输出误差,由 H 决定(§1.2)。
- “H 是每一行各自的。” H = 2XXᵀ 只取决于输入,所有行共用(§1.1)。
- “OBQ 的贪心顺序一定比固定顺序好。” 在我们的层上反而更差;GPTQ 放弃贪心换来所有行共用计算(§2.4)。
- “惰性批量更新是一种近似。” 结果逐位相同(§3.2)。
- “Cholesky 只是为了快。” 更重要的是数值稳定(§3.3)。
- “阻尼是可有可无的小技巧。” 校准 token 少于输入维时 H 奇异,λ = 0 直接失败;它也是正则化(§3.5)。
- “校准数据越多越好。” 几千个 token 之后基本饱和(§4.4)。
- “GPTQ 能让计算变少。” 计算量不变,省的是读权重的字节(§6.3)。
- “量化之后自然就快了。” 不融合的反量化反而更慢(§6.2)。
- “GPTQ 解决了激活的离群值。” 它只量化权重;激活要靠 LLM.int8() 或 SmoothQuant(§5)。
9. 练习 · Exercises
- 证明 ‖WX − ŴX‖_F² = Σ_r e_rᵀ(XXᵀ)e_r。为什么各行之间没有相互作用?
- H = [[2, 1.8], [1.8, 2]],w = (0.3, 0.3),网格间距 0.5。算 RTN 和 (0.5, 0) 两个格点的 eᵀHe/2。
- 写出 OBS 剪掉第 q 个权重之后的补偿 δ,并验证 δ_q = −w_q。
- 用消元公式从 H⁻¹ 里去掉一个变量,复杂度是多少?OBQ 量化一个 d_row × d_col 的层总共要多少次这样的消元?GPTQ 呢?
- 为什么 GPTQ 能所有行共用一个 H⁻¹ 的更新,而 OBQ 不能?
- 解释惰性批量更新为什么不改变结果。B = 1 和 B = d 分别对应什么?
- 为什么消元到第 j 步时 H⁻¹ 的第 j 行(从对角线往右)等于 H⁻¹ 的 Cholesky 因子的第 j 行乘 √[H⁻¹]_jj?
- 一个层的输入维是 4096,你只有 2 条 1024 token 的校准序列。不加阻尼会怎样?你会选多大的 λ?
- act-order 先量化 diag(H) 大的列。为什么这有道理?它和分组一起用时有什么麻烦?
- SmoothQuant 的 α = 0 和 α = 1 各意味着什么?为什么不能直接取 α = 1?
- AWQ 和 GPTQ 都只量化权重。各自用了 H 的哪部分信息?什么情况下 AWQ 会和 GPTQ 差不多?
- 一个 13B 的模型、INT4(组 128)、A100 带宽 2 TB/s。batch = 1 时每生成一个 token 至少要多久?FP16 呢?为什么实际的加速比这个比值小?
参考答案
- (WX − ŴX) 的第 r 行是 e_rᵀX,它的平方和是 e_rᵀXXᵀe_r;对 r 求和就是 Frobenius 范数的平方。每一行的输出只依赖这一行的权重,所以行与行之间没有交叉项。
- 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。
- δ = −w_q / [H⁻¹]qq · H⁻¹{:,q};第 q 个分量 δ_q = −w_q / [H⁻¹]_qq · [H⁻¹]_qq = −w_q,所以 w_q + δ_q = 0。
- 一次 O(d_col²)。OBQ:每行 d_col 次、共 d_row·d_col 次,总 O(d_row·d_col³)。GPTQ:每列一次(所有行共用),共 d_col 次,O(d_col³)——而且用 Cholesky 一次算完。
- OBQ 每行的贪心顺序不同,每行的 H_F⁻¹(已量化的变量集合不同)也不同;GPTQ 所有行按同样的顺序,第 j 步时每行”已经量化的变量”都是前 j 列,H_F⁻¹ 相同。
- 第 j 列被量化之前,它需要的只是前 j 列的补偿都已经作用到它身上;补偿是线性的、可以累加,块外的列晚一点、一次性地收到所有补偿,结果一样。B = 1 是”每列都对后面所有列做一次外积”;B = d 是”整个矩阵一块”,块外更新不存在(全在块内)。
- 消元公式和 Cholesky 分解都是对 H⁻¹ 做对称的高斯消元:第 j 步消元后剩下的子矩阵就是 Schur 补,而 Cholesky 因子的第 j 行正是”消元到第 j 步时第 j 行 ÷ √主元”。
- 2048 个 token < 4096 维,XXᵀ 的秩 ≤ 2048,H 奇异,Cholesky 失败。λ 要比默认的 1% 大一些(
03号脚本:64 个 token、384 维时 10% 最好)——数据越少越应该保守。 - diag(H) 大的列对应输入大的维,它们的量化误差对输出影响最大;先量化它们,后面还有最多的权重可以帮它补偿。和分组一起用时,组是按”量化顺序”划分的,一组里的列不再相邻,部署时要存每列的组号(g_idx),反量化内核要按它查 scale。
- α = 0:s_j = 1/max|W_{:,j}|,权重的每一列被归一化,激活反而乘上了权重的最大值——难度全留在(甚至加重在)激活上;α = 1:s_j = max|X_j|,激活的每一维都被归一化到 1,所有的”幅度”都挪进了权重,权重的列被乘上激活的最大值,权重变得很难量化。α 在中间平衡两者。(
05号脚本表里的”α = 0”一行指的是不做平滑,不是公式里的 α = 0。) - AWQ 用了 diag(H)(每个输入维的大小),GPTQ 用了整个 H(还有维与维之间的相关)。输入维之间几乎不相关(H 接近对角阵)时,两者差不多。
- 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 的硬件特性。