你有没有想过,给一个数字四舍五入这么简单的事,也能变得极其复杂?
如果只是把 3.7 变成 4,把 2.3 变成 2,这当然简单。但如果这些数字彼此牵连,改一个数就得连锁改动一整片,情况就完全不一样了。这正是大模型量化压缩里天天要面对的问题:把一个由几十亿个浮点数组成的权重矩阵,转换成整数表示,同时尽量不破坏模型的表现。
这篇论文要解决的,正是这类"连锁反应"式四舍五入问题里一个悬而未决的效率难题。故事得从一个已经很成功的方法说起。
(资料图片仅供参考)
GPTQ:一次只改一个数,但要通知所有"邻居"
如果你关注过大语言模型的压缩技术,大概率听说过 GPTQ。
**GPTQ*:一种把神经网络权重从浮点数转换成低比特整数的量化方法,广泛用于压缩大语言模型,让模型能在更小的显存里跑起来。**
它的核心思路其实不难理解。假设你面前摆着一列数字,要把它们逐个四舍五入成整数。如果这些数字互相独立,那简单,一个个取整就完了。但实际情况是,这些数字之间存在着某种"权重关系",改动某个数字的取整方式,会对后面还没处理的数字产生连带影响,因为它们共同决定了最终矩阵与原始矩阵之间的误差大小。
GPTQ 的做法是:按照固定顺序,一次只圆整一个数,圆整之后,把这次操作产生的误差通过一个特意计算好的"反馈矩阵"传递给后面还没处理的数字,让它们提前"感知"到前面发生的偏差,从而做出更聪明的调整。
**这就像多米诺骨牌的传递游戏,但骨牌之间的传递力度是提前算好的:每倒下一张牌,后面每张牌该受到多大的推力,都事先用数学方式精确计算过,不是随便碰一下就完事。**
如果不做这种误差传递会怎样?每个数字各自独立地就近取整,看起来最简单最直接,但矩阵整体的误差会明显更大,因为前面的取整偏差完全没有被后面的数字"消化"和"补偿"。GPTQ 之所以好用,正是因为它承认了这种连带关系,并且用一种巧妙的方式让计算量保持可控,从原本可能是四次方级别的复杂度压到了三次方。
不过,这里有一个限制条件:GPTQ 只处理"单边"情况。
**单边(one-sided):指矩阵只在一侧被一个固定的基矩阵作用,比如乘在左边或右边,而另一边保持不变的情形。**
也就是说,GPTQ 处理的对象,本质上是一个只在"行"方向上有相互牵连关系的矩阵,列与列之间是独立的,可以并行处理。这在很多实际场景里够用了。但现实中,还存在一种更复杂的情况。
两边都被"牵连":问题从三次方变成四次方
想象一下,如果矩阵不仅在行方向上有相互影响,在列方向上也有相互影响,会发生什么?
这就是论文提出的"两侧"版本问题。用数学语言说,就是原来的度量标准从 $\|A(Z-X)\|^2$ 变成了 $\|A(Z-X)B\|^2$,多了一个作用在右边的基矩阵 B。
**Kronecker 积(Kronecker product):一种矩阵运算,把两个矩阵按特定方式组合成一个更大的矩阵,常用来描述"行方向"和"列方向"分别有各自结构、又同时起作用的复合关系。**
论文里提到一个关键的数学事实:把矩阵"拉直"成一个长向量之后,这个两侧问题恰好可以用一维的取整算法直接套用,因为两侧的度量标准变成了一个 Kronecker 积形式的度量。这听起来是个好消息,理论上问题被解决了。
但代价是什么?如果矩阵大小是 $m \times n$,直接把整个矩阵拉成 $mn$ 长的向量再跑一维算法,计算量会变成 $O(m^2n^2)$,对于方阵来说就是四次方级别,而原来单边 GPTQ 只需要三次方。
**这就好比你原本只需要通知同一条流水线上的工人"前面出错了,请调整",现在却变成了整个车间纵横交错,每个工人不仅要听自己流水线上的消息,还要同时接收隔壁流水线传来的消息。如果你选择最笨的办法,让每条消息都单独跑一遍全车间广播,那通信量会呈指数级膨胀,车间效率立刻崩溃。**
这就是论文开篇提出的核心矛盾:理论上两侧问题可以套用一维算法解决,但直接套用的代价是从三次方猛增到四次方,对于动辄几千维的大模型权重矩阵,这个差距是灾难性的。研究者们于是面临一个明确的问题:能不能找到一种方法,让两侧版本的计算量重新降回三次方,和单边 GPTQ 打平?
这就是 GPTQ-2D 要回答的问题。
反对角线上的秘密:为什么某些数字互不相干
研究者们的突破口,在于发现了一个此前被忽略的结构性事实。
论文证明,当某个位置 $(i,j)$ 上的数字被圆整、产生了一个误差时,这个误差通过反馈机制传播出去的影响,只会波及矩阵右下方的一个矩形区域,也就是所有行号大于等于 $i$、列号大于等于 $j$ 的位置。这是因为反馈矩阵具有特殊的三角结构,行方向的反馈矩阵是下三角的,列方向的反馈矩阵是上三角的,两者相乘之后天然把影响范围限制在了"右下角"。
**这带来一个重要推论:把矩阵按"反对角线"分组,也就是把所有满足 $i+j$ 等于同一个数值的位置归为一组,同一组内的位置彼此之间不会互相影响。**
为什么呢?因为一个误差要从 $(i,j)$ 传到 $(p,q)$,必须满足 $p \geq i$ 且 $q \geq j$。如果 $(p,q)$ 和 $(i,j)$ 在同一条反对角线上,意味着 $p+q = i+j$,结合前面两个不等式,唯一的可能就是 $p=i, q=j$,也就是它们其实是同一个点。换句话说,同一条反对角线上的所有位置,彼此之间没有任何依赖关系。
**这就像一群互不相识的人在同一天排队办理业务,虽然大家在同一个时间点出现,但彼此之间没有任何交集,谁先谁后完全不影响别人,因此完全可以同时叫号处理,不需要排成一条长队一个个来。**
这个发现意味着,整个矩阵的取整过程可以按"反对角线"分批进行:先处理和为 2 的那条对角线(也就是左上角那个单独的格子),再处理和为 3 的对角线,以此类推,直到处理完和为 $m+n$ 的那条对角线(右下角)。每一条对角线内部的所有格子都可以并行处理,互不干扰。
论文用一个定理(Theorem 1,"顺序无关性")严格证明了:只要处理顺序尊重了依赖关系,也就是先处理"上游"再处理"下游",那么无论具体按什么顺序遍历,最终得到的圆整结果都是完全一致的。这就为按反对角线分批处理提供了理论保障,因为反对角线遍历顺序天然满足这个依赖约束。
不过,光是发现"可以按反对角线并行"还不够快。论文里提到一种直接实现方式(Algorithm 2):每处理完一条反对角线,就把这条线上所有格子产生的误差,通过一次"稠密的矩形更新"应用到右下方整个矩形区域上。这样做虽然利用了并行性,但每次更新触及的矩形区域面积可以达到 $mn$ 那么大,累积下来总的工作量还是 $O(m^2n^2)$,也就是没有真正降低复杂度,只是把四次方的计算变成了并行执行,计算总量并没有减少。
GPTQ-2D 的真正巧思:只往自己所在的行和列推
真正的突破发生在这里。论文观察到,与其把一个误差"整块地"推给整个右下矩形,不如换个思路:只把这个误差沿着它自己所在的那一行和那一列往后推,剩下的矩形区域,让后面的格子自己去"顺路"接收这份信息。
具体来说,GPTQ-2D 算法维护两样东西:一个是修正后的矩阵 $Y$(相当于当前的"待圆整"版本),另一个是一个辅助缓冲区 $C$,它记录的是"已经通过行方向反馈矩阵传递下去,但还没经过列方向反馈矩阵传递"的中间误差。
当格子 $(i,j)$ 被圆整、产生误差之后,算法做三件事:第一,把这份误差沿着它所在的列向下折叠进缓冲区 $C$;第二,把这份误差向下推给同一列后面还没处理的格子;第三,把缓冲区里已经积累好的信息沿着这一行向右推给后面的格子。
**这就好比一个物流中转站的做法:与其把每一件包裹都单独打包送到全国每一个城市(对应稠密矩形更新的做法),不如先把包裹沿着一条主干道送到中转仓库(对应向下推、折叠进缓冲区),再由中转仓库统一沿着另一条方向的干线分发出去(对应向右推)。这样一来,虽然最终每个包裹还是要送到目的地,但运输路径被拆解成了两段更短的、有规律的线路,总的运输量大大减少。如果不这样拆解,每个包裹都要单独规划一条从起点直达终点的路线,运输网络的复杂度会随着城市数量的增加而急剧膨胀。**
这个设计的精妙之处在于,它没有改变最终结果,只是改变了"误差传递"的组织方式。论文用 Theorem 2("轨迹等价性")严格证明了:无论是最原始的稠密矩形更新(Algorithm 2),还是这种沿行列分别推送的方式(Algorithm 3,也就是 GPTQ-2D 本体),最终得到的圆整矩阵都完全一致,一个数字都不差。
证明的关键在于一个不变量:缓冲区 $C$ 时刻保持着"$C$ 等于 $L$ 乘以已产生误差 $E$"这个关系($L$ 是行方向的反馈矩阵)。每当一个格子被圆整,它对缓冲区的贡献立刻被正确地折叠进去,而这个贡献恰好补全了它对后续所有格子应有的影响里"列方向"那部分,不多不少。
这样一来,每个格子的处理开销从原来的"整个矩形"降到了"自己所在的一行加一列",总的工作量从 $O(m^2n^2)$ 降到了 $O(mn \cdot \max(m,n))$,对于方阵而言正好是三次方,和单边 GPTQ 打平。
把零散更新拼成大块矩阵乘法:为什么"批量处理"更快
理论复杂度降下来了,但论文没有止步于此,因为理论上的低复杂度不等于实际跑起来快。
论文指出一个现实问题:GPTQ-2D 里那些"往一行一列推"的更新,本质上是很多次短小的、零散的向量操作。这种操作在现代硬件(比如 GPU)上其实效率很低,因为硬件擅长的是大块的、规整的矩阵乘法,而不是无数次琐碎的小规模读写。
**这就好比让你去银行办事,如果柜员每来一位顾客就单独跑一趟后台系统查询一次,效率会很低,即便每次查询本身很快,频繁的来回奔波也会拖慢整体效率。真正高效的做法是攒够一批顾客的请求,一次性批量查询处理,虽然要等一等,但省去了大量来回奔波的开销。**
于是论文借鉴了 GPTQ 本身早就采用的一个技巧,叫做"惰性块更新"。
**惰性块更新(lazy block update):先在一个小范围(块)内完成零散的小规模更新,等这个块处理完了,再用一次大规模、规整的矩阵乘法,把这个块积累的全部影响一次性发送给后面所有还没处理的部分。**
具体做法是把反对角线分成若干组,每组包含 $w$ 条连续的反对角线($w$ 是一个可调的块宽度)。在组内,依然按照前面说的方式做零散的小更新,但这些更新都被限制在组的边界之内。等一整个组处理完,用两次"带状矩阵乘法"把这个组积累的全部误差信息,一次性推送给后面所有格子:一次是把误差通过行方向反馈矩阵向下推,一次是把缓冲区信息通过列方向反馈矩阵向右推。
论文特别提到一个巧妙的地方:向下推送这一步是"一举两得"的,它既完成了对后续格子的实际数值更新,同时也顺带补全了缓冲区 $C$ 在这个组下方的正确取值,不需要额外再算一遍。
这个分块设计不改变总的计算量级别,仍然是 $O(mn \cdot \max(m,n))$,但它把大量零散操作合并成了少数几次大矩阵乘法,这在实际运行速度上会有巨大差别,因为现代计算硬件对大矩阵乘法的优化程度远远超过对零散小操作的优化。
存储也要讲究:把矩阵斜着放进内存
论文还讨论了一个容易被忽视但很实际的问题:怎么在计算机内存里存储这些数据,才能让"按反对角线处理""按列处理""按行处理"这三种访问方式都能高效进行。
**如果用最常见的按行存储或按列存储方式,反对角线上的元素在内存里的位置是七零八落的,每次访问反对角线都得东一个西一个地去"抓"数据,这在很多计算框架里会退化成低效的零散读取(gather 操作),拖累整体速度。**
论文提出了一种"斜向排列"的存储布局:把矩阵重新排成一个新的二维数组,新数组的每一行对应原矩阵的一条反对角线,行内位置按列号自然排列。这样一来,反对角线变成了连续的一段内存,可以高效整体读取;同时,原矩阵的某一列在新布局里变成了一条固定间隔(stride)的竖直线,某一行则变成了一条固定间隔的斜线,三种访问方式都变成了规整的、有固定步长的切片操作,都能高效执行。
这个设计的代价是需要预留一些"填充"位置,因为斜向排列会让数组形状变得不规则,两端会有一些用不上的空格。论文估算这种额外开销的量级是 $O(\max(m,n)^2)$,对于接近方阵的情况开销不大,但如果矩阵长宽比很悬殊,开销会变得明显,这时候论文建议把问题转置一下,让短边变成计算的主导方向,从而降低这个额外开销。
复杂度对比一览
论文给出了一张清晰的对比表格,把不同实现方式的代价摆在一起看,其中 $\mu = \max(m,n)$:
| 方法 | 预处理 | 主计算 | 并行深度 |
| 单边 GPTQ | $O(m^3)$ | $O(m^2n)$ | $O(m)$ |
| 直接向量化取整(两侧) | $O(\mu^3)$ | $O(m^2n^2)$ | $O(mn)$ |
| 稠密反对角线更新 | $O(\mu^3)$ | $O(m^2n^2)$ | $O(\mu)$ |
| **GPTQ-2D(缓冲式反对角线)** | $O(\mu^3)$ | **$O(mn\mu)$** | $O(\mu)$ |
| 嵌套顺序遍历 | $O(\mu^3)$ | $O(mn\mu)$ | $O(mn)$ |
可以清楚看到,GPTQ-2D 在保持较低并行深度的同时,把主计算量压到了和单边 GPTQ 同一个数量级,这正是这篇论文最核心的贡献所在。
论文诚实交代的边界
论文也坦率地指出了这个方法的适用边界。
首先,等价性的证明前提是左右两个基矩阵 A 和 B 在整个圆整过程中保持固定不变。如果这两个矩阵会根据中间的圆整结果动态调整,比如某种数据依赖的自适应缩放,那么 GPTQ-2D 和原始向量化方法之间的精确等价关系就不再成立了。
其次,GPTQ-2D 和它所扩展的 GPTQ 一样,走的是一条"贪心逐步决策"的路线,也就是按固定顺序一个个圆整,每一步都基于已经确定的前面结果做局部最优选择,但并不保证得到的是全局意义上误差最小的那个整数矩阵。论文明确提到,寻找真正全局最优的最近整数点,在数学上属于 NP 困难问题,这意味着即便有无限计算资源,也没有已知的高效算法能保证找到绝对最优解。GPTQ-2D 继承的是 GPTQ 一贯的实用主义思路:不追求完美,但求又快又好。
论文最后也强调了这项工作的定位:它解决的是"给定两侧的量化目标,该如何高效计算这个既定的取整轨迹"这个纯算法问题,而不是回答"这两个基矩阵 A 和 B 到底该怎么选"这个建模问题。在实际的神经网络量化场景里,这两个矩阵通常来自某种 Kronecker 分解的海森矩阵近似,但这属于另一个研究课题,论文明确表示不在这里讨论。
Q&A
Q1:GPTQ-2D 是用来解决什么问题的?
A:GPTQ-2D 解决的是"两侧量化"场景下取整效率低的问题。传统 GPTQ 只能处理矩阵一侧被固定基矩阵作用的情况,如果矩阵左右两侧同时被不同的基矩阵作用,直接套用一维算法会导致计算复杂度从三次方猛增到四次方。GPTQ-2D 通过按反对角线分组处理,并只沿行列方向传递误差,把复杂度重新降回三次方。
Q2:GPTQ-2D 和原来的 GPTQ 有什么关系?
A:GPTQ-2D 是 GPTQ 的扩展而不是替代品。单边 GPTQ 其实就是 GPTQ-2D 里右侧基矩阵取单位矩阵的特殊情形。对于行数不小于列数的矩阵,两者的计算量是同一个数量级,也就是说两侧扩展在效率上几乎是"免费"获得的。
Q3:GPTQ-2D 的核心加速技巧是什么?
A:关键在于发现同一条反对角线上的矩阵元素彼此互不影响,可以并行处理;同时不再把每个误差整块地推给整个右下矩形区域,而是只沿着它所在的一行和一列分别传递,剩下的部分靠后续格子顺路接收,这样把总计算量从 $O(m^2n^2)$降到了 $O(mn\max(m,n))$。