大模型量化从0到1(五):GPTQ 原理详解 + 从零量化一个真实大模型
前四篇,我们一直在打地基:浮点数是怎么表示的,INT8、INT4 到底压缩了什么,scale 和 zero point 有什么用,per-tensor、per-channel、per-group 又有什么区别,以及 PTQ 和 QAT 分别适合什么场景。
从这一篇开始,我们不再只讲概念。
我们会拿一个真实的语言模型,把它从 FP16/BF16 权重压缩到 4bit,完整跑通:
- 准备校准数据;
- 收集每一层的输入激活;
- 构造 Hessian;
- 逐列量化并补偿误差;
- 保存 GPTQ 模型;
- 加载并执行推理;
- 对比量化前后的显存、困惑度和生成速度;
- 分析
group_size、damp_percent、desc_act等参数到底在控制什么。
这一篇的主角是 GPTQ。
GPTQ 是最早把超大语言模型的 3bit、4bit 后训练量化真正做到可扩展、可部署的代表性方法之一。原论文展示了:在单张 A100 上,可以用数小时量化 175B 级别模型,并让 4bit 模型的困惑度非常接近 FP16 基线。(DOI)
但 GPTQ 真正值得学习的,不只是“它能把模型压到 4bit”。
更重要的是,它改变了我们看待量化误差的方式:
量化不应该只问“每个权重和原来差了多少”,而应该问“这些权重共同作用之后,模型的输出差了多少”。
这就是 GPTQ 的灵魂。
目录
一、先把 GPTQ 放到正确的位置上
二、GPTQ 到底要解决什么问题
三、为什么普通四舍五入在 4bit 下容易失效
四、从输出误差推导出 Hessian
五、最容易混淆的问题:到底是按行还是按列量化
六、GPTQ 的单步误差补偿公式
七、GPTQ 如何从 OBQ 变成可扩展算法
八、阻尼、Cholesky 与数值稳定性
九、两个“128”不是一回事
十、关键参数应该怎么理解
十一、用几十行 PyTorch 写一个教学版 GPTQ
十二、量化一个真实模型:当前正确的工具链
十三、准备高质量校准数据
十四、执行 Qwen2.5-0.5B 的 GPTQ INT4 量化
十五、加载量化模型并生成文本
十六、科学对比显存、困惑度与速度
十七、怎样设计一组有说服力的消融实验
十八、GPTQ、AWQ、bitsandbytes 怎么选
十九、十二个常见误区
二十、故障排查指南
二十一、把整篇浓缩成一张认知地图
二十二、小结
一、先把 GPTQ 放到正确的位置上
在深入公式之前,先建立三个坐标。
1.1 GPTQ 是后训练量化
GPTQ 属于 Post-Training Quantization,PTQ。
它不需要重新训练整个模型,也不需要像 QAT 那样,在训练过程中模拟量化误差并反向传播。
它需要的是:
- 一个已经训练好的高精度模型;
- 一小批校准数据;
- 一次离线量化过程。
量化结束后,得到一个新的低比特权重模型。
这里的 “one-shot” 不是说整个模型只做一次矩阵运算,也不是说完全不需要数据,而是说:
不进行常规的梯度训练或长时间微调,只通过一次离线校准和权重重建完成量化。
1.2 GPTQ 通常是权重量化
大多数 GPTQ 模型属于:
[
\text{W4A16}
]
其中:
- W4:权重以 4bit 形式存储;
- A16:激活通常仍以 FP16 或 BF16 计算和传递。
所以,所谓“GPTQ INT4 模型”,通常不是说整个网络从输入到输出都只做 INT4 运算。
更准确的描述是:
模型权重以压缩的 4bit 格式存储,推理 kernel 在计算时按块读取、解码或反量化,并与 FP16/BF16 激活完成矩阵乘法。
当前 Hugging Face 文档也将 GPTQ 描述为权重压缩方案:4bit 权重不会在显存中永久展开成一整份 FP16 权重,而是在融合 kernel 中按需恢复和计算。(Hugging Face)
这点非常重要。
因为它意味着 GPTQ 主要压缩的是参数显存和权重读取带宽,而不是:
- KV Cache;
- 中间激活;
- attention score;
- 所有临时 workspace;
- 所有层的计算数据类型。
1.3 GPTQ 优化的是局部层重建目标
GPTQ 并不直接优化最终模型在 MMLU、数学推理、代码生成或对话任务上的分数。
它采用一个更容易计算的代理目标:
让每个线性层在校准数据上的输出,尽量接近量化前的输出。
也就是说,它进行的是逐层重建。
这是一个局部目标,但因为 Transformer 中绝大多数参数都位于线性层中,而且这些线性层的输出会继续传给后续模块,所以局部输出误差通常比单纯的权重误差更有意义。
二、GPTQ 到底要解决什么问题
设一个线性层的权重矩阵为:
[
W\in\mathbb{R}^{d_{\text{out}}\times d_{\text{in}}}
]
收集到的校准输入为:
[
X\in\mathbb{R}^{d_{\text{in}}\times N}
]
这里:
- (d_{\text{in}}) 是输入维度;
- (d_{\text{out}}) 是输出维度;
- (N) 可以理解为所有校准样本中收集到的 token 数量。
采用这个矩阵方向时,原始层输出为:
[
Y=WX
]
量化后的权重记为:
[
\widehat W
]
量化层输出为:
[
\widehat Y=\widehat W X
]
GPTQ 要解决的问题是:
[
\min_{\widehat W\in\mathcal Q}
\left|WX-\widehat W X\right|_F^2
]
其中 (\mathcal Q) 表示量化网格,也就是 (\widehat W) 中每个权重只能取一组离散值。
例如,4bit 有 16 个整数编码状态。结合 scale 和 zero point 后,这 16 个状态会映射到 16 个离散浮点值。
GPTQ 论文使用的正是这种逐层输出重建目标。(arXiv)
需要提醒一下:PyTorch 的 torch.nn.Linear 通常写作:
output = input @ weight.T + bias
也就是输入放在左边、权重转置后放在右边。
本文为了让 Hessian 维度和论文表达更清晰,统一采用:
[
WX
]
两种写法只是矩阵排列约定不同,本质完全相同。
2.1 普通量化在优化什么
最普通的 round-to-nearest,简称 RTN,可以近似理解为在做:
[
\min_{\widehat W\in\mathcal Q}
\left|W-\widehat W\right|_F^2
]
它关心的是:
每个量化后的权重,离原权重有多远。
如果某个权重是 1.37,最近的量化刻度是 1.40,那么就把它变成 1.40。
如果某个权重是 0.62,最近刻度是 0.60,就把它变成 0.60。
每个权重独立处理。
这种做法简单、快速,而且在 8bit 时经常已经足够好。因为 8bit 有 256 个状态,刻度密,单个权重的舍入误差通常不大。
但到了 4bit,只有 16 个状态。
量化网格变得很稀疏,独立舍入产生的误差迅速放大。
2.2 GPTQ 在优化什么
GPTQ 不执着于:
[
\widehat W\approx W
]
它更关心:
[
\widehat W X\approx WX
]
这两件事并不等价。
完全可能出现以下情况:
[
\left|W-\widehat W_1\right|_F
<
\left|W-\widehat W_2\right|_F
]
但与此同时:
[
\left|WX-\widehat W_1X\right|_F
\left|WX-\widehat W_2X\right|_F
]
翻译成人话:
第二组量化权重在数值上离原权重更远,但在真实输入上产生的输出反而更接近原模型。
这听起来反直觉,却正是 GPTQ 能赢过普通四舍五入的原因。
三、为什么普通四舍五入在 4bit 下容易失效
3.1 所有权重并不是同等重要
考虑一个极端例子。
某个输入通道在校准数据里永远接近 0:
[
x_j\approx 0
]
那么与这个通道对应的权重 (w_j),即使量化误差很大,对输出的影响也可能很小:
[
\Delta y=\Delta w_jx_j\approx 0
]
另一个输入通道经常出现很大的激活:
[
|x_k|\gg 0
]
那么 (w_k) 只发生一点点误差,就可能明显改变输出:
[
\Delta y=\Delta w_kx_k
]
普通 RTN 只看 (\Delta w),看不到 (x)。
GPTQ 会利用校准输入,让量化误差具有“输入感知能力”。
3.2 不同输入通道之间还可能高度相关
假设两个输入通道满足:
[
x_2\approx 2x_1
]
某个输出神经元原本计算:
[
y=w_1x_1+w_2x_2
]
由于 (x_2\approx 2x_1),所以:
[
y\approx (w_1+2w_2)x_1
]
这时,(w_1) 和 (w_2) 不是各自独立地影响输出。
假设量化 (w_1) 时多了 0.2,那么可以适当减少 (w_2),让组合:
[
w_1+2w_2
]
尽量保持不变。
这种“一个权重多一点,另一个权重少一点”的补偿,普通 RTN 完全不会做。
GPTQ 则会利用输入相关性,计算最合适的补偿方向。
3.3 一个更接地气的比喻
假设你要把一排数字都变成整数,但要求最终总和尽量不变。
原数字是:
[
[1.4,\ 1.4,\ 1.4]
]
普通四舍五入得到:
[
[1,\ 1,\ 1]
]
原总和是 4.2,新总和是 3,误差是 1.2。
更聪明的做法是:
第一项从 1.4 变成 1,产生了 -0.4 的误差。
那就不要假装这个误差不存在,而是把它交给后面的数字。让第二项暂时从 1.4 调整到 1.8,再量化为 2。
现在前两项原本的和是 2.8,量化后是 3,只多了 0.2。
再把这 0.2 传给第三项处理。
核心思想不是“每个数字都最接近”,而是:
前面已经无法改变的误差,交给后面仍有自由度的数字去吸收。
GPTQ 的逐列处理,就是类似的逻辑。
但必须补充一点:真实 GPTQ 并不是简单地平均分配误差,更不是只维护总和。
它维护的是:
[
WX
]
误差应该分到哪些权重上、每个权重分多少,由输入相关性和逆 Hessian 决定。
四、从输出误差推导出 Hessian
这一节是整篇最核心的数学部分。
公式不难,关键是看懂每一步的意义。
令量化误差矩阵为:
[
E=W-\widehat W
]
那么输出误差是:
[
WX-\widehat W X=EX
]
GPTQ 的目标可以写成:
[
\mathcal L
|EX|_F^2
]
利用迹运算,可以得到:
[
\begin{aligned}
\mathcal L
&=\operatorname{Tr}\left(EXX^\top E^\top\right)
\end{aligned}
]
定义:
[
H=XX^\top
]
则:
[
\mathcal L
\operatorname{Tr}\left(EHE^\top\right)
]
如果严格求二阶导数,Hessian 是:
[
2XX^\top
]
很多讲解直接写:
[
H=XX^\top
]
是因为常数 2 不会改变最优补偿方向。只要整套公式保持一致,这个常数通常可以省略。
4.1 为什么这里会出现 Hessian
把权重矩阵的第 ® 行记为:
[
w_r
]
其量化误差行向量记为:
[
e_r=w_r-\widehat w_r
]
总误差可以分解为:
[
\mathcal L
\sum_{r=1}^{d_{\text{out}}}
e_r^\top H e_r
]
这说明两个重要事实。
第一,不同输出行之间可以独立优化。
每个输出神经元对应权重矩阵中的一行。总目标是所有行误差之和,不同的行之间没有交叉项。
第二,同一行中的不同输入维度不是独立的。
矩阵 (H) 的非对角元素描述了输入通道之间的相关性。
如果 (H) 是单位矩阵:
[
H=I
]
那么:
[
e_r^\top He_r
|e_r|_2^2
]
此时输出误差就退化成普通权重误差,独立地 round-to-nearest 基本合理。
但真实神经网络中的输入通道通常:
- 尺度不同;
- 方差不同;
- 存在相关性;
- 存在异常值;
- 有些通道几乎不活跃;
- 有些通道高度敏感。
因此 (H) 一般不会是单位矩阵。
GPTQ 的优势,恰恰来自对这个非平凡几何结构的利用。
4.2 它到底是不是“近似 Hessian”
这里有一个经常被模糊处理的细节。
对于局部层重建目标:
[
|WX-\widehat W X|_F^2
]
它本身就是一个关于权重误差的二次函数。
因此:
[
2XX^\top
]
是这个局部目标的精确 Hessian,不是通过有限差分猜出来的,也不是通过某种神经网络近似器估出来的。
那为什么 GPTQ 论文和大量资料仍然说它使用“近似二阶信息”?
因为真正关心的最终目标其实是:
- 模型完整语言建模损失;
- 最终任务准确率;
- 对话质量;
- 推理能力;
- 整个网络量化后的端到端误差。
GPTQ 没有构造整个模型任务损失关于全部参数的完整 Hessian。
它采用的是每一层局部输出重建目标的 Hessian。
所以更准确的说法是:
对局部二次重建目标而言,(2XX^\top) 是精确 Hessian;对整个模型的真实任务损失而言,它是一种可计算、可扩展的二阶代理。
4.3 Hessian 对角线和非对角线分别代表什么
对于:
[
H=XX^\top
]
其第 (j) 个对角元素为:
[
H_{jj}=\sum_{n=1}{N}X_{jn}2
]
它反映第 (j) 个输入通道在校准数据中的能量。
通道激活越大、出现越频繁,通常 (H_{jj}) 越大。
非对角元素:
[
H_{jk}=\sum_{n=1}^{N}X_{jn}X_{kn}
]
则反映输入通道 (j) 和 (k) 的相关性。
如果两个通道经常一起增大、一起减小,(H_{jk}) 可能较大。
如果它们大体独立或正负抵消,(H_{jk}) 可能接近 0。
因此可以把 Hessian 理解为:
输入特征空间中的局部几何地图。
但还要再精确一步。
GPTQ 真正在补偿公式里大量使用的,不只是 (H),而是:
[
H^{-1}
]
(H) 描述“往某个方向移动有多贵”,而 (H^{-1}) 描述“为了满足一个固定量化约束,其他维度应该怎样协同移动”。
五、最容易混淆的问题:到底是按行还是按列量化
这是大量入门文章最容易讲错的地方。
有时你会看到:
GPTQ 对权重矩阵逐行量化。
另一些文章又会说:
GPTQ 一列一列地量化。
到底哪个对?
答案是:
优化问题按行分解,工程算法按列同步处理。
两句话都对,但描述的是不同层面。
5.1 为什么说按行独立
权重矩阵:
[
W\in\mathbb{R}^{d_{\text{out}}\times d_{\text{in}}}
]
每一行对应一个输出神经元。
前面已经推导出:
[
\mathcal L
\sum_r e_r^\top H e_r
]
因此每一行都可以作为一个独立的二次优化问题。
从数学问题分解的角度看:
GPTQ 独立处理每个输出行的量化误差。
5.2 为什么实际又是一列一列处理
虽然每一行的权重值不同,但所有行面对的是同一组输入 (X)。
因此它们共享同一个:
[
H=XX^\top
]
也就是说,第 1 行要处理输入维度 (q),第 2 行也可以同时处理输入维度 (q),第 3 行同样如此。
当所有输出行同时量化第 (q) 个输入维度时,矩阵层面上处理的正是:
[
W_{:,q}
]
也就是权重矩阵的第 (q) 列。
所以实际实现通常会:
- 选择一个输入维度 (q);
- 同时量化所有输出行在这个维度上的权重;
- 同时计算每一行产生的量化误差;
- 将这些误差补偿到尚未量化的输入维度。
因此,从程序执行和矩阵操作角度看,它是:
按列推进、跨行向量化。
GPTQ 论文正是利用了这一点:不再让每个输出行都采用各自不同的贪心量化顺序,而是让所有行共享同一个固定列顺序,从而把昂贵的 Hessian 更新次数从“每个权重一次”减少为“每个输入列一次”。(arXiv)
5.3 为什么列对应输入特征
权重矩阵的第 (q) 列:
[
W_{:,q}
]
控制第 (q) 个输入特征对所有输出神经元的贡献。
而 Hessian 的尺寸是:
[
d_{\text{in}}\times d_{\text{in}}
]
它描述的正是输入特征之间的关系。
因此 GPTQ 的顺序维度自然位于输入维度,也就是权重矩阵的列方向。
这也是为什么:
- act-order 排序的是输入列;
- Hessian 的对角线长度等于
in_features; - group_size 通常也沿输入维度划分;
- 逐列补偿发生在尚未量化的输入通道之间。
六、GPTQ 的单步误差补偿公式
现在考虑权重矩阵中的某一行,记为:
[
w\in\mathbb R^{d_{\text{in}}}
]
假设我们准备量化其中第 (q) 个权重。
原权重是:
[
w_q
]
量化后必须变为某个离散值:
[
Q(w_q)
]
于是这一维被强制改变了:
[
\Delta_q=Q(w_q)-w_q
]
这个变化已经不可避免。
问题变成:
在保证第 (q) 维必须移动 (\Delta_q) 的前提下,其他尚未量化的权重应该怎样移动,才能让输出误差最小?
设所有权重的调整向量为:
[
\delta
]
我们要求:
[
\delta_q=\Delta_q
]
并最小化:
[
\delta^\top H\delta
]
于是得到约束优化问题:
[
\begin{aligned}
\min_\delta\quad
&\delta^\top H\delta
\text{s.t.}\quad
&\delta_q=\Delta_q
\end{aligned}
]
通过拉格朗日乘子可以得到最优调整:
[
\delta^*
\frac{\Delta_q}{[H^{-1}]{qq}}
H^{-1}{:,q}
]
代入:
[
\Delta_q=Q(w_q)-w_q
]
得到:
[
\delta^*
-\frac{w_q-Q(w_q)}
{[H^{-1}]{qq}}
H^{-1}{:,q}
]
这就是 GPTQ/OBQ 误差补偿公式的核心形式。(arXiv)
6.1 这个公式应该怎么读
先看:
[
w_q-Q(w_q)
]
这是当前权重的量化残差。
再看:
[
[H^{-1}]_{qq}
]
它用于对当前误差进行归一化,也决定了把这一维固定到量化值后,最小可能增加多少目标误差。
最后看:
[
H^{-1}_{:,q}
]
这是逆 Hessian 的第 (q) 列。
它告诉我们:
当前维度被迫变化后,其他输入维度应该按什么比例一起变化。
如果某个元素:
[
[H^{-1}]_{jq}
]
很大,说明第 (j) 维是吸收第 (q) 维误差的重要方向。
如果接近 0,说明不需要让第 (j) 维承担太多补偿。
6.2 量化一个权重的代价
将最优补偿代回目标,可以得到量化第 (q) 个权重带来的最小误差增量:
[
\Delta\mathcal L_q
\frac{(w_q-Q(w_q))^2}
{[H^{-1}]_{qq}}
]
这条公式很有启发。
量化代价不只取决于:
[
(w_q-Q(w_q))^2
]
也就是原始舍入误差。
它还取决于:
[
[H^{-1}]_{qq}
]
也就是考虑其他权重可以共同补偿之后,这个误差到底有多难消化。
因此,“权重离量化刻度远”并不自动意味着“这个权重不能量化”。
如果其他方向很容易补偿,它仍然可能以较低代价被量化。
6.3 如果输入特征完全不相关会怎样
假设:
[
H=I
]
那么:
[
H^{-1}=I
]
于是:
[
H^{-1}_{:,q}
]
只有第 (q) 个位置为 1,其他位置全部为 0。
补偿公式退化为:
[
\delta_q=Q(w_q)-w_q
]
其他权重不发生变化。
这时 GPTQ 基本就退化成独立量化。
因此可以把 GPTQ 的收益理解为来自两部分:
- 识别不同输入通道的敏感度差异;
- 利用输入通道之间的相关性进行误差转移。
七、GPTQ 如何从 OBQ 变成可扩展算法
GPTQ 并不是从零开始发明整套二阶补偿框架。
它建立在 OBQ,也就是 Optimal Brain Quantization 的思想上。
OBQ 的策略非常直接:
- 在一行权重里,计算每个尚未量化权重的量化代价;
- 找出当前代价最小的权重;
- 将它量化;
- 更新其他所有权重进行补偿;
- 更新剩余变量对应的逆 Hessian;
- 重复直到整行量化完成。
这个方法很精细,但代价也很高。
因为每个输出行可能选择不同的权重量化顺序,所以每一行都要维护自己的剩余变量集合和逆 Hessian 更新。
对于大型语言模型,这是不可接受的。
GPTQ 做了三个关键改造。
7.1 改造一:放弃每行独立的贪心顺序
OBQ 会在每一步寻找当前“最便宜”的权重。
GPTQ 的实验发现,在大型、过参数化的线性层中:
严格贪心排序相比采用一个统一固定顺序,最终收益通常没有想象中那么大。
原因可以这样理解。
贪心策略确实会优先量化当前容易量化的权重。
但那些困难的权重会被拖到最后。
越到后面,可以用来补偿误差的未量化权重越少。
于是“前面每一步都很聪明”不一定能转化为显著更好的最终结果。
GPTQ 因而让所有输出行使用同一个列顺序。
这样,Hessian 的消元和更新只需要针对输入维度执行一次,而不是针对每一个权重重复执行。
论文给出的复杂度分析表明,这一变化能带来数量级上的加速,使二阶量化真正扩展到数十亿乃至千亿参数模型。(arXiv)
7.2 改造二:延迟批量更新
即使所有行共享列顺序,如果每量化一列,就立刻更新整个巨大权重矩阵,GPU 利用率仍然很差。
原因是这种操作:
- 读取大量权重;
- 每个权重只执行少量浮点运算;
- 计算强度低;
- 容易受到显存带宽限制。
GPTQ 采用了 lazy batch update,也就是延迟批量更新。
它会先拿一小块连续列,例如:
[
B=128
]
在块内部逐列完成量化和局部补偿。
等整个块处理完成后,再用一次矩阵运算,把累积误差统一传播到右侧尚未处理的所有列。
这样做并没有减少理论计算量,却把大量细碎的向量操作变成了更适合 GPU 的矩阵操作。
论文报告这种分块延迟更新对大型模型的实际量化速度至关重要。(arXiv)
7.3 改造三:用 Cholesky 重写逆 Hessian 更新
如果每删除一个已量化变量,都直接用高斯消元更新逆 Hessian,浮点误差会不断积累。
模型越大、层越宽,这个问题越明显。
严重时,原本应该正定的矩阵可能因为数值误差变得不再正定,导致:
- 补偿方向异常;
- 权重被大幅错误更新;
- 某一层量化结果彻底崩坏;
- 最终模型输出乱码或困惑度爆炸。
GPTQ 利用 Cholesky 分解预先计算后续需要的逆 Hessian信息。
Cholesky 分解适合对称正定矩阵,并且有成熟、高性能、数值稳定的 GPU 实现。
配合阻尼后,它能够显著提升大模型量化时的稳定性。(arXiv)
7.4 GPTQ 的整体伪代码
忽略工程细节,可以把 GPTQ 写成:
输入:
权重矩阵 W
校准输入 X
量化器 Q
量化块大小 B
1. 构造局部 Hessian:
H = 2 X Xᵀ
2. 添加阻尼:
H = H + λI
3. 计算逆 Hessian 所需的 Cholesky 信息
4. 以 B 列为一块遍历 W:
对块中的每一列 q:
a. 将当前列量化到离散网格:
W_hat[:, q] = Q(W[:, q])
b. 计算归一化量化残差:
error = (W[:, q] - W_hat[:, q]) / h_q
c. 用逆 Hessian 信息更新块内尚未量化列
块处理完后:
d. 将整个块累计的误差
一次性传播到右侧剩余列
5. 保存量化权重、scale、zero point 和分组信息
真实实现还会处理:
- 模型层识别;
- Conv1D 权重转置;
- attention 模块顺序;
- CPU/GPU offload;
- 多 GPU;
- 量化权重打包;
- kernel 格式;
- act-order 排列与恢复;
- 模块跳过;
- MoE 专家层;
lm_head是否量化。
但算法主干就是上面这些。
八、阻尼、Cholesky 与数值稳定性
8.1 为什么 Hessian 可能不可逆
校准 Hessian 为:
[
H=XX^\top
]
它可能是奇异或近似奇异的。
常见原因包括:
第一,校准 token 数量不足。
如果有效观测数量小于输入维度,(X) 的秩可能不足。
第二,输入通道高度相关。
如果某些通道几乎是其他通道的线性组合,Hessian 会病态。
第三,存在不活跃通道。
某些通道在校准数据中几乎始终为 0,其对应方向缺乏足够信息。
第四,浮点计算误差。
即使理论上可逆,有限精度运算也可能导致 Cholesky 失败。
8.2 阻尼在做什么
GPTQ 会在 Hessian 对角线上添加一个小量:
[
H_{\text{damped}}
H+\lambda I
]
常见定义是:
[
\lambda
\alpha\cdot
\operatorname{mean}(\operatorname{diag}(H))
]
其中 (\alpha) 就是配置中的 damp_percent。
加入阻尼后,相当于告诉算法:
不要百分之百相信有限校准数据估计出的相关性,在所有方向上保留一点基础曲率。
它能:
- 避免零特征值;
- 改善条件数;
- 减少逆矩阵中的极端值;
- 防止补偿更新过于激进;
- 提高 Cholesky 成功率。
8.3 论文中的 1% 和当前库的默认值为什么不同
原始 GPTQ 论文描述的是:
[
\alpha=0.01
]
也就是平均 Hessian 对角线的 1%。
但当前 Transformers 的 GPTQConfig 文档中,damp_percent 默认值和推荐值为:
0.1
也就是 10%。(Hugging Face)
这并不意味着论文错了,或者所有模型都必须改成 10%。
它说明的是:
阻尼不是一个不可改变的数学常数,而是实现、模型结构、校准规模和数值稳定性共同决定的超参数。
实践建议是:
- 初次使用当前 GPTQModel 工具链,先使用库默认值;
- 希望更贴近经典论文配置时,可测试 0.01;
- 遇到 Cholesky 失败、NaN 或某层异常时,可以适当增大;
- 量化精度异常时,不要只靠无限增大阻尼解决,应同时检查校准数据。
九、两个“128”不是一回事
阅读 GPTQ 代码时,经常会同时看到:
blocksize = 128
group_size = 128
它们碰巧都常用 128,但含义完全不同。
9.1 blocksize:算法更新块大小
GPTQ 的 lazy batch update 会一次处理一块连续列。
这个块有多少列,就是算法意义上的 blocksize。
它主要影响:
- 量化速度;
- GPU 利用率;
- 临时 buffer;
- 全局误差更新频率。
它属于算法执行调度参数。
9.2 group_size:量化参数共享范围
4bit 整数本身不能还原成原始权重尺度,还需要 scale,非对称量化通常还需要 zero point。
如果每个权重都保存独立 scale,元数据开销会非常大。
所以常见做法是让连续若干个权重共享一组量化参数。
例如:
group_size = 128
表示沿输入维度,每 128 个权重共享一组 scale 和 zero point。
它主要影响:
- 量化误差;
- checkpoint 大小;
- scale/zero 元数据开销;
- kernel 支持;
- 推理效率。
它属于量化网格粒度参数。
9.3 为什么它们经常都设为 128
不是因为两者在数学上必须相等。
而是因为 128 通常:
- 足够大,适合 GPU 批量计算;
- 又不会让量化 scale 的覆盖范围太大;
- 方便向量化和权重打包;
- 在精度、元数据和 kernel 效率之间较平衡。
所以一定要记住:
算法 blocksize 的 128,和量化 group_size 的 128,是两个完全不同的 128。
十、关键参数应该怎么理解
10.1 bits
最常见的是:
bits=4
理论上:
- FP16 每个权重 16bit;
- INT4 每个权重 4bit;
- 纯权重部分的理论压缩率是 4 倍。
但真实 checkpoint 和显存不会严格缩小到四分之一,因为还要存储:
- scale;
- zero point;
- 分组索引;
- 未量化层;
- embedding;
- LayerNorm;
lm_head;- 模型配置;
- kernel workspace。
4bit 通常是 GPTQ 的甜点位。
3bit 能进一步压缩,但:
- 网格更稀疏;
- 对 group size 更敏感;
- 对校准数据更敏感;
- kernel 支持更有限;
- 掉点风险明显增加。
2bit 则进入更极端的量化区间,通常不能简单理解为“把 bits 从 4 改成 2 就行”。
原论文确实探索了 2bit 和三值量化,但需要更细粒度分组和更谨慎的配置。(ar5iv)
10.2 group_size
典型配置:
group_size=128
更小的 group size,例如 32,意味着:
- 每组权重的动态范围更相似;
- scale 更贴合局部权重分布;
- 量化误差一般更低;
- 需要保存更多 scale 和 zero point;
- 部分 kernel 的效率可能下降。
更大的 group size,例如 256,意味着:
- 元数据更少;
- 权重打包更紧凑;
- 一个 scale 要覆盖更宽的数值范围;
- 极端权重更容易拉大量化步长;
- 精度可能下降。
不存在“group size 越小一定越好”。
更准确的目标是:
在可接受的精度、模型大小和目标推理 kernel 之间找平衡。
10.3 对称和非对称量化
对称量化常写成:
[
q=
\operatorname{clip}
\left(
\operatorname{round}\frac{w}{s},
-q_{\max},
q_{\max}
\right)
]
反量化:
[
\widehat w=sq
]
非对称量化通常引入 zero point:
[
q=
\operatorname{clip}
\left(
\operatorname{round}\frac{w}{s}+z,
q_{\min},
q_{\max}
\right)
]
反量化:
[
\widehat w=s(q-z)
]
原始 GPTQ 论文实验主要采用逐行非对称 min-max 网格。
当前 GPTQModel 也支持非对称量化,但并非所有格式和 kernel 都完整支持。Hugging Face 文档特别提醒,某些非对称 GPTQModel 格式与旧 AutoGPTQ checkpoint 不兼容,Marlin 等 kernel 也可能有格式限制。(Hugging Face)
因此教学和兼容性优先时,常见配置是:
sym=True
追求更低量化误差,并确认目标 runtime 支持时,可以进一步测试:
sym=False
10.4 calibration data
GPTQ 需要用校准输入构造:
[
H=XX^\top
]
所以校准数据决定了算法“认为哪些输入方向重要”。
原论文使用了:
- 128 个随机片段;
- 每个片段 2048 token;
- 数据来自 C4。
也就是说,论文里的“128 条数据”实际覆盖大约 26 万 token,并不是 128 个短句。(ar5iv)
因此不要迷信 n_samples=128 这个数字。
真正需要考虑的是:
- 总 token 数;
- 样本长度;
- 语言分布;
- 领域分布;
- 对话模板;
- 代码、数学或自然语言比例;
- 是否覆盖实际输入风格。
如果你只拿 128 个二三十 token 的短句校准,总信息量可能远低于论文设置。
10.5 damp_percent
它控制:
[
H+\lambda I
]
中的阻尼强度。
经验上:
- 太小:矩阵可能病态,补偿过激;
- 太大:Hessian 被过度“抹平”,输入相关性利用不足;
- 合适:数值稳定,同时保留有效二阶结构。
它不是用于直接控制模型压缩率的参数。
10.6 desc_act,也就是 act-order
desc_act=True 通常表示:
按输入激活大小从高到低重排列,再执行量化。
常见重要性指标来自 Hessian 对角线:
[
H_{jj}
]
因为它反映输入通道的激活能量。
需要修正一个常见说法:
不是简单地“先量化权重值最大的列”,而是:
按输入激活统计决定列顺序。
当前 GPTQConfig 对 desc_act 的描述也是“按激活大小递减的顺序量化列”。(Hugging Face)
它经常能改善精度,尤其是在:
- 3bit;
- 小模型;
- 存在明显异常通道;
- 部分难量化模型。
但它也可能需要保存或处理额外排列信息。
旧实现中,这可能给推理带来额外开销;静态分组和现代 group-aware activation reorder 能减少部分问题。
所以不能简单写成:
desc_act=True 永远更好
更合理的策略是:
- 先用
False建立稳定基线; - 精度不够时再测试
True; - 同时在目标 runtime 上测真实吞吐。
10.7 true_sequential
一个 Transformer block 内通常包含多个线性层:
- q_proj;
- k_proj;
- v_proj;
- o_proj;
- gate_proj;
- up_proj;
- down_proj。
如果先收集整个 block 的 FP16 输入,再分别量化所有子层,那么后面的子层看到的仍是原始高精度输入。
但真实量化模型中,后面的子层会接收到前面已量化子层产生的输出。
true_sequential=True 会更贴近真实执行顺序:
前一个子层量化后,使用它的实际量化输出作为后续子层的校准输入。
原始 GPTQ 仓库后来也加入了 true-sequential,并报告它能改善部分 LLaMA 模型的量化结果。(GitHub)
10.8 当前配置参数速查
| 参数 | 控制内容 | 常见起点 | 精度不够时 |
|---|---|---|---|
bits |
权重位宽 | 4 | 不建议先降到 3 |
group_size |
scale/zero 共享范围 | 128 | 测试 64 或 32 |
damp_percent |
Hessian 阻尼 | 当前库默认 0.1 | 检查 0.01、0.05、0.1 |
desc_act |
是否启用 act-order | False | 测试 True |
sym |
对称或非对称量化 | True | runtime 支持时测试 False |
true_sequential |
block 内顺序量化 | True | 通常保持 |
batch_size |
校准前向批量 | 1 | 显存足够再增大 |
model_seqlen |
校准序列长度相关配置 | 与校准长度一致 | 根据实际场景调整 |
当前 Transformers 的 GPTQConfig 支持 2、3、4、8bit,并暴露了 group_size、damp_percent、desc_act、sym、true_sequential 等参数。(GitHub)
十一、用几十行 PyTorch 写一个教学版 GPTQ
在直接调用库之前,我们先写一个极简版本。
这段代码不会处理:
- 分组量化;
- 权重打包;
- GPU kernel;
- 大模型 offload;
- 分块延迟更新;
- Cholesky 加速;
- 复杂模型结构。
它的目标只有一个:
让你亲眼看到,GPTQ 可以让权重误差更大,但输出误差更小。
import torch
def quantize_rowwise_symmetric(
W: torch.Tensor,
bits: int = 4,
) -> torch.Tensor:
"""普通逐行对称 RTN 量化。"""
qmax = 2 ** (bits - 1) - 1
scale = (
W.abs()
.amax(dim=1, keepdim=True)
.clamp_min(1e-8)
/ qmax
)
q = torch.clamp(
torch.round(W / scale),
-qmax,
qmax,
)
return q * scale
def gptq_fixed_order(
W: torch.Tensor,
X: torch.Tensor,
bits: int = 4,
damp: float = 0.01,
order: torch.Tensor | None = None,
) -> torch.Tensor:
"""
教学版 GPTQ。
参数:
W: [d_out, d_in]
X: [d_in, n_tokens]
简化:
1. 使用固定逐列顺序;
2. 每个输出行使用一个固定对称 scale;
3. 显式计算逆 Hessian;
4. 显式执行 Schur complement;
5. 不做真实 GPTQ 的分块和 Cholesky 优化。
"""
d_in = W.shape[1]
# 对局部重建目标,Hessian 为 2XX^T
H = 2.0 * X @ X.T
# 添加阻尼,避免矩阵病态
lam = damp * torch.diag(H).mean()
H = H + lam * torch.eye(
d_in,
dtype=H.dtype,
device=H.device,
)
if order is None:
order = torch.arange(d_in, device=W.device)
inverse_order = torch.argsort(order)
# 按指定列顺序排列
W_work = W[:, order].clone()
H_work = H[order][:, order]
H_inv = torch.linalg.inv(H_work)
qmax = 2 ** (bits - 1) - 1
# 为便于观察,固定使用原始 W 计算出的逐行 scale
scale = (
W.abs()
.amax(dim=1, keepdim=True)
.clamp_min(1e-8)
/ qmax
)
Q_perm = torch.empty_like(W_work)
for j in range(d_in):
# 当前待量化列
current = W_work[:, :1]
# 对当前列执行 RTN
q = torch.clamp(
torch.round(current / scale),
-qmax,
qmax,
) * scale
Q_perm[:, j : j + 1] = q
# GPTQ 的归一化量化误差
normalized_error = (
current - q
) / H_inv[0, 0]
# 将误差补偿到当前及后续列
W_work = (
W_work
- normalized_error * H_inv[0:1, :]
)
if j < d_in - 1:
# 删除已量化维度对应的逆 Hessian 行列
# 这是论文中高斯消元更新的显式形式
H_inv = (
H_inv[1:, 1:]
- H_inv[1:, 0:1]
@ H_inv[0:1, 1:]
/ H_inv[0, 0]
)
W_work = W_work[:, 1:]
# 恢复原始列顺序
return Q_perm[:, inverse_order]
构造一组存在相关性的输入:
torch.manual_seed(7)
d_in = 8
d_out = 4
n_tokens = 512
base = torch.randn(1, n_tokens)
X = torch.randn(d_in, n_tokens) * 0.3
# 人为制造输入通道相关性
X[0] = base
X[1] = 0.95 * base + 0.05 * torch.randn(n_tokens)
X[2] = (
0.7 * X[0]
- 0.2 * X[3]
+ 0.1 * torch.randn(n_tokens)
)
W = torch.randn(d_out, d_in) * 1.5
W_rtn = quantize_rowwise_symmetric(W)
W_gptq = gptq_fixed_order(W, X)
def mse(A: torch.Tensor, B: torch.Tensor) -> float:
return torch.mean((A - B) ** 2).item()
print(
f"RTN weight MSE : "
f"{mse(W, W_rtn):.6f}"
)
print(
f"GPTQ weight MSE : "
f"{mse(W, W_gptq):.6f}"
)
print(
f"RTN output MSE : "
f"{mse(W @ X, W_rtn @ X):.6f}"
)
print(
f"GPTQ output MSE : "
f"{mse(W @ X, W_gptq @ X):.6f}"
)
这组固定随机种子会得到:
RTN weight MSE : 0.012213
GPTQ weight MSE : 0.014273
RTN output MSE : 0.033483
GPTQ output MSE : 0.010977
注意这个结果。
GPTQ 的权重 MSE:
0.014273
反而比 RTN 的:
0.012213
更大。
也就是说,从“权重是否接近”来看,GPTQ 甚至更差。
但输出 MSE 从:
0.033483
降到了:
0.010977
这就是整篇文章最重要的实验:
GPTQ 愿意牺牲权重空间里的距离,换取函数输出空间里的接近。
它不是在努力把每个权重都复刻得最像,而是在努力保住这一层真正执行的计算。
十二、量化一个真实模型:当前正确的工具链
原稿使用的是:
auto-gptq
但这已经不适合作为新教程的主线。
AutoGPTQ 仓库已在 2025 年归档,维护者建议迁移到 GPTQModel;当前 Hugging Face Transformers 也明确写明,不再支持 AutoGPTQ,应安装 GPT-QModel。(GitHub)
本文采用:
Transformers
+
GPTQModel
+
GPTQConfig
GPTQModel 是当前 Transformers 中活跃维护的 GPTQ 后端,并扩展了模型、平台和硬件支持。(Hugging Face)
12.1 教程使用的模型
我们选:
Qwen/Qwen2.5-0.5B-Instruct
原因很简单:
- 是真实的指令模型;
- 参数量较小;
- 普通消费级 GPU 更容易完成量化;
- 支持中文;
- 方便快速验证完整流程。
0.5B 适合教学,但要注意:
小模型的固定开销占比更大,量化后总显存不会严格缩小到 FP16 的四分之一;同时,小模型有时也比大模型更容易受到量化扰动。
完成流程后,把模型 ID 换成 1.5B、3B、7B 或你的微调模型即可。
12.2 环境准备
建议创建独立环境:
python -m venv .venv
source .venv/bin/activate
Windows PowerShell:
.venv\Scripts\Activate.ps1
先按照自己的 CUDA 版本安装对应的 PyTorch。
随后安装:
python -m pip install -U \
transformers \
accelerate \
optimum \
datasets \
safetensors \
sentencepiece
安装 GPTQModel:
python -m pip install -U \
gptqmodel \
--no-build-isolation
这也是当前 Hugging Face GPTQ 文档给出的主线安装方式。(Hugging Face)
检查环境:
import importlib.metadata
import torch
import transformers
print("torch:", torch.__version__)
print("transformers:", transformers.__version__)
print(
"gptqmodel:",
importlib.metadata.version("gptqmodel"),
)
print("CUDA available:", torch.cuda.is_available())
if torch.cuda.is_available():
print(
"GPU:",
torch.cuda.get_device_name(0),
)
本文后续示例默认:
- 有一张 NVIDIA CUDA GPU;
- PyTorch 能正常识别 CUDA;
- 模型和数据能够从 Hugging Face 下载;
- CPU 内存能够容纳模型加载和保存过程。
GPTQModel 当前也覆盖更多 CPU、AMD、Apple Silicon 和其他硬件路径,但不同平台的 kernel、安装方式和性能表现不完全相同。本文不展开跨平台细节。(GitHub)
十三、准备高质量校准数据
13.1 教程数据和生产数据要分开看
为了让代码开箱即用,我们可以从 WikiText 中准备一批文本。
但必须明确:
WikiText 只是验证流程的通用数据,不是中文对话模型的理想生产校准数据。
如果你的模型实际服务于:
- 中文客服;
- 法律问答;
- 医疗文本;
- Python 代码;
- 数学证明;
- 多轮角色扮演;
- SQL 生成;
那么校准数据应该尽量覆盖对应分布。
13.2 构造固定长度的校准片段
原稿按“文本行”取样,会产生一个问题:
有些文本行很短,最终 128 条样本可能只有很少的 token。
更稳妥的方式是:
- 把非空文本拼成 token 流;
- 按固定长度切块;
- 打乱;
- 选出指定数量。
import random
from datasets import load_dataset
def build_calibration_texts(
tokenizer,
n_samples: int = 128,
seq_len: int = 512,
seed: int = 42,
) -> list[str]:
"""
从 WikiText 构造固定 token 长度的校准文本。
注意:
这适合验证流程。
生产环境应替换为目标领域数据。
"""
dataset = load_dataset(
"wikitext",
"wikitext-2-raw-v1",
split="train",
)
paragraphs = []
for item in dataset:
text = item["text"].strip()
if text:
paragraphs.append(text)
merged_text = "\n\n".join(paragraphs)
token_ids = tokenizer.encode(
merged_text,
add_special_tokens=False,
)
chunks = []
for start in range(
0,
len(token_ids) - seq_len + 1,
seq_len,
):
chunk_ids = token_ids[
start : start + seq_len
]
chunks.append(chunk_ids)
rng = random.Random(seed)
rng.shuffle(chunks)
selected = chunks[:n_samples]
if len(selected) < n_samples:
raise RuntimeError(
f"校准片段不足:需要 {n_samples},"
f"实际只有 {len(selected)}"
)
texts = [
tokenizer.decode(
ids,
skip_special_tokens=True,
)
for ids in selected
]
return texts
13.3 中文对话数据应该怎样处理
假设你有 JSONL:
{"messages":[{"role":"user","content":"什么是模型量化?"},{"role":"assistant","content":"模型量化是……"}]}
{"messages":[{"role":"user","content":"写一个快速排序。"},{"role":"assistant","content":"下面是 Python 实现……"}]}
最好先用模型自己的 chat template 渲染:
import json
def load_chat_calibration_texts(
jsonl_path: str,
tokenizer,
limit: int = 128,
) -> list[str]:
texts = []
with open(
jsonl_path,
"r",
encoding="utf-8",
) as f:
for line in f:
item = json.loads(line)
messages = item["messages"]
rendered = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
)
texts.append(rendered)
if len(texts) >= limit:
break
if len(texts) < limit:
raise RuntimeError(
f"校准数据不足:"
f"需要 {limit} 条,"
f"实际 {len(texts)} 条"
)
return texts
这样 Hessian 看到的输入形式,会更接近真实部署时的:
- system token;
- user token;
- assistant token;
- 对话分隔符;
- 特殊模板结构。
13.4 校准数据选择的五条原则
第一,校准集和评测集必须分开。
否则你无法判断模型是在整体上保住了能力,还是只在校准文本上表现好。
第二,总 token 数比样本条数更重要。
128 个 2048-token 片段和 128 个 30-token 短句不是同一个量级。
第三,长度分布要接近部署输入。
只用短文本校准,不一定能覆盖长上下文时的隐藏状态分布。
第四,语言与领域要匹配。
中文模型只拿英文百科校准,未必立即崩坏,但通常不是最佳选择。
第五,不要只放“干净答案”。
如果线上输入包含错别字、代码块、表格、长指令、多轮对话,校准数据也应适当覆盖。
十四、执行 Qwen2.5-0.5B 的 GPTQ INT4 量化
下面给出完整脚本。
保存为:
quantize_qwen_gptq.py
from pathlib import Path
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
GPTQConfig,
)
MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
OUTPUT_DIR = Path(
"./qwen2.5-0.5b-instruct-gptq-int4"
)
N_SAMPLES = 128
SEQ_LEN = 512
def main() -> None:
tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
use_fast=True,
)
if tokenizer.pad_token_id is None:
tokenizer.pad_token = tokenizer.eos_token
calibration_texts = build_calibration_texts(
tokenizer=tokenizer,
n_samples=N_SAMPLES,
seq_len=SEQ_LEN,
seed=42,
)
print(
f"已准备 {len(calibration_texts)} "
f"条校准片段"
)
gptq_config = GPTQConfig(
bits=4,
# 每 128 个权重共享一组量化参数
group_size=128,
# 当前 GPTQConfig 的常见默认值。
# 原始论文使用过 0.01。
damp_percent=0.1,
# 先关闭经典 act-order,
# 建立容易复现的基线
desc_act=False,
# 对称量化兼容性通常更广
sym=True,
# block 内使用真实顺序量化
true_sequential=True,
# 为了更接近经典 GPTQ 基线,
# 显式关闭当前扩展的 GAR。
# 生产测试时可删除这一行,
# 使用当前库默认策略。
act_group_aware=False,
dataset=calibration_texts,
tokenizer=tokenizer,
model_seqlen=SEQ_LEN,
batch_size=1,
pad_token_id=tokenizer.pad_token_id,
)
print("开始加载并量化模型……")
quantized_model = (
AutoModelForCausalLM.from_pretrained(
MODEL_ID,
device_map="auto",
quantization_config=gptq_config,
low_cpu_mem_usage=True,
)
)
OUTPUT_DIR.mkdir(
parents=True,
exist_ok=True,
)
# 使用 device_map 量化后,
# 保存前可按当前 HF 文档建议移回 CPU。
quantized_model.to("cpu")
quantized_model.save_pretrained(
OUTPUT_DIR
)
tokenizer.save_pretrained(
OUTPUT_DIR
)
print("量化完成")
print(f"模型已保存到:{OUTPUT_DIR}")
if __name__ == "__main__":
main()
运行:
python quantize_qwen_gptq.py
在当前 Transformers 集成中,核心入口是:
GPTQConfig(...)
然后将它传给:
AutoModelForCausalLM.from_pretrained(
...,
quantization_config=gptq_config,
)
这与当前 Hugging Face 官方 GPTQ 流程一致。(Hugging Face)
14.1 这段代码背后发生了什么
虽然我们只写了几十行,但库内部大致做了以下事情:
- 加载模型结构和高精度权重;
- 找到 Transformer block;
- 将校准文本 tokenize;
- 前向执行模型;
- 捕获各线性层输入;
- 累积每层的 (XX^\top);
- 添加阻尼;
- 计算 Cholesky/逆 Hessian 信息;
- 对目标线性层执行 GPTQ;
- 生成量化整数权重;
- 保存 scale、zero point、分组映射;
- 将低比特权重打包成 runtime 支持的格式;
- 把量化配置写入模型配置文件。
14.2 为什么量化时仍需要不少内存
有人会问:
最终模型只有 4bit,为什么量化过程还会显存不足?
因为量化过程需要同时处理:
- 原始高精度权重;
- 校准激活;
- Hessian;
- 逆 Hessian或 Cholesky 因子;
- 临时误差矩阵;
- 量化输出;
- 层级缓存。
假设某层输入维度是 4096。
一个 FP32 Hessian 的尺寸为:
[
4096\times4096\times4\text{ bytes}
]
约为:
[
64\text{ MiB}
]
输入维度是 8192 时:
[
8192\times8192\times4
]
约为:
[
256\text{ MiB}
]
这还只是一份矩阵,不包括其他 buffer。
所以:
最终模型省显存,不等于离线量化过程不吃显存。
十五、加载量化模型并生成文本
保存完成后,不需要再次提供校准数据。
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
)
MODEL_DIR = (
"./qwen2.5-0.5b-instruct-gptq-int4"
)
tokenizer = AutoTokenizer.from_pretrained(
MODEL_DIR
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR,
device_map="auto",
)
model.eval()
messages = [
{
"role": "user",
"content": (
"请用通俗但准确的语言解释:"
"为什么 GPTQ 量化需要 Hessian?"
),
}
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(
text,
return_tensors="pt",
)
input_device = (
model
.get_input_embeddings()
.weight
.device
)
inputs = {
key: value.to(input_device)
for key, value in inputs.items()
}
with torch.inference_mode():
outputs = model.generate(
**inputs,
max_new_tokens=256,
do_sample=False,
pad_token_id=tokenizer.eos_token_id,
)
new_tokens = outputs[
0,
inputs["input_ids"].shape[1] :,
]
response = tokenizer.decode(
new_tokens,
skip_special_tokens=True,
)
print(response)
这里最好使用:
do_sample=False
因为我们正在做量化前后对比。
如果开启随机采样,即使两个模型完全相同,也可能因为随机性生成不同内容,干扰判断。
十六、科学对比显存、困惑度与速度
只看一个回答“好像还挺通顺”,远远不够。
一个完整的量化评估至少应包含四类指标:
- 模型存储和显存;
- 语言建模损失或困惑度;
- 下游任务能力;
- 推理吞吐与延迟。
16.1 不要再写“INT4 显存一定是 FP16 的四分之一”
理论上,如果所有参数都从 16bit 变成 4bit:
[
\frac{4}{16}
\frac14
]
但真实总显存近似为:
[
M_{\text{total}}
M_{\text{weights}}
+
M_{\text{KV}}
+
M_{\text{activations}}
+
M_{\text{workspace}}
+
M_{\text{runtime}}
]
GPTQ 主要降低的是:
[
M_{\text{weights}}
]
不会自动把其他部分都缩小四倍。
此外,量化权重还需要:
- scales;
- zero points;
g_idx;- 打包元数据;
- 未量化模块。
所以更严谨的写法是:
GPTQ 可以让权重存储接近 4 倍压缩,但端到端显存下降比例取决于非量化权重、KV Cache、激活和 runtime 开销。
对于短上下文、大模型、单 batch 的自回归解码,权重显存占比通常较高,GPTQ 的收益明显。
对于超长上下文,KV Cache 占比会快速上升,此时仅压缩权重的边际收益会下降。
16.2 查看模型内存占用
Transformers 模型通常提供:
footprint = model.get_memory_footprint()
print(
f"Model footprint: "
f"{footprint / 1024**2:.1f} MiB"
)
GPU 当前分配:
if torch.cuda.is_available():
torch.cuda.synchronize()
allocated = (
torch.cuda.memory_allocated()
/ 1024**2
)
reserved = (
torch.cuda.memory_reserved()
/ 1024**2
)
print(
f"CUDA allocated: "
f"{allocated:.1f} MiB"
)
print(
f"CUDA reserved: "
f"{reserved:.1f} MiB"
)
注意:
memory_allocated是当前被张量占用的显存;memory_reserved是 PyTorch allocator 保留的显存;nvidia-smi看到的还可能包含 CUDA context 和其他进程;- 不同指标不能直接混为一谈。
为了准确比较 FP16 和 GPTQ,最好分别启动两个独立 Python 进程。
否则前一个模型留下的 allocator 缓存可能污染第二个结果。
16.3 用滑动窗口计算困惑度
困惑度定义为:
[
\operatorname{PPL}
\exp(\text{average negative log-likelihood})
]
PPL 越低,表示模型对真实文本越不“意外”。
下面给出一个滑动窗口版本。
import math
import torch
@torch.inference_mode()
def sliding_window_ppl(
model,
tokenizer,
text: str,
max_length: int = 512,
stride: int = 256,
) -> float:
model.eval()
encoded = tokenizer(
text,
return_tensors="pt",
add_special_tokens=False,
)
all_ids = encoded["input_ids"]
input_device = (
model
.get_input_embeddings()
.weight
.device
)
total_nll = 0.0
total_loss_tokens = 0
previous_end = 0
sequence_length = all_ids.shape[1]
for begin in range(
0,
sequence_length,
stride,
):
end = min(
begin + max_length,
sequence_length,
)
target_length = end - previous_end
input_ids = all_ids[
:,
begin:end,
].to(input_device)
labels = input_ids.clone()
# 重叠窗口中,只计算新出现 token 的损失
if target_length < labels.shape[1]:
labels[
:,
:-target_length,
] = -100
outputs = model(
input_ids=input_ids,
labels=labels,
)
# Causal LM 内部会做 shift,
# 因此统计 labels[:, 1:] 中的有效位置
valid_tokens = (
labels[:, 1:] != -100
).sum().item()
total_nll += (
outputs.loss.float().item()
* valid_tokens
)
total_loss_tokens += valid_tokens
previous_end = end
if end == sequence_length:
break
if total_loss_tokens == 0:
raise ValueError(
"用于计算 PPL 的有效 token 数为 0"
)
return math.exp(
total_nll / total_loss_tokens
)
评测文本必须来自未参与校准的数据。
例如:
from datasets import load_dataset
test_dataset = load_dataset(
"wikitext",
"wikitext-2-raw-v1",
split="test",
)
test_text = "\n\n".join(
item["text"].strip()
for item in test_dataset
if item["text"].strip()
)
ppl = sliding_window_ppl(
model,
tokenizer,
test_text,
max_length=512,
stride=256,
)
print(f"PPL: {ppl:.4f}")
16.4 为什么原稿里重复一句话算 PPL 不够可靠
原稿使用类似:
test_text = "模型量化是一种……" * 5
这种测试可以用于检查代码是否跑通,但不适合做严肃结论。
因为重复文本会产生异常简单的统计模式。
模型看到同样的片段反复出现后,预测难度会下降,PPL 不能代表真实语料表现。
更合理的是:
- 使用数万 token 的保留集;
- 覆盖多个文档;
- 校准集和测试集分离;
- FP16 与 INT4 使用完全相同的文本;
- 固定 tokenizer 和窗口策略。
16.5 测量端到端生成速度
import time
import torch
@torch.inference_mode()
def benchmark_generation(
model,
tokenizer,
prompt: str,
max_new_tokens: int = 128,
warmup_runs: int = 2,
test_runs: int = 5,
) -> dict:
messages = [
{
"role": "user",
"content": prompt,
}
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(
text,
return_tensors="pt",
)
input_device = (
model
.get_input_embeddings()
.weight
.device
)
inputs = {
key: value.to(input_device)
for key, value in inputs.items()
}
generation_kwargs = dict(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
pad_token_id=tokenizer.eos_token_id,
)
for _ in range(warmup_runs):
model.generate(**generation_kwargs)
if torch.cuda.is_available():
torch.cuda.synchronize()
torch.cuda.reset_peak_memory_stats()
total_time = 0.0
total_new_tokens = 0
for _ in range(test_runs):
if torch.cuda.is_available():
torch.cuda.synchronize()
start = time.perf_counter()
outputs = model.generate(
**generation_kwargs
)
if torch.cuda.is_available():
torch.cuda.synchronize()
elapsed = time.perf_counter() - start
new_tokens = (
outputs.shape[1]
- inputs["input_ids"].shape[1]
)
total_time += elapsed
total_new_tokens += new_tokens
result = {
"seconds": total_time,
"new_tokens": total_new_tokens,
"tokens_per_second": (
total_new_tokens / total_time
),
}
if torch.cuda.is_available():
result["peak_vram_mib"] = (
torch.cuda.max_memory_allocated()
/ 1024**2
)
return result
调用:
result = benchmark_generation(
model=model,
tokenizer=tokenizer,
prompt=(
"解释 GPTQ、AWQ 和普通四舍五入量化"
"之间的区别。"
),
max_new_tokens=128,
)
print(result)
这个测试测的是:
包含 prompt prefill 和 token decode 的端到端吞吐。
它不是纯 decode tokens/s,也不是首 token 延迟。
如果要精确区分:
- TTFT,Time To First Token;
- ITL,Inter-Token Latency;
- prefill throughput;
- decode throughput;
需要使用更专业的 serving benchmark 或 profiler。
16.6 GPTQ 不保证在所有硬件上都更快
量化模型通常读取的权重更少,因此自回归 decode 这种容易受显存带宽限制的场景,经常能受益。
但最终速度仍取决于:
- GPU 架构;
- kernel;
- batch size;
- prompt 长度;
- 生成长度;
- 是否发生 CPU offload;
- 模型大小;
- act-order 排列;
- 权重格式;
- 是否使用 Marlin、ExLlama、Triton 等优化后端。
对于非常小的模型:
- FP16 权重本来就不大;
- dequantization 和 kernel 调度开销可能占比较高;
- INT4 不一定明显更快;
- 某些场景甚至可能更慢。
因此不要写:
GPTQ 量化后推理一定更快。
更准确的表述是:
GPTQ 降低了权重存储和权重读取带宽;在有合适融合 kernel、且推理受内存带宽限制时,通常能带来速度收益。
原论文展示了特定 GPU 和定制 kernel 下的显著加速;当前 Transformers 也支持使用 Marlin 等 4bit GPTQ 推理 kernel,但实际收益需要在目标硬件上测量。(DOI)
16.7 一张正确的结果表应该长这样
不要提前写死示例数字。
运行完成后,把真实结果填入:
| 指标 | FP16/BF16 | GPTQ INT4 | 变化 |
|---|---|---|---|
| 模型 footprint | |||
| CUDA allocated | |||
| 峰值显存 | |||
| 保留集 PPL | |||
| 生成 tokens/s | |||
| 下游任务分数 | |||
| checkpoint 大小 |
只有同时看到:
- 显存明显下降;
- PPL 增幅可接受;
- 下游任务没有异常退化;
- 推理速度符合部署目标;
才能说这次量化成功。
16.8 论文结果能说明什么
原始 GPTQ 论文在 WikiText2 上报告:
| 模型 | FP16 | RTN 4bit | GPTQ 4bit |
|---|---|---|---|
| OPT-175B PPL | 8.34 | 10.54 | 8.37 |
也就是说,在论文设置下:
- 普通 4bit RTN 从 8.34 恶化到 10.54;
- GPTQ 4bit 只从 8.34 变为 8.37。
这组结果非常直观地展示了二阶补偿相对独立舍入的价值。(ar5iv)
但不能把这个结果直接写成:
所有模型做 GPTQ 4bit 都只掉 0.03 PPL。
模型、数据、量化器、分组、kernel 和评测集变化后,结果都会变化。
十七、怎样设计一组有说服力的消融实验
如果只是跑一次:
4bit + group_size 128
你只能知道这个配置能不能用,无法知道哪个因素最重要。
建议至少设计下面五组实验。
17.1 基线组
bits=4
group_size=128
desc_act=False
sym=True
damp_percent=0.1
这是当前教程基线。
17.2 更小分组
bits=4
group_size=32
desc_act=False
观察:
- PPL 是否改善;
- checkpoint 增加多少;
- 推理速度是否变化。
17.3 开启 act-order
bits=4
group_size=128
desc_act=True
观察精度改善是否值得潜在的 runtime 复杂度。
17.4 更匹配的校准数据
量化配置完全相同,只把:
英文 WikiText 校准
替换为:
中文目标领域校准
这是非常有价值的一组实验。
它能直接验证“校准数据分布是否重要”。
17.5 校准规模实验
例如:
32 × 512 token
64 × 512 token
128 × 512 token
128 × 1024 token
不要只比较样本数,同时记录总 token 数。
17.6 实验时一次只改一个变量
错误做法:
实验 A:
g128、无 act-order、英文数据、0.1 阻尼
实验 B:
g32、有 act-order、中文数据、0.01 阻尼
即使实验 B 更好,你也不知道是哪个因素造成的。
正确做法是每次只改变一项:
| 实验 | group | act-order | 校准数据 | 阻尼 |
|---|---|---|---|---|
| A | 128 | 关 | 通用 | 0.1 |
| B | 32 | 关 | 通用 | 0.1 |
| C | 128 | 开 | 通用 | 0.1 |
| D | 128 | 关 | 目标领域 | 0.1 |
| E | 128 | 关 | 通用 | 0.01 |
这样才能形成可解释结论。
十八、GPTQ、AWQ、bitsandbytes 怎么选
这三者经常被放在同一张表里,但它们的定位并不完全相同。
| 方法 | 核心思想 | 外部校准数据 | 二阶信息 | 常见目标 | 典型产物 |
|---|---|---|---|---|---|
| RTN | 每个权重独立舍入 | 否 | 否 | 快速基线 | 简单低比特权重 |
| GPTQ | 局部输出重建、逐列补偿 | 是 | 是 | 高质量离线 W4 | 预量化 checkpoint |
| AWQ | 用激活统计识别并保护重要通道 | 是 | 否 | 高质量硬件友好 W4 | 预量化 checkpoint |
| bitsandbytes 4bit | 分块 FP4/NF4、加载时替换线性层 | 通常不需任务校准集 | 否 | 快速加载、QLoRA、实验 | 加载时量化模型 |
18.1 GPTQ 与 RTN
RTN:
- 权重之间互不沟通;
- 不使用真实输入;
- 不补偿前面产生的误差;
- 非常快。
GPTQ:
- 使用校准激活;
- 构造局部 Hessian;
- 逐列量化;
- 把当前误差传播给后续权重;
- 离线量化更慢;
- 4bit 下通常明显优于简单 RTN。
两者真正的差异,不只是“GPTQ 算得更多”。
而是优化目标发生了变化:
RTN:权重接近
GPTQ:输出接近
18.2 GPTQ 与 AWQ
AWQ 的出发点是:
不同权重通道的重要性不同,而激活分布比单看权重幅值更能识别重要通道。
AWQ 不像 GPTQ 那样,对每一列执行逆 Hessian 误差补偿。
它通过等价缩放:
- 放大需要保护的权重通道;
- 对输入激活做反向缩放;
- 保持原始函数等价;
- 让重要权重在分组量化网格中获得更好的相对精度。
AWQ 论文强调,它不依赖反向传播或逐层重建,而是通过离线激活统计选择缩放策略。(arXiv)
可以把二者概括为:
GPTQ:
误差已经产生后,
计算怎样补偿最划算。
AWQ:
量化之前先调整尺度,
尽量保护重要通道。
谁一定更好?
没有统一答案。
真实结果取决于:
- 模型结构;
- 位宽;
- 校准集;
- 目标任务;
- 推理框架;
- kernel;
- 是否量化 MoE;
- 是否涉及多模态模块。
算法精度只是一部分,部署兼容性同样重要。
18.3 GPTQ 与 bitsandbytes
bitsandbytes 的最大优势是简单。
BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
)
然后在加载模型时量化即可。
它还与 QLoRA 紧密结合,可以:
- 用 4bit 基座降低训练显存;
- 保留低秩适配器参数进行训练;
- 支持 NF4;
- 支持 double quantization。
当前 Hugging Face 文档也主要围绕 FP4、NF4、计算 dtype 和嵌套量化介绍 bitsandbytes 4bit。(Hugging Face)
GPTQ 更像是:
提前花时间,把模型做成一个面向低比特推理的静态 checkpoint。
bitsandbytes 更像是:
在加载模型时,快速把线性层替换成低比特实现,尤其适合实验和参数高效微调。
所以:
- 想快速试模型、做 QLoRA:优先考虑 bitsandbytes;
- 想提前量化、保存、分发并部署:GPTQ 或 AWQ 更自然;
- 追求线上吞吐:先确认目标 serving 框架和 kernel 支持什么格式;
- 追求最高精度:必须在自己的任务上实测,不能只看方法名称。
18.4 选型时先问五个问题
第一,模型要跑在哪里?
- NVIDIA GPU;
- AMD GPU;
- CPU;
- Apple Silicon;
- 移动端。
第二,目标框架支持什么?
- Transformers;
- vLLM;
- SGLang;
- TensorRT-LLM;
- 自定义 runtime。
第三,是推理还是微调?
- 纯推理;
- LoRA;
- 全参数训练;
- 在线服务。
第四,主要瓶颈是什么?
- 模型放不进显存;
- decode 太慢;
- prefill 太慢;
- KV Cache 太大;
- batch 吞吐不足。
第五,能接受多少离线处理成本?
GPTQ 量化可能需要较长时间,但这是一次性成本。
十九、十二个常见误区
误区一:GPTQ 的 Hessian 是整个大模型训练损失的完整 Hessian
不是。
它是每个局部线性层输出重建目标的 Hessian。
对于这个局部二次目标,它是精确的;对于整网任务损失,它是代理。
误区二:GPTQ 按列处理,所以每一列互相独立
恰恰相反。
如果列互相独立,就不需要 Hessian 补偿了。
GPTQ 逐列处理,是因为列之间存在耦合,而且后续尚未量化列可以吸收前面列的误差。
误区三:GPTQ 到底按行还是按列,两个说法必有一个错
数学目标按输出行分解。
实现让所有行共享同一输入列顺序,因此矩阵层面按列向量化执行。
两者描述的是不同层次。
误区四:4bit 模型所有计算都是 INT4
通常不是。
常见 GPTQ 是 W4A16:
- 权重低比特存储;
- 激活保持 FP16/BF16;
- kernel 融合反量化和乘法;
- 累加精度由 backend 决定。
误区五:4bit 模型总显存一定是 FP16 的四分之一
只有纯权重部分接近四倍压缩。
KV Cache、激活、未量化层、scale、zero point 和 workspace 不会全部同步缩小。
误区六:量化过程慢,所以推理也慢
量化和推理是两个阶段。
GPTQ 离线量化要收集 Hessian、做分解和误差补偿,确实较慢。
推理速度则主要由低比特 kernel、硬件和 workload 决定。
误区七:量化后一定比 FP16 快
不一定。
如果:
- kernel 不合适;
- 模型很小;
- 存在 CPU offload;
- batch 设置不匹配;
- dequantization 开销占比高;
INT4 可能不比 FP16 快。
误区八:校准数据只要有 128 条就行
不对。
应该关注:
- 每条多少 token;
- 总 token 数;
- 领域是否匹配;
- 是否使用正确 chat template;
- 是否覆盖真实输入长度。
误区九:group_size 越小越好
小分组通常提高精度,但会增加:
- scale/zero 元数据;
- checkpoint 大小;
- kernel 复杂度;
- 读取开销。
最后应根据端到端指标选择。
误区十:desc_act 能提精度,所以必须开
它通常有机会改善精度,但也可能改变:
- 权重排列;
- 分组行为;
- checkpoint 格式;
- 推理 kernel 路径;
- 实际吞吐。
应同时测质量和速度。
误区十一:PPL 上升一点就代表量化失败
量化本来就是有损压缩。
应该看:
- 相对增幅;
- 是否在多个数据集上一致;
- 下游任务是否退化;
- 真实业务样本是否异常。
例如从 8.40 变成 8.48,和从 8.40 变成 20,显然不是一个级别。
误区十二:PPL 没变化就代表所有能力都保住了
也不对。
PPL 是语言建模指标,不能完整覆盖:
- 数学推理;
- 代码正确率;
- 指令遵循;
- 长上下文;
- 工具调用;
- 结构化输出;
- 多语言能力;
- 安全对齐。
一个模型可能整体 PPL 变化很小,但某类敏感任务明显退化。
二十、故障排查指南
20.1 量化时 CUDA OOM
优先检查:
校准 seq_len 是否过大
batch_size 是否大于 1
是否有其他 GPU 进程
模型是否全部常驻 GPU
CPU 内存是否足够
可以尝试:
batch_size=1
降低:
SEQ_LEN = 256
或者设置:
device_map="auto"
大模型还可以通过 max_memory 控制 CPU/GPU 分配。当前 Hugging Face GPTQ 文档也提供了这种 offload 配置。(Hugging Face)
但降低序列长度可能影响校准质量。
所以它首先是排障手段,不一定是最终生产配置。
20.2 Cholesky 失败或出现非正定错误
可能原因:
- 校准 token 太少;
- 大量输入通道无激活;
- Hessian 条件数太差;
- 阻尼过小;
- 模型 dtype 或 kernel 存在兼容问题。
尝试:
- 增加校准数据;
- 增大
damp_percent; - 升级
transformers和gptqmodel; - 检查校准输入是否为空;
- 检查 tokenizer 是否返回有效 token;
- 先用较小模型验证环境。
20.3 量化结果 PPL 爆炸
优先排查:
- 用错 tokenizer;
- 校准文本为空或过短;
- 输入模板与模型不匹配;
- 位宽过低;
- group size 过大;
- 某些模型模块未被正确识别;
- 量化了不应量化的输出层;
- 保存或加载格式不匹配;
- 校准数据分布严重偏离。
可以先退回保守配置:
bits=4
group_size=128
desc_act=False
sym=True
true_sequential=True
然后逐项测试。
20.4 模型能加载,但生成乱码
检查特殊 token:
print(tokenizer.eos_token_id)
print(tokenizer.pad_token_id)
确认使用:
tokenizer.apply_chat_template(...)
而不是直接把裸 prompt 喂给指令模型。
同时检查:
- 模型路径是否完整;
- tokenizer 是否与量化模型一起保存;
- 是否加载了错误的基座 tokenizer;
- 输入是否放到正确设备;
- 是否错误截断了 prompt;
generation_config.json是否异常。
20.5 INT4 显存下降很少
可能原因:
- 模型太小;
- embedding 和
lm_head未量化; - 当前上下文很长,KV Cache 占比高;
- runtime 预留了较多 workspace;
memory_reserved被误当成真实模型权重;- 同一进程中曾加载 FP16 模型;
- CPU/GPU 多设备映射造成统计混乱。
先分别在独立进程测量:
model.get_memory_footprint()
和:
torch.cuda.memory_allocated()
20.6 INT4 推理比 FP16 慢
优先检查:
- 是否使用了优化 kernel;
- 是否发生 CPU offload;
- 模型是否过小;
- batch 是否过大或过小;
- checkpoint 格式是否匹配 runtime;
desc_act是否触发额外排列;- GPU 架构是否适合当前 kernel;
- 是否把第一次 JIT 编译计入 benchmark;
- 是否做了 warmup;
- 是否在计时前调用
torch.cuda.synchronize()。
Marlin 之类的 kernel 只负责量化模型推理,不负责离线量化,并且有明确的位宽和硬件约束。(Hugging Face)
20.7 旧 AutoGPTQ 模型无法用新工具加载
不要默认所有名为 “GPTQ” 的 checkpoint 都完全兼容。
可能存在差异:
- legacy GPTQ 格式;
- GPTQ v2;
- 对称或非对称;
- static group;
- act-order;
- Marlin 专用格式;
- ExLlama 格式;
- zero point 打包方式。
当前 GPTQModel 已经明显偏离早期 AutoGPTQ 实现,部分新格式并不与旧 checkpoint 双向兼容。(Hugging Face)
迁移时应检查模型目录中的:
config.json
quantize_config.json
quantization_config
不要只看文件名里是否包含 gptq。
二十一、把整篇浓缩成一张认知地图
可以把 GPTQ 分成五层理解。
第一层:优化目标
普通量化:
[
\min|W-\widehat W|^2
]
GPTQ:
[
\min|WX-\widehat W X|^2
]
从权重对齐,转向输出对齐。
第二层:数据统计
[
H=2XX^\top
]
Hessian 编码:
- 输入通道能量;
- 输入通道相关性;
- 局部误差曲率。
第三层:单步补偿
量化当前权重后:
[
\delta^*
-\frac{w_q-Q(w_q)}
{[H^{-1}]{qq}}
H^{-1}{:,q}
]
用剩余未量化权重吸收当前误差。
第四层:可扩展工程
GPTQ 相对 OBQ 的关键改造:
- 所有输出行共享列顺序;
- 按列向量化;
- 分块延迟更新;
- Cholesky 重写;
- 阻尼稳定;
- 逐层、逐 block 执行。
第五层:真实部署
最终 checkpoint 不只包含 4bit 整数,还包含:
- packed weights;
- scales;
- zero points;
- group mapping;
- quantization config;
- kernel 格式信息。
推理时通常是:
压缩权重读取
↓
融合解码/反量化
↓
与 FP16/BF16 激活计算
↓
高精度累加或指定精度累加
二十二、小结
这一篇,我们从直觉、数学、工程和实战四个层面完整走了一遍 GPTQ。
最值得记住的是下面十点。
第一,GPTQ 的根本目标不是让每个量化权重都尽量接近原值,而是让量化层的输出尽量接近原输出。
它优化的是:
[
|WX-\widehat W X|_F^2
]
而不是简单的:
[
|W-\widehat W|_F^2
]
第二,校准输入决定了量化误差应该怎样衡量。
同样大小的权重误差,落在高激活通道和低激活通道上,影响完全不同。
第三,Hessian 来自输入相关性。
对于局部重建目标:
[
H=2XX^\top
]
它描述输入通道的能量和相关结构。
第四,局部目标里的 Hessian 是精确的;所谓“近似二阶信息”,是相对于整网真实任务损失而言。
第五,GPTQ 的数学问题按权重行分解,但工程上让所有行共享列顺序,因此通常表现为逐列处理。
这不是矛盾。
第六,GPTQ 的核心操作是逐列量化和误差补偿。
当前列被固定到量化网格后,利用逆 Hessian 计算后续权重应该如何调整。
第七,GPTQ 能扩展到大模型,不只是因为用了二阶信息,更因为它做了固定顺序、延迟批量更新和 Cholesky 重写。
第八,group_size 和算法 blocksize 不是同一个参数。
前者控制 scale 共享粒度,后者控制误差更新的计算分块。
第九,4bit 不等于整网 INT4,也不代表总显存一定缩小到四分之一。
大多数 GPTQ 是 W4A16,KV Cache 和激活不会自动压缩。
第十,当前新项目应优先使用 GPTQModel,而不是继续围绕已归档的 AutoGPTQ 编写主线教程。
GPTQ 真正带来的认知升级是:
量化不是把每个数字单独变短,而是在有限的离散表示能力下,尽量保住模型原本实现的函数。
普通四舍五入只看一个权重。
GPTQ 看的是一个权重被改变之后,整个输入特征空间会发生什么。
这就是二阶信息的价值,也是高质量低比特量化与简单数值压缩之间的分界线。
更多推荐




所有评论(0)