第5章 GPTQ 实战:如何把 Hugging Face 模型量化成 4-bit

第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 FP16INT8INT4

可以明显降低模型权重的存储需求。

对于一个 7B 模型,粗略估算:

7B×2Byte≈14GB 7B\times2Byte\approx14GB 7B×2Byte14GB

而 4-bit 权重:

7B×0.5Byte≈3.5GB 7B\times0.5Byte\approx3.5GB 7B×0.5Byte3.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}} WRdout×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 } WWq

之后:

WX→WqX \boxed{ WX\rightarrow W_qX } WXWqX

到底变化了多少。


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 理解成:

FP16 Model

Calibration

分析 Layer

GPTQ

4-bit Weight

Inference


5.1.3 GPTQ 与简单 Round 的区别

现在我们把两个方法直接放在一起。

简单量化

W

Scale

Round

Wq(INT4 码)

反量化 Ŵ = s·Wq

关注:

W−W^ W-\hat{W} WW^


GPTQ

W

Calibration

观察 Layer 输入

量化某部分权重

估计量化误差

对其他权重进行补偿

继续量化

它更关心:

WX−WqX \boxed{ WX-W_qX } WXWqX

所以:

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 YqY

因此希望最小化:

∥WX−WqX∥2 \boxed{ \|WX-W_qX\|^2 } WXWqX2

也可以写成:

∥(W−Wq)X∥2 \boxed{ \|(W-W_q)X\|^2 } (WWq)X2

这就是一个非常重要的理解。


不只最小化 ∥W−Wq∥2\|W-W_q\|^2WWq2 的原因

因为模型最终不是直接输出:

W W W

而是计算:

WX WX WX

也就是说:

权重是通过输入参与模型计算的。

所以真正影响下一层的是:

WX WX WX

而不是单独的:

W W W

这就解释了为什么 GPTQ 需要 Calibration Data。


5.1.5 Calibration Dataset 在 GPTQ 中做什么?

第三章已经介绍过 Calibration。

现在我们可以更准确地理解它在 GPTQ 中的用途。

假设:

Calibration Dataset

Tokenizer

LLM

Layer Input X

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 为什么非要用它?

原因在于,量化的真正目标不是让权重本身尽量接近,而是让层的输出尽量接近:

min⁡Wq∥WX−WqX∥2 \min_{W_q}\|WX-W_qX\|^2 WqminWXWqX2

如果按"简单量化"那样,只最小化 ∥W−Wq∥2\|W-W_q\|^2WWq2,就等于假设每个权重对输出的影响都一样大。但事实并非如此——权重是通过 WXWXWX 参与计算的,不同权重经由输入 XXX 之后,对输出的影响天差地别:

有的权重稍微量化偏一点,输出几乎不变;
有的权重只偏一点点,输出就明显变化。

我们需要一把"尺子"来衡量每个权重方向对输出误差有多敏感,并据此决定:

  • 哪些权重要量化得更准;
  • 某个权重量化后产生的误差,应该如何补偿到其他权重上。

这把"敏感度尺子"正是本节前面(5.1.7)讲的二阶信息(曲率),在数学上就是重构误差目标的 Hessian。换句话说,用 Hessian 是为了把"权重误差"翻译成"输出误差",让量化朝着"输出影响最小"的方向进行,而不是盲目地让权重数值最接近。

下面就来看这个 Hessian 具体长什么样。


Hessian 与输入的关系

对于:

Y=WX Y=WX Y=WX

它的误差目标:

∥(W−Wq)X∥2 \|(W-W_q)X\|^2 (WWq)X2

可以写成与:

XXT XX^T XXT

有关的二次型。这里出现的 XXTXX^TXXT 正是该层重构误差对权重的 Hessian(局部 Hessian,H=XXTH=XX^TH=XXT,差一个常数系数),它由 Calibration 数据估计得到——并不是整个模型训练损失的 Hessian。

因此可以用直观的方式表示:

Calibration Data

X

XXᵀ

输入方向的敏感性

GPTQ 量化

因此:

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=w1q1

简单方法:

直接把这个误差留下。

GPTQ:

w1

q1

产生误差

利用二阶信息

调整其他未量化权重

再量化 w2

继续:

w2

q2

再次补偿

w3

...

所以可以把 GPTQ 记成:

量化 + 补偿 + 再量化。

这就是它比独立 Round 更聪明的地方。


“补偿”到底补什么、补多少

先回到目标:我们要最小化的是层输出误差

∥(W−Wq)X∥2 \|(W-W_q)X\|^2 (WWq)X2

对同一层里权重的一行 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=wwq 的二次型:

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ΔwHΔ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_1w1q1,产生误差 e1=w1−q1e_1 = w_1 - q_1e1=w1q1
  • 这个 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=[H1]qqwqquant(wq)(H1):,q

拆开看这三块:

  • wq−quant(wq)w_q - \text{quant}(w_q)wqquant(wq):当前这个权重被量化产生的误差 eqe_qeq(补偿的"源头")。
  • [H−1]qq[H^{-1}]_{qq}[H1]qq:这个权重方向的"敏感度归一化项"(H−1H^{-1}H1 的第 qqq 个对角元)。
  • (H−1):,q\big(H^{-1}\big)_{:,q}(H1):,qH−1H^{-1}H1 的第 qqq 列——它告诉每个未量化权重应该按什么比例分摊这份误差。

一句话:误差 eqe_qeq 越大、或某个未量化权重与它越相关,那个权重就被调整得越多;调整方向恰好是"抵消 eqe_qeq 对输出影响"的方向。这一步之后,剩余权重的误差目标里就把 wqw_qwq 的影响"消化"掉了。

为什么必须用 HHH(而不是简单平均)

如果不看二阶信息,你顶多能把误差"平均"分给其他权重,但那忽略了权重之间的相关性,往往越补越糟。H−1H^{-1}H1 精确地编码了"每个未量化权重对输出的边际影响",所以能算出最优的分摊比例——这正是"利用二阶信息补偿"的含义。


逐列进行:一次只解一个变量

GPTQ 不会一次性解整个大矩阵,而是按列(逐个权重)顺序量化:

量化 w_q → q_q

算误差 e_q

用 H⁻¹ 把 e_q 补偿到未量化权重

更新 H(去掉已处理列)

量化下一个 w_{q+1}

每量化一个权重,就:

  1. 记录它的量化误差 eqe_qeq
  2. 用上面的公式把 eqe_qeq 摊到还没处理的权重上(真正修改它们的浮点值);
  3. H−1H^{-1}H1 做一次相应的降维更新(Cholesky / 高斯消元式的增量更新),保证后续列的补偿依然最优。

因为每一步都是"解一个变量 + 闭式补偿",GPTQ 才能在不反向传播、不重训练的前提下,一次遍历就完成整层量化,同时把输出误差压到很低。

这也解释了 5.1.5 节说的:Calibration 数据的唯一作用,就是估出这个 H=XX⊤H = XX^{\top}H=XX——有了它,上面所有补偿量才算得出来。


误差是怎么被“消化”的:OBS 闭式解与 Cholesky 加速

前面给出了补偿公式,但还没回答一个更本质的问题:为什么这样补偿是"最优"的?它又怎么做到既快又稳? 这一节把"消化误差"这个动作背后的算法讲透。

要解的其实是一个带整数约束的二次优化

对一层权重的一行 w∈Rnw\in\mathbb{R}^nwRn,目标是:

min⁡wq  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)=(wwq)H(wwq),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=wqquant(wq)

(2) 对所有剩余权重的最优补偿(消化误差):

  δ=− eq[H−1]qq (H−1):,q   \boxed{\;\delta=-\,\frac{e_q}{[H^{-1}]_{qq}}\,\big(H^{-1}\big)_{:,q}\;} δ=[H1]qqeq(H1):,q

(3) 这一步造成的最小误差增量:

ΔE=eq2[H−1]qq \Delta E=\frac{e_q^{2}}{[H^{-1}]_{qq}} ΔE=[H1]qqeq2

推导上是对"未量化权重"在"第 qqq 个变量被钉死"的等式约束下用拉格朗日乘子法求极小。直觉是:H−1H^{-1}H1 的第 qqq 列描述了"只动 wqw_qwq 会如何牵动其他权重对输出的贡献",顺着这一列反向挪,正好抵消 wqw_qwq 取整带来的输出偏移。

ΔE\Delta EΔE 那个式子还顺带给出了一个重要提示:[H−1]qq[H^{-1}]_{qq}[H1]qq 越小(方向越敏感),同样的 eqe_qeq 造成的误差越大——这正是"敏感权重优先处理"(act-order)的依据。

朴素做法太慢

如果每量化一列,就重新求一次 H−1H^{-1}H1 并删去对应行列,单步是 O(n3)O(n^3)O(n3)、整层 O(n4)O(n^4)O(n4)。对 LLM 动辄几千维的层根本跑不动。

GPTQ 的三个工程关键

1. 固定顺序 + 一次性 Cholesky。 不再每步重算逆矩阵,而是先算一次 H−1H^{-1}H1,再做 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 HH+λmean(diag(H))I

λ\lambdaλ 典型取 1e-2,保证正定、[H−1]qq[H^{-1}]_{qq}[H1]qq 不至于过小,从而补偿量 δ\deltaδ 不会失控。这也是防止"误差越滚越大、把尾部权重推飞"的关键正则。

3. 分块批量更新(lazy batch update)。 一层有成千上万列,GPTQ 把列切成小块(如 128 列一块):块逐列做"量化+补偿",块内产生的对后续列的补偿先累积起来,处理完一块后一次性用矩阵乘法作用到后面尚未处理的块,充分利用 GPU。

逐列流程

取第 q 列权重

quant(w_q) 得格点,算 e_q

δ = -e_q/[H⁻¹]_qq · (H⁻¹)_:,q,更新剩余列

读 Cholesky 因子下一行

q ← q+1,继续直到全部量化

贴近实现的伪代码

输入: 权重 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}H1 及其 Cholesky 因子)算出一组对"尚未量化权重"的最优微调量,把这一步取整造成的输出偏移,在还自由的权重上就地抵消掉。 Cholesky 让这组系数一次算好、逐列直接读取,damping 保证数值稳定,分块保证 GPU 效率。


5.1.9 GPTQ 非常适合 4-bit 的原因

因为随着 bit 数下降:

16bit→8bit→4bit 16bit\rightarrow8bit\rightarrow4bit 16bit8bit4bit

离散状态越来越少。

例如:

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

这个概念前面已经介绍过。

可以理解成:

Weight

Group 1

Group 2

Group 3

...

每个 Group 独立量化

例如:

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 清单,如果里面出现了比你当前更新的 torchtritonnvidia-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-125mfacebook/opt-1.3b)即可复用同一套代码——本节末尾(5.2.10.1)会给出三个模型的实测对比。

💡 提示(量化对象的架构限制):本章示例用 gptqmodel 2.2.0 + transformers 4.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 主要负责:

Calibration Text

Tokenizer

Token IDs


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 256average 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 原生 APIQuantizeConfig + 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(...) 才真正触发完整的量化流程:

Load FP Model

Load Calibration Data

Run Model

Collect Layer Inputs

GPTQ

Quantize Weights

4-bit Model

量化过程中会逐层打印每个 module(q_proj/k_proj/v_proj/fc1/fc2 等)的重构 loss,这正是 5.1.4 讲的层输出重构误差。


5.2.7 第一次量化比较慢的原因

因为 GPTQ:

不是简单的 float16 → int4 数据类型转换。

它需要进行:

Calibration

Layer Forward

统计输入

构建相关信息

量化

误差补偿

继续下一层

所以:

量化属于离线计算成本。

我们愿意在离线阶段花时间,是因为:

以后部署时,可以得到一个更小的模型。

这是一种典型的:

Offline Cost→Runtime Savings \boxed{ Offline\ Cost \rightarrow Runtime\ Savings } Offline CostRuntime 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)

只要能够正常生成文本,就说明:

GPTQ Quantization

Save

Reload

Inference

整个链路已经跑通。


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-125m120.47 GB0.12 GB75.0%43s294.7 t/s0.13 GB
opt-1.3b244.90 GB0.79 GB84.0%266s191.5 t/s0.81 GB
opt-6.7b3224.81 GB3.52 GB85.8%1260s133.3 t/s3.69 GB

两个结论:

  1. 模型越大,压缩比越高(越接近理论 4×):75.0% → 84.0% → 85.8%。因为量化只压 Linear 层权重,而 embedding / layernorm / bias 等不被量化的部分在小模型里占比更大;模型越大可量化权重占比越高(对应 5.3.7)。
  2. 显存收益显著: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.3bfacebook/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.3b166.4 t/s191.5 t/s×1.15
opt-6.7b57.2 t/s133.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=N1iWiW^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 PPLGPTQ 4-bit PPLPPL 变化输出 logits MAE
opt-1.3b32.12932.693+0.564(+1.8%)0.2715
opt-6.7b24.72325.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 表示,同时利用低比特存储来减少内存使用。

可以先抽象成:

INT4 Weight

Quantized Kernel

FP16 Activation

高效计算

所以:

“4-bit 权重”不等于“整个模型所有计算都是 4-bit”。


5.3.7 GPTQ 的真正收益是什么?

可以总结为三个方面。

第一:减少模型存储

理论上:

FP16→INT4 FP16\rightarrow INT4 FP16INT4

每个参数:

16bit→4bit 16bit\rightarrow4bit 16bit4bit

所以理论存储量约降低:

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 4bit3bit2bit

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 浓缩成一张图:

训练完成的 FP16 LLM

Calibration Data

Layer Input

GPTQ 分析

量化 + 误差补偿

INT4 Weight

保存模型

Quantized Model

Inference

GPTQ 最核心的目标:

min⁡∥WX−WqX∥2 \boxed{ \min \|WX-W_qX\|^2 } minWXWqX2

而不是简单地:

min⁡∥W−Wq∥2 \min \|W-W_q\|^2 minWWq2

这两者之间的区别,就是理解 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 WXWqX

而不只是:

W−Wq W-W_q WWq

6. Hessian

提供近似二阶敏感性信息,用于帮助量化与误差补偿。

7. Group Size

控制量化粒度。


5.4.5 一句话理解 GPTQ

如果只允许记住一句话:

GPTQ 是一种 PTQ 权重量化方法,它利用 Calibration 数据和近似二阶信息,不是简单地逐个 Round 权重,而是尽量让量化后的 Layer 输出接近原模型,从而在 4-bit 等低比特下尽可能保持模型能力。

这就是 GPTQ 最核心的思想。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值