把大模型部署到消费级显卡或边缘设备时,量化几乎是绕不开的一步。FP16权重占用的显存通常超过推理框架能接受的范围,而4-bit整型量化能把模型体积降到原来的四分之一左右,同时利用整数运算单元加速。但量化不是简单的四舍五入,权重中少量远离分布中心的值会对最终输出产生不成比例的影响。GPTQ、AWQ和SqueezeLLM分别从误差累积、激活感知和稀疏分布切入,形成了三种有代表性的训练后量化方案。它们要解决的核心问题可以概括为:如何在降低位宽的同时,尽量把精度损失压到最低。

一、GPTQ:在每一列权重上做二阶误差补偿
GPTQ的前身是OBQ,目标是在逐层量化权重时最小化输出误差。量化线性层时,输出误差可以写成权重误差矩阵与输入矩阵乘积的F范数平方。直接对每个权重做最近邻取整虽然简单,但误差会在后续输出中累积。GPTQ引入权重之间的二阶信息,也就是用Hessian矩阵来刻画不同权重对损失的相互影响。具体来说,它先利用少量校准数据计算Hessian矩阵的逆,然后按列处理权重。每量化一列权重,就把该列产生的舍入误差通过Hessian逆矩阵补偿到尚未量化的列上,相当于让其他权重稍微调整,抵消刚刚那列量化带来的影响。
这个思路的优点是可以用纯数学的方法修正误差,不需要反向传播或梯度下降。实际操作中,由于Hessian矩阵可能很大,GPTQ会按块处理或对矩阵做近似分解来降低内存开销。量化顺序也可以按权重重要程度排序,让误差尽量被后面的权重吸收。对于4-bit量化,GPTQ通常能获得非常接近原始模型的困惑度表现,而且校准数据只需要几十到几百条文本即可。
import torch
def quantize_gptq(layer, X, bits=4):
W = layer.weight.data.clone().float()
H = 2 * (X.T @ X) / X.shape[0]
H = H + 1e-6 * torch.eye(H.shape[0])
Hinv = torch.inverse(H)
Q = W.shape[1]
scale = W.abs().max() / (2 ** (bits - 1) - 1)
for q in range(Q):
col = W[:, q]
q_col = torch.round(col / scale).clamp(-8, 7) * scale
err = col - q_col
W[:, q] = q_col
if q < Q - 1:
W[:, q+1:] -= err.unsqueeze(1) * Hinv[q, q+1:] / Hinv[q, q]
return W
从上面的伪代码可以看到,误差补偿项由Hinv的对应行和当前列误差共同决定。代码中的scale只是简单按最大绝对值计算,实际框架里通常会采用分组量化或per-channel缩放。GPTQ的不足在于它没有显式考虑激活异常值,当某些输入通道的激活幅度特别大时,即使权重误差很小,经过这些通道放大后仍可能造成明显偏移。
二、AWQ:用缩放系数保护重要通道
AWQ的核心观察来自对真实LLM激活分布的分析。权重矩阵的某些输出通道总是面对较大的输入激活值,这些通道即使权重值不大,其量化误差也会在输出端被放大。反过来,如果直接对权重做uniform量化,所有通道使用同一套量化步长,那么对敏感通道的保护就不够。AWQ的做法是在量化之前给每个输入通道的权重乘上一个缩放系数,量化完成后再把系数除回来。从数学上看,缩放和反缩放并不改变原始的线性运算结果,但它改变了量化步长对这些通道的映射方式。
寻找缩放系数并不需要训练。AWQ作者提出根据激活幅度的统计特征,比如按通道计算激活绝对值的平均值或分位数,给幅度大的通道分配更大的缩放因子,让这些权重在量化时落在更精细的区间。另一种更直接的方法是搜索:在几个候选缩放值之间做快速评估,选择输出误差最小的那个。由于只是增加了一组标量缩放向量,AWQ的额外存储和计算开销非常小。
import torch
def awq_scale(W, X, bits=4):
act_abs = X.abs().mean(dim=0)
scale = torch.ones_like(act_abs)
k = max(1, int(act_abs.numel() * 0.01))
topk_idx = torch.topk(act_abs, k).indices
scale[topk_idx] = act_abs[topk_idx] / act_abs[topk_idx].mean()
W_scaled = W * scale
q_W = torch.round(W_scaled / 0.1)
return q_W, scale
这段代码只展示了按激活均值分配缩放系数的简化版本。实际实现中,缩放因子会通过最小化输出差异来微调。AWQ的优势在于工程落地非常快,几乎不需要额外的校准迭代过程,对硬件的整数单元也天然友好。它和GPTQ可以组合使用,例如先用AWQ的缩放策略调整权重分布,再用GPTQ做逐层误差补偿,进一步压缩位宽。
三、SqueezeLLM:利用稀疏结构做非均匀量化
SqueezeLLM换了一个角度看待量化问题。它观察到LLM权重矩阵不仅稠密区域集中,还有明显的长尾分布和可压缩的稀疏性。如果使用均匀量化,那些远离中心的极端值会迫使量化范围变大,导致大量小权重在量化后全部变成同一个整数,信息丢失严重。SqueezeLLM的解决方案是分而治之:把权重按照敏感程度分开,对极少数敏感权重保留更高的数值精度,对大量非敏感权重采用更低的位宽,并且利用稀疏矩阵格式只保存非零元素的位置和索引。
具体实现上,SqueezeLLM使用K-means聚类来构建非均匀码本。权重值被聚类成若干质心,每个权重最终只保存其所属质心的索引。对敏感值集合,使用更密集的码本;对非敏感值集合,使用更稀疏的码本,甚至直接做剪枝或稀疏化。推理时通过查找表把索引恢复成实际数值,再参与矩阵乘法。由于非敏感部分稀疏,实际访存和计算量都能下降。
from sklearn.cluster import KMeans
import numpy as np
def squeezellm_quant(W, n_clusters=16):
flat = W.flatten()
threshold = np.quantile(np.abs(flat), 0.99)
sensitive_mask = np.abs(flat) >= threshold
sensitive_vals = flat[sensitive_mask]
km_s = KMeans(n_clusters=n_clusters).fit(sensitive_vals.reshape(-1, 1))
km_r = KMeans(n_clusters=n_clusters).fit(flat[~sensitive_mask].reshape(-1, 1))
return km_s.cluster_centers_, km_r.cluster_centers_, sensitive_mask
SqueezeLLM在极低位宽场景下通常能保留更好的精度,尤其是3-bit甚至更低时。但它对推理引擎的要求更高,需要自定义的稀疏矩阵乘法kernel和查找表操作。如果部署平台不支持非结构化稀疏加速,实际速度可能不如理论预期。因此选择SqueezeLLM之前,应先确认目标硬件和推理框架是否提供了对应的算子支持。
四、横向对比与选型建议
| 方法 | 核心策略 | 校准成本 | 典型位宽 | 精度表现 | 推理加速 | 实现难度 |
|---|---|---|---|---|---|---|
| GPTQ | 二阶误差补偿 | 中,少量校准数据 | 4-bit | 优秀 | 较高,有成熟kernel | 中 |
| AWQ | 激活感知缩放 | 低,统计激活分布 | 4-bit | 优秀 | 高,硬件友好 | 低 |
| SqueezeLLM | 稀疏非均匀量化 | 中,聚类构建码本 | 3-bit至4-bit | 极低位宽下较好 | 取决于稀疏kernel | 高 |
如果只考虑通用性和生态成熟度,GPTQ是当前最稳妥的选择。它的实现已经集成到多个推理框架中,针对不同模型结构的适配也相对完善。校准过程虽然比AWQ稍重,但对大部分开源模型来说,跑几十条样本就能完成量化。对于需要快速实验或者显存非常紧张的用户,AWQ的轻量校准流程非常有吸引力,尤其是在CPU或边缘设备上,缩放向量几乎不增加额外负担。
SqueezeLLM更适合那些希望把模型压到3-bit甚至更低,同时模型本身有一定稀疏性的场景。它的精度在低位宽下很突出,但对kernel的要求更高。如果目标硬件是通用GPU,可以使用支持稀疏运算的库;如果部署到手机或嵌入式设备,就要先确认框架支持自定义LUT和稀疏矩阵乘法。实际选型时,建议先用GPTQ或AWQ生成一个4-bit版本作为基线,再根据精度余量决定是否尝试SqueezeLLM做进一步压缩。
最后还要注意校准数据分布和推理分布的一致性。任何训练后量化都依赖校准集来统计权重和激活的动态范围,如果校准数据与线上请求差异很大,量化误差会被放大。实操中可以混合多种领域文本,并预留一部分样本做量化后评估,避免在未知分布上出现意外退化。