第5章 GPTQ 实战:如何把 Hugging Face 模型量化成 4-bit
本章目录:
5.1 GPTQ 原理
- 5.1.1 GPTQ 要解决的问题
- 5.1.2 GPTQ 是什么?
- 5.1.3 GPTQ 与简单 Round 的区别
- 5.1.4 GPTQ 的核心:重构误差
- 5.1.5 Calibration Dataset 在 GPTQ 中做什么?
- 5.1.6 Calibration 数据不能随便选
- 5.1.7 GPTQ 中的 Hessian
- 5.1.8 GPTQ 的“逐步量化 + 误差补偿”
- 5.1.9 GPTQ 非常适合 4-bit 的原因
- 5.1.10 Group-wise GPTQ
- 5.1.11 Group Size 的影响
- 5.1.12 GPTQ 常见配置
5.2 GPTQ 实战
- 5.2.1 当前 GPTQ 工具链
- 5.2.2 安装 GPTQ 实战环境
- 5.2.3 选择量化模型:从小模型跑通到真实规模
- 5.2.4 加载 Tokenizer
- 5.2.5 准备 Calibration 数据集与量化配置
- 5.2.6 开始量化
- 5.2.7 第一次量化比较慢的原因
- 5.2.8 保存量化模型
- 5.2.9 重新加载 GPTQ 模型
- 5.2.10 进行一次推理
- 5.2.10.1 完整脚本与实测结果
- 5.2.11 但“能生成”远远不够
5.3 Benchmark 与评测
- 5.3.1 Benchmark 1:模型大小
- 5.3.2 Benchmark 2:显存
- 5.3.3 Benchmark 3:Tokens/s
- 5.3.4 Benchmark 4:量化误差
- 5.3.5 Benchmark 5:Perplexity
- 5.3.6 GPTQ 的实际推理是不是“INT4 × FP16”?
- 5.3.7 GPTQ 的真正收益是什么?
5.4 局限、对比与总结
- 5.4.1 GPTQ 的局限
- 5.4.2 GPTQ 与 AWQ:下一步仍需要 AWQ 的原因
- 5.4.3 本章知识总结
- 5.4.4 本章必须记住的 7 个关键词
- 5.4.5 一句话理解 GPTQ
5.1 GPTQ 原理
5.1.1 GPTQ 要解决的问题
前面四章,我们已经逐渐建立了 LLM 量化的完整基础。
我们知道:
FP16→INT8→INT4 FP16\rightarrow INT8\rightarrow INT4 FP16→INT8→INT4
可以明显降低模型权重的存储需求。
对于一个 7B 模型,粗略估算:
7B×2Byte≈14GB 7B\times2Byte\approx14GB 7B×2Byte≈14GB
而 4-bit 权重:
7B×0.5Byte≈3.5GB 7B\times0.5Byte\approx3.5GB 7B×0.5Byte≈3.5GB
这意味着:
把 FP16 权重压缩到 4-bit,可以显著降低模型的存储和显存压力。
但是,一个很现实的问题出现了:
我们不能简单地把每一个 Float 参数直接 Round 成 INT4。
因为这样做虽然简单,但很可能带来明显的模型精度损失。
最简单的量化方式
回忆第二章的公式:
xq=round(xs) x_q= round\left(\frac{x}{s}\right) xq=round(sx)
然后:
x^=sxq \hat{x}=sx_q x^=sxq
这是一种基本的量化方式。
但是对于一个大型 LLM:
W∈Rdout×din W\in\mathbb{R}^{d_{out}\times d_{in}} W∈Rdout×din
如果对每一个权重都独立进行:
id="j3hz9p"
W1 → Round
W2 → Round
W3 → Round
...
实际上隐含了一个假设:
每个权重产生相同大小的误差,对模型的影响也差不多。
这个假设通常并不成立。
不同权重重要性不同的原因
考虑一个 Linear Layer:
Y=XW Y=XW Y=XW
假设某两个权重的量化误差都是:
0.01 0.01 0.01
看起来一样。
但是如果第一个权重对应的输入:
X1=0.1 X_1=0.1 X1=0.1
而第二个权重对应:
X2=100 X_2=100 X2=100
那么它们对输出的影响可能完全不同。
因为:
ΔY=XΔW \Delta Y=X\Delta W ΔY=XΔW
对于第一个:
0.1×0.01=0.001 0.1\times0.01=0.001 0.1×0.01=0.001
而第二个:
100×0.01=1 100\times0.01=1 100×0.01=1
可以看到:
相同的权重误差,在不同输入方向下可能产生完全不同的输出误差。
所以真正应该关心的是:
W→Wq \boxed{ W\rightarrow W_q } W→Wq
之后:
WX→WqX \boxed{ WX\rightarrow W_qX } WX→WqX
到底变化了多少。
5.1.2 GPTQ 是什么?
GPTQ(全称 Generative Pre-trained Transformer Quantization,出自论文《GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers》)是一种:
Post-Training Quantization(PTQ)
方法。
它的目标是:
在不重新训练大型 LLM 的情况下,将模型权重量化到很低的 bit,同时尽量保持原模型的行为。
GPTQ 论文发表于 2022 年,提出了一种基于近似二阶信息的单次权重量化方法,并展示了 GPT 类大型模型的低比特量化效果。
当前 Hugging Face 文档对 GPTQ 的描述也很直接:
GPTQ 对权重矩阵逐行量化,并试图最小化量化误差;典型结果可以使用 4-bit 权重,并在推理时进行恢复/计算。
因此,可以把 GPTQ 理解成:
5.1.3 GPTQ 与简单 Round 的区别
现在我们把两个方法直接放在一起。
简单量化
关注:
W−W^ W-\hat{W} W−W^
GPTQ
它更关心:
WX−WqX \boxed{ WX-W_qX } WX−WqX
所以:
GPTQ 的核心并不是创造一种神奇的 INT4 表示,而是更聪明地处理“量化误差”。
5.1.4 GPTQ 的核心:重构误差
考虑某个 Linear Layer:
Y=WX Y=WX Y=WX
量化以后:
Yq=WqX Y_q=W_qX Yq=WqX
我们希望:
Yq≈Y Y_q\approx Y Yq≈Y
因此希望最小化:
∥WX−WqX∥2 \boxed{ \|WX-W_qX\|^2 } ∥WX−WqX∥2
也可以写成:
∥(W−Wq)X∥2 \boxed{ \|(W-W_q)X\|^2 } ∥(W−Wq)X∥2
这就是一个非常重要的理解。
不只最小化 ∥W−Wq∥2\|W-W_q\|^2∥W−Wq∥2 的原因
因为模型最终不是直接输出:
W W W
而是计算:
WX WX WX
也就是说:
权重是通过输入参与模型计算的。
所以真正影响下一层的是:
WX WX WX
而不是单独的:
W W W
这就解释了为什么 GPTQ 需要 Calibration Data。
5.1.5 Calibration Dataset 在 GPTQ 中做什么?
第三章已经介绍过 Calibration。
现在我们可以更准确地理解它在 GPTQ 中的用途。
假设:
GPTQ 通过这些代表性数据,观察:
模型各个 Layer 在典型输入下会看到什么样的输入。
这样就可以判断:
哪些方向的权重误差更加敏感。
所以 Calibration Dataset 并不是为了:
重新训练模型。
也不是推理时要用的数据,而是只在量化(quantize)这一步被用到:
为量化动作提供所需的输入统计信息(如各 Layer 的输入分布 / Hessian 信息),用来决定权重怎么取整、如何补偿误差。
量化完成后,Calibration 数据就不再需要,推理阶段也不会用到它。
5.1.6 Calibration 数据不能随便选
这是一个非常容易被忽略的问题。
假设最终应用场景是:
中文技术问答。
但 Calibration 数据全部是:
英文小说。
可能会导致:
真实数据分布
↓
中文技术问题
Calibration 分布
↓
英文自然语言
两者差异很大。
如果模型的内部 Activation 分布因此不同,那么:
GPTQ 在量化时获得的信息就可能不够贴近真实应用。
因此:
Calibration Data≈真实业务分布 \boxed{ Calibration\ Data \approx 真实业务分布 } Calibration Data≈真实业务分布
通常是一个重要的工程原则。
5.1.7 GPTQ 中的 Hessian
Hessian 到底是什么?
这是 GPTQ 最容易让读者望而却步的部分。
我们不妨先不要直接钻进复杂矩阵推导。
先理解:
一阶信息
f′(x) f'(x) f′(x)
告诉你:
往哪个方向变化。
二阶信息
f′′(x) f''(x) f′′(x)
进一步告诉你:
这个方向有多敏感。
因此可以粗略理解成:
一阶信息
→ 方向
二阶信息
→ 曲率 / 敏感程度
GPTQ 利用近似 Hessian 信息来帮助进行更加合理的量化与误差补偿。
为什么要用 Hessian
在讲清楚 Hessian 是什么之前,先回答一个更根本的问题:GPTQ 为什么非要用它?
原因在于,量化的真正目标不是让权重本身尽量接近,而是让层的输出尽量接近:
minWq∥WX−WqX∥2 \min_{W_q}\|WX-W_qX\|^2 Wqmin∥WX−WqX∥2
如果按"简单量化"那样,只最小化 ∥W−Wq∥2\|W-W_q\|^2∥W−Wq∥2,就等于假设每个权重对输出的影响都一样大。但事实并非如此——权重是通过 WXWXWX 参与计算的,不同权重经由输入 XXX 之后,对输出的影响天差地别:
有的权重稍微量化偏一点,输出几乎不变;
有的权重只偏一点点,输出就明显变化。
我们需要一把"尺子"来衡量每个权重方向对输出误差有多敏感,并据此决定:
- 哪些权重要量化得更准;
- 某个权重量化后产生的误差,应该如何补偿到其他权重上。
这把"敏感度尺子"正是本节前面(5.1.7)讲的二阶信息(曲率),在数学上就是重构误差目标的 Hessian。换句话说,用 Hessian 是为了把"权重误差"翻译成"输出误差",让量化朝着"输出影响最小"的方向进行,而不是盲目地让权重数值最接近。
下面就来看这个 Hessian 具体长什么样。
Hessian 与输入的关系
对于:
Y=WX Y=WX Y=WX
它的误差目标:
∥(W−Wq)X∥2 \|(W-W_q)X\|^2 ∥(W−Wq)X∥2
可以写成与:
XXT XX^T XXT
有关的二次型。这里出现的 XXTXX^TXXT 正是该层重构误差对权重的 Hessian(局部 Hessian,H=XXTH=XX^TH=XXT,差一个常数系数),它由 Calibration 数据估计得到——并不是整个模型训练损失的 Hessian。
因此可以用直观的方式表示:
因此:
GPTQ 并不是凭空决定哪个权重重要。
而是:
借助 Calibration 数据获得的输入结构,估计哪些方向对输出更加敏感。
5.1.8 GPTQ 的“逐步量化 + 误差补偿”
这是理解 GPTQ 最核心的一张逻辑图。
假设一列权重:
w1,w2,w3,w4 w_1,w_2,w_3,w_4 w1,w2,w3,w4
我们量化:
w1
↓
q1
此时产生误差:
e1=w1−q1 e_1=w_1-q_1 e1=w1−q1
简单方法:
直接把这个误差留下。
GPTQ:
继续:
所以可以把 GPTQ 记成:
量化 + 补偿 + 再量化。
这就是它比独立 Round 更聪明的地方。
“补偿”到底补什么、补多少
先回到目标:我们要最小化的是层输出误差
∥(W−Wq)X∥2 \|(W-W_q)X\|^2 ∥(W−Wq)X∥2
对同一层里权重的一行 w=[w1,w2,…,wn]w=[w_1,w_2,\dots,w_n]w=[w1,w2,…,wn] 来说,把它写成关于权重扰动 Δw=w−wq\Delta w=w-w_qΔw=w−wq 的二次型:
E(Δw)≈12 Δw⊤H Δw,H=XX⊤ E(\Delta w)\approx \tfrac{1}{2}\,\Delta w^{\top} H\,\Delta w, \qquad H = XX^{\top} E(Δw)≈21Δw⊤HΔw,H=XX⊤
这里的 HHH 就是前面 5.1.7 讲的 Hessian。关键点:HHH 一般不是对角矩阵,也就是说各个权重并不是相互独立的——HijH_{ij}Hij 描述了"动 wiw_iwi 会怎样影响到 wjw_jwj 对输出的贡献"。正因为它们相关,量化 w1w_1w1 造成的误差,才可以通过微调其他还没量化的权重 w2,…,wnw_2,\dots,w_nw2,…,wn 来抵消一部分。
补偿的直觉
- 量化 w1→q1w_1 \to q_1w1→q1,产生误差 e1=w1−q1e_1 = w_1 - q_1e1=w1−q1。
- 这个 e1e_1e1 会让输出偏一点。
- 但 w2,…,wnw_2,\dots,w_nw2,…,wn 还没量化,还是"自由"的浮点数。
- 于是 GPTQ 顺着 HHH 里记录的相关性,把 w2,…,wnw_2,\dots,w_nw2,…,wn 各自朝能抵消 e1e_1e1 影响的方向挪一点,让整层输出尽量回到原来的样子。
- 挪完之后再量化 w2w_2w2,如此逐个进行。
补偿量的公式
对"量化第 qqq 个权重、把误差摊到其余未量化权重"这个操作,GPTQ(沿用 OBS / OBQ 的闭式解)给出的更新是:
δrest=− wq−quant(wq)[H−1]qq (H−1):,q \boxed{\;\delta_{\text{rest}} = -\,\frac{w_q - \text{quant}(w_q)}{[H^{-1}]_{qq}}\;\big(H^{-1}\big)_{:,q}\;} δrest=−[H−1]qqwq−quant(wq)(H−1):,q
拆开看这三块:
- wq−quant(wq)w_q - \text{quant}(w_q)wq−quant(wq):当前这个权重被量化产生的误差 eqe_qeq(补偿的"源头")。
- [H−1]qq[H^{-1}]_{qq}[H−1]qq:这个权重方向的"敏感度归一化项"(H−1H^{-1}H−1 的第 qqq 个对角元)。
- (H−1):,q\big(H^{-1}\big)_{:,q}(H−1):,q:H−1H^{-1}H−1 的第 qqq 列——它告诉每个未量化权重应该按什么比例分摊这份误差。
一句话:误差 eqe_qeq 越大、或某个未量化权重与它越相关,那个权重就被调整得越多;调整方向恰好是"抵消 eqe_qeq 对输出影响"的方向。这一步之后,剩余权重的误差目标里就把 wqw_qwq 的影响"消化"掉了。
为什么必须用 HHH(而不是简单平均)
如果不看二阶信息,你顶多能把误差"平均"分给其他权重,但那忽略了权重之间的相关性,往往越补越糟。H−1H^{-1}H−1 精确地编码了"每个未量化权重对输出的边际影响",所以能算出最优的分摊比例——这正是"利用二阶信息补偿"的含义。
逐列进行:一次只解一个变量
GPTQ 不会一次性解整个大矩阵,而是按列(逐个权重)顺序量化:
每量化一个权重,就:
- 记录它的量化误差 eqe_qeq;
- 用上面的公式把 eqe_qeq 摊到还没处理的权重上(真正修改它们的浮点值);
- 把 H−1H^{-1}H−1 做一次相应的降维更新(Cholesky / 高斯消元式的增量更新),保证后续列的补偿依然最优。
因为每一步都是"解一个变量 + 闭式补偿",GPTQ 才能在不反向传播、不重训练的前提下,一次遍历就完成整层量化,同时把输出误差压到很低。
这也解释了 5.1.5 节说的:Calibration 数据的唯一作用,就是估出这个 H=XX⊤H = XX^{\top}H=XX⊤——有了它,上面所有补偿量才算得出来。
误差是怎么被“消化”的:OBS 闭式解与 Cholesky 加速
前面给出了补偿公式,但还没回答一个更本质的问题:为什么这样补偿是"最优"的?它又怎么做到既快又稳? 这一节把"消化误差"这个动作背后的算法讲透。
要解的其实是一个带整数约束的二次优化
对一层权重的一行 w∈Rnw\in\mathbb{R}^nw∈Rn,目标是:
minwq E(wq)=(w−wq)⊤H (w−wq),H=XX⊤ \min_{w_q}\;E(w_q)=(w-w_q)^{\top}H\,(w-w_q),\qquad H=XX^{\top} wqminE(wq)=(w−wq)⊤H(w−wq),H=XX⊤
约束是 wqw_qwq 每个分量都必须落在量化格点上。这是一个带整数约束的二次优化,直接求解是 NP 难的(通俗地说:量化前的 FP16 权重格点极密,相对 INT4 可近似看作"连续"、几乎能取任意值;可量化后每个权重只能从少数几个离散格点里挑一个,4-bit 就是 16 个候选值,一层有 nnn 个权重就有 16n16^{n}16n 种组合——nnn 稍大就是天文数字,无法穷举出全局最优)。GPTQ 的做法是逐个权重贪心量化,但每敲定一个,就用二阶信息把它的误差最优地摊进剩下的自由权重——这就是"消化"。
OBS 闭式解:固定一个权重,最优微调其余权重
这套思想源自 Optimal Brain Surgeon(OBS)。核心子问题是:
把第 qqq 个权重强制取整成格点值,同时允许微调其余所有权重,怎样调能让 EEE 增加得最少?
因为 EEE 是二次型,这个"钉死一个变量、优化其余"的子问题有闭式最优解:
(1) 当前权重取整产生的误差:
eq=wq−quant(wq) e_q=w_q-\text{quant}(w_q) eq=wq−quant(wq)
(2) 对所有剩余权重的最优补偿(消化误差):
δ=− eq[H−1]qq (H−1):,q \boxed{\;\delta=-\,\frac{e_q}{[H^{-1}]_{qq}}\,\big(H^{-1}\big)_{:,q}\;} δ=−[H−1]qqeq(H−1):,q
(3) 这一步造成的最小误差增量:
ΔE=eq2[H−1]qq \Delta E=\frac{e_q^{2}}{[H^{-1}]_{qq}} ΔE=[H−1]qqeq2
推导上是对"未量化权重"在"第 qqq 个变量被钉死"的等式约束下用拉格朗日乘子法求极小。直觉是:H−1H^{-1}H−1 的第 qqq 列描述了"只动 wqw_qwq 会如何牵动其他权重对输出的贡献",顺着这一列反向挪,正好抵消 wqw_qwq 取整带来的输出偏移。
ΔE\Delta EΔE 那个式子还顺带给出了一个重要提示:[H−1]qq[H^{-1}]_{qq}[H−1]qq 越小(方向越敏感),同样的 eqe_qeq 造成的误差越大——这正是"敏感权重优先处理"(act-order)的依据。
朴素做法太慢
如果每量化一列,就重新求一次 H−1H^{-1}H−1 并删去对应行列,单步是 O(n3)O(n^3)O(n3)、整层 O(n4)O(n^4)O(n4)。对 LLM 动辄几千维的层根本跑不动。
GPTQ 的三个工程关键
1. 固定顺序 + 一次性 Cholesky。 不再每步重算逆矩阵,而是先算一次 H−1H^{-1}H−1,再做 Cholesky 分解。Cholesky 因子的每一行恰好编码了"处理到第 qqq 列时,误差该按什么比例摊给后面各列"的系数。逐列量化时直接读因子对应行,O(n)O(n)O(n) 拿到补偿系数,无需再更新逆矩阵。整层复杂度从 O(n4)O(n^4)O(n4) 降到约 O(n2)O(n^2)O(n2)。
2. 阻尼(damping)。 HHH 可能病态、接近奇异,求逆会数值爆炸。给对角加一个小阻尼:
H←H+λ mean(diag(H)) I H\leftarrow H+\lambda\,\text{mean}\big(\text{diag}(H)\big)\,I H←H+λmean(diag(H))I
λ\lambdaλ 典型取 1e-2,保证正定、[H−1]qq[H^{-1}]_{qq}[H−1]qq 不至于过小,从而补偿量 δ\deltaδ 不会失控。这也是防止"误差越滚越大、把尾部权重推飞"的关键正则。
3. 分块批量更新(lazy batch update)。 一层有成千上万列,GPTQ 把列切成小块(如 128 列一块):块内逐列做"量化+补偿",块内产生的对后续列的补偿先累积起来,处理完一块后一次性用矩阵乘法作用到后面尚未处理的块,充分利用 GPU。
逐列流程
贴近实现的伪代码
输入: 权重 W (out×in), Hessian H = X·Xᵀ (in×in), 量化器 quant(·)
1. H += λ·mean(diag(H))·I # 阻尼,保证正定
2. Hinv = Cholesky(inverse(H)) # 一次性分解,得到三角因子
3. for 每个列块 (blocksize = 128):
4. for 块内每一列 q:
5. w = W[:, q] # 当前列(浮点,可能已被前面补偿过)
6. q_int = quant(w) # 用固定 scale round 到格点
7. err = (w - q_int) / Hinv[q, q] # 归一化误差 = e_q / [H⁻¹]_qq
8. W[:, q] = q_int # 敲定这一列(冻结)
9. # 消化:按 Cholesky 因子把 err 摊给块内“后面还没处理”的列
10. W[:, q+1:blockend] -= err ⊗ Hinv[q, q+1:blockend]
11. 记录 err
12. # 块间:把本块累积的补偿一次性作用到后续所有未处理列
13. W[:, blockend:] -= (本块 err 矩阵) @ Hinv[block, blockend:]
14. 输出: 量化后的 W(每列 INT4)+ 固定的 scale
第 7、10 行就是"消化误差"的核心:err 是当前列的归一化量化误差,第 10 行把它按 Cholesky 因子给出的比例减到后面每一列上——这就是把误差"吃进"后续权重的动作。
一句话理解
消化误差 = 每敲定一个权重的整数值,就用 Hessian(H−1H^{-1}H−1 及其 Cholesky 因子)算出一组对"尚未量化权重"的最优微调量,把这一步取整造成的输出偏移,在还自由的权重上就地抵消掉。 Cholesky 让这组系数一次算好、逐列直接读取,damping 保证数值稳定,分块保证 GPU 效率。
5.1.9 GPTQ 非常适合 4-bit 的原因
因为随着 bit 数下降:
16bit→8bit→4bit 16bit\rightarrow8bit\rightarrow4bit 16bit→8bit→4bit
离散状态越来越少。
例如:
INT8:28=256 INT8:2^8=256 INT8:28=256
而:
INT4:24=16 INT4:2^4=16 INT4:24=16
所以:
4-bit 量化面对的量化误差问题明显更加严重。
如果只是简单 Round:
更容易破坏模型能力。
而 GPTQ 通过优化量化误差:
可以尽量降低这种影响。
这也是为什么 GPTQ 在 4-bit LLM 压缩中非常有代表性。
5.1.10 Group-wise GPTQ
GPTQ 中经常会看到:
group_size=128
这个概念前面已经介绍过。
可以理解成:
例如:
GroupSize=128 GroupSize=128 GroupSize=128
表示每 128 个权重共享一套相关量化参数。
不用整个矩阵一个 Scale 的原因
假设整个矩阵:
大多数权重:
[-0.2, 0.3]
少数权重:
[-8.0, 7.5]
如果只用一个 Scale:
为了照顾极大的权重,需要使用一个很大的范围。
结果大量普通权重:
会失去很多量化分辨率。
所以:
更细的 Group 能让每一部分使用更合适的量化范围。
5.1.11 Group Size 的影响
假设:
GroupSize=32 GroupSize=32 GroupSize=32
那么:
量化粒度更细。
通常:
- 更灵活
- 更容易适应局部权重分布
- 需要更多 Scale 等辅助信息
如果:
GroupSize=256 GroupSize=256 GroupSize=256
则:
量化粒度更粗。
所以:
GroupSize↔量化精度↔辅助开销 \boxed{ GroupSize \leftrightarrow 量化精度 \leftrightarrow 辅助开销 } GroupSize↔量化精度↔辅助开销
是一种典型的工程权衡。
5.1.12 GPTQ 常见配置
实际量化时,经常看到:
GPTQConfig(
bits=4,
group_size=128,
...
)
常见参数包括:
bits
4
表示:
使用 4-bit 权重。
group_size
128
表示:
每组 128 个权重。
dataset
表示:
Calibration Dataset。
tokenizer
负责把 Calibration Text 转换成 Token IDs。
5.2 GPTQ 实战
5.2.1 当前 GPTQ 工具链
这里需要特别注意一个现实问题:
网上很多 GPTQ 教程仍然使用:
pip install auto-gptq
但这已经不是当前推荐路线。
Hugging Face 当前文档明确说明:
AutoGPTQ 已不再受 Transformers 支持。
当前推荐:
GPT-QModel
其 Python 包名是:
gptqmodel
GPT-QModel 是当前在 Transformers 生态中维护的 GPTQ 后端,并提供了包括非对称量化等在内的一些能力。
因此本系列后续文章应优先使用:
GPT-QModel
而不是:
AutoGPTQ
5.2.2 安装 GPTQ 实战环境
先说明本章实战所用的环境(读者可对照自己的环境):
Python : 3.12
PyTorch : 2.7.0+cu126 (CUDA 12.6)
本章安装的是 gptqmodel 2.2.0 这个版本。
根据当前 Hugging Face 文档,可以先安装:
pip install --upgrade accelerate optimum transformers
然后安装 gptqmodel:
pip install "gptqmodel==2.2.0" --no-build-isolation
⚠️ 安装前请务必注意:如果不指定版本、直接
pip install gptqmodel安装最新版本,它可能会升级你环境里已经装好的 torch 和 CUDA 相关库(新版 gptqmodel 要求更高的 torch 版本),从而破坏原本已经跑通的环境。因此建议在正式安装前,先执行
--dry-run预览它到底会安装/升级哪些包:pip install gptqmodel --no-build-isolation --dry-run查看输出的
Would install清单,如果里面出现了比你当前更新的torch、triton、nvidia-cuda等库,就说明它会改动你的底层环境,应改为指定一个兼容当前 torch 的旧版本(如本章的gptqmodel==2.2.0)再安装,以防环境被破坏。
安装完成以后:
python -c "import gptqmodel; print('GPTQModel OK')"
如果能够正常导入:
GPTQModel OK
说明基础环境已经准备完成。
5.2.3 选择量化模型:从小模型跑通到真实规模
学习真实量化时,不建议第一步直接使用 70B。
因为 GPTQ 本身需要:
- 加载模型
- 读取 Calibration Dataset
- 执行 Layer
- 进行量化
- 保存结果
模型过大时:
很容易把问题变成“显存不够”。
因此推荐的学习路径是:先用小模型把整套流程跑通、理解参数,再换到真实规模的模型做 Benchmark。
本章下面的代码示例,直接使用一个接近真实规模的模型来演示完整流程:
facebook/opt-6.7b
它在 24GB 显存的 GPU 上即可量化。你只需把示例里的 MODEL_ID 改成别的模型(如 facebook/opt-125m、facebook/opt-1.3b)即可复用同一套代码——本节末尾(5.2.10.1)会给出三个模型的实测对比。
💡 提示(量化对象的架构限制):本章示例用 gptqmodel
2.2.0+ transformers4.56.2。在这个组合下量化 Qwen2 系列会在apply_rotary_pos_emb处报The size of tensor a (...) must match the size of tensor b (...)——这是旧版 gptqmodel 跟不上新版 transformers 的 Qwen2 RoPE 实现所致。OPT 系列(无 RoPE)不受影响,可正常量化。若必须量化 Qwen2,需要升级到torch>=2.8+ 新版 gptqmodel(建议单独新建虚拟环境)。
5.2.4 加载 Tokenizer
从这一步开始会第一次联网下载模型文件。国内网络可先设置镜像与传输选项(必须放在 import transformers / datasets 之前):
import os os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" # 使用国内镜像加速下载 os.environ["HF_HUB_DISABLE_XET"] = "1" # 禁用 xet 传输,避免部分环境下载异常
from transformers import AutoTokenizer
MODEL_ID = "facebook/opt-6.7b" # 换成 opt-125m / opt-1.3b 即可复用本套代码
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
这里得到的 Tokenizer 主要负责:
5.2.5 准备 Calibration 数据集与量化配置
准备校准数据集。 GPTQ 需要一批代表性文本来估计每层的输入统计(H=XX⊤H=XX^\topH=XX⊤)。gptqmodel 的 quantize() 直接接受一个字符串列表作为校准集:
from datasets import load_dataset
# 要求:样本数尽量 >= 256,且每条文本尽量长(> 256 token)
ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_dataset = [t for t in ds["text"] if len(t.strip()) > 256][:256]
💡 提示(校准数据要充分):如果只给几十条很短的文本(例如手写几句、复制成 32 条),gptqmodel 会警告
Calibration dataset size should be more than 256、average length ... should be greater than 256,并且量化后模型生成质量明显变差(容易重复输出)。这与 5.1.5 / 5.1.6 讲的“校准数据要充分、要贴近真实分布”完全一致——本节末尾的对比会直观展示这个差异。
创建量化配置。
from gptqmodel import QuantizeConfig
quant_config = QuantizeConfig(
bits=4, # 4-bit
group_size=128, # 每组 128 个权重共享量化参数
)
这里最重要的就是两个参数:
bits=4
group_size=128
即:
4-bit + Group-wise 量化。
💡 提示(原生 API vs GPTQConfig):Hugging Face 文档演示的是
transformers.GPTQConfig(内部由 optimum 转调 gptqmodel)。但较新版 optimum 会要求gptqmodel>=7.0.0(进而要求torch>=2.8);在torch 2.7等较旧环境下这条路走不通。本章改用 gptqmodel 原生 API(QuantizeConfig+GPTQModel.load+model.quantize),做的是完全相同的 GPTQ 算法,只是绕开了 optimum 的版本约束。
5.2.6 开始量化
from gptqmodel import GPTQModel
# 加载 FP 模型(配好量化配置)
model = GPTQModel.load(MODEL_ID, quant_config)
# 用校准数据执行量化(这一步才是真正的 GPTQ 计算)
model.quantize(calibration_dataset, batch_size=1, tokenizer=tokenizer)
GPTQModel.load(...) 负责加载原始 FP 模型,model.quantize(...) 才真正触发完整的量化流程:
量化过程中会逐层打印每个 module(q_proj/k_proj/v_proj/fc1/fc2 等)的重构 loss,这正是 5.1.4 讲的层输出重构误差。
5.2.7 第一次量化比较慢的原因
因为 GPTQ:
不是简单的
float16 → int4数据类型转换。
它需要进行:
所以:
量化属于离线计算成本。
我们愿意在离线阶段花时间,是因为:
以后部署时,可以得到一个更小的模型。
这是一种典型的:
Offline Cost→Runtime Savings \boxed{ Offline\ Cost \rightarrow Runtime\ Savings } Offline Cost→Runtime Savings
5.2.8 保存量化模型
量化结束后:
QUANT_DIR = "./opt-6.7b-gptq"
model.save(QUANT_DIR)
tokenizer.save_pretrained(QUANT_DIR)
保存后目录里大致包含:
opt-6.7b-gptq/
├── config.json # 内含 quantization_config
├── quantize_config.json
├── model...(权重)
├── tokenizer...
└── ...
具体文件名称和数量会根据版本及模型实现有所不同。
5.2.9 重新加载 GPTQ 模型
保存成功以后,我们再重新加载:
from gptqmodel import GPTQModel
from transformers import AutoTokenizer
QUANT_DIR = "./opt-6.7b-gptq"
q_tokenizer = AutoTokenizer.from_pretrained(QUANT_DIR)
q_model = GPTQModel.load(QUANT_DIR) # 直接加载已量化模型
这时候已经不是:
FP16 Model
而是:
GPTQ Quantized Model
加载时 gptqmodel 会自动挑选可用的推理 kernel(如 MarlinQuantLinear),这对应 5.3.6 讲的“由专门的量化 Kernel 处理低比特权重”。
5.2.10 进行一次推理
import torch
prompt = "Artificial intelligence is"
inputs = q_tokenizer(
prompt,
return_tensors="pt"
).to(q_model.device)
with torch.no_grad():
outputs = q_model.generate(
**inputs,
max_new_tokens=50,
)
text = q_tokenizer.decode(
outputs[0],
skip_special_tokens=True,
)
print(text)
只要能够正常生成文本,就说明:
整个链路已经跑通。
5.2.10.1 完整脚本与实测结果
把 5.2.4~5.2.10 各步拼成一个可直接运行的脚本(含模型大小 / 显存 / 速度三个 benchmark)。只需修改开头的 MODEL_ID / QUANT_DIR 就能换模型:
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" # 使用国内镜像加速下载
os.environ["HF_HUB_DISABLE_XET"] = "1" # 禁用 xet 传输,避免部分环境下载异常
import time
import torch
from pathlib import Path
from gptqmodel import GPTQModel, QuantizeConfig
from transformers import AutoTokenizer
from datasets import load_dataset
# ============ 配置 ============
MODEL_ID = "facebook/opt-6.7b" # 可换成 facebook/opt-125m、facebook/opt-1.3b
QUANT_DIR = "./opt-6.7b-gptq"
PROMPT = "Artificial intelligence is"
def get_directory_size(path):
total = sum(f.stat().st_size for f in Path(path).rglob("*") if f.is_file())
return total / 1024**3
def run_inference(model, tokenizer, prompt, max_new_tokens=50):
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
if torch.cuda.is_available():
torch.cuda.synchronize()
t0 = time.perf_counter()
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=max_new_tokens)
if torch.cuda.is_available():
torch.cuda.synchronize()
dt = time.perf_counter() - t0
new_tokens = out.shape[-1] - inputs["input_ids"].shape[-1]
text = tokenizer.decode(out[0], skip_special_tokens=True)
return text, new_tokens / dt
# 1. tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
# 2. 校准数据(真实数据集,>=256 条)
ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_dataset = [t for t in ds["text"] if len(t.strip()) > 256][:256]
# 3. 量化配置
quant_config = QuantizeConfig(bits=4, group_size=128)
# 4. 加载 + 量化
t0 = time.perf_counter()
model = GPTQModel.load(MODEL_ID, quant_config)
model.quantize(calibration_dataset, batch_size=1, tokenizer=tokenizer)
print(f"量化耗时: {time.perf_counter() - t0:.1f}s")
# 5. 保存
model.save(QUANT_DIR)
tokenizer.save_pretrained(QUANT_DIR)
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
# 6. 重新加载 + benchmark
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
q_model = GPTQModel.load(QUANT_DIR)
q_tokenizer = AutoTokenizer.from_pretrained(QUANT_DIR)
text, tps = run_inference(q_model, q_tokenizer, PROMPT)
peak = torch.cuda.max_memory_allocated() / 1024**3 if torch.cuda.is_available() else 0
size = get_directory_size(QUANT_DIR)
print(f"模型大小: {size:.2f} GB | 峰值显存: {peak:.2f} GB | 速度: {tps:.1f} tokens/s")
print("生成结果:\n", text)
实测结果(RTX 4090,bits=4, group_size=128)。 除主示例 opt-6.7b 外,另用同一套脚本跑了 opt-125m、opt-1.3b 作对比:
| 模型 | 层数 | FP16 原始 | GPTQ 4-bit | 压缩比 | 量化耗时 | 推理速度 | 推理峰值显存 |
|---|---|---|---|---|---|---|---|
| opt-125m | 12 | 0.47 GB | 0.12 GB | 75.0% | 43s | 294.7 t/s | 0.13 GB |
| opt-1.3b | 24 | 4.90 GB | 0.79 GB | 84.0% | 266s | 191.5 t/s | 0.81 GB |
| opt-6.7b | 32 | 24.81 GB | 3.52 GB | 85.8% | 1260s | 133.3 t/s | 3.69 GB |
两个结论:
- 模型越大,压缩比越高(越接近理论 4×):75.0% → 84.0% → 85.8%。因为量化只压 Linear 层权重,而 embedding / layernorm / bias 等不被量化的部分在小模型里占比更大;模型越大可量化权重占比越高(对应 5.3.7)。
- 显存收益显著:24.81 GB 的 opt-6.7b 量化后推理峰值显存仅 3.69 GB,这正是量化让“同一块 GPU 跑更大模型”的价值(对应 5.3.2 / 5.3.7)。
💡 提示(校准数据的效果对照):早期用不足的校准数据(约 32 条、平均 11 token)量化 opt-125m / opt-1.3b 时,生成会明显重复退化(如反复输出 “Artificial intelligence is a technology…”);改用 wikitext-2 的 256 条真实长文本后(如脚本步骤 2),警告消除、生成明显更通顺。这实证了 5.1.5 / 5.1.6:校准数据越充分、越贴近真实分布,GPTQ 估计的 H=XX⊤H=XX^\topH=XX⊤ 越准,量化质量越好。
5.2.11 但“能生成”远远不够
很多量化教程到这里就结束了:
“模型可以成功输出,所以量化成功。”
从工程角度看是不够的。
我们真正应该回答:
模型到底变小了多少?
显存到底降低了多少?
速度有没有变快?
精度损失了多少?
因此下一步必须 Benchmark。
5.3 Benchmark 与评测
量化到底好不好,不能只看"能不能生成",要用一组指标衡量。我们关心 5 个 Benchmark,可以分成两类:
- 资源类(量化带来的收益):① 模型大小、② 显存、③ 速度(Tokens/s)——回答"省了多少、快不快"。
- 质量类(量化付出的代价):④ 量化误差、⑤ Perplexity——回答"模型能力掉了多少"。
评测的核心思路始终是 FP16 原模型 vs GPTQ 量化模型的对比:资源类希望差距越大越好(省得多),质量类希望差距越小越好(掉得少)。
本节的 ①②③ 已在 5.2.10.1 用三个 OPT 模型实测(见那张对比表);④⑤ 下面用
facebook/opt-1.3b与facebook/opt-6.7b(FP16 vs 校准充分的 GPTQ)实测给出真实数字。
5.3.1 Benchmark 1:模型大小
首先比较模型在磁盘上的体积(FP16 目录 vs GPTQ 目录):
from pathlib import Path
def get_directory_size(path):
path = Path(path)
total = 0
for file in path.rglob("*"):
if file.is_file():
total += file.stat().st_size
return total / 1024**3
print("GPTQ model size:", get_directory_size("./opt-1.3b-gptq"), "GB")
4-bit 的理论上限是压缩到 FP16 的 1/4(省 75%),实测通常还更高一点(因为权重占大头)。以 5.2.10.1 的实测为例:
opt-1.3b: FP16 4.90 GB → GPTQ 0.79 GB (压缩 84.0%)
opt-6.7b: FP16 24.81 GB → GPTQ 3.52 GB (压缩 85.8%)
模型越大,可量化的 Linear 权重占比越高,压缩比越接近上限。
5.3.2 Benchmark 2:显存
推理时真正要关心的是运行峰值显存(max_memory_allocated),而不是某一刻的 memory_allocated:
import torch
torch.cuda.reset_peak_memory_stats() # 推理前清零峰值统计
# ... 执行一次推理 ...
print("Peak VRAM:", torch.cuda.max_memory_allocated() / 1024**3, "GB")
量化的核心价值之一就是降低显存、让同一块卡跑更大的模型。以 5.2.10.1 实测为例,opt-6.7b 的 FP16 约需 13 GB 显存,而 GPTQ 4-bit 版本推理峰值显存仅 3.69 GB——一张 24GB 的卡因此能轻松容纳原本吃紧的模型。
5.3.3 Benchmark 3:Tokens/s
定义:
Tokens/s=GeneratedTokensTime Tokens/s = \frac{GeneratedTokens}{Time} Tokens/s=TimeGeneratedTokens
import time
import torch
prompt = "Artificial intelligence is"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
if torch.cuda.is_available():
torch.cuda.synchronize() # 关键:CUDA 异步,计时前先同步
start = time.perf_counter()
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=100)
if torch.cuda.is_available():
torch.cuda.synchronize() # 计时后再同步
elapsed = time.perf_counter() - start
generated_tokens = outputs.shape[1] - inputs["input_ids"].shape[1]
print("Tokens/s:", generated_tokens / elapsed)
一定要注意:CUDA 是异步执行的,
generate返回不代表 GPU 算完。计时前后都要torch.cuda.synchronize(),否则测出来的时间偏小、Tokens/s 虚高。
4-bit 权重更小、显存带宽压力更低,通常能加速推理,但 “4-bit ≠ 4 倍速度”——真实速度取决于 kernel(本章加载时自动选了 Marlin)、GPU 架构、batch size 等,必须实测。实测 FP16 与 GPTQ 的推理速度对比(贪心解码,生成 100 token):
| 模型 | FP16 速度 | GPTQ 4-bit 速度 | 加速比 |
|---|---|---|---|
| opt-1.3b | 166.4 t/s | 191.5 t/s | ×1.15 |
| opt-6.7b | 57.2 t/s | 133.3 t/s | ×2.33 |
两点值得注意:
- 量化确实带来加速,但都远没到 4 倍——加速幅度取决于模型规模与瓶颈类型。
- 模型越大,加速越明显:opt-1.3b 只快 15%,opt-6.7b 快到 2.3 倍。这是因为大模型推理主要卡在显存带宽(权重搬运是主要开销),4-bit 权重只有 FP16 的 1/4,需要搬运的数据量大幅下降,于是加速显著;而小模型计算量本就不大,反量化的额外开销把加速大部分抵消了。
5.3.4 Benchmark 4:量化误差
量化误差衡量"量化后模型的行为偏离原模型多少"。有两个层面:
(1) 权重层面(最简单)——量化前后权重的平均绝对误差:
MAE=1N∑i∣Wi−W^i∣ MAE = \frac1N \sum_i |W_i - \hat{W}_i| MAE=N1i∑∣Wi−W^i∣
(2) 输出层面(更有意义)——同一个 prompt 下,FP16 与 GPTQ 的输出 logits 差多少。因为我们真正关心的是 WXWXWX(层输出)而非 WWW 本身(呼应 5.1.4),所以对比输出更能反映真实影响:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from gptqmodel import GPTQModel
MODEL_ID = "facebook/opt-1.3b"
QUANT_DIR = "./opt-1.3b-gptq"
PROMPT = "Artificial intelligence is"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
# FP16:取该 prompt 的输出 logits
fp16 = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.float16, device_map="auto")
ids = tokenizer(PROMPT, return_tensors="pt").to(fp16.device)["input_ids"]
with torch.no_grad():
fp16_logits = fp16(ids).logits.float().cpu()
del fp16; torch.cuda.empty_cache()
# GPTQ:同一 prompt 的输出 logits
q = GPTQModel.load(QUANT_DIR)
ids_q = tokenizer(PROMPT, return_tensors="pt").to(q.device)["input_ids"]
with torch.no_grad():
q_logits = q(ids_q).logits.float().cpu()
# 输出 logits 的平均绝对误差
mae = (fp16_logits - q_logits).abs().mean().item()
print(f"输出 logits MAE: {mae:.4f}")
实测结果(输出 logits MAE):
opt-1.3b: MAE = 0.2715
opt-6.7b: MAE = 0.1166 ← 模型越大,量化误差越小
同时对比两者在同一 prompt 上的生成(以 opt-6.7b 为例,贪心解码):
FP16: Artificial intelligence is a hot topic in the tech world, and it's not
hard to see why. AI is a technology that can be used to solve a wide
variety of problems ... a new report from the United Nations' OHCHR ...
GPTQ: Artificial intelligence is a hot topic in the tech world, and it's not
hard to see why. AI is a technology that can be used to solve a wide
variety of problems ... a new report from the Center for Democracy and
Technology (CDT) ...
两者开头整段几乎逐字一致,说明量化基本保留了模型行为;到中段才分叉(一个引 UN 报告、一个引 CDT 报告),这是正常的——量化引入的微小扰动会改变贪心解码的轨迹。
需要说明的是,两段输出末尾都出现了大量重复句子。这不是量化导致的:benchmark 为了结果可复现,用的是贪心解码(do_sample=False),即每步都取概率最高的 token;这种确定性解码本身就容易陷入"重复循环",加上 OPT 是未经指令微调的 base 模型,更易复读——FP16 原模型在相同设置下同样会重复。因此:
单看某段生成文本会有误导,输出会"看起来不同"(甚至都在重复)但模型能力未必变化。判断量化质量要靠下一节的 Perplexity 这种客观指标(MAE 的完整对比也一并列在 5.3.5 的表里)。
5.3.5 Benchmark 5:Perplexity
Perplexity(困惑度,PPL) 是衡量语言模型好坏最常用的客观指标——模型对一段真实文本的"意外程度",越低越好。它比看某段生成文本客观得多,是判断"量化有没有伤到模型能力"的关键。
计算方法:在一个测试集(这里用 wikitext-2 test)上累加每个 token 的负对数似然,再取指数:
import torch
from datasets import load_dataset
def compute_ppl(model, tokenizer, n_samples=40, max_len=512):
ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="test")
texts = [t for t in ds["text"] if len(t.strip()) > 256][:n_samples]
nlls, total = [], 0
model.eval()
for t in texts:
ids = tokenizer(t, return_tensors="pt", truncation=True,
max_length=max_len).to(model.device)["input_ids"]
with torch.no_grad():
loss = model(ids, labels=ids).loss # 平均每 token 交叉熵
n = ids.shape[1]
nlls.append(loss.float() * n)
total += n
return torch.exp(torch.stack(nlls).sum() / total).item()
分别对 FP16 原模型和 GPTQ 量化模型调用 compute_ppl,即可对比。
实测结果:分别对 opt-1.3b、opt-6.7b 的 FP16 与校准充分的 GPTQ 计算 PPL(wikitext-2 test):
| 模型 | FP16 PPL | GPTQ 4-bit PPL | PPL 变化 | 输出 logits MAE |
|---|---|---|---|---|
| opt-1.3b | 32.129 | 32.693 | +0.564(+1.8%) | 0.2715 |
| opt-6.7b | 24.723 | 25.023 | +0.300(+1.2%) | 0.1166 |
这就是一次"成功的 4-bit 量化"应有的样子——PPL 只轻微上升(1~2 个百分点),语言建模能力基本保留,而模型体积却压掉了约 84~86%。判断质量看的就是这个"PPL 涨了百分之几":涨几个百分点通常可接受,涨几十个百分点甚至翻倍,就说明量化伤到了模型。
上表还揭示一个重要规律:模型越大,越"抗量化"。opt-6.7b 的 PPL 增幅(+1.2%)和 logits 误差(0.117)都比 opt-1.3b(+1.8%、0.272)更小——大模型参数冗余更多,4-bit 量化对它的相对影响更小。这也是为什么工程上对大模型做 4-bit 量化往往性价比极高。
💡 提示(校准数据决定成败——反面案例):同一个 opt-1.3b,若改用不足的校准数据(约 32 条、平均 11 token)量化,实测 GPTQ PPL 会飙到 约 1876(相比 FP16 的 32 暴涨约 5700%),生成也严重重复退化。对照校准充分时的 32.69(+1.8%),差距触目惊心。这正是 5.1.5 / 5.1.6 的直接实证:校准数据是否充分、是否贴近真实分布,直接决定 GPTQ 的成败。
当然:
Perplexity 不能代表所有下游任务。
因此更完整的评估还应该加入:
- 中文任务
- 数学任务
- 代码任务
- 指令跟随
- 推理能力
这些内容会在第8章统一展开。
5.3.6 GPTQ 的实际推理是不是“INT4 × FP16”?
这里需要特别澄清一个容易出现的误解。
假设:
WINT4 W_{INT4} WINT4
输入:
XFP16 X_{FP16} XFP16
并不意味着 GPU 一定直接执行:
INT4×FP16 INT4\times FP16 INT4×FP16
然后原样得到结果。
实际推理通常需要由专门的量化 Kernel 处理低比特权重,并在计算过程中进行必要的解量化/转换。当前 Hugging Face 的 GPTQ 文档也指出,GPTQ 权重可以在推理过程中恢复到 FP16 表示,同时利用低比特存储来减少内存使用。
可以先抽象成:
所以:
“4-bit 权重”不等于“整个模型所有计算都是 4-bit”。
5.3.7 GPTQ 的真正收益是什么?
可以总结为三个方面。
第一:减少模型存储
理论上:
FP16→INT4 FP16\rightarrow INT4 FP16→INT4
每个参数:
16bit→4bit 16bit\rightarrow4bit 16bit→4bit
所以理论存储量约降低:
4× \boxed{4\times} 4×
第二:减少权重显存
同样可以降低:
GPU 权重存储压力。
于是同一块 GPU 可以尝试运行:
更大的模型。
第三:减少数据搬运压力
如果低比特权重能够通过高效 Kernel 使用:
从内存读取的数据量可以下降。
因此可能降低:
Memory Bandwidth 压力。
但一定要注意:
4bit≠4倍速度 \boxed{ 4bit\neq4倍速度 } 4bit=4倍速度
真正速度必须 Benchmark。
5.4 局限、对比与总结
5.4.1 GPTQ 的局限
GPTQ 也不是万能的。
极低 bit 可能产生更大精度损失
例如:
4bit→3bit→2bit 4bit\rightarrow3bit\rightarrow2bit 4bit→3bit→2bit
bit 越低:
量化难度通常越高。
Calibration 数据会影响效果
Calibration:
如果和实际业务分布差距太大,量化结果可能受到影响。
不同硬件性能不同
同一个 GPTQ 模型:
GPU A
→ 快
GPU B
→ 慢
原因可能来自:
- Kernel
- GPU 架构
- Memory Bandwidth
- Backend
- Batch Size
所以:
量化算法和推理硬件必须一起考虑。
5.4.2 GPTQ 与 AWQ:下一步仍需要 AWQ 的原因
现在问题来了:
GPTQ 已经可以很好地进行 4-bit 量化,为什么还需要 AWQ?
因为:
GPTQ 和 AWQ 解决量化问题的思路不同。
GPTQ 更强调:
Layer Output Reconstruction
+
误差补偿
AWQ 更强调:
Activation-aware
+
保护重要权重通道
AWQ 论文指出,并非所有权重同等重要,可以根据 Activation 分布识别显著权重,并通过等价变换保护这些敏感部分;论文报告只需保护很小比例的显著权重即可显著降低量化误差。
因此下一章会自然进入:
第6章 AWQ 实战:为什么不是所有权重都同样重要?
届时我们会把:
GPTQ
vs
AWQ
放在一起比较。
5.4.3 本章知识总结
现在把 GPTQ 浓缩成一张图:
GPTQ 最核心的目标:
min∥WX−WqX∥2 \boxed{ \min \|WX-W_qX\|^2 } min∥WX−WqX∥2
而不是简单地:
min∥W−Wq∥2 \min \|W-W_q\|^2 min∥W−Wq∥2
这两者之间的区别,就是理解 GPTQ 的关键。
5.4.4 本章必须记住的 7 个关键词
1. PTQ
Post-Training Quantization \boxed{ Post\text{-}Training\ Quantization } Post-Training Quantization
训练完成之后进行量化。
2. Calibration
提供代表性输入,帮助量化器了解模型内部数值分布。
3. Weight-only
GPTQ 最典型的应用场景。
4. W4A16
Weight → 4-bit
Activation → 16-bit
5. Reconstruction Error
关注:
WX−WqX WX-W_qX WX−WqX
而不只是:
W−Wq W-W_q W−Wq
6. Hessian
提供近似二阶敏感性信息,用于帮助量化与误差补偿。
7. Group Size
控制量化粒度。
5.4.5 一句话理解 GPTQ
如果只允许记住一句话:
GPTQ 是一种 PTQ 权重量化方法,它利用 Calibration 数据和近似二阶信息,不是简单地逐个 Round 权重,而是尽量让量化后的 Layer 输出接近原模型,从而在 4-bit 等低比特下尽可能保持模型能力。
这就是 GPTQ 最核心的思想。

233

被折叠的 条评论
为什么被折叠?



