GPTQ 从零推导:4-bit 权重量化为什么能“量化一列,补偿一列”¶
⏱ 约 102 分钟本文整理于2026年08月07日晚上十点
前言¶
最近在学习大模型相关的量化算法,从LLM.int8()、ZeroQuant、再到SmoothQuant,一路追到GPTQ,在GPTQ的推导上花了小一周的晚上时间,写下此文记录一下,方便后续也对此感兴趣的读者。
本文只需要读者学过矩阵乘法、逆矩阵、正定矩阵等线性代数基础知识。我们将从“一个浮点数怎样变成 4-bit”开始,完整推到 GPTQ 的 blockwise 伪代码;关键计算均标出输入、输出 shape 和含义。 本文聚焦经典 W4A16 GPTQ:权重保存为 4-bit,计算时激活仍用 FP16/BF16。真实 Linear 层若含偏置,本教程假设偏置保持不变,因此它在量化前后的输出差中抵消。
一、读前准备:一个数怎样变成 4-bit¶
1.1. 4-bit 存的不是任意小数,而是 16 个编号之一¶
4 个二进制位最多表示 \(2^4=16\) 个不同编号。本文采用无符号码
给定正的量化步长(scale)\(\Delta_q>0\) 和整数零点(zero point)\(z_q\),常见的仿射量化写成
这两行分别做两件事:
- 编码:把浮点权重 \(w\) 映射为整数码 \(c\);模型文件中真正打包保存的是 \(c\)。
- 反量化:把 \(c\) 对应回量化网格上的浮点值 \(q\);GPTQ 做误差补偿时使用的是 \(q\)。
以 \(\Delta_q=0.25,z_q=8,w=0.62\) 为例:
| 步骤 | 输入 | 输出 | 含义 |
|---|---|---|---|
| 除以 scale | \(w=0.62,\Delta_q=0.25\),均为标量 | \(2.48\),标量 | 用“网格间隔”为单位表示权重 |
| round 并加零点 | 标量 \(2.48,z_q=8\) | \(c=10\),4-bit 整数码 | 选择最近的合法编号 |
| clip | 整数码 | \(c\in[0,15]\) | 超出范围时饱和到端点 |
| 反量化 | \(c=10,\Delta_q=0.25,z_q=8\) | \(q=0.50\),标量 | 该编号在实数轴上代表的值 |
\(\Delta_q\) 决定相邻网格点的距离;\(z_q\) 让整数码 \(z_q\) 精确代表实数 \(0\);clip 表示超出可表示范围的数只能落在最左或最右端点。若一组权重范围约为 \([w_{\min},w_{\max}]\),一种粗略的 min-max 拟合是
真实量化器还会处理对称网格、异常值和常数区间。本文把“怎样拟合网格”和“怎样用二阶信息补偿舍入误差”分开:推导补偿时,当前权重可用的网格视为已经确定。
1.2. PTQ、W4A16 和校准数据分别是什么¶
| 术语 | 小白版解释 |
|---|---|
| PTQ(post-training quantization) | 模型训练完成后再量化,不重新训练整个模型 |
| W4A16 | 权重用 4-bit 码保存;输入激活在计算时仍是 FP16/BF16 |
| 校准数据 | 少量有代表性的真实输入,只用来观察各层收到什么向量,不需要标签,也不更新权重 |
| 激活 | 一条输入运行到某层时,该层实际收到的数值向量;它不是 token ID |
| 反量化权重 | 4-bit 码按 scale/zero 还原出的网格值 \(q\);它通常不等于原权重 \(w\) |
普通 Linear 层把输入向量 \(x\in\mathbb R^d\) 映射为
运行几条代表性文本,记录到达这一层的 \(N\) 个输入向量,并把它们并排放成 \(X\in\mathbb R^{d\times N}\)。偏置 \(b_{\mathrm{bias}}\) 不变时,原层与量化层的输出差是
所以后文只写 \(Y=WX\)。GPTQ 的问题不是“怎样训练 \(W\)”,而是“把 \(W\) 换成只能落在 16 级网格上的 \(\hat W\) 后,怎样让这批输入看到的输出尽量不变”。
1.3. 为什么公式里没有量化 \(X\)¶
因为本文推导的是经典 W4A16 GPTQ,而不是 W4A8 或 W4A4:W4 表示权重被压到 4-bit,A16 表示激活仍用 FP16/BF16,不再额外投影到 INT8/INT4 网格。数学上把捕获到的这些 16-bit 数值记作 \(X\in\mathbb R^{d\times N}\);写 \(\mathbb R\) 只是为了使用矩阵代数,并不表示计算机真的用“无限精度实数”。工程上常把 \(X\) 以 FP16/BF16 捕获,再用 FP32 累积 \(XX^\top\)。
1.4. GPTQ 工作脉络¶
先用四句话概括整篇文章:
- 量化当前工作权重,得到最近网格点 \(q\)。
- 计算被舍掉的残差 \(\rho=w_{\mathrm{work}}-q\)。
- 已量化坐标不再动,让尚未量化的坐标根据校准输入共同吸收这份残差。
- 冻结当前坐标,继续处理下一列;所有输出行可以并行做同一件事。
把一个线性层写成
GPTQ 的出发点不是让每个 \(W_{rj}\) 单独离原值最近,而是让层输出的变化最小:
这里 \(\mathcal G_{r,\operatorname{grp}(j)}\) 是第 \(r\) 个输出行、列 \(j\) 所属量化组可用的标量网格,\(\hat W\) 是反量化后的近似权重。\(\|\cdot\|_F^2\) 表示把矩阵中每个元素的平方加起来,\(1/N\) 表示再按校准位置数取平均。
若直接枚举 \(md\) 个权重各自的 16 种选择,需要比较 \(16^{md}\) 个矩阵,实际不可行。GPTQ 因此按输入特征列向前推进,每一步同时处理 \(W_{:,j}\) 中的 \(m\) 个权重。“量化一列”并不是让整列共用一个量化值:每个权重仍按自己所在输出行和量化组的网格分别舍入。某个网格点一旦选定,其他尚未量化的浮点权重应该怎样移动,就有一个精确公式。所以最终结果很聪明,但不保证是式 (1) 所有可能 INT4 矩阵中的全局最优解。
下面先固定一个输出行 \(r\),只研究这一行的权重;其他输出行做完全相同的计算,工程上可以并行执行。
先把“自由权重”翻译成人话:当前正在处理的权重,以及后面还没有量化的权重,暂时都能移动;已经量化完成的权重则不许再动。把仍能移动的坐标集合记为 \(\mathcal F\),其中共有 \(k\) 个权重,并把这些位置临时编号为 \(0,\ldots,k-1\)。
现在处理 \(\mathcal F\) 中局部编号为 \(p\) 的权重。它的当前工作值是 \(w_{\mathrm{work},p}\),最近的量化网格点是 \(q_p\)。
例如 \(w_{\mathrm{work},p}=0.62\)、\(q_p=0.50\),便有 \(\rho_p=0.12\)。为了让当前权重恰好落到 \(0.50\),它自己必须移动
问题只剩下:其他 \(k-1\) 个权重各自应该移动多少,才能尽量抵消这次舍入对层输出的影响?
现在先把 \(G_F\in\mathbb R^{k\times k}\) 当作一张由校准输入 \(X\) 算出的 \(k\times k\) 数字表。取它的第 \(p\) 列,再除以第 \(p\) 个对角数,就得到各权重随当前权重一起移动的比例:
后文会证明,这张表是阻尼后代价矩阵的逆;在这里先把它看成一张补偿路线图即可。
完整更新为
加入阻尼后 \(G_F\) 是正定矩阵,因此 \([G_F]_{pp}>0\),这里不会除以 0。
这条式子可以分三步读:
| 计算 | 输入 shape | 输出 shape | 小白版含义 |
|---|---|---|---|
| 取第 \(p\) 列 \((G_F)_{:,p}\) | \(G_F:[k,k]\) | \([k,1]\) | 取出所有自由权重与当前权重的关系 |
| 除以 \([G_F]_{pp}\) | \([k,1]\) 除以标量 | \([k,1]\) | 把当前坐标自己的比例归一化为 1 |
| 乘以 \(-\rho_p\) | 标量乘 \([k,1]\) | \(\delta^*:[k,1]\) | 让当前权重移动正确距离,其余权重按比例补偿 |
为什么它一定能把当前权重送到 \(q_p\)?只看输出向量的第 \(p\) 个元素:
分母和同一列的第 \(p\) 个元素正好约掉了。其他任意坐标 \(i\ne p\) 则移动
移动多少由它与第 \(p\) 个坐标的联动关系决定。例如 \(k=2\),当前处理第一个坐标,即 \(p=0\);若
那么
第一个数把当前权重从 \(0.62\) 减到 \(0.50\);第二个数让另一个尚未量化的权重增加 \(0.06\),主动接住一部分输出误差。
这样补偿以后,这一步无法再降低的最小代价为
先把符号逐个翻译:
- \(\Delta\) 表示“这一步新增加的”;
- \(\ell\) 表示我们正在尽量压低的输出误差代价;
- \(\lambda\) 表示为了数值稳定而加入的阻尼;
- 星号 \(*\) 表示其他自由权重已经按最佳方式补偿后得到的最小值。
现在可以先把式 (3) 当成一个“这次舍入有多难补救”的分数:
- 分子是 \(\rho_p^2\):舍入距离越大,代价越高;距离变成 2 倍,平方代价会变成 4 倍。
- 分母里的 \([G_F]_{pp}\) 越大,表示把当前权重移动同样距离后,能够达到的最小代价越低。原因可能是这个输入方向本身不敏感,也可能是其他自由权重容易补偿。
无阻尼时,式 (3) 就是按 token 平均的单行层重建损失增量;有阻尼时,它是带稳定项的代理代价。式 (2)、(3) 在这里只是先建立直觉,后文会先用二维配方法,再用拉格朗日乘子把每一步完整推出。
这里先把存储量算清楚。只计算裸权重、忽略 scale/zero 等元数据时:
| 存储格式 | 每参数位数 | 70B 参数的理想存储量 | 相对 FP16 的权重压缩 |
|---|---|---|---|
| FP32 | 32 bit | 280 GB | 0.5x |
| FP16 / BF16 | 16 bit | 140 GB | 1x |
| INT8 | 8 bit | 70 GB | 2x |
| INT4 | 4 bit | 35 GB | 4x |
所以 INT4 是相对 FP32 的 \(8\times\) 压缩、相对 FP16 的 \(4\times\) 压缩。实际模型还要存储分组 scale、zero point、层元数据,打包和对齐也会带来额外开销,因此不能把 \(35\,\text{GB}\) 当作最终显存占用。
难点在于量化误差不是孤立地发生在一个标量上。若只做 round-to-nearest(RTN),定义
则层输出误差是
同样大小的 \(\Delta W_{ij}\),在不同的输入方向上,可能几乎没有影响,也可能被 \(X\) 放大。更关键的是:多个权重误差会在输出中相加、抵消或放大。只最小化每个元素的 \(|\Delta W_{ij}|\),并不等于最小化 \(\lVert \Delta W X\rVert_F\)。
这正是 GPTQ 的入口:校准输入 \(X\) 决定什么方向的误差昂贵,二阶信息决定剩余权重怎样共同吸收误差。
1.5. 统一记号与 shape 约定¶
本文固定一个 Linear 层。为了让矩阵乘法不来回转置,使用下列约定:
| 符号 | shape | 含义 |
|---|---|---|
| \(d\) | 标量 | 输入特征维度,\(d=d_{\mathrm{in}}\) |
| \(m\) | 标量 | 输出特征维度,\(m=d_{\mathrm{out}}\) |
| \(n_b\) | 标量 | 校准序列总数;它是 \(A\) 的第一维,不是 DataLoader 跑了多少批 |
| \(T\) | 标量 | 为简化记号,假设每条校准序列保留的 token 数相同 |
| \(N\) | 标量 | 有效 token 位置总数;定长无 padding 时 \(N=n_bT\) |
| \(W\) | \([m,d]\) | 浮点权重矩阵 |
| \(W_{\mathrm{work}}\) | \([m,d]\) | 量化过程中的浮点工作副本;未处理列会不断接收补偿 |
| \(\hat W\) | \([m,d]\) | 反量化后的近似权重矩阵 |
| \(K\) | \([m,d]\) | 低比特整数码,4-bit 时每元素属于 16 个码之一 |
| \(A\) | \([n_b,T,d]\) | 框架在该层捕获的原始输入激活 |
| \(X\) | \([d,N]\) | 将 \(A\) 展平 token 维后再转置的校准矩阵 |
| \(Y=WX\) | \([m,N]\) | 原始层在校准输入上的输出 |
| \(H\) | \([d,d]\) | 单个输出行对应的 Hessian |
| \(G=H_\lambda^{-1}\) | \([d,d]\) | 阻尼后 Hessian 的逆;它描述可补偿性 |
| \(R\) | \([d,d]\) | \(G=R^\top R\) 中的上三角 Cholesky 因子 |
| \(b\) | 标量 | GPTQ 的列 block 大小,论文默认 \(128\) |
| \(g\) | 标量 | 量化 group size;\(g=-1\) 或 \(g=d\) 表示整行共用网格 |
| \(n_g\) | 标量 | 每行的 group 数;\(g=-1\) 时为 \(1\),否则为 \(\lceil d/g\rceil\) |
| \(\Delta_q\) | 单个行/组内为标量 | 量化网格步长(scale);下标 \(q\) 用来和损失增量 \(\Delta\ell\) 区分 |
| \(q=\mathcal Q(w_{\mathrm{work}})\) | 与输入相同 | 当前量化器返回的反量化网格值 |
| \(\rho=w_{\mathrm{work}}-q\) | 与输入相同 | 全文统一使用的“被量化舍掉的残差” |
1.6. 单行推导与实现列的区别¶
矩阵 \(W\) 的第 \(r\) 行是一个长度为 \(d\) 的行向量 \(W_{r,:}\)。在推导单行时,为了使用标准二次型,把它转写为列向量:
完整参数空间中的 \(w\)、\(\delta\) 是 \([d,1]\) 列向量;若只看当前 \(k\) 个自由坐标,它们的自由部分则是 \([k,1]\)。标准基写成粗体 \(\mathbf e_p\),同样随所在空间取 \([d,1]\) 或 \([k,1]\)。实现中一次处理所有输出行时,W_work[:, j] 又是长度为 \(m\) 的列。这两种“列”来自不同维度:
| 记号 | shape | 它沿着哪一维 |
|---|---|---|
| 单行参数 \(w_r\) | \([d]\) | 一个输出神经元对所有输入特征的权重 |
实现中的 W_work[:, j] |
\([m]\) | 一个输入特征连接到所有输出神经元的当前工作权重 |
GPTQ 恰恰利用了这一点:数学上每个 \(w_r\) 独立,工程上所有行能同时处理同一个输入特征列 \(j\)。
本文的特征列与伪代码统一使用 0-based 索引:\(j=0,1,\ldots,d-1\)。例如长度为 4 的向量中,“第二个坐标”是 \(p=1\),对应
二、从层输出重建得到 Hessian¶
2.1. 把校准激活整理成 \(X\)¶
Transformer 中某个 Linear 层的输入通常由框架给成:
把前两维合并,并转置:
| 计算 | 输入 shape | 输出 shape | 含义 |
|---|---|---|---|
| 捕获激活 | \(A:[n_b,T,d]\) | 同左 | 多个 batch、多个 token 的层输入 |
| 合并 token 维 | \([n_b,T,d]\) | \([N,d]\) | 每一行变成一个 token 位置 |
| 转置 | \([N,d]\) | \(X:[d,N]\) | 每一列变成一个 token 位置,适配 \(WX\) |
下图把从校准输入到 INT4 权重的全链路串起来。之后的每个公式都会落在图中的某一个节点。
图 1:单层 GPTQ 的数据流。绿色是张量输入/产物,蓝色是统计与变形,橙色是参数和阻尼,紫色是二阶量化计算。
2.2. 层重建目标¶
原始输出与量化后输出分别是:
使用“按 token 平均、对输出通道求和”的平方重建误差:
Frobenius 范数只是逐元素平方和:若 \(M\in\mathbb R^{m\times N}\),则
因此式 (6) 可以读成:“把每个输出通道、每个校准位置的输出差平方后全部相加,再除以位置数 \(N\),寻找使它最小的量化权重。”它不是再除以 \(m\) 的逐元素 MSE。原论文常写不带 \(1/N\) 的版本;两者只差一个正的常数,若阻尼也按同一尺度调整,最优补偿方向不变。
| 计算 | 输入 shape | 输出 shape | 含义 |
|---|---|---|---|
| \(WX\) | \([m,d]\cdot[d,N]\) | \([m,N]\) | 原始层的校准输出 |
| \(\hat W X\) | \([m,d]\cdot[d,N]\) | \([m,N]\) | 量化层的校准输出 |
| 差与 Frobenius 范数 | \([m,N]\) | 标量 | 所有输出通道、所有 token 位置的总误差 |
2.3. 为什么问题能拆成 \(m\) 个独立的行问题¶
令 \(\Delta W=\hat W-W\)。把 \(\Delta W\) 的第 \(r\) 行转为列向量 \(\delta w_r\in\mathbb R^d\),则
这一步非常重要:不同输出行之间没有交叉项。因此可以把每一行当成一个独立的长度 \(d\) 参数向量来优化;但它们共享同一个 \(XX^\top\),因为所有行看的是同一批输入 \(X\)。
也可以用迹写成:
这只是在压缩记号,实际含义仍然是式 (7) 的逐行求和。
2.4. Hessian 从哪里来¶
固定某一个输出行,省略行下标。令当前权重变化为 \(\delta\in\mathbb R^d\),则该行的重建损失是
如果熟悉微积分,可以对 \(\delta\) 求一阶、二阶导:
| 计算 | 输入 shape | 输出 shape | 含义 |
|---|---|---|---|
| \(XX^\top\) | \([d,N]\cdot[N,d]\) | \([d,d]\) | 输入特征两两共同出现的统计量 |
| \(H=2XX^\top/N\) | \([d,d]\) | \([d,d]\) | 改动一行权重时的精确曲率矩阵 |
| \(v^\top Hv\) | \([1,d]\cdot[d,d]\cdot[d,1]\) | 标量 | 沿方向 \(v\) 改权重的二阶代价 |
注意两个常被混淆的事实。
- 对式 (8) 这个层重建目标,\(H=2XX^\top/N\) 是精确 Hessian,不是近似,也不需要假设原模型训练到了极小点。因为式 (8) 本身就是二次函数。
- 在整个网络的原始训练 loss 上谈 Hessian/Fisher 时,事情会复杂得多。GPTQ 在这里没有直接求那个巨大网络 loss 的 Hessian,而是求这个局部重建问题的 Hessian。
不熟悉多元微积分也不影响后面的阅读:式 (8) 已经直接告诉我们,误差由矩阵 \(XX^\top\) 构成的二次型衡量。“Hessian”在本文里只是这个二次型前面的矩阵名称。
在 \(\delta=0\) 处,\(\nabla\ell(0)=0\) 恒等成立,因为目标 \(WX\) 正是由原始 \(W\) 构造出来的。于是
不仅是 Taylor 二阶近似,而是精确等式。
2.5. 本文只需要的优化工具箱¶
后面只会反复使用三个线性代数事实:
- 若对任意非零 \(v\) 都有 \(v^\top Mv>0\),就称对称矩阵 \(M\) 正定,记作 \(M\succ0\)。
- 当 \(M\succ0\) 时,\(\frac12x^\top Mx\) 像一个只有一个谷底的碗;在仿射约束下,它也只有一个最小点。
-
配方法给出更强的证明。若能写成下面的形式,则第二项对所有 \(x\) 都不小于 0,并且只有 \(x=x^*\) 时为 0,所以 \(x^*\) 是唯一全局最小值:
\[ f(x)=f(x^*)+\frac12(x-x^*)^\top M(x-x^*), \] -
正定矩阵的主子矩阵及其逆仍然正定。主子矩阵的证明只需把它的任意非零向量 \(u\) 在未选坐标上补 0,得到 \(v\ne0\),于是 \(u^\top M_{FF}u=v^\top Mv>0\)。对逆矩阵,令 \(z=M^{-1}y\ne0\),便有 \(y^\top M^{-1}y=z^\top Mz>0\)。
拉格朗日乘子只是把“找满足约束的谷底”压缩成一个线性方程组。核心推导会先用配方法把最小值直接看出来,再给出拉格朗日写法作为核对;读者不需要预先学过优化课程。
2.6. \(H\) 的直觉:校准数据在给误差方向标价¶
对任意方向 \(v\in\mathbb R^d\):
如果校准输入经常沿着 \(v\) 方向有很大分量,那么沿 \(v\) 改权重会显著改变输出,代价就高;若 \(v^\top X\) 很小,那个方向对校准集几乎不可见,代价就低。
现实中 \(X\) 常常列数不足、特征相关或秩亏,因此 \(H\) 可能不可逆。GPTQ 使用阻尼:
为什么加上任意 \(\lambda>0\) 就能保证正定?对任意非零 \(v\),
第一项可能因为数据看不见方向 \(v\) 而等于 0,但第二项一定严格大于 0。直觉上,阻尼是在所有方向上加一点“基础代价”,既让矩阵可逆,也避免后续除以极小数。
从这里开始要区分两件事。若 \(\lambda=0\) 且 \(H\) 可逆,后文的二次型就是式 (6) 的精确层重建损失;实际 GPTQ 通常以 \(H_\lambda\) 做补偿,每一步连续更新精确最小化相应的阻尼条件子问题:
这不会改变“往哪个方向补偿”的基本机制,却会改变数值损失和补偿大小。离散落点仍按既定顺序贪心选择,完整 GPTQ 不保证全局最小化 \(\ell_\lambda\)。最终模型好不好,仍要回到裸的层重建误差或语言模型困惑度上验证。
三、标准 GPTQ 的全貌¶
这一章只负责建立全局印象,不在推导前塞入实现细节。读者先知道标准 W4A16 GPTQ 大致按什么顺序处理一个 Linear 层,接下来的章节再逐一回答“为什么这样补偿”和“代码怎样高效实现”。
先看一行总流程:
校准文本 \(\longrightarrow\) 捕获当前层输入 \(X\) \(\longrightarrow\) 配置量化器与网格策略 \(\longrightarrow\) 构造 \(H\) 并处理死特征 \(\longrightarrow\) 排列处理顺序并加入阻尼 \(\longrightarrow\) 预计算补偿表 \(R\) \(\longrightarrow\) 逐列“拟合或选择网格、舍入、算残差、补偿” \(\longrightarrow\) 每个 block 合并写回 \(\longrightarrow\) 反置换 \(\longrightarrow\) 按部署格式打包。
这里先知道“需要选择一种网格策略”即可;整行网格、static groups 和 dynamic groups 的具体拟合时机属于工程细节,放到后文实现章节再区分。
3.1. 输入与最终输出¶
对一个 Linear 层,GPTQ 接收:
| 输入 | shape | 含义 |
|---|---|---|
| 浮点权重 \(W\) | \([m,d]\) | 要量化的原始 Linear 权重 |
| 校准激活 \(X\) | \([d,N]\) | \(N\) 个代表性 token 到达这一层时的输入 |
| bit 数 | 标量 | 本文固定为 4,因此每个权重有 16 个整数码可选 |
| group size \(g\) | 标量 | 多少个连续权重共享一套 scale/zero;它会改变量化网格 |
| block size \(b\) | 标量 | 多少列攒在一起做一次块外矩阵更新;固定顺序和网格时只改变计算组织 |
| 阻尼比例 | 标量 | 给 Hessian 加多少数值稳定项 |
其中,\(d\) 是每个 token 输入向量的长度;\(N\) 是进入该层的全部有效 校准 token 数,不是 batch size。比如 3 条文本去掉 padding 后分别有 \(4,2,3\) 个 token,则 \(N=9\)、\(X:[d,9]\),而 \(X_{:,t}:[d,1]\) 就是第 \(t\) 个 token 到达该层时的输入向量。
要把“GPTQ 算法的结果”和“部署文件中的存法”分开看:
| 阶段 | 输出 | shape | 含义 |
|---|---|---|---|
| GPTQ 数学核心 | 反量化权重 \(Q=\hat W\) | \([m,d]\) | 经典算法直接构造的量化近似;每个元素已经落在某个 4-bit 网格点上 |
| 量化器/部署工具链 | 逻辑整数码 \(K\) | \([m,d]\) | 每个位置是 \(0,\ldots,15\) 中的一个码,此时还没有按字节或机器字压紧 |
| 量化器/部署工具链 | scale、zero | 通常为 \([m,n_g]\) | 把整数码还原成网格值所需的元数据 |
| dynamic groups 与 act-order 联用时 | group_id |
\([d]\) | 告诉部署端每个原始输入列应使用哪一组 scale/zero |
| 打包之后 | packed codes | 由 kernel 布局决定 | 理论最低存储量约为 \(md/2\) byte;实际常按 int32、对齐和分块布局保存 |
3.2. 标准 GPTQ 的十步¶
这一小节只给读者一张路线图,暂时不证明公式:
- 第 1 步:准备少量有代表性的无标签校准文本,让浮点模型正常前向运行;
- 第 2 步:捕获这些文本的每个有效 token 到达当前 Linear 层时的输入,并按列整理成 \(X:[d,N]\);
- 第 3 步:配置 4-bit 量化器,确定整行或分组共享 scale/zero 的网格策略;
- 第 4 步:由校准输入计算 \(H=2XX^\top/N:[d,d]\),得到输入特征之间的二阶关系;
- 第 5 步:处理校准集中从未出现的死特征,并按需要加入阻尼和确定列处理顺序;
- 第 6 步:由阻尼 Hessian 预计算逆 Hessian \(G\) 及其 Cholesky 因子 \(R\),把后续每一列所需的补偿系数提前准备好;
- 第 7 步:建立浮点工作副本 \(W_{\mathrm{work}}:[m,d]\) 和保存最终量化结果的缓冲区;
- 第 8 步是 GPTQ 的核心:算法按既定顺序取出当前工作列 \(w_j=W_{\mathrm{work}}[:,j]:[m]\),注意这里读的是已经接收过前面误差补偿的工作值,而不是原始 \(W[:,j]\);量化器让这一列的每个输出行分别落到自己的 4-bit 网格点,得到整数码 \(c_j:[m]\) 和反量化值 \(q_j:[m]\);随后计算本列舍弃的残差 \(\rho_j=w_j-q_j:[m]\),再依据 \(G\)(高效实现中使用 \(R\))描述的输入特征关系,把这份残差分配给尚未量化的列,使当前列精确变成 \(q_j\),而后续列的工作值相应改变;最后冻结当前列,再从补偿后的下一列继续重复,所以 GPTQ 与普通逐元素舍入最关键的区别就是“量化当前列以后,让未量化列主动接住误差”。接下来的核心推导正是要证明:在当前量化点已经选定时,这种补偿为什么是二次重建目标下的最优补偿;
- 第 9 步:每完成一个 block,就把此前暂存的块外补偿合并成一次矩阵乘法写回尾部;
- 第 10 步:所有列处理完后恢复原列顺序,保存或打包 4-bit 码及 scale/zero,验证层重建误差,再继续量化下一层。
3.3. 放大第 8 步¶
十步流程中,第 1--7 步都在准备数据、量化网格和补偿系数,第 9 步只是把许多补偿合并成更快的矩阵乘法,第 10 步负责整理输出;真正决定 GPTQ 数学结构的是第 8 步反复执行的列循环,它可以压缩成一句话:
读当前工作列 \(\rightarrow\) 落到 4-bit 网格 \(\rightarrow\) 计算舍入残差 \(\rightarrow\) 用 \(R\) 把残差传给尚未量化的列 \(\rightarrow\) 冻结当前列。
接下来的“核心推导”暂时不讨论 group、block 和代码,只研究第 8 步中的一个问题:当前列已经选定量化点后,为什么按逆 Hessian 的一列给其余权重分配补偿,恰好能使层输出重建代价最小? 得到这个数学答案以后,后文再分别解释逆 Hessian 更新、Cholesky 预计算和 lazy block update 怎样实现同一件事。
四、核心推导:固定一个坐标时怎样得到最优补偿¶
4.1. 先看第一次补偿到底做了什么¶
设
人为指定先量化第 0 个坐标。它从 \(0.62\) 落到 \(q_0=0.50\),所以 \(\rho_0=0.62-0.50=0.12\)。完整更新是
结果为
第一项 \(-0.12\) 把当前坐标锁到网格点;第二项 \(+0.06\) 才是其他坐标接住的补偿。后续量化必须处理工作值 \(0.68\),不能回头再量化原值 \(0.62\)。这里的第 0 列顺序是人为指定的,不是公式推导出来的。
4.2. 当前子问题:只固定一个坐标,其余都能动¶
设仍自由的坐标集合为 \(\mathcal F\),大小为 \(k=|\mathcal F|\)。这里的“坐标”就是当前输出行中权重所在的全局列号,不是权重的数值。
先看一个具体例子。假设这一行共有 \(d=5\) 个权重,当前处理状态如下:
| 全局列号 | 权重 | 当前状态 | 是否属于 \(\mathcal F\) | 在自由子问题中的局部编号 |
|---|---|---|---|---|
| 0 | \(w_0\) | 已量化并冻结 | 否 | 无 |
| 1 | \(w_1\) | 已量化并冻结 | 否 | 无 |
| 2 | \(w_2\) | 本轮正要量化 | 是 | 0 |
| 3 | \(w_3\) | 尚未量化,可以补偿 | 是 | 1 |
| 4 | \(w_4\) | 尚未量化,可以补偿 | 是 | 2 |
因此
只取这些仍能移动的位置,工作权重和本轮更新分别是
这里 \(\Delta w_2\) 表示“全局第 2 列的更新量”,其余两个同理。把自由部分取出以后,后面的通用证明会把 \(\delta_{\mathcal F}\) 简写成 \(\delta\),并按自由集合内部重新编号为 \(0,1,2\)。
完整的 \(H_\lambda\) 是 \([5,5]\),而自由子问题只保留全局行、列 \(2,3,4\):
这一轮处理的是全局第 2 列,但它在 \(\mathcal F=\{2,3,4\}\) 中排第一个,所以局部编号是 \(p=0\)。于是
约束 \(\mathbf e_p^\top\delta_{\mathcal F}=-\rho_p\) 只把 \(\Delta w_2\) 固定为当前权重必须移动的距离;\(\Delta w_3,\Delta w_4\) 仍可自由选择,用来补偿输出误差。本轮结束后,全局第 2 列也被冻结,下一轮便有 \(\mathcal F=\{3,4\}\)。
一般地,自由部分的阻尼 Hessian 及其逆记为
当前要量化 \(\mathcal F\) 中局部编号为 \(p\) 的坐标。先对当前工作值量化,再定义残差:
本次完整更新记为 \(\delta\in\mathbb R^k\)。为了让当前坐标从 \(w_{\mathrm{work},p}\) 变成 \(q_p\),必须满足
令 \(\mathbf e_p\in\mathbb R^k\) 为第 \(p\) 个标准基向量,则问题是
约束只固定 \(\delta_p\),没有要求另外 \(k-1\) 个分量为零;它们正是用来补偿的自由度。又因为 \(H_F\succ0\),式 (14) 是严格凸二次问题,所以只会有一个全局最小解。
把这个唯一最优解记为 \(\delta^*\)。由于最优解首先必须是式 (14) 的可行解,它当然也要满足同一个硬约束,因此
这里的星号只表示“在所有可行更新中代价最小的那一个完整向量”;它没有改变第 \(p\) 个分量必须把当前权重送到 \(q_p\) 这一事实。真正需要优化的,是其余 \(k-1\) 个分量\(\delta_{\mathcal T}^*\) 应该怎样移动,才能最有效地补偿这次舍入。
| 对象 | 输入 shape | 输出 shape | 含义 |
|---|---|---|---|
| 量化 \(w_{\mathrm{work},p}\) | 标量 | \(q_p\)、\(\rho_p\),均为标量 | 选择网格点并记录残差 |
| \(\mathbf e_p^\top\delta\) | \([1,k]\cdot[k,1]\) | 标量 | 取出必须等于 \(-\rho_p\) 的第 \(p\) 个更新量 |
| \(\delta^\top H_F\delta\) | \([1,k]\cdot[k,k]\cdot[k,1]\) | 标量 | 本次更新造成的条件二次代价 |
| \(G_F=H_F^{-1}\) | \([k,k]\) | \([k,k]\) | 把代价矩阵转成可补偿方向 |
4.3. 只用线性代数和配方法求解¶
先把后文会反复使用的字母固定下来。\(\mathcal F\) 保存的是当前自由坐标的全局列号,而 \(p\) 是它们在自由子问题中的局部编号。为了不把两种编号混在一起,定义自由子问题的局部索引集
例如前面的 \(\mathcal F=\{2,3,4\}\) 中,当前全局第 2 列的局部编号是 \(p=0\)。此时 \(\mathcal I_F=\{0,1,2\}\)、\(\mathcal T=\{1,2\}\),对应的全局列才是 \(\{3,4\}\)。因此本节\(H_F\)、\(G_F\) 和 \(\delta\) 下标中的 \(p\)、\(\mathcal T\) 都是局部索引。
下面先讨论 \(k\ge2\),也就是至少还有一个坐标能参与补偿的情况。若 \(k=1\),\(\mathcal T\) 为空且根本没有补偿变量;唯一可行更新就是\(\delta^*=-\rho_p\)。此时 \(G_F=1/h_{pp}\),所以代价直接为\(\rho_p^2h_{pp}/2=\rho_p^2/(2[G_F]_{pp})\),与下面的最终公式一致。
下面要把 \(p\) 对应的行和列临时写在最前面。为了不暗中偷换向量顺序,把这个动作明确写成置换矩阵。把集合 \(\mathcal T\) 中互异的局部索引按原顺序列为 \((t_1,\ldots,t_{k-1})\),定义
\(P\) 的每行、每列恰有一个 1,所以 \(P^\top P=PP^\top=I\)。\(P^\top\) 把原向量换成 \((p,\mathcal T)\) 顺序,\(P\) 则把它换回原局部顺序。先只定义重排后的坐标向量
由于 \(PP^\top=I\),这个定义也等价于\(\delta=P\bar\delta\)。现在把它代回原来的二次代价,而不是另外猜一个新的 Hessian:
因此,一旦选择了坐标变换\(\bar\delta=P^\top\delta\),为了让重排前后的二次代价完全相同,新坐标下的矩阵就被唯一诱导为
它不是额外假设,也不是用校准数据重新计算了一次 Hessian;它只是同一个 \(H_F\) 按 \((p,\mathcal T)\) 顺序同时重排行和列。若直接对新坐标下的二次函数求 Hessian,也会得到
shape 上,\([k,k][k,k][k,k]\to[k,k]\);并且对任意非零 \(z\),\(z^\top\bar H_Fz=(Pz)^\top H_F(Pz)>0\),所以重排不会改变正定性。现在才能把它按“当前坐标 \(p\) 在前、其余坐标 \(\mathcal T\) 在后”严格分块:
各个块的 shape 和含义如下。由于 \(H_F\) 对称,\(\bar H_F\) 也对称,\(h_{p\mathcal T}=h_{\mathcal T p}^\top\)。
| 符号 | shape | 含义 |
|---|---|---|
| \(h_{pp}=[H_F]_{pp}\) | 标量 | 当前坐标 \(p\) 自己的二次代价系数 |
| \(h_{p\mathcal T}\) | \([1,k-1]\) | 坐标 \(p\) 与其余自由坐标的耦合行 |
| \(h_{\mathcal T p}\) | \([k-1,1]\) | 上一行的转置,即耦合列 |
| \(H_{\mathcal T}=(H_F)_{\mathcal T,\mathcal T}\) | \([k-1,k-1]\) | 去掉 \(p\) 后,剩余自由坐标的 Hessian |
| \(\delta_{\mathcal T}\) | \([k-1,1]\) | 待求的补偿量 |
置换不改变正定性:任取 \(x\ne0\),有\(x^\top\bar H_Fx=(Px)^\top H_F(Px)>0\),所以\(\bar H_F\succ0\)。再确认稍后出现的 \(H_{\mathcal T}^{-1}\) 确实存在。任取非零向量 \(z\in\mathbb R^{k-1}\),按照\((p,\mathcal T)\) 分块顺序在最上面补一个 0,得到\(y=[0;z]\in\mathbb R^k\),于是
所以 \(H_{\mathcal T}\succ0\),它必然可逆。
现在把式 (15) 代入目标。因为 \(\delta=P\bar\delta\) 且\(PP^\top=I\),换序前后的二次型数值完全相同。先做右侧的矩阵乘法,再做左侧的行向量乘法:
最后一步把两个交叉项合并了。依赖 Hessian 的对称性 \(h_{\mathcal T p}=h_{p\mathcal T}^\top\),使用标量的转置等于 自身:
因此式 (16) 中两个交叉项才可以合并为 \(-2\rho_p h_{p\mathcal T}\delta_{\mathcal T}\)。
接下来配方。候选“中心”
与 \(\delta_{\mathcal T}\) 的 shape 相同。把下面的平方项一行一行展开:
这里用到了 \(H_{\mathcal T}^{-1}\) 仍然对称。最后一项的 shape 是 \([1,k-1][k-1,k-1][k-1,1]\to\) 标量。把它移到等号另一边,再代回式 (16),便得到完整的配方结果:
第二行与 \(\delta_{\mathcal T}\) 无关,是一个常数。第一行是正定二次型,因此总是大于等于 0,并且只有括号内的向量为 0 时才能等于0。所以唯一最优补偿是
到这里,配方法已经在 Hessian 的分块记号下求出了补偿量。为了和后面小节以及 GPTQ 的实际计算直接对应,还需要把 \(H_{\mathcal T}^{-1}h_{\mathcal T p}\) 改写成逆 Hessian \(G_F\) 的第 \(p\) 列。
令 \(G_F=H_F^{-1}\)。重排后的逆矩阵是
等号 \(\bar H_F^{-1}=P^\top G_F P\) 可以直接相乘验证:
这里 \([G_F]_{pp}\) 是标量,\((G_F)_{\mathcal T,p}\) 是 \([k-1,1]\) 列向量。由于 \(H_F\succ0\),它的逆也正定。具体地,任取 \(x\ne0\) 并令 \(z=G_Fx\),则 \(z\ne0\)、\(x=H_Fz\),因此
所以 \(G_F\succ0\),进而 \([G_F]_{pp}=\mathbf e_p^\top G_F\mathbf e_p>0\) ;下面的除法不会除以 0。
由 \(\bar H_F\bar G_F=I\) 的左下块得到一个 \([k-1,1]\) 向量等式:
第一行的 shape 是
后面三行只是在等式两边依次左乘 \(H_{\mathcal T}^{-1}\),再除以正标量 \([G_F]_{pp}\)。
把式 (19) 代入式 (18):
再与式 (14) 后已经明确的硬约束 \(\delta_p^*=-\rho_p\) 上下拼起来,先得到 \((p,\mathcal T)\) 顺序下的完整更新:
这里 \(\mathbf e_0\) 是重排空间中的第一个标准基向量;因为 \(P\) 的第一列是 \(\mathbf e_p\),所以 \(P\mathbf e_0=\mathbf e_p\)。现在乘 \(P\) 恢复原局部顺序:
重排空间中的拼接是“标量 \([1,1]\) + 列向量 \([k-1,1]\) \(\longrightarrow\) 完整列向量 \([k,1]\)”;乘置换矩阵 \(P:[k,k]\) 以后,shape 仍为 \([k,1]\),只是元素回到原来的局部位置。若把下一节的标量记号提前写成
那么同一个答案就是 \(\delta^*=-\mu G_F\mathbf e_p\):这分别对应第 3 节的式 (20) 和式 (21)。
这就是第 4 节的式 (22),并不是只说明“方向大致相同”,而是把比例系数也精确求了出来。
最后还要把最小代价推完。由式 (17)、(18),最优点处的平方项为0,因此
括号里的量看起来仍然只有 Hessian。利用 \(\bar H_F\bar G_F=I\) 的左上块,并代入式 (19),可以把它改写成逆 Hessian 的对角元:
这里左上块等式的两项分别是“标量乘标量”和 \([1,k-1][k-1,1]\),输出都为标量,所以才能相加并等于标量 1。
所以
这正是第 4 节的式 (23)。至此,配方法已经完整推出了后文的两个最终答案:最优完整更新式 (22),以及它造成的最小损失增量式(23)。换用第 4 节的损失记号,就是\(f(\delta^*)=\Delta\ell_{\lambda,p}^*\)。
4.4. 直接求出最优补偿¶
这一节不是又提出了一个新问题,而是用论文中更常见的拉格朗日乘子法,重新求解式 (14)。上一节按照同一套 \(p\)、\(\mathcal T\)、\(H_{\mathcal T}\)、\(G_F\) 记号分块,并用配方法推出了式 (22)、(23);这一节不需要把 \(p\) 写在分块矩阵的第一格,能够直接在原来的局部编号中得到“逆 Hessian 第 \(p\) 列”的公式,也为上一节提供一条独立核对路线:
先把问题翻译下:
- 当前权重必须移动 \(-\rho_p\),才能从 \(w_{\mathrm{work},p}\) 精确落到网格点 \(q_p\);
- 其余 \(k-1\) 个自由权重可以跟着移动,用来抵消输出误差;
- 我们要在所有满足“第 \(p\) 个更新量等于 \(-\rho_p\)”的方案中,找出二次代价最小的那一个。
这一节要严格回答三个问题:
- 最优的完整更新向量 \(\delta^*:[k,1]\) 是什么?
- 为什么答案会用到逆 Hessian \(G_F=H_F^{-1}\) 的第 \(p\) 列?
- 为什么算出的驻点一定是唯一全局最小点,而不只是某个局部候选点?
已知量、未知量和目标可以整理成:
| 对象 | shape | 已知/未知 | 含义 |
|---|---|---|---|
| \(H_F\) | \([k,k]\) | 已知,且 \(H_F\succ0\) | 自由权重变化的二次代价矩阵 |
| \(\mathbf e_p\) | \([k,1]\) | 已知 | 只选出第 \(p\) 个分量 |
| \(\rho_p\) | 标量 | 已知 | 当前权重到量化点的舍入残差 |
| \(\delta\) | \([k,1]\) | 待求 | 当前权重的强制移动与其他权重的补偿 |
| \(\mu\) | 标量 | 待求的辅助量 | 为了同时满足最小代价和等式约束引入的乘子 |
4.4.1 为什么会出现拉格朗日乘子¶
目标函数
像一个只有一个谷底的碗,因为 \(H_F\succ0\)。约束
表示只能在“第 \(p\) 个分量已经固定”的平面上移动。可行的小移动 \(h:[k,1]\) 必须满足
因为它不能再次改变第 \(p\) 个分量。
在最优点 \(\delta^*\),沿任何可行方向 \(h\) 都不应再有一阶下降。更具体地说:若方向导数小于 0,沿 \(h\) 走一点就能继续降低代价;若方向导数大于 0,那么沿同样可行的反方向 \(-h\) 走一点就能降低代价。因此最优点必须满足
也就是说,目标函数的梯度 \(H_F\delta^*\) 必须垂直于约束平面中的所有方向。这个平面的法向量就是 \(\mathbf e_p\),所以必有某个标量 \(\mu\) 使
拉格朗日乘子法只是把这条“谷底梯度必须垂直于约束平面”的条件系统地写出来。
4.4.2 建立拉格朗日函数并逐项求导¶
先把约束移到等号左边:
定义拉格朗日函数
分别对 \(\delta\) 和 \(\mu\) 求偏导。由于 \(H_F=H_F^\top\),
约束项的梯度为
而对 \(\mu\) 求导会原样取回约束:
令两组导数都等于 0,得到一阶最优条件
也可以把它写成一个标准的 KKT 线性方程组。KKT 是 Karush-Kuhn-Tucker 的缩写;在这里只有等式约束,所以它只是把“梯度条件”和“原约束”上下拼成一个线性方程组,不需要额外的优化知识:
4.4.3 解出 \(\delta\) 和 \(\mu\)¶
由第一行
因为 \(H_F\succ0\),所以 \(H_F\) 一定可逆。两边左乘
便得到
式 (20) 右侧的关键矩阵乘法是
外面的标量 \(-\mu\) 只缩放数值,不改变输出 shape。
为什么 \(G_F\mathbf e_p\) 正好是 \(G_F\) 的第 \(p\) 列?因为 \(\mathbf e_p\) 除了第 \(p\) 个位置为 1,其余位置全是 0。矩阵乘以它时,
现在把式 (20) 代回约束:
其中
因为左边的 \(\mathbf e_p^\top\) 又从第 \(p\) 列中取出了第 \(p\) 个元素。因此
这个除法一定有定义:\(H_F\succ0\) 推出 \(G_F=H_F^{-1}\succ0\),而 \(\mathbf e_p\ne0\),所以
把 \(\mu\) 代回式 (20),就得到下一小节将要框出的完整更新公式。它的第 \(p\) 个分量会精确等于 \(-\rho_p\),其余分量则是代价最小的补偿。
4.4.4 用开头的二维数值例子核对¶
回到本章开头的二维例子。此时
先求乘子:
再代入式 (20):
第一个数把当前权重送到量化点,第二个数就是另一个自由权重接住的补偿,与本章开头直接写出的结果完全一致。
4.4.5 严格证明它是唯一全局最小点¶
只解出一阶方程还不够;现在证明它确实是全局最优解。
设上面求出的可行点为 \(\delta^*\)。任取另一个满足同一约束的可行点 \(\tilde\delta\),令
因为二者的第 \(p\) 个分量都等于 \(-\rho_p\),
把 \(\tilde\delta=\delta^*+h\) 代入目标函数并完整展开:
由驻点方程 \(H_F\delta^*=-\mu\mathbf e_p\),
因此
当 \(\tilde\delta\ne\delta^*\) 时,必有 \(h\ne0\)。又因为 \(H_F\succ0\),
所以任何其他可行点的代价都严格大于 \(f(\delta^*)\)。这就证明了:
式 (20)、(21) 求出的 \(\delta^*\) 不只是一个驻点,而是约束问题唯一的全局最小解。
4.5. 最优完整更新与最小损失增量¶
式 (20)、(21) 沿两条路线得到了同一个完整更新。统一写成:
这里继续沿用第 2 节的局部索引定义:\(\mathcal I_F=\{0,1,\ldots,k-1\}\),\(\mathcal T=\mathcal I_F\setminus\{p\}\)。因此 \(\mathcal T\) 表示自由子问题中除 \(p\) 外的其余 \(k-1\) 个局部位置。在这套记号下,“完整更新”包含两部分:
第一项把当前坐标强制移到 \(q_p\);第二项才是其他自由坐标的补偿。
损失增量也逐行算清楚。由驻点方程 \(H_F\delta^*=-\mu\mathbf e_p\) 和约束 \(\mathbf e_p^\top\delta^*=-\rho_p\):
式 (22) 给出更新向量,式 (23) 给出最小代价。下面分别从约束、无耦合边界和非负性验证它们,而不是只把公式再复述一遍。
验证 1:当前权重确实落到了量化点。 取式 (22) 的第 \(p\) 个分量:
因此
恰好满足最初的硬约束,而不是近似满足。
验证 2:没有坐标耦合时,不应凭空出现补偿。 若 \(G_F\) 是对角矩阵,那么对任意 \(i\ne p\) 都有 \([G_F]_{ip}=0\),所以
这时只有当前权重移动,其他权重不动,符合“各坐标互不影响”的边界情况。
验证 3:最小代价不可能为负。 因为 \(G_F\succ0\),所以 \([G_F]_{pp}>0\);又因为 \(\rho_p^2\ge0\),故
三项验证分别检查了式 (22) 的可行性、补偿方向的合理性和式 (23) 的符号。
在 GPTQ 已给定的共享列顺序中,式 (23) 衡量当前这一步的条件代理代价。不要把 \([G_F]_{pp}\) 简单叫作“重要性”:在 \(|\rho_p|\) 相同时,它越大,式 (23) 越小,说明改变该坐标越容易被其他自由坐标补偿。
五、多轮之后为什么还能重复使用同一个纯二次问题¶
5.1. 这一节解决的问题¶
第一次量化之前,工作权重就是原始权重,总改变量为 0,所以损失天然是
但完成第一轮以后,当前工作权重已经包含“强制舍入”和“其他权重补偿”,不再等于原始权重。设对某个输出行而言,
其中 \(\zeta\) 是此前所有轮累计产生的总改变量。下一轮若再增加一个更新,直接展开损失时通常会出现一个与新更新成一次关系的交叉项。
为了不用读者翻回前文,第一轮求解的式 (14) 完整抄写如下:
这里的 \(\delta:[k,1]\) 是第一轮在当前 \(k\) 个自由坐标上施加的完整更新。进入后续轮次后,为了不把“历史累计变化 \(\zeta\)”和“本轮准备新增的变化”混在一起,本文把本轮新增量改记为
\(u\) 与式 (14) 中的 \(\delta\) 扮演同一个角色,只是名字变了:若本轮开始时自由部分是 \(w_{\mathrm{work},\mathcal F}\),施加更新后就是
其中 \(u_p\) 是当前待量化坐标新增的变化,仍必须把该权重送到量化点, 所以
因此,这一节真正需要回答的是:
当 \(\zeta\ne0\) 时,为什么下一轮仍能把式 (14) 中的 \(\delta\) 换成 本轮新增量 \(u\),继续求解 \(\min_u\tfrac12u^\top H_Fu\)、 \(\text{s.t. }\mathbf e_p^\top u=-\rho_p\)?从累计状态展开后本来会 出现的一次项,究竟为什么等于 0?
如果不能证明这一点,就不能把第一轮的式 (14) 想当然地重复用于第二轮、第三轮。
下面的证明针对给定的当前量化点 \(q_p\),并假设每轮按式 (22) 在精确算术下完成补偿。
5.2. 先把“本来会出现的一次项”完整展开¶
设本轮开始时:
| 对象 | shape | 含义 |
|---|---|---|
| \(\mathcal P\) | $ | \mathcal P |
| \(\mathcal F\) | $k= | \mathcal F |
| \(\zeta\) | \([d,1]\) | 当前工作权重相对原始权重的累计变化 |
| \(u\) | \([k,1]\) | 本轮准备新增到自由坐标上的更新;它对应式 (14) 中的 \(\delta\),不包含历史累计量 \(\zeta\) |
| \(S_{\mathcal F}\) | \([d,k]\) | 把自由向量 \(u\) 放回完整 \(d\) 维空间的位置矩阵 |
在完整的 \(d\) 维坐标中,本轮结束后的累计变化因此是
位置矩阵只是一个“把短向量塞回原位置”的工具。例如 \(d=5\)、\(\mathcal F=\{3,4\}\) 时,
所以冻结坐标得到的更新量都是 0,只有自由坐标能改变。
对单个输出行,阻尼二次代价为
从当前累计改变量 \(\zeta\) 出发,再施加 \(S_{\mathcal F}u\),损失增量是
从第二行到第三行用到了 \(H_\lambda=H_\lambda^\top\):展开后出现的两个交叉项互为转置且都是标量,所以合并成
其中
一次项中的向量
就是当前损失对所有自由坐标的梯度。一般情况下它并不自动等于 0;下面要证明 GPTQ 的最优补偿恰好让它在每轮开始时保持为 0。
5.3. 要维持的不变量¶
需要证明的不变量是
它的通俗含义是:
已冻结的权重虽然不能再动,但在所有仍可移动的权重方向上,当前工作权重已经没有任何一阶下降空间。
例如 \(\mathcal F=\{3,4\}\) 时,它只要求梯度的第 3、4 个分量为 0。已冻结坐标上的梯度可以不为 0,因为那些方向已经不允许移动。
一旦这个不变量成立,上节展开式中的一次项就是
下一轮才会重新变成纯二次问题。现在严格证明这个不变量会被每一轮更新保留下来。
5.4. 数学归纳法严格证明不变量¶
第一步:证明第一次量化前成立。
第一次量化前还没有修改任何权重,所以
因此归纳基础成立。
第二步:写出归纳假设。
假设某一轮开始时,全局自由列集合为 \(\mathcal F=\{f_0,\ldots,f_{k-1}\}\),并固定处理顺序 \((f_0,\ldots,f_{k-1})\)。归纳假设是
自由子问题的局部索引集仍记为 \(\mathcal I_F=\{0,1,\ldots,k-1\}\)。本轮选择局部位置 \(p\in\mathcal I_F\),它对应的全局列号是
量化完成后,全局列 \(j\) 被冻结。为了同时区分“旧矩阵里的局部 位置”和“完整 \(d\) 维空间里的全局列号”,分别定义
这里必须把两套编号说死:
| 集合 | 编号属于哪里 | 元素取值 | 生命周期 |
|---|---|---|---|
| \(\mathcal F^+\) | 原始权重矩阵的全局列号 | 原始范围 \(\{0,\ldots,d-1\}\) 中的列号 | 是下一轮真正保存和使用的自由列集合 |
| \(\mathcal T\) | 本轮自由子问题 \(H_F:[k,k]\) 内的临时局部位置 | 本轮范围 \(\{0,\ldots,k-1\}\) 中除 \(p\) 外的位置 | 只服务本轮的分块、取子向量和证明;进入下一轮后重新编号 |
从局部位置到全局列号的映射是
因此二者的准确关系不是“\(\mathcal T=\mathcal F^+\)”,而是
并且这些全局列号继承 \(\mathcal F\) 删除 \(j=f_p\) 后的相对顺序。
例如,若本轮按顺序保存的全局自由列为
那么
这里绝不能把 \(\mathcal T=\{0,2,3\}\) 误读成原权重矩阵的第 \(0,2,3\) 列。进入下一轮后,\(\mathcal F^+=(2,7,9)\) 会重新建立新的 局部索引集
所以旧的局部标签 \(\{0,2,3\}\) 到此失效;下一轮局部位置 \(0,1,2\) 才分别对应全局列 \(2,7,9\)。
第三步:利用本轮最优补偿的驻点方程。
由归纳假设,5.2 节展开式中的一次项在本轮开始时等于 0,所以本轮确实重新化为式 (14) 的纯二次约束问题。令其最优完整更新为 \(\delta^*:[k,1]\),于是可以使用式 (20) 的驻点方程:
标准基向量 \(\mathbf e_p\) 只有第 \(p\) 个局部分量非零。把等式限制到 仍会保留的局部位置 \(\mathcal T\),右侧全部变为 0:
这一步表达的正是:本轮补偿完成后,除刚被冻结的局部位置 \(p\) (全局列 \(j\))以外,其余自由方向的一阶变化已经被消掉。
第四步:证明更新后的梯度仍在新自由集合上为 0。
把自由空间中的更新嵌回完整空间:
更新后,在下一轮的全局自由集合 \(\mathcal F^+\) 上,梯度为
第二行的第一个 0 来自归纳假设,因为 \(\mathcal F^+\subset\mathcal F\)。第二项之所以能写成 \((H_F\delta^*)_{\mathcal T}\),是因为先在全部旧自由列上有
再在左侧保留全局列 \(\mathcal F^+\)、在右侧保留与之对应的局部位置 \(\mathcal T\),就得到
因此,“本轮开始时不变量成立”必然推出“下一轮开始时不变量仍成立”。结合第一次之前的归纳础,不变量对所有轮次都成立。
5.5. 回答本节开头的问题¶
下一轮的全局自由集合是 \(\mathcal F^+\)。令 \(S_{\mathcal F^+}:[d,k-1]\) 为对应的位置矩阵,并任取新的自由更新 \(v:[k-1,1]\)。由于刚刚证明
所以
其中
这就严格证明了:每轮开始时,一次项都会因为上一轮的最优补偿而消失,所以式 (14)--(23) 的纯二次问题结构可以重复使用。
但“结构相同”不等于“所有输入不变”。进入下一轮后:
- 全局自由集合从 \(\mathcal F\) 缩小为 \(\mathcal F^+\);
- 工作权重已经改变,新的舍入残差必须重新计算;
- Hessian 子矩阵从 \(H_F\) 变成 \(H_{\mathcal F^+}=(H_F)_{\mathcal T,\mathcal T}\);
- 因而逆 Hessian 也必须从 \(G_F\) 更新为 \(G_{\mathcal F^+}=H_{\mathcal F^+}^{-1}\)。
第 4 点正好引出下一章:量化一个坐标以后,为什么必须更新逆 Hessian?
六、量化后为什么必须更新逆 Hessian¶
回到当前自由子问题的局部编号。局部索引集为\(\mathcal I_F=\{0,1,\ldots,k-1\}\);第 \(p\) 个局部坐标一旦固定,它就不再是自由变量。旧矩阵内仍然保留的局部位置是
若换回上一节的全局列号,这个局部集合对应的正是下一轮全局自由集合 \(\mathcal F^+\)。
下一轮需要的是 \(H_{\mathcal T}=(H_F)_{\mathcal T,\mathcal T}\) 的逆,而不是旧逆矩阵随手截出来的一块:
先用 \(2\times2\) 反例把区别钉死。设
冻结第 0 个坐标后,真正剩下的 Hessian 是 \([2]\),所以新逆矩阵应为 \([1/2]\);但直接截取 \(G_F\) 右下角会得到 \([2/3]\),显然不对。正确的 rank-1 修正恰好给出
下面把正在固定的坐标临时换到第一个位置。这个换序只为书写方便,不改变结论。把两个矩阵分块:
其中 \(\alpha,\gamma\) 是标量,\(\beta,v\in\mathbb R^{k-1}\),\(C,V\in\mathbb R^{(k-1)\times(k-1)}\)。这里 \(C\) 正是冻结 \(p\) 后的 Hessian \(H_{\mathcal T}\),我们希望求 \(C^{-1}\)。
利用 \(H_FG_F=I\) 的左下和右下分块:
于是
所以
这就是 GPTQ 在冻结当前坐标后使用的“逆 Hessian 消元”,也称rank-1 downdate。它也可以先写成完整矩阵:
外积会使被冻结坐标对应的行和列变成零;随后把它删掉即可。形状完全匹配:
| 计算 | 输入 shape | 输出 shape | 意义 |
|---|---|---|---|
| \((G_F)_{:,p}\) | \([k,1]\) | \([k,1]\) | 当前坐标的逆曲率列 |
| \((G_F)_{:,p}(G_F)_{p,:}\) | \([k,1]\cdot[1,k]\) | \([k,k]\) | 本轮消去的 rank-1 信息 |
| 式 (26) 的外积 | \([k-1,1]\cdot[1,k-1]\) | \([k-1,k-1]\) | 剩余自由空间的校正 |
| \(G_{\mathcal T}\) | \([k-1,k-1]\) | \([k-1,k-1]\) | 下一轮真正应使用的逆 Hessian |
这一步只由 \(X\)、阻尼和处理顺序决定,不依赖某一行的具体量化误差。因此 \(m\) 个输出行可以共享同一次消元,这正是 GPTQ 能并行化的地基。
七、GPTQ 块补偿所需的多约束通式(理论)¶
上一节只有一个硬约束:指定某个坐标必须移动 \(-\rho_p\)。现在把它推广到一组坐标。这节只讨论一个数学问题:既然这 \(s\) 个坐标必须发生指定变化,其他坐标应该怎样补偿,才能让二次代价最小?
7.1. 问题引入¶
当前自由子问题有 \(k\) 个局部坐标。设\(\mathcal B\subset\mathcal I_F\) 是含有 \(s\) 个互异局部索引的集合,并固定一个枚举顺序 \((b_1,\ldots,b_s)\)。于是
这里 \(1\le s\le k\);\(\mathcal B\) 的枚举顺序\((b_1,\ldots,b_s)\) 也就是向量和矩阵切片的排列顺序。
对每个局部标准基向量 \(\mathbf e_{b_i}\),组成选择矩阵
它的作用只是“从一个 \(k\) 维向量中挑出 \(\mathcal B\) 的分量”:
后面的 \(a_{\mathcal B}\)、\(q_{\mathcal B}\) 和矩阵切片都采用同一个枚举顺序,因此各分量始终一一对应。
若当前工作权重和已经选定的量化落点分别为\(w_{\mathrm{work},\mathcal B}\)、\(q_{\mathcal B}\),那么必须发生的变化量是
其中\(\rho_{\mathcal B}:=w_{\mathrm{work},\mathcal B}-q_{\mathcal B}\in\mathbb R^{s\times1}\),各分量按\((b_1,\ldots,b_s)\) 排列。注意 \(a_{\mathcal B}\) 不是量化后的绝对权重 \(q_{\mathcal B}\),而是“从当前值走到落点要走多远”。因此问题的输入、输出是:
| 对象 | shape | 在问题中的角色 |
|---|---|---|
| \(H_F\) | \([k,k]\) | 已知的二次代价矩阵 |
| \(S_{\mathcal B}\) | \([k,s]\) | 已知的选坐标工具 |
| \(a_{\mathcal B}\) | \([s,1]\) | 已知的 \(s\) 个强制变化量 |
| \(\delta\) | \([k,1]\) | 待求的完整变化,包括补偿 |
| \(S_{\mathcal B}^{\top}\delta\) | \([s,k][k,1]\to[s,1]\) | 必须等于 \(a_{\mathcal B}\) |
于是精确问题是
用一句话说:\(\mathcal B\) 中的坐标不能讨价还价,它们的改变量已经给定;\(\mathcal T\) 中的 \(k-s\) 个坐标可以自由移动,专门用来减小损失。这个约束总是可行的,例如令被选坐标等于 \(a_{\mathcal B}\)、其余坐标等于 0;又因为 \(H_F\succ0\),可行集合上的严格凸二次函数只有一个全局最小点。若 \(s=1\) 且\(S_{\mathcal B}=\mathbf e_p\)、\(a_{\mathcal B}=-\rho_p\),式 (28) 就退化为式 (14),所以它不是另一个互不相干的问题,而是单坐标结论的直接推广。
7.2. 一个可以手算的三维例子¶
取 \(k=3\),并设
可以把它想成:两个被固定的当前工作值都是 \(1\),量化器已经为它们选好了落点 \(0\),所以各自必须改变 \(-1\);这里故意选整数只是为了让手算更清楚。
这个 \(H_F\) 正定(它的三个顺序主子式是 \(2,3,4\)),因此确实是一个只有一个谷底的二次代价。选择矩阵为
令 \(\delta=[\delta_0,\delta_1,\delta_2]^\top\),则
所以 \(\delta_0\)、\(\delta_2\) 已经被固定,只有中间的 \(\delta_1\) 能 调节。写成 \(\delta=[-1,t,-1]^\top\),逐步计算:
平方项在 \(t=1\) 时为 0,所以
这说明“补偿”不是口号:如果让中间坐标不动(\(t=0\)),代价是 \(2\);让它跟着移动 \(+1\),代价降到 \(1\)。两个被固定的坐标仍然 严格满足各自的变化量。
7.3. 通式、推导与具体计算步骤¶
我们直接从式 (28) 出发,先求出满足约束的最优 \(\delta\);
理论输入:
| 量 | shape | 含义 |
|---|---|---|
| \(H_F\) | \([k,k]\) | 当前 \(k\) 个自由坐标的正定 Hessian |
| \(G_F=H_F^{-1}\) | \([k,k]\) | 由 \(H_F\) 派生的逆 Hessian;用来传播补偿 |
| \(S_{\mathcal B}\) | \([k,s]\) | 选择矩阵,抽出要同时固定的 \(s\) 个坐标 |
| \(a_{\mathcal B}\) | \([s,1]\) | 这 \(s\) 个坐标必须发生的变化量 |
纯数学问题把 \(a_{\mathcal B}\) 当作已知约束向量;\(G_F\) 是由\(H_F\) 求出的派生量,并不是另一个独立假设。工程中如何由量化落点和工作权重构造 \(a_{\mathcal B}\),
输出:
- 完整最优变化 \(\delta^*:[k,1]\);
- 这次变化的最小二次代价 \(\Delta\ell_{\mathcal B}^*\)(标量);
- 冻结 \(\mathcal B\) 后,剩余坐标 \(\mathcal T=\mathcal I_F\setminus\mathcal B\) 使用的 \(G_{\mathcal T}:[k-s,k-s]\)。
从原问题开始推导。 式 (28) 是
未知量只有 \(\delta:[k,1]\)。目标要求二次代价尽量小,约束要求 \(\mathcal B\) 中的 \(s\) 个坐标精确移动到指定位置。为了同时处理这\(s\) 条等式,给每条约束配一个乘子,组成\(\nu:[s,1]\)。拉格朗日函数为
分别对两个未知量求导。对 \(\delta\) 的梯度 shape 是 \([k,1]\);对\(\nu\) 求导,会原样取回 \(s\) 条约束:
最优解必须同时满足这两行。先用第一行消去 \(\delta\)。左乘\(G_F=H_F^{-1}\),得到
这一步已经说明:最优 \(\delta\) 一定是 \(G_FS_{\mathcal B}\) 的各列的线性组合,只是组合系数 \(-\nu\) 还不知道。把它代回第二行:
现在 \(\Gamma\) 才自然出现:它就是上式中乘在 \(\nu\) 前面的系数矩阵。 定义
为什么最后一个等号成立?右乘 \(S_{\mathcal B}\) 从 \(G_F\) 中选出\(\mathcal B\) 对应的 \(s\) 列,左乘 \(S_{\mathcal B}^{\top}\) 再选出同样的 \(s\) 行,所以结果正是 \(G_F\) 的\(\mathcal B\times\mathcal B\) 主子阵。整个乘法的 shape 是\([s,k][k,k][k,s]\to[s,s]\)。
还不能直接写 \(\Gamma^{-1}\),必须先证明它可逆。对任意非零\(z\in\mathbb R^s\),选择矩阵的列互异,所以\(S_{\mathcal B}z\ne0\);又因为 \(G_F\succ0\),有
因此 \(\Gamma\succ0\),必然可逆。令
刚才的方程就变成
所以 \(y\) 不是凭空定义的算法变量,而是拉格朗日乘子的相反数。它的第 \(i\) 个分量说明要把 \(G_F\) 的第 \(b_i\) 列取多少。把\(y=-\nu\) 代回 \(\delta=-G_FS_{\mathcal B}\nu\),得到
接着求最小代价,由\(H_FG_F=I_k\),
再利用原约束\(S_{\mathcal B}^{\top}\delta^*=a_{\mathcal B}\):
至此才得到完整通式:
第一条的 shape 是\([k,k][k,s][s,s][s,1]\to[k,1]\)。第二条的 shape 是\([1,s][s,s][s,1]\to\) 标量。
任取另一个满足式 (28) 的可行解 \(\widetilde\delta=\delta^*+h\)。两者满足同一组约束,所以 \(S_{\mathcal B}^{\top}h=0\)。于是
只要 \(\widetilde\delta\ne\delta^*\),就有 \(h\ne0\);由\(H_F\succ0\),最后一项严格大于 0。因此式 (30) 给出的确实是式(28) 的唯一全局最小解。
三个步骤整理如下:
- 计算 \(\Gamma=S_{\mathcal B}^{\top}G_FS_{\mathcal B}\)。它来自把 \(\delta=-G_FS_{\mathcal B}\nu\) 代回原约束后的系数矩阵;
- 解 \(\Gamma y=a_{\mathcal B}\)。其中 \(y=-\nu\),是使 \(s\) 条硬约束 同时成立所需的混合系数;
- 计算 \(\delta^*=G_FS_{\mathcal B}y\),再计算 \(\Delta\ell_{\mathcal B}^*=\tfrac12a_{\mathcal B}^{\top}y\)。 前者把 \(s\) 个系数传播到全部 \(k\) 个坐标,后者是该可行解的最小 二次代价。
这三步只解决本轮的最优补偿和代价。冻结 \(\mathcal B\) 以后,下一轮究竟应使用哪个逆 Hessian,还需要从块矩阵关系单独推出,不能提前当作已知公式。下文先用三维例子核对本轮解,再完成这个推导。
现在用刚才的例子验证式 (30)。有
因此
并且 \(\frac12a_{\mathcal B}^{\top}\Gamma^{-1}a_{\mathcal B}=1\),与直接配方完全一致。
冻结 \(\mathcal B\) 后,剩余坐标是 \(\mathcal T\)。下一轮真正需要的逆 Hessian 不是简单截取,而是
为了不把“Schur 补”当成一句黑箱术语,下面把式 (31) 的两行块乘法写出来。令
即 \(P\) 的列按\((\mathcal B,\mathcal T)\) 排列;于是 \(P^\top\) 把原坐标换成该顺序,则
为简写,下面把四个 Hessian 分块和四个逆 Hessian 分块直接用上下标表示;它们的 shape 依次是 \([s,s],[s,k-s],[k-s,s],[k-s,k-s]\)。又因为 \(G_F\) 对称,\((G_F)_{\mathcal T,\mathcal B}=(G_F)_{\mathcal B,\mathcal T}^{\top}\)。由\((P^\top H_FP)(P^\top G_FP)=I_k\),它的下左、下右两个分块分别是
用第一行消去\(H_{\mathcal T,\mathcal T}(G_F)_{\mathcal B,\mathcal T}^{\top}\):
由于 \(H_{\mathcal T,\mathcal T}\) 正定,它可逆,所以括号内正是\(H_{\mathcal T,\mathcal T}^{-1}\);恢复原坐标顺序就得到式 (31)。因此 \(G_{\mathcal T}\) 的准确含义是
即“冻结 \(\mathcal B\) 后剩余 Hessian 的逆”,不是旧矩阵\(G_F\) 的右下角直接截取。式 (31) 的修正项 shape 为\([k-s,s][s,s][s,k-s]\to[k-s,k-s]\)。在数值例中,
而剩余 Hessian 是 \([2]\),其逆正好也是 \([1/2]\)。
把本节推导整理成完整的数学计算顺序。 前 6 步来自式 (29)、(30),第 7 步来自刚刚证明的式 (31):
- 把已知约束向量 \(a_{\mathcal B}:[s,1]\) 按\((b_1,\ldots,b_s)\) 的顺序排列;它表示这 \(s\) 个坐标各自必须发生的变化。
- 从 \(G_F:[k,k]\) 抽取列块\((G_F)_{:,\mathcal B}:[k,s]\),并计算\(\Gamma=(G_F)_{\mathcal B,\mathcal B}:[s,s]\)。
- 解 \(\Gamma y=a_{\mathcal B}\):\([s,s]\,[s,1]\to[s,1]\)。因为 \(\Gamma\succ0\),解唯一。
- 计算 \(\delta^*=(G_F)_{:,\mathcal B}y\):\([k,s]\,[s,1]\to[k,1]\)。其中 \(\delta_{\mathcal B}^*\) 是硬约束指定的变化,\(\delta_{\mathcal T}^*\) 是最优连续补偿。
- 验证\(S_{\mathcal B}^{\top}\delta^*:[s,k][k,1]\to[s,1]\)等于 \(a_{\mathcal B}\),确认所有硬约束确实成立。
- 计算\(\Delta\ell_{\mathcal B}^*=\tfrac12a_{\mathcal B}^{\top}y\):\([1,s][s,1]\to\) 标量,得到本轮的最小二次代价。
-
若还要继续下一轮,解\(\Gamma Z=(G_F)_{\mathcal B,\mathcal T}\),其中\(Z:[s,k-s]\);再计算
\[ G_{\mathcal T}=(G_F)_{\mathcal T,\mathcal T} -(G_F)_{\mathcal T,\mathcal B}Z. \]
修正项 shape 为 \([k-s,s][s,k-s]\to[k-s,k-s]\)。
以上是从约束问题严格推出的数学顺序。量化器怎样逐列产生\(q_{\mathcal B}\),以及 \(\delta^*\) 怎样写回多行工作矩阵,仍放在下一章的工程语境中处理。
本节结论。 本节开头的问题是:当 \(\mathcal B\) 中的 \(s\) 个坐标必须发生指定变化 \(a_{\mathcal B}\) 时,其余坐标怎样补偿才最省代价?
- 取 \(\Gamma=(G_F)_{\mathcal B,\mathcal B}\),解 \(\Gamma y=a_{\mathcal B}\);
- 用 \(\delta^*=(G_F)_{:,\mathcal B}y\) 得到唯一全局最优补偿, 最小代价是 \(\tfrac12a_{\mathcal B}^{\top}y\);
- 冻结 \(\mathcal B\) 后,用式 (31) 得到下一轮真正需要的 \(G_{\mathcal T}\),不能直接截取旧 \(G_F\) 的剩余子块。
三维例子同时验证了约束、最优代价和下一轮逆 Hessian。以上仍是纯数学:它把 \(a_{\mathcal B}\) 当作已知量,并不要求 GPTQ 联合枚举一个块的量化组合。下一章负责说明 GPTQ 怎样逐列得到量化落点、构造 \(a_{\mathcal B}\),再把这个结论应用到所有输出行。
八、工程实现:GPTQ 怎样把二阶补偿变成高效矩阵计算¶
从本章开始进入工程语境:前一章的 \(a_{\mathcal B}\) 在这里由量化器实际选出的落点和工作矩阵计算出来;下面还会说明列顺序、Cholesky、block 和 lazy update 怎样把理论公式组织成可运行的 GPTQ。
8.1. 让所有输出行共享同一列顺序¶
GPTQ 为一个 Linear 层的所有 \(m\) 个输出行规定同一个输入特征处理顺序
在尚未显式置换矩阵的写法中,第 \(j\) 步统一量化\((W_{\mathrm{work}})_{:,\pi_j}\);在后文代码采用的先置换矩阵写法中,同一列已经被搬到局部位置 \(j\),所以写成\((W'_{\mathrm{work}})_{:,j}\)。两种写法处理的是同一个原始输入特征\(\pi_j\),不能把原始列号 \(\pi_j\) 与置换后的局部位置 \(j\) 混用。所有行同时处理同一个输入特征,会带来三件直接结果:
- 所有输出行的自由集合完全一致,故共享 \(H_\lambda\)、\(G\)、消元和 Cholesky 因子。
- 在置换后的局部坐标中,一列 \((W'_{\mathrm{work}})_{:,j}\in\mathbb R^m\) 可以一次量化,残差 向量也能一次补偿所有 \(m\) 行。
- 大量标量更新可以合并为外积和 GEMM;二阶预处理是 \(O(d^3)\),全部权重的补偿主循环是\(O(md^2)\),主导复杂度合起来约为 \(O(\max\{md^2,d^3\})\)。
这里要把“最优”的范围说准确:GPTQ 在给定共享顺序、已经选定当前量化落点、其余坐标仍可连续移动的条件下,求式 (23) 的局部二次最优补偿;它不保证找到式 (1) 所有 INT4 组合中的全局最优解。
8.2. 把纯数学结论应用到多行工作矩阵¶
现在把前一章的多约束数学结论应用到所有输出行。令\(W_F\in\mathbb R^{m\times k}\) 表示当前 \(k\) 个自由列按处理顺序组成的工作矩阵视图;实际计算块记为 \(\mathcal B\),实际宽度为 \(b'\)。 这时把前一章的数学约束数具体取成
在这个局部视图里,\(\mathcal B\) 表示当前剩余自由列中的前 \(b'\)个位置;它们对应的原始全局列标签另行保存,不能把两种索引混用。
一块数据如何流动
下面把前一章的数学变量落到代码,作为验证块入口/出口公式的清单。它说明输入输出和数据流,但经典 GPTQ 的高效实现通常用后面Cholesky 章的逐列、lazy 内核,不会在每个 block 重新做一次这个矩阵方程。这里 \(b'\) 是当前 block 的实际宽度,最后一个 block可能小于设定的 block size。
- 取块入口状态\(W_F^{\mathrm{in}}:[m,k]\)、\(G_F:[k,k]\) 和当前列集合\(\mathcal B\)(宽度 \(b'\))。
- 在块内仍按列处理:每次把当前工作列\(w_j:[m,1]\) 交给量化器,得到对应落点 \(q_j:[m,1]\);得到后立即按单列补偿公式更新块内尚未处理的工作列,下一列从更新后的当前值继续量化。把所有已经确定的列按处理顺序拼成 \(Q_{\mathcal B}:[m,b']\)。这里不是一次枚举整块组合,而是逐列收集最终落点。
- 用块入口值计算残差\(D=(W_F^{\mathrm{in}})_{:,\mathcal B}-Q_{\mathcal B}:[m,b']\)。
- 取\(\Gamma=(G_F)_{\mathcal B,\mathcal B}:[b',b']\),解矩阵方程\(\Gamma Z_D=D^\top\),输入输出\([b',b']\,[b',m]\to[b',m]\);实现中用线性方程求解器,不显式形成逆矩阵。
-
因为 \(G_F\) 对称,\(Z_D^\top=D\Gamma^{-1}:[m,b']\)。先计算补偿矩阵,再写出块出口:
\[ \begin{aligned} \Delta W_F^{\mathrm{comp}} &=Z_D^\top(G_F)_{\mathcal B,:} :[m,b']\,[b',k]\to[m,k],\\ W_F^{\mathrm{out}} &=W_F^{\mathrm{in}}-\Delta W_F^{\mathrm{comp}} :[m,k]. \end{aligned} \] -
验收两个结果:块内列\((W_F^{\mathrm{out}})_{:,\mathcal B}\) 应等于\(Q_{\mathcal B}:[m,b']\);块外尾部只发生补偿,shape 为\([m,k-b']\)。随后再把剩余列交给下一 block。
上面的清单先给出了数据流;下面专门证明其中第 4、5 步为什么等于前一章的通式。先再次强调计算顺序:GPTQ 在块内逐列运行量化器,得到已经选定的反量化落点\(Q_{\mathcal B}\in\mathbb R^{m\times b'}\);它不会用式 (28) 联合搜索整块量化点。只有这些落点确定以后,才定义相对块入口工作值的残差矩阵
对第 \(r\) 个输出行,式 (28) 中规定的变化量是
把式 (30) 独立应用到 \(m\) 个输出行,再把这些行上下堆起来,得到
这里
恰好与 \(W_F^{\mathrm{in}}\)、\(W_F^{\mathrm{out}}:[m,k]\) 同 shape。这里明确从块入口状态算到块出口状态,不能把右侧误读成“已经完成块内更新后的 \(W_F\)”,否则会把同一补偿减两次。所有输出行的最小二次代理代价之和为
其中 \(D:[m,b']\)、\(\Gamma^{-1}:[b',b']\)、\(D^\top:[b',m]\),所以\(D\Gamma^{-1}D^\top:[m,m]\),再取\(\operatorname{tr}:[m,m]\to\) 标量;这个标量等于 \(m\) 个输出行的二次代价之和。
本小节结论。 式 (32) 已经把单行解析解严格堆叠成了\(m\) 行的块入口到块出口公式;式 (33) 则把 \(m\) 行代价相加成一个标量。二者都要求 \(Q_{\mathcal B}\) 已由块内逐列量化确定,不能反过来把式 (32) 当作联合搜索量化点的公式。它们也还不是 lazy update 的实际内核;Cholesky 章的式 (47)--(49) 会证明,块末尾一次执行的\(ER_{\mathcal B\mathcal T}\) 正好等于式 (32) 的尾部切片。
8.3. act-order 本质上只是一次一致的置换¶
常见实现会把 \(\operatorname{diag}(H)\) 较大的输入特征放到前面处理,俗称 act-order。因为
对角元大表示第 \(j\) 个输入特征在校准数据中经常具有较大幅值。act-order 的直觉是:先处理这些敏感特征,让它们在剩余自由度最多时获得补偿。它是经验启发式,不是全局最优顺序的证明。
令 \(\Pi\in\{0,1\}^{d\times d}\) 是由 \(\pi\) 构成的置换矩阵,则
形状是
并且
所以它不是换了一个层,只是把“先处理哪个输入特征”的顺序显式搬到矩阵索引上。量化完成后,对\(\hat W'\) 做反置换即可回到原列顺序。注意:只排序 \(W\) 而忘记同样排序 \(H\)(或 \(X\))会让二阶统计量与权重列错位,是非常隐蔽的实现错误。
若同时使用 group quantization,必须先问一句:group 是在原始列顺序中预先拟合,还是在置换后的处理顺序中动态拟合?两种契约不能混用。
对 static groups,先在原始连续列上拟合各组的 scale/zero。置换后位置 \(j\) 对应原列 \(\pi_j\),所以量化时应选择
这里的 \(j\) 是量化时的置换位置,不是部署时的原列号。把 \(K\) 和 \(\hat W\) 反置换后,原列\(k\) 仍自然属于 \(\lfloor k/g\rfloor\) 组,因此部署端继续使用普通连续 group 布局,不需要额外的非平凡映射。这正是 static groups 能与 act-order 共用而不改变推理格式的原因。
对 dynamic groups,网格按置换后的连续处理位置 \(j\) 拟合。反置换后,原列 \(\pi_j\) 属于处理组 \(\lfloor j/g\rfloor\),所以必须保存
部署端必须支持这份映射,或把权重和元数据重新打包成等价布局;否则只反置换码矩阵会把权重和网格配错。
8.4. 4-bit 网格:量化器究竟返回什么¶
读前准备已经演示了一个标量。对某个输出行的某个 group,给定正 scale \(\Delta_q\) 和整数 zero point \(z_q\),同一公式写成
这里 \(c\) 是真正要打包存储的 4-bit 无符号码,\(q\) 是仍以 FP16/FP32 表示、供补偿计算使用的反量化值。某些硬件接口会把等价的 16 个码写成 \(-8,\ldots,7\);只要 scale、zero 和反量化公式同步,数学上是同一个有限网格。
| 对象 | 输入 shape | 输出 shape | 含义 |
|---|---|---|---|
| 当前列 \(w=W_{\mathrm{work}}[:,j]\) | \([m]\) | \([m]\) | 所有输出行在第 \(j\) 个输入特征上的工作权重 |
| 量化器 | \([m]\) | \(c:[m]\)、\(q:[m]\) | 4-bit 码与反量化值 \(q=\mathcal Q(w)\) |
| scale、zero | 通常按行/组统计 | \([m,n_g]\) | 每一行的每个 group 各有一组元数据 |
| 最终 \(K\) | \([m,d]\) | \([m,d]\) | 每元素 4 bit 的整数码 |
论文中的补偿推导把当前量化网格视为已知。这个前提在 \(g=-1\)(一整行一个网格)或提前估计好的 static group 下最清楚。工程实现若使用 \(g>0\),有时会在每个 group 开始时根据当前工作权重重新估计 scale/zero;这会额外引入网格更新的启发式。不要把“固定网格下的代数等价”误读为“所有动态 group 实现都逐位相同”。
8.5. 最后分清三个矩阵角色¶
到这里可以把对象彻底分开:
| 名称 | 是否随输出行变化 | 是否在量化中变化 | 作用 |
|---|---|---|---|
| \(X,H_\lambda,G,R\) | 否,所有行共享 | 数值不变;列循环只读取相应尾部视图 | 输入统计与补偿系数 |
| \(W_{\mathrm{work}}\) | 是,shape 恒为 \([m,d]\) | 是 | 尚未量化列会不断接收补偿 |
| \(K,\hat W\) | 是 | 已量化列只写一次 | 最终 INT4 码与反量化结果 |
读代码时最常见的混乱是把 \(W_{\mathrm{work}}\) 当成最终 \(\hat W\)。前者是“为了给后续列留出补偿空间”的浮点工作副本;一列被量化后,应把它的码和反量化值保存到输出,而不是在之后再次浮动修改它。
本章结论。 本章要回答的是“前面的单行数学公式怎样变成一层权重都能执行的计算”。答案分成四点:所有输出行共享同一个列顺序和同一套 \(H_\lambda,G\);每行仍使用自己的量化网格得到 \(q\);工作矩阵\(W_{\mathrm{work}}\) 接收补偿,而 \(K,\hat W\) 保存最终结果;act-order只改变处理位置,权重、Hessian 和 group 元数据必须使用同一套映射。式 (32) 给出了块入口到块出口的数学结果,下一章再把它改写成无需逐轮显式更新 \(G\) 的高效 Cholesky 计算。
九、Cholesky 形式:把重复消元变成一次预计算¶
前一章已经给出正确的块补偿公式,但若每量化一列都显式更新\(G^{(j)}\),计算和访存都会很重。本章只解决一个工程问题:怎样在不改变二阶补偿结果的前提下,预先计算一张可重复使用的“补偿表”?答案是把 \(G\) 分解为 \(R^\top R\):\(R\) 的尾块编码每一轮条件逆Hessian,\(R\) 的一行产生单列外积,多行缓存后产生块外 GEMM。
9.1. 上三角因子里已经包含了每一轮的消元结果¶
显式地按照式 (27) 反复更新 \(G\) 是正确的,但既耗时又容易积累数值误差。GPTQ 先对阻尼逆 Hessian 做上三角 Cholesky 分解:
假设已经处理完前 \(j\) 列。把当前仍自由变量上的上三角尾部写成
这里 \(\tau=R_{jj}>0\) 是标量,\(v:[d-j-1,1]\),\(R^+:[d-j-1,d-j-1]\);特意不用字母 \(r\),因为全文的 \(r\) 已表示输出行编号。
那么对应的当前逆 Hessian 为
\(G^{(j)}\) 的实际 shape 是 \([d-j,d-j]\);为便于和原矩阵、代码对齐,本文保留原始特征标签\(j,j+1,\ldots,d-1\)。因此后文写 \(G^{(j)}_{jj}\) 表示这个尾矩阵左上角元素;若改用局部数组下标,它就是 \(G^{(j)}_{0,0}\)。
对第一个自由变量套用式 (26):
这就是关键。消去当前变量后,下一轮的 Cholesky 因子不需要重新算,它恰好就是 \(R\) 的右下角尾块。归纳地说:
所以一个 \([d,d]\) 的 Cholesky 分解,预先编码了从第 0 列到第 \(d-1\) 列的所有逆 Hessian 消元状态。
9.2. 单列补偿如何变成一个外积¶
第 \(j\) 列量化前,定义所有输出行的原始量化误差
由式 (40):
把 GPTQ 的单坐标二阶补偿写成所有输出行一起处理的行向量形式:
最后一行是外积,输出为 \([m,d-j]\)。它包含当前列:因为 \(R_{jj}/R_{jj}=1\),第 \(j\) 列恰好从\(W_{\mathrm{work}}[:,j]\) 变成 \(Q_{:,j}\);后面的列则收到补偿。
| 量 | shape | 作用 |
|---|---|---|
| \(\rho_j\) | \([m]\) | 当前列量化前的误差,约定为 \(w-q\) |
| \(R_{jj}\) | 标量 | 当前自由变量的 Cholesky 对角元 |
| \(\eta_j=\rho_j/R_{jj}\) | \([m]\) | 代码中缓存的缩放残差;用希腊字母避免和标准基 \(\mathbf e_p\) 混淆 |
| \(R_{j,j:}\) | \([d-j]\) | 当前列对自己和所有后续列的补偿系数 |
| \(\eta_j[:,\mathrm{None}]\,R_{j,j:}[\mathrm{None},:]\) | \([m,1]\cdot[1,d-j]\) | \([m,d-j]\) 的列更新 |
单个输出行在这一步的阻尼代理损失增加为
有些参考实现把 Cholesky 后的变量仍命名为类似 Hinv 的名字。概念上它此时是 \(R\),不是正在被显式更新的 \(G\);分母中的 \(R_{jj}^2\) 被拆成了一次除以 \(R_{jj}\) 和一次行外积。
把前面的二维例子也改用 \(R\) 核对一次:
直接相乘可见 \(R^\top R=G\)。在 \(j=0\)、\(\rho_0=0.12\) 时,\(\eta_0=\rho_0/R_{00}=0.12\),式 (42) 减去
所以两个工作权重分别改变 \(-0.12\)、\(+0.06\),与配方法例子完全一致。消去第 0 个坐标后只剩 \(R_{11}=\sqrt{3/4}\),故下一轮的条件逆 Hessian 是 \(R_{11}^2=0.75\)。
9.3. Lazy block update¶
令一个计算块从列 \(i\) 开始,实际长度为 \(b'=\min(b,d-i)\);块内索引记为 \(\mathcal B=[i,i+b')\),块外尾部记为 \(\mathcal T=[i+b',d)\)。把 Cholesky 因子分成
块内的列仍必须按顺序量化,因为第 \(j\) 列的工作值会受前面列补偿影响。每处理一列,保存
把这 \(b'\) 个向量并排堆叠:
块内立即做式 (42) 的局部版本;对块外尾部先不写。块处理完后,一次矩阵乘法就补齐所有延迟更新:
形状为
为何它与逐列立即写到尾部相同?令
是进入这个块时的块内工作权重与最终量化落点之差。块内逐列外积给出
若 \(b'=2\),把块内两列临时编号为 1、2(与全局列号无关),写
则式 (47) 逐列展开就是
第二个等式明确展示了:第二列进入量化器之前,已经包含第一列带来的补偿,因此块内仍必须顺序处理。
这里的 \(G^{(i)}\) 指已经消去前 \(i\) 列后的条件逆 Hessian。把式(32) 套到当前计算块时,前文的符号具体对应为
由式 (40) 可得
\(R\) 是正定矩阵 \(G\) 的 Cholesky 因子,所以它的对角元为正;上三角主块 \(R_{\mathcal B\mathcal B}\) 因而可逆,式 (48) 中的逆和下面的消去都有定义。于是式 (32) 中被减去的尾部项正好等于
因此,在量化网格固定、最终 \(Q_{\mathcal B}\) 已确定的前提下,lazy batching 是延迟写回,不是少算一项二阶补偿。它的价值在于把许多小的、访存不友好的尾部外积折叠为一次高吞吐矩阵乘法(GEMM)。浮点舍入次序或动态重拟合 group 网格时,不承诺逐 bit 相同。
图 2:第 \(j\) 列的并行 4-bit 量化。缩放残差 \(\eta_j\in\mathbb R^m\) 与 Cholesky 行尾形成外积,块内立即补偿;完成实际长度 \(b'=\min(b,d-i)\) 的 block 后,\(E\in\mathbb R^{m\times b'}\) 与 \(R_{\mathcal B\mathcal T}\in\mathbb R^{b'\times(d-i-b')}\) 一次相乘并更新块外尾部。
9.4. 空间时间复杂度分析¶
把一层的输入维度记为 \(d\)、输出维度记为 \(m\)、校准 token 总数记为 \(N\)。主要成本如下:
| 阶段 | 主计算 | 时间复杂度 | 主要额外存储 | 解释 |
|---|---|---|---|---|
| 收集统计量 | \(XX^\top\) | \(O(Nd^2)\) | \(O(d^2)\),或额外保存 \(X:[d,N]\) | 可分批累加,不必永久保留所有 token |
| 阻尼、求逆、Cholesky | \(H_\lambda^{-1}\)、\(R\) | \(O(d^3)\) | \(O(d^2)\) | 通常是单层量化的二阶预处理 |
| 补偿主循环 | 外积/GEMM | \(O(md^2)\) | \(W_{\mathrm{work}}:[m,d]\)、\(E:[m,b']\),且 \(b'\le b\) | 所有输出行并行共享 \(R\) |
| 打包权重 | 4-bit 码 | \(O(md)\) | 约 \(md/2\) bytes,外加元数据 | 裸 INT4 的理论存储,不含对齐/scale/zero |
例如 \(d=11008\) 时,一个 FP32 的 \([d,d]\) 矩阵约占 \(11008^2\times4\) bytes,即约 \(462\) MiB。若同时留着 \(H\)、\(G\) 和 \(R\),显存峰值很容易超过 1 GiB。实际实现常按层处理、及时释放中间矩阵,并尽量复用缓冲区。
这里的 \(O(d^3)\) 也解释了 GPTQ 不是“完全免费”的量化法。它之所以仍实用,是因为每个 Linear 层只做一次二阶预处理,而不是为每个输出行各自维护一套 \(d\times d\) 逆 Hessian;并且大模型推理会运行远多于一次量化。
本章结论。 Cholesky 形式没有更换 GPTQ 的优化目标,也没有省略补偿:式 (40) 说明一个 \(R\) 保存了所有后续条件逆 Hessian;式 (42)把单列补偿写成外积;式 (49) 证明 lazy block 的尾部 GEMM 与逐列立即写回在代数上相同。至此,前一章的正确块公式已经变成了实际 GPTQ 主循环可以高效执行的计算形式。
十、从公式到 blockwise 伪代码¶
下面是不可直接运行的 NumPy 风格教学伪代码。为了把数学主线和 group 元数据分开,它明确限定为 \(g=-1\):每个输出行只拟合一套固定网格,因此 scale, zero 的 shape 都是\([m,1]\)。若扩展到 \(g>0\),static groups 仍可使用普通连续 group 布局;dynamic groups 与act-order 联用时,则必须使用前面“act-order 本质上只是一次一致的置换”小节定义的 group_id:[d],或等价的重排元数据。
# W: [m, d], X: [d, N]
# Returns K:[m,d], scale:[m,1], zero:[m,1], W_hat:[m,d]
def gptq_linear_fixed_row_grid(
W, X, quantizer, block_size=128, percdamp=0.01, perm=None
):
m, d = W.shape
N = X.shape[1]
# Classic whole-row grid: fit once on the original row, then keep it fixed.
scale, zero = quantizer.fit_rows(W) # each [m, 1]
Wwork = W.copy() # [m, d], floating work matrix
H = (2.0 / N) * (X @ X.T) # [d, d]
dead = (diag(H) == 0) # [d], unseen input features
Wwork[:, dead] = 0 # [m, n_dead]
H[dead, dead] = 1 # positive diagonal placeholders
if perm is None:
perm = arange(d) # or argsort(diag(H), descending=True)
Wwork = Wwork[:, perm] # [m, d]
H = H[perm, :][:, perm] # [d, d]
eps = finfo(H.dtype).eps # positive scalar
damp = max(percdamp * mean(diag(H)), eps) # scalar > 0
H = H + damp * eye(d) # [d, d], positive definite
# Numerically safer than a generic inverse(H) in finite precision.
L = cholesky(H, lower=True) # [d, d], H = L @ L.T
G = cholesky_inverse(L) # [d, d], H^{-1}
G = 0.5 * (G + G.T) # [d, d], remove roundoff asymmetry
R = cholesky(G, upper=True) # [d, d], G = R.T @ R
K = empty_code_matrix([m, d]) # [m, d], each entry in 0..15
Q = empty_float([m, d]) # [m, d], dequantized values
for i in range(0, d, block_size):
i2 = min(i + block_size, d)
E = zeros([m, i2 - i]) # [m, b'], scaled residuals
for j in range(i, i2):
code, q = quantizer.quantize(
Wwork[:, j], scale[:, 0], zero[:, 0]
) # code:[m], q:[m]
rho = Wwork[:, j] - q # [m], work value minus q
eta = rho / R[j, j] # [m], scaled residual
K[:, j] = code
Q[:, j] = q
E[:, j - i] = eta
Wwork[:, j:i2] -= eta[:, None] * R[j, j:i2][None, :]
# [m,1] * [1,i2-j] -> [m,i2-j]
Wwork[:, i2:] -= E @ R[i:i2, i2:] # [m,b'] @ [b',d-i2]
K = undo_column_permutation(K, perm) # [m, d]
W_hat = undo_column_permutation(Q, perm) # [m, d]
return K, scale, zero, W_hat
dead 表示校准数据中从未出现的输入特征;它们对应 \(H\) 的零对角元。教学代码沿用经典实现思路,把相关工作权重置 0,并在 Hessian 对角线上放一个正占位值,使后续分解有定义。实际代码还应在 Cholesky 失败时提高阻尼并重试,而不是继续使用无效因子。
伪代码里几个看似神秘的写法只有切片和广播:
| 写法 | 输入 shape | 输出 shape | 含义 |
|---|---|---|---|
Wwork[:, j] |
Wwork:[m,d] |
[m] |
取所有输出行的第 \(j\) 列 |
Wwork[:, j:i2] |
[m,d] |
[m,i2-j] |
当前列到计算块末尾的视图 |
eta[:, None] |
eta:[m] |
[m,1] |
插入一维,准备做外积 |
R[j,j:i2][None,:] |
[i2-j] |
[1,i2-j] |
插入一维,准备做外积 |
undo_column_permutation |
[m,d] 与 perm:[d] |
[m,d] |
把 act-order 后的位置恢复为原特征列顺序 |
empty_code_matrix 只是“每个位置暂存一个 \(0\ldots15\) 码”的概念容器;真正部署时通常再把两个 4-bit 码压进一个 byte。块内更新包含当前列,是为了让工作矩阵当前列恰好变成 \(q\);而 \(Q\) 单独保存最终反量化值。块外更新等收集完 \(E\) 才执行,它与逐列写回尾部的数学结果相同。
| 伪代码位置 | 输入 shape | 输出 shape | 对应公式 |
|---|---|---|---|
| \(X X^\top\) | \([d,N]\cdot[N,d]\) | \([d,d]\) | 式 (9) |
| Cholesky\((G)\) | \([d,d]\) | \(R:[d,d]\) | 式 (37) |
| 量化当前列 | 当前列、scale、zero 均为 \([m]\) | code、\(q:[m]\) | 式 (36),每行使用自己的网格 |
| 块内外积 | \([m,1]\cdot[1,i2-j]\) | \([m,i2-j]\) | 式 (42) |
| 块外 GEMM | \([m,b']\cdot[b',d-i2]\) | \([m,d-i2]\) | 式 (46) |
| 反置换 | \([m,d]\)、perm:[d] |
\([m,d]\) | 式 (34) 的逆操作 |
本节结论。 伪代码的控制流与公式是一一对应的:外层循环选择block,内层循环按顺序确定每列的 \(q\) 并立即更新块内列,块结束后用一次 GEMM 补齐块外尾部,最后反置换回原列顺序。\(W_{\mathrm{work}}\) 是可变的浮点工作副本,\(Q\) 和 \(K\) 才是只写一次的最终量化结果;混淆这两类矩阵会直接破坏下一列的量化输入。
十一、一个完整的 16 级量化数值例子¶
前面的符号很多,下面用一个 \(m=2,d=2,N=3\) 的小层把全过程算到底。它刻意很小,但保留了 GPTQ 的两个关键现象:两个输出行共享同一个 \(G\),以及第一列的误差会改变第二列最终落到哪个 4-bit 点。
11.1. 输入、权重和 16 个量化点¶
设捕获到一个 batch 的三个 token 输入。为明确 batch 维,先写出 \(A[0,:,:]\):
按式 (5) 合并 token 维并转置:
每一列确实是一个 token 的输入:分别为 \([1,1]^\top\)、\([0,1]^\top\)、\([1,0]^\top\)。浮点权重取为
本例故意固定一个教学用的仿射 INT4 网格:
因此反量化点是
这里 scale 和 zero 的 shape 都是 \([m,1]=[2,1]\),即每个输出行只有一个 group;两行恰好使用相同数值。真实 GPTQ 会按行或按 group 估计它们,本例把网格预先固定,只让我们聚焦二阶补偿。
原始层输出为
| 对象 | shape | 这个例子中的意义 |
|---|---|---|
| \(A\) | \([1,3,2]\) | 1 条校准序列、3 个 token、2 个输入特征 |
| \(X\) | \([2,3]\) | 3 个 token 位置的列向量 |
| \(W\) | \([2,2]\) | 2 个输出通道各有 2 个权重 |
| \(Y=WX\) | \([2,3]\) | 浮点层的基准输出 |
| \(K\) | \([2,2]\) | 最后要存储的 4-bit 码 |
11.2. 先把 Hessian 和逆 Hessian 算出来¶
它的行和列都对应两个输入特征,不是两个输出行。这里 \(\det(H)=4/3>0\),因此为了手算清楚取 \(\lambda=0\)。求逆后
非对角项 \(-1/2\) 意味着两个输入特征在校准集上有关联,因而一个特征上的误差可以由另一个权重部分吸收。若这里是对角阵,下面的 \(+0.06\)、\(-0.06\) 补偿将全部消失。
11.3. 量化第 0 列,并让第 1 列接住误差¶
本例预先指定处理顺序为第 0 列、再第 1 列;这不是由下面的公式自动选出的。第 0 列当前的两个工作权重是
按式 (36) 量化:
采用全文一致的误差约定 \(\rho=w-q\):
对下一列的并行补偿是
因此,第 0 列锁为 \(q\),第 1 列的工作值被改写:
这一步每个输出行的最小层重建损失增量是
与此同时,第 0 个输入特征已不再自由,必须消元:
11.4. 量化已经补偿过的第 1 列¶
重点是:现在不能拿原始 \([0.62,-0.62]^\top\) 去 round,而要量化工作矩阵里的
它们落到
已经没有后续自由变量可补偿,故每个输出行增加
最终存储的整数码、元数据和反量化权重是
11.5. 回到真实层输出验算,而不是只看权重差¶
所以
它恰好等于二阶增量按行求和:
作为对照,若完全忽略补偿,直接对原始 \(W\) 做最近邻 round:
在这个玩具例子中,GPTQ 的重建误差约为 RTN 的 \(0.3634\),即低约 \(63.7\%\)。特别看第一行第二列:原始 \(0.62\) 单独 round 会到 \(0.50\);它先因补偿变为 \(0.68\),随后量化到 \(0.75\)。GPTQ允许单个权重离原值看起来更远,只要整体 \(WX\) 更接近。
这个例子之所以能把式 (67) 与裸重建误差严格对上,是因为 \(H\) 满秩且取了 \(\lambda=0\)、网格也固定。真实大模型中通常会加入阻尼;那时应把式 (43) 当作阻尼代理的记账,并在量化结束后单独测量真实层误差和任务指标。
本例结论。 第 0 列的舍入误差先把第 1 列工作值从\([0.62,-0.62]^\top\) 推到 \([0.68,-0.68]^\top\),使它最终选择了不同的 4-bit 网格点;两行在两步中的条件代价全部相加,又与最终层输出重建误差严格相等。这同时验证了三件事:量化器读的是当前工作权重,不是原始权重;两个输出行共享补偿系数但分别量化;GPTQ 优先保持 \(WX\),而不是让每个 量化权重都尽量贴近原值。
参考资料¶
- 知乎文章:GPTQ 相关讲解:适合先建立直觉;本文补齐了其中容易被略过的条件最优、消元与 Cholesky 中间步骤。
- Frantar et al., GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers, ICLR 2023:层重建目标、共享列顺序、lazy block update 与 Cholesky 加速的原始来源。
- GPTQ 官方实现,固定提交 2d65066:阻尼、Cholesky、block 更新、act-order 和 group 行为的可执行对照。
读完后,最值得自己重新推一遍的是式 (26)、(42) 和 (49)。三式连起来,正好解释了 GPTQ 最核心的一句话:量化一列,剩下的列按输入统计主动接住误差。


评论