上一篇我们讲了 GPTQ。

GPTQ 的思路可以粗略概括成一句话:

一个权重被量化以后产生了误差,就利用二阶信息调整后面的权重,让这些误差尽量不要传到层输出上。

这一篇换一条路线,讲 AWQ。

AWQ,全称 Activation-aware Weight Quantization,中文通常叫“激活感知权重量化”。

它同样要解决低比特权重量化的精度问题,但思考方式完全不同:

既然 INT4 一共只有十几个离散刻度,精度资源天然有限,那就别把这些刻度平均分给所有权重,而应该优先照顾真正影响输出的那些通道。

问题随之而来:

哪些权重才是真正重要的?

最直觉的答案是看权重本身:

  • 绝对值大的权重重要;
  • 范数大的权重通道重要;
  • 离群权重重要。

AWQ 论文发现,这些标准并不好用。

真正更有效的判断依据,居然是权重另一边的输入——激活

一个权重即使数值不大,只要它总是和很大的激活相乘,它的量化误差就会被明显放大;反过来,一个权重虽然很大,但它对应的输入通道几乎总是接近零,那么它对真实输出的影响可能并没有想象中那么大。

AWQ 就是从这个观察出发:

  1. 用校准数据观察各个输入通道的激活幅度;
  2. 找到那些容易放大量化误差的通道;
  3. 在量化前对权重做一个严格等价的通道缩放;
  4. 让共享的 INT4 量化网格把更多有效分辨率留给重要通道;
  5. 最后仍然把全部权重统一存成低比特格式。

论文将 AWQ 定位为一种硬件友好的低比特 weight-only PTQ 方法。论文中的动机实验显示,按照激活选出的少量显著通道对模型精度有非常大的影响;最终方法则用等价缩放代替混合精度,从而避免单独保存 FP16 权重。(ar5iv)

先把这篇最重要的一句话放在这里:

AWQ 不是简单地“找出大激活,然后把对应权重放大”。更准确地说,它利用激活统计构造通道缩放,在不改变浮点模型函数的前提下,重新分配 group-wise 量化中的共享分辨率,使最终的层输出误差更小。

读完这一篇,你应该能回答下面几个问题:

  • 为什么权重的重要性不能只看权重?
  • “保护 1% 权重”究竟是实验发现还是最终算法?
  • W·X = (W·s)·(X/s) 明明是恒等式,为什么放到量化里就能提升精度?
  • 为什么 AWQ 必须放在 group-wise 量化的背景下理解?
  • 为什么缩放得越大不一定越好?
  • AWQ 搜索的到底是每个通道独立的 scale,还是一个更低维的参数?
  • 为什么 AWQ 甚至可能让普通的权重 MSE 变大,却让模型输出更准确?
  • AWQ 和 GPTQ 到底是竞争关系,还是可以组合?

目录

一、AWQ 到底在解决什么问题
二、先补一块地基:W4A16、量化粒度与 group scale
三、什么才叫“重要权重”
四、论文里的 0.1%~1% 实验究竟证明了什么
五、AWQ 的核心:等价缩放
六、从量化误差公式看缩放为什么有效
七、缩放系数不是拍脑袋定的:AWQ 如何搜索
八、用正确的代码验证 AWQ 核心思想
九、AWQ 与 GPTQ 横向对比
十、从论文原理到工程实现,中间还有哪些细节
十一、常见误区与避坑
十二、怎样用一分钟把 AWQ 讲给别人听
十三、小结


一、AWQ 到底在解决什么问题

1.1 低比特量化真正稀缺的是什么

假设我们有一个 FP16 权重矩阵。

FP16 中每个权重大致可以取非常多的数值;换成 INT4 后,一个权重通常只能落到 16 个整数编码中的某一个。

不管是有符号的:

-8, -7, ..., 0, ..., 6, 7

还是带零点的非对称编码:

0, 1, 2, ..., 15

本质都一样:

原来连续的权重空间,被压缩成了一张非常稀疏的离散网格。

因此,INT4 量化的核心矛盾不只是“数值有没有被表示出来”,而是:

有限的十几个刻度,应该怎样覆盖一组权重的动态范围?

假设一组权重位于:

[-0.7, 0.7]

用大约 15 个间隔覆盖,量化步长可能在 0.09 左右。

这意味着:

0.01、0.03、0.05

可能全都被量化成同一个值。

权重原本细微的区别消失了。

如果模型恰好非常依赖这些细微区别,输出就会明显变化。


1.2 RTN 的问题不只是“每个权重有误差”

最基础的量化通常是 round-to-nearest,简称 RTN:

q = round(w / scale)
w_hat = q * scale

它为每个权重找到最近的量化格点。

单看某一个权重,这个策略似乎很合理:

离哪个格点最近,就放到哪个格点上。

但神经网络从来不会孤立地使用一个权重。

在线性层中,真正被使用的是:

[
Y=XW^\top
]

其中:

  • (X) 是输入激活;
  • (W) 是权重;
  • (Y) 是输出。

量化后变成:

[
\hat Y=XQ(W)^\top
]

输出误差为:

[
\Delta Y

\hat Y-Y

X\left(Q(W)-W\right)^\top
]

记权重量化误差为:

[
E=Q(W)-W
]

那么:

[
\Delta Y=XE^\top
]

这条式子揭示了一个非常重要的事实:

权重误差本身不是最终目标,权重误差经过激活放大以后形成的输出误差,才是模型真正承受的损失。

两个权重可能拥有完全相同的量化误差:

Δw₁ = 0.01
Δw₂ = 0.01

但如果对应激活是:

x₁ = 20
x₂ = 0.02

它们对输出造成的误差分别是:

Δy₁ = 0.01 × 20   = 0.2
Δy₂ = 0.01 × 0.02 = 0.0002

相差一千倍。

所以,低比特量化中最重要的并不是让每个权重的误差都一样小,而是:

把较小的量化误差分配给那些会被激活显著放大的位置。

这就是 AWQ 的出发点。


1.3 AWQ 的问题意识:量化精度不应该平均分配

可以把一个 group 的 INT4 网格想象成有限预算。

你只有十几个刻度,却要表示几十个甚至上百个权重。

普通 RTN 的态度是:

大家共用同一张网格,各自找最近的格点。

AWQ 的态度是:

既然有些通道对输出特别敏感,就先改变权重在这张网格中的相对位置,让关键通道获得更高的有效分辨率。

注意,这里说的是“有效分辨率”,不是凭空多创造几个 INT4 数值。

INT4 仍然只有原来的编码数量。

AWQ 做的是重新分配:

  • 重要通道在共享范围里占据更显著的位置;
  • 不重要通道可以承受稍粗的表示;
  • 总体格式不变;
  • 层输出误差下降。

这是一种非常典型的思想:

不追求让所有局部误差都最小,而是根据真实使用频率和敏感度,重新分配有限资源。


二、先补一块地基:W4A16、量化粒度与 group scale

AWQ 最容易被讲错的地方,不是激活,也不是缩放,而是很多文章没有先讲清楚:

权重到底按什么粒度共享量化参数?

如果忽略这件事,后面“放大权重为什么会更准”很容易变成似是而非的解释。


2.1 AWQ 通常是 weight-only,而不是 W4A4

常见的 AWQ 部署形式可以写成:

W4A16

含义是:

  • Weight:4 bit;
  • Activation:16 bit,通常是 FP16 或 BF16。

也就是说,AWQ 的重点是压缩权重。

它并不意味着:

权重 INT4
激活 INT4
累加 INT4
输出 INT4

真实推理中通常是:

  1. 权重以 4 bit 压缩形式存储;
  2. 内核读取并解包权重;
  3. 在融合内核中完成反量化和矩阵计算;
  4. 激活仍保持较高精度。

原始 AWQ 仓库明确把高效内核描述为 W4A16,即 4-bit 权重和 16-bit 激活。(GitHub)

这点非常重要,因为后面会看到:

x / s

这个操作只是等价变换的一部分。

它不代表 AWQ 把激活也量化成 4 bit。


2.2 量化不只有 bit 数,还有粒度

同样是 INT4,量化方式可能完全不同。

第一种:per-tensor

整个矩阵共享一个 scale:

整个 W → 一个 scale

优点是元数据少、实现简单。

缺点是不同区域的动态范围可能差别很大,一个离群值就可能抬高整张矩阵的量化步长。


第二种:per-output-channel

每个输出通道,也就是权重矩阵的每一行,共享一个 scale:

W[0, :] → scale₀
W[1, :] → scale₁
...

精度通常比 per-tensor 更好。


第三种:group-wise

每一行再沿输入维度切成多个小组,每组共享一个 scale 和 zero point:

一行有 4096 个权重
group_size = 128

第 0~127 个权重   → 一套量化参数
第 128~255 个权重 → 一套量化参数
...

这就是很多 INT4 LLM 量化格式中的 g128

W4-g128
W3-g128
INT4-g128

AWQ 论文主要研究 group-wise weight-only 量化,实验中通常使用 group size 128。原始实现也会沿权重矩阵最后一维把权重 reshape 成固定大小的 group,再为每组计算 scale 和 zero point。(ar5iv)


2.3 为什么 group-wise 是理解 AWQ 的关键

假设一个 group 中有 128 个权重,它们共享同一个量化步长:

[
\Delta
]

这个步长通常由该 group 的最大值、最小值或者裁剪范围决定。

以简化的对称量化为例:

[
\Delta

\frac{\max |w|}{2^{b-1}-1}
]

4 bit 时:

[
2^{4-1}-1=7
]

于是:

[
Q(w)

\Delta\cdot
\mathrm{clip}
\left(
\mathrm{round}\left(\frac{w}{\Delta}\right),
-7,7
\right)
]

关键是:

一个 group 内的所有权重都使用同一个 (\Delta)。

因此,权重之间是在竞争同一张网格。

假设同一个 group 中有两个权重:

w₁ = 0.48
w₂ = 0.03

如果 group 的步长是:

Δ = 0.07

那么:

w₁ / Δ ≈ 6.86 → 7
w₂ / Δ ≈ 0.43 → 0

w₁ 还能被表示,w₂ 直接被压到零。

如果 w₂ 恰好对应一个非常大的激活,那么即使它看起来只是“小权重”,把它量化成零也可能对输出产生很大影响。

AWQ 想做的事情,就是在量化之前改变这种相对关系。


2.4 一个必须提前知道的反例

假如我们不是 group-wise,而是每一个输入通道单独拥有自己的量化 scale,会发生什么?

考虑某一列权重 (w),将它整体乘以正数 (s)。

该列的量化步长也会乘以 (s):

[
\Delta’ = s\Delta
]

于是:

[
Q(sw)

sQ(w)
]

再除以 (s):

[
\frac{Q(sw)}{s}

Q(w)
]

也就是说:

如果每一列都有完全独立的量化范围,先乘 (s) 再除 (s),结果可能和不缩放完全相同。

缩放没有重新分配任何共享资源,因为这一列本来就独占一张量化网格。

这也是原稿代码最需要修正的地方:

for c in range(W.shape[1]):
    Wq[:, c] = quantize_int4_symmetric(W[:, c])

这段代码让每一列单独计算 scale。

在这种量化定义下,对整列做 AWQ 式缩放,scale 会同步变化,缩放前后理论上可能完全等价,不能用来演示 AWQ 收益。

真正能体现 AWQ 的实验,必须让多个输入通道的权重共享量化参数,例如:

每个输出行中的连续 32 个权重共享 scale

或者真实模型中常见的:

每个输出行中的连续 128 个权重共享 scale

请把这一点记牢:

AWQ 的“精度腾挪”发生在共享量化尺度的 group 内。没有共享,就没有重新分配。


三、什么才叫“重要权重”

3.1 从一个乘法开始

先看最简单的标量乘法:

[
y=wx
]

量化权重后:

[
\hat w=w+e
]

其中 (e) 是权重量化误差。

量化后的输出:

[
\hat y=(w+e)x
]

输出误差:

[
\hat y-y=ex
]

因此:

[
|\Delta y|=|e|\cdot|x|
]

这意味着:

  • 权重误差越大,输出误差越大;
  • 激活越大,同样的权重误差越危险。

这是 AWQ 最基本的数学直觉。


3.2 扩展到完整线性层

对某个输出通道 (j):

[
y_j=\sum_i w_{ji}x_i
]

量化后:

[
\hat y_j

\sum_i (w_{ji}+e_{ji})x_i
]

因此:

[
\Delta y_j

\sum_i e_{ji}x_i
]

这里的每个输入通道 (i),都对应:

  • 一个激活 (x_i);
  • 权重矩阵中的一整列 (W[:,i])。

当某个 (x_i) 经常很大时,这一列里的量化误差就更容易传到输出。

所以 AWQ 关注的不是孤立的一个权重,而是:

输入通道及其对应的权重列。

这也是论文中 “salient weight channels” 的含义。

假设权重矩阵形状为:

[out_features, in_features]

那么第 (i) 个输入通道对应的权重是:

W[:, i]

整列权重都会和激活通道:

X[..., i]

相乘。


3.3 从输出 MSE 看,激活天然是权重误差的加权系数

如果我们进一步观察平方误差:

[
(\Delta y_j)^2

\left(
\sum_i e_{ji}x_i
\right)^2
]

完整展开会出现不同通道之间的交叉项。

为了建立直觉,暂时作一个简化假设:

  • 各通道量化误差近似不相关;
  • 误差均值近似为零;
  • 先忽略交叉项。

那么可以得到:

[
\mathbb E[(\Delta y_j)^2]
\approx
\sum_i
\mathbb E[e_{ji}^2]
\mathbb E[x_i^2]
]

这条式子特别重要。

它告诉我们:

输出误差并不是普通的权重 MSE,而是被激活能量加权过的权重误差。

普通权重 MSE 是:

[
\sum_{j,i}e_{ji}^2
]

它默认每个位置同等重要。

而近似的输出 MSE 是:

[
\sum_{j,i}
e_{ji}^2
\mathbb E[x_i^2]
]

它认为激活大的通道更重要。

这正是 AWQ “activation-aware” 的数学来源。

当然,真实 Transformer 激活之间并不独立,量化误差之间也不一定完全独立。

所以这不是一个严格描述所有情况的定理,而是一个帮助理解的近似。

AWQ 后面还会用真实校准输入计算层输出误差,正是为了把通道相关性、权重分布以及 group scale 的变化都纳入最终选择。


3.4 为什么只看权重大小不够

很多人会自然地认为:

|w| 越大,权重越重要

这句话不是完全错误,但它混淆了两个概念:

  1. 权重本身的数值大小;
  2. 权重误差对模型输出的敏感度。

一个大权重可能对应长期接近零的输入:

w = 3.0
x ≈ 0

它当前对输出的贡献仍然很小:

w × x ≈ 0

另一个权重可能只有:

w = 0.08

但它经常对应:

x = 30

贡献则是:

0.08 × 30 = 2.4

更关键的是,我们关心的不是原始贡献,而是量化误差的传播:

e × x

即使两个权重的误差相同,后者也危险得多。

因此,一个权重是否重要,并不是它与生俱来的、完全静态的属性。

更准确地说:

权重的重要性取决于它在什么输入分布下被使用。

同一个模型面对不同数据分布时:

  • 某些通道可能频繁活跃;
  • 某些通道可能很少出现;
  • 某些专业领域特征可能突然变得重要。

这也是 AWQ 需要校准数据的根本原因。


3.5 AWQ 到底怎样统计激活

真实模型不会只观察一个激活向量。

AWQ 会拿一小批校准数据跑前向传播,收集某个线性层的输入:

[
X\in
\mathbb R^{N\times T\times C}
]

其中:

  • (N) 是样本数;
  • (T) 是 token 数;
  • © 是隐藏维度,也就是输入通道数。

然后把样本和 token 维度展开,对每个输入通道计算平均绝对幅度:

[
a_i

\frac{1}{NT}
\sum_{n,t}
|X_{n,t,i}|
]

代码形式大致是:

act_scale = X.abs().reshape(-1, X.shape[-1]).mean(dim=0)

原始 AWQ 代码中的 get_act_scale 正是按最后一个维度保留通道,并对其他位置的绝对值取均值。(GitHub)

注意两个细节。

第一,AWQ 用的是一批数据上的通道统计量,不是单个 token 的瞬时值。

第二,论文和实现通常采用平均绝对值,而不是严格使用刚才推导中的二阶矩:

[
\mathbb E[x_i^2]
]

原因是平均绝对值简单、稳定,而且在实验中足以提供有效的显著性排序。


3.6 “看激活,不看权重”不能理解得太绝对

为了传播方便,我们经常说:

AWQ 判断重要权重时看激活,不看权重。

这句话适合作为标题,但技术上需要补全。

AWQ 的“通道显著性信号”主要来自激活。

但是最终量化过程当然仍然会使用权重:

  • group 的 scale 由权重范围决定;
  • 缩放后范围怎样变化取决于权重;
  • 候选缩放的输出误差同时由 (X) 和 (W) 决定;
  • clipping 搜索也会观察权重分布。

所以更准确的说法是:

AWQ 不用权重范数来直接定义通道重要性,而是用激活幅度构造候选缩放,再利用真实权重和层输出误差选择最终方案。

AWQ 不是“完全不看权重”。

它只是发现:

激活比单纯的权重大小,更适合作为显著通道的入口。


3.7 一个更准确的生活比方

还是用调音台来比喻。

每个推子可以看成一组权重,推子接收到的音频信号是激活。

两个推子的刻度误差都偏了 1 毫米:

  • 第一个接的是主唱麦克风;
  • 第二个接的是一条几乎静音的备用轨道。

物理上的偏差一样,但听感上的影响完全不同。

不过,AWQ 不只是“给主唱推子上锁”。

它做得更巧:

  1. 先观察哪些轨道长期信号更强;
  2. 调整推子和前级增益之间的配合;
  3. 保证理想状态下总音量完全不变;
  4. 让有限精度的数字推子更多地覆盖主唱附近的有效范围。

也就是说:

AWQ 不是简单地减少所有推子的机械误差,而是在有限控制精度下,优先保证最影响最终混音的轨道。


四、论文里的 0.1%~1% 实验究竟证明了什么

4.1 先做一个看似不实用的实验

AWQ 论文先没有立即提出缩放。

作者先做了一个“如果硬件完全不重要,我们最想怎样保护权重”的实验。

基本做法是:

  1. 大部分权重照常做 INT3 group-wise 量化;
  2. 选出少量输入通道;
  3. 这些通道对应的权重列保留 FP16;
  4. 比较不同通道选择策略。

三种选择方式包括:

  • 按激活幅度选择;
  • 按权重范数选择;
  • 随机选择。

结果差别非常明显。

例如论文在 OPT-6.7B、INT3-g128 设置下报告:

FP16 模型 PPL                     10.86
普通 RTN INT3-g128 PPL           23.54
按激活选 0.1% 通道保留 FP16      11.58
按激活选 1% 通道保留 FP16        11.39
按权重大小选 1% 通道保留 FP16    22.37
随机选 1% 通道保留 FP16          24.23

在这个实验里,按激活选择极少量通道就能显著恢复精度,而按权重范数选择的效果接近随机。论文在其他 OPT 模型上也观察到了类似现象。(ar5iv)

这组实验真正证明的是:

量化误差的破坏性高度集中在少数激活显著的输入通道上,而且这种显著性不能可靠地由权重范数替代。


4.2 “1% 权重”实际是怎样的 1%

这里也容易产生误解。

论文说保护少量 salient weights,但实验操作通常是按输入通道选择。

假设:

in_features = 4096

选择其中约 1% 输入通道,相当于选择约 41 个通道。

每个通道对应权重矩阵的一整列:

W[:, i]

因此,被保护的是这些列中的所有权重。

如果每一列长度相同,那么保护 1% 输入通道,也大致对应保护 1% 的矩阵权重。

所以它不是从整个矩阵里无结构地挑出零散的 1% 元素,而是带有明显的通道结构


4.3 这个实验不是最终 AWQ

很多二手文章会写成:

AWQ 找出最重要的 1% 权重,然后保护它们。

这句话只讲了一半。

“保留 1% FP16”是论文用于验证核心观察的动机实验。

它不是最终部署方案。

原因很直接:

99% INT4 + 1% FP16

虽然平均 bit 数增加不多,但计算布局变得不规则。

一个矩阵乘法内核需要同时处理:

  • 普通 INT4 权重;
  • 稀疏分布的 FP16 权重;
  • 两套加载方式;
  • 两套计算路径;
  • 最后的结果合并。

理论压缩率可能很好,实际硬件效率却不一定好。

论文明确把这种混合精度方案视为系统实现上的障碍,因此继续寻找一种不保留 FP16 权重、却能近似获得保护效果的方法。(ar5iv)


4.4 最终 AWQ 没有固定的“1% 阈值”

最终 AWQ 不会简单执行:

important = top_1_percent(act_scale)
important 保持 FP16
其他通道 INT4

它采用的是连续缩放:

[
s_i=f(a_i)
]

激活较大的通道通常获得较大的相对缩放,激活较小的通道获得较小的相对缩放。

所以最终方法更像是:

按照激活重要性连续地重新分配量化分辨率。

不是:

重要或不重要,二选一。

“1%”应该记成:

  • 一个很有启发性的实验现象;
  • 说明误差敏感度高度集中;
  • 不是 AWQ 实现里的固定超参数。

五、AWQ 的核心:等价缩放

5.1 一个完全不改变模型输出的恒等式

设:

[
X\in\mathbb R^{N\times C_{\text{in}}}
]

[
W\in\mathbb R^{C_{\text{out}}\times C_{\text{in}}}
]

线性层输出为:

[
Y=XW^\top
]

现在为每个输入通道定义一个正缩放系数:

[
s_1,s_2,\ldots,s_{C_{\text{in}}}
]

组成对角矩阵:

[
D=\mathrm{diag}(s)
]

将权重矩阵的第 (i) 列乘以 (s_i):

[
W’=WD
]

同时将激活的第 (i) 个通道除以 (s_i):

[
X’=XD^{-1}
]

那么:

[
X’(W’)^\top

XD{-1}(WD)\top
]

由于 (D) 是对角矩阵:

[
(WD)\top=DW\top
]

所以:

[
XD{-1}DW\top

XW^\top
]

最终得到:

[
XW^\top

(XD{-1})(WD)\top
]

这不是近似,而是严格恒等式。

只要:

  • 权重乘 (s_i);
  • 对应激活除以同一个 (s_i);

浮点模型的数学输出完全不变。


5.2 缩放本身零损失

这一点必须强调。

AWQ 的误差不来自缩放。

在无限精度或者普通 FP32 运算中:

原模型输出 = 缩放后模型输出

缩放只是把同一个函数换了一种参数化方式。

类似于:

10 × 3 = 5 × 6

数字变了,乘积没有变。

因此:

AWQ 的等价变换本身没有精度损失。真正产生误差的是后面的权重量化。


5.3 量化让这个恒等式产生了价值

问题在于,量化不是线性运算。

量化后,我们计算的不是:

[
WD
]

而是:

[
Q(WD)
]

最终的近似输出变成:

[
\hat Y_{\text{AWQ}}

XD{-1}Q(WD)\top
]

也可以把它写成一个等效权重:

[
\hat W

Q(WD)D^{-1}
]

然后:

[
\hat Y_{\text{AWQ}}

X\hat W^\top
]

注意:

[
Q(WD)D^{-1}
\neq
Q(W)
]

一般情况下,两者并不相等。

这就是 AWQ 发挥作用的地方。

浮点世界中,(D) 和 (D^{-1}) 完全抵消。

但中间插入非线性的量化函数 (Q) 后,缩放会改变:

  • 权重落在哪个整数格点;
  • group 的最大值和最小值;
  • group 的量化步长;
  • 各通道相对于共享范围的位置;
  • 最终误差怎样分配。

可以把它概括成一句话:

等价缩放不改变原函数,但会改变量化器看到的数值分布。


5.4 “放大权重就更精确”需要加一个限定

常见解释是:

把重要权重放大后,它在 INT4 网格里占据更多刻度,因此更精确。

这个说法作为直觉没有问题,但要加上三个限定。

限定一:刻度总数没有增加

INT4 仍然只有原来的编码数量。

AWQ 不是为重要通道额外增加 bit。

它改变的是重要通道相对于 group 共享范围的位置。


限定二:真正关心的是除回 (s) 后的有效误差

假设缩放后的权重被量化为:

[
Q(sw)
]

模型中对应的等效权重是:

[
\frac{Q(sw)}{s}
]

所以我们关心的误差是:

[
\frac{Q(sw)}{s}-w
]

而不是只看:

[
Q(sw)-sw
]

即使缩放后权重的绝对量化误差看起来没有下降,除以 (s) 还原到原参数空间后,重要通道的有效误差仍可能明显减小。


限定三:多个通道必须共享量化尺度

如果某一列拥有独立 scale:

[
Q(sw)=sQ(w)
]

那么:

[
\frac{Q(sw)}s=Q(w)
]

完全没有变化。

只有当多个通道共享 group scale 时,缩放才会改变它们对共享动态范围的占用比例。

所以更准确的说法是:

AWQ 通过通道缩放,改变重要权重相对于共享 group scale 的大小,从而降低其还原到原参数空间后的有效量化误差。


5.5 用一个小数值例子理解

假设某个 group 共享步长:

Δ = 0.1

其中一个重要权重:

w = 0.14

直接量化:

0.14 / 0.1 = 1.4
round(1.4) = 1
Q(w) = 0.1

误差:

0.1 - 0.14 = -0.04

现在将它放大 4 倍:

s = 4
sw = 0.56

假设这个缩放没有明显抬高整个 group 的步长,新的步长仍接近:

Δ' ≈ 0.1

那么:

0.56 / 0.1 = 5.6
round(5.6) = 6
Q(sw) = 0.6

除以 4:

Q(sw) / s = 0.6 / 4 = 0.15

有效误差:

0.15 - 0.14 = 0.01

原来误差是:

0.04

现在只有:

0.01

重要权重得到了更细的有效表示。

但这个例子隐含了一个关键假设:

Δ' 没有随着 s 同比例增长

如果缩放后的权重成为 group 中新的极端值,导致:

Δ' = 4Δ

那么收益可能被抵消。

这就引出了 AWQ 中最重要的权衡。


六、从量化误差公式看缩放为什么有效

6.1 直接量化的误差

用简化的对称量化表示:

[
Q(w)

\Delta\cdot
\mathrm{round}\left(\frac{w}{\Delta}\right)
]

定义取整误差:

[
\epsilon

\mathrm{round}\left(\frac{w}{\Delta}\right)

\frac{w}{\Delta}
]

那么:

[
Q(w)-w

\Delta\epsilon
]

乘以激活后,对输出造成的误差为:

[
\Delta y

x\Delta\epsilon
]

忽略符号,只看数量级:

[
|\Delta y|
\propto
|x|\Delta|\epsilon|
]


6.2 缩放后的误差

将权重乘以 (s),激活除以 (s)。

缩放后 group 的量化步长记为:

[
\Delta’
]

缩放权重的量化误差为:

[
Q(sw)-sw

\Delta’\epsilon’
]

除以 (s) 回到原始权重空间:

[
\frac{Q(sw)}s-w

\frac{\Delta’\epsilon’}s
]

对输出造成的误差:

[
\Delta y’

x
\frac{\Delta’\epsilon’}s
]

如果把缩放前后的取整误差分布近似认为相似,那么误差比大致为:

[
\frac{|\Delta y’|}{|\Delta y|}
\approx
\frac{\Delta’}{\Delta}
\cdot
\frac1s
]

这就是 AWQ 缩放分析里最核心的关系。


6.3 最理想的情况

如果缩放一个显著通道,并没有改变 group 的极值,那么:

[
\Delta’\approx\Delta
]

于是:

[
\frac{|\Delta y’|}{|\Delta y|}
\approx
\frac1s
]

假设:

s = 2

有效误差大致减半。

假设:

s = 4

有效误差可能降到四分之一。

这就是为什么放大重要通道能够保护它。


6.4 为什么不能无限放大

如果 (s) 太大,缩放后的显著权重可能开始决定整个 group 的动态范围。

这时:

[
\Delta’>\Delta
]

其他没有获得同等缩放的通道也必须使用更大的量化步长。

结果是:

  • 显著通道误差下降;
  • 普通通道误差上升;
  • group 的总误差可能先降后升。

假设显著通道放大 8 倍,并让 group 步长增大 3 倍。

显著通道的误差比大约是:

[
\frac38
]

仍然受益。

但是普通通道没有除以 8 的补偿,其量化误差可能接近原来的 3 倍。

如果普通通道数量很多,总体输出误差就可能反而变差。

论文的缩放实验也观察到这种现象:继续增大显著通道的缩放,虽然这些通道的相对误差继续下降,但非显著通道受到更大 group scale 的影响,最终模型困惑度会在某个中间缩放处达到最好,而不是缩放越大越好。(ar5iv)


6.5 主要风险不是“自动溢出”

原稿中有一种常见说法:

缩放太大会把权重推出量化范围,导致 clip。

对普通 min-max 权重量化来说,这个表述不够准确。

如果量化器每次都根据缩放后的权重重新计算最小值和最大值,那么权重一般不会因为放大就自动溢出。

量化器会扩大 scale,把新的极值纳入范围。

真正的问题是:

范围变大以后,整个 group 的量化步长变粗。

当然,AWQ 还可能主动搜索 clipping 阈值。

那是另一件事:

  • 不使用完整 min-max 范围;
  • 故意裁掉少量权重极值;
  • 换取更小的量化步长;
  • 通过输出 MSE 判断是否值得。

论文在通道缩放之外也加入了基于 MSE 的权重 clipping 搜索。(ar5iv)

所以应当区分:

缩放过大导致 group scale 增大

和:

算法主动选择更窄范围进行 clipping

它们不是同一个机制。


6.6 AWQ 优化的不是普通权重 MSE

这一点非常有深度,也很容易被忽略。

普通权重 MSE 是:

[
|Q(W)-W|_F^2
]

它把所有权重位置看得一样重要。

AWQ 更关心:

[
|XQ(W)\top-XW\top|_F^2
]

也就是层输出 MSE。

因此完全可能出现这种情况:

普通权重 MSE:变大
层输出 MSE:变小

为什么?

因为 AWQ 可能把更多误差“挪”给激活较小的通道,同时减少激活较大通道的误差。

从参数空间看,整体权重误差变大了。

从模型真正使用这些权重的方式看,输出反而更准确。

这正是“activation-aware”的精髓:

重要的不是权重矩阵在欧氏距离上离原矩阵多近,而是它在真实输入分布下产生的函数离原函数多近。


七、缩放系数不是拍脑袋定的:AWQ 如何搜索

7.1 理论上可以为每个通道任意选 (s_i)

我们已经知道,每个输入通道都可以有独立缩放:

[
s_1,s_2,\ldots,s_C
]

最直接的优化问题是:

[
s^*

\arg\min_s
\left|
XD{-1}Q(WD)\top-XW^\top
\right|_F^2
]

其中:

[
D=\mathrm{diag}(s)
]

但这会带来一个问题:

如果隐藏维度是 4096,那么一层就有 4096 个待优化变量。

一个几十层的模型会产生大量参数。

而量化函数中包含 round 和 clip,不可微、分段常数,直接用普通梯度下降并不稳定。

AWQ 没有这样做。


7.2 用激活统计构造低维搜索空间

AWQ 先计算每个通道的激活尺度:

[
a_i=\mathrm{mean}(|X_i|)
]

然后将缩放向量参数化为:

[
s_i=a_i^\alpha
]

其中:

[
\alpha\in[0,1]
]

这样,本来几千维的缩放搜索,被压缩成了一个一维搜索。

当:

[
\alpha=0
]

有:

[
s_i=a_i^0=1
]

即完全不做 activation-aware 缩放。

当:

[
\alpha=1
]

有:

[
s_i=a_i
]

激活幅度被最直接地映射为通道缩放。

当:

[
0<\alpha<1
]

则是在“不缩放”和“激进缩放”之间折中。

论文使用这种由激活幅度和单个指数参数构成的搜索空间,并通过网格搜索选择最佳 (\alpha)。论文实验设置使用了 20 个网格点。(ar5iv)


7.3 原始代码实际怎样搜索

原始 AWQ 代码中的核心逻辑可以概括为:

x_max = mean(abs(x), dim=all_except_channel)

for ratio in grid:
    scales = x_max ** ratio
    scales = normalize(scales)

    weight_scaled = weight * scales
    weight_q = quantize(weight_scaled)
    weight_effective = weight_q / scales

    output_q = block(x, weight_effective)
    loss = mse(output_fp, output_q)

选择 loss 最小的 scales

当前原始仓库代码使用 20 个候选比例,并按照:

ratio = 0 / 20
ratio = 1 / 20
...
ratio = 19 / 20

搜索,即:

0.00, 0.05, 0.10, ..., 0.95

代码还会将 (a_i^\alpha) 按最大值与最小值的几何均值做归一化,从而保留通道间的相对比例,同时避免整体数值尺度无意义地偏大或偏小。随后,代码将缩放后的权重做伪量化,再除回通道 scale,用真实模块输出 MSE 选择最优候选。(GitHub)


7.4 为什么只搜索一个 (\alpha) 仍然能产生每通道 scale

这里有个容易混淆的地方。

AWQ 是 per-channel scaling。

但搜索的通常不是:

通道 0 搜一个 α₀
通道 1 搜一个 α₁
通道 2 搜一个 α₂
...

而是一个模块搜索一个公共的指数 (\alpha),然后通过各通道不同的激活幅度产生不同 scale:

[
s_i=a_i^\alpha
]

所以:

  • (\alpha) 是低维控制参数;
  • (s_i) 仍然是逐通道不同的;
  • 激活幅度越大,通常 (s_i) 越大;
  • 通道间相对关系由数据决定;
  • 整体保护强度由 (\alpha) 控制。

这种设计的好处是:

  • 搜索空间小;
  • 不需要反向传播;
  • 不容易在少量校准数据上过度拟合;
  • 仍能表达通道间显著性差异。

7.5 搜索目标为什么是模块输出,而不只是线性层权重

在 Transformer 中,有些线性层是组合工作的。

例如注意力模块中:

同一个 LayerNorm 输出
       ↓
q_proj、k_proj、v_proj

这些线性层共享同一份输入通道。

若要缩放 LayerNorm 输出,就需要同时调整 q、k、v 三个权重矩阵的对应列。

因此,原始实现会在某些位置一起搜索:

input_layernorm
→ q_proj
→ k_proj
→ v_proj

并计算整个 self-attention 模块的输出误差。

在 MLP 中,也可能一起处理:

post_attention_layernorm
→ gate_proj
→ up_proj

原始仓库针对 Llama、OPT、Qwen 等结构明确指定了哪些前置操作和后续线性层需要共同缩放。(GitHub)

这说明真实 AWQ 并不是机械地对每个 nn.Linear 完全独立处理。

它还需要理解模型拓扑中的等价变换位置。


7.6 逆缩放通常怎样消失在模型里

从公式看,推理时需要计算:

x / s

很多人会担心:

每层多一次逐元素除法,会不会抵消量化加速?

实际实现通常不会真的在每个 token、每个线性层前单独启动一次除法 kernel。

对于:

LayerNorm / RMSNorm → Linear

可以做:

Norm 的权重除以 s
Linear 的输入列乘以 s

因为 Norm 的输出通道会被相应缩小,后面的 Linear 权重再相应放大,整体函数不变。

对于:

Linear₁ → Linear₂

可以把前一个线性层对应的输出行和 bias 除以 (s),再把后一个线性层的输入列乘以 (s)。

原始 AWQ 代码中可以看到针对 LayerNorm 与多个全连接层、前后两个全连接层的参数融合操作。论文也指出逆缩放通常可以融合进前一个操作。(ar5iv)

“通常”二字很重要。

不同架构可能包含:

  • 特殊激活函数;
  • 并行分支;
  • 非标准归一化;
  • MoE 路由;
  • 共享权重;
  • 不规则残差路径。

这些情况需要实现针对模型结构正确放置缩放,而不是随便修改一个线性层。


7.7 AWQ 完整流程

把整个过程串起来,可以得到下面这条主线。

第一步:收集校准激活

拿少量、尽量有代表性的文本跑前向。

缓存各层线性模块的输入激活。


第二步:统计通道尺度

对每个输入通道计算:

[
a_i=\mathrm{mean}(|X_i|)
]


第三步:构造缩放候选

对每个候选 (\alpha):

[
s_i=a_i^\alpha
]

并做必要的数值归一化。


第四步:模拟缩放和量化

构造:

[
\hat W_\alpha

Q(WD_\alpha)D_\alpha^{-1}
]


第五步:计算输出误差

使用校准激活比较:

[
X\hat W_\alpha^\top
]

和:

[
XW^\top
]

选择 MSE 最小的候选。


第六步:搜索 clipping

在需要时,为权重量化范围搜索合适的裁剪比例,避免少数极值把 group scale 拉得过大。


第七步:应用等价变换

把逆缩放融合到前一个 Norm、Linear 或适配的激活模块中。

把正向缩放应用到目标线性层的输入列。


第八步:统一量化权重

所有目标权重最终仍然进入统一的低比特 group-wise 格式。

没有那种:

这一列 FP16
下一列 INT4
再下一列 FP16

的不规则混合布局。


八、用正确的代码验证 AWQ 核心思想

这一节不实现完整 Transformer AWQ。

我们只验证四件事:

  1. 相同权重误差落在大激活通道上更危险;
  2. 按激活保护通道比按权重范数保护更有效;
  3. 在 group-wise 共享 scale 下,等价缩放能降低输出误差;
  4. AWQ 可能增大普通权重 MSE,却降低真实输出 MSE。

8.1 先看一个错误实验为什么错

原稿按下面方式量化每一列:

for c in range(W.shape[1]):
    Wq[:, c] = quantize(W[:, c])

这意味着每列单独计算 scale。

假设第 © 列整体乘以 (s_c)。

该列的最大绝对值也乘以 (s_c),量化 scale 同样乘以 (s_c)。

于是:

Q(W[:, c] × s_c) / s_c

会和:

Q(W[:, c])

相同。

用代码可以直接验证:

import numpy as np

def quantize_per_input_channel_int4(W):
    """每个输入通道,也就是每一列,单独计算量化 scale。"""
    Wq = np.empty_like(W, dtype=np.float64)
    qmin, qmax = -7, 7

    for c in range(W.shape[1]):
        col = W[:, c]
        scale = np.max(np.abs(col)) / qmax
        scale = max(scale, 1e-12)

        q = np.clip(np.round(col / scale), qmin, qmax)
        Wq[:, c] = q * scale

    return Wq


rng = np.random.default_rng(0)
W = rng.normal(0, 0.1, size=(64, 128))

s = np.ones(128)
s[[10, 30, 77]] = 3.0

plain = quantize_per_input_channel_int4(W)
scaled_then_restored = quantize_per_input_channel_int4(W * s[None, :]) / s[None, :]

print(np.max(np.abs(plain - scaled_then_restored)))

输出为:

0.0

这不是 AWQ 无效。

而是量化器的定义让缩放完全被抵消了。

所以接下来必须实现一个真正的 group-wise 量化器。


8.2 实现一个简化的 group-wise INT4 量化器

为了更接近常见 AWQ 格式,下面使用:

  • 权重矩阵形状 [out_features, in_features]
  • 沿输入维度分组;
  • 每个输出行中的连续 32 个权重共享 scale 和 zero point;
  • 使用 4-bit 非对称量化;
  • 整数编码范围为 [0, 15]

真实模型中常见 group size 128。

这里使用 32,是为了让小矩阵中存在更多 group,方便观察。

import numpy as np


def quantize_groupwise_int4(W, group_size=32):
    """
    简化版 INT4 group-wise 非对称伪量化。

    参数
    ----
    W:
        二维权重矩阵,形状 [out_features, in_features]。
    group_size:
        每个输出行中,连续多少个输入权重共享一套 scale / zero point。

    返回
    ----
    Wq:
        反量化回浮点数后的矩阵。
        它用于模拟 INT4 量化误差,不包含真实 bit packing。
    """
    W = np.asarray(W, dtype=np.float64)

    if W.ndim != 2:
        raise ValueError("W 必须是二维矩阵")

    out_features, in_features = W.shape

    if in_features % group_size != 0:
        raise ValueError("in_features 必须能被 group_size 整除")

    qmin, qmax = 0, 15
    Wq = np.empty_like(W)

    for row in range(out_features):
        for start in range(0, in_features, group_size):
            end = start + group_size
            group = W[row, start:end]

            min_val = group.min()
            max_val = group.max()

            scale = (max_val - min_val) / (qmax - qmin)
            scale = max(scale, 1e-12)

            zero_point = np.round(-min_val / scale)
            zero_point = np.clip(zero_point, qmin, qmax)

            q = np.round(group / scale) + zero_point
            q = np.clip(q, qmin, qmax)

            Wq[row, start:end] = (q - zero_point) * scale

    return Wq

注意,这个函数返回的是:

量化 → 反量化

后的浮点矩阵。

它模拟量化误差,但没有真正:

  • 压缩成 4 bit;
  • 做 bit packing;
  • 调用 CUDA kernel;
  • 测量显存和速度。

8.3 构造一个带显著激活通道的线性层

这次不再只用一个激活向量。

我们模拟 512 个 token 的输入:

rng = np.random.default_rng(0)

out_features = 64
in_features = 128
n_tokens = 512

W = rng.normal(
    loc=0.0,
    scale=0.08,
    size=(out_features, in_features),
)

# 大部分通道的激活标准差是 1。
act_std = np.ones(in_features)

# 人为埋入 3 个显著通道。
planted_salient = np.array([10, 30, 77])
act_std[planted_salient] = 20.0

X = rng.normal(
    loc=0.0,
    scale=act_std,
    size=(n_tokens, in_features),
)

Y = X @ W.T

在这个实验中:

通道 10、30、77

的激活幅度大约是普通通道的 20 倍。

权重矩阵本身并不知道这三列特殊。

这正适合验证:

只看权重,能不能发现它们?


8.4 实验一:按权重保护,还是按激活保护

先把所有权重都做 group-wise INT4 伪量化:

Wq_all = quantize_groupwise_int4(W, group_size=32)

err_none = np.mean(
    (Y - X @ Wq_all.T) ** 2
)

然后比较两种保护策略。

策略 A:保护权重范数最大的 3 列

weight_score = np.linalg.norm(W, axis=0)
protect_by_weight = np.argsort(weight_score)[-3:]

策略 B:保护平均激活幅度最大的 3 列

act_score = np.mean(np.abs(X), axis=0)
protect_by_act = np.argsort(act_score)[-3:]

所谓“保护”,在这个动机实验里就是:

  1. 先把全部权重量化;
  2. 再将被保护的列恢复成原始浮点权重。
def restore_columns(Wq, W_fp, columns):
    restored = Wq.copy()
    restored[:, columns] = W_fp[:, columns]
    return restored


Wq_weight_protected = restore_columns(
    Wq_all,
    W,
    protect_by_weight,
)

Wq_act_protected = restore_columns(
    Wq_all,
    W,
    protect_by_act,
)

err_weight = np.mean(
    (Y - X @ Wq_weight_protected.T) ** 2
)

err_act = np.mean(
    (Y - X @ Wq_act_protected.T) ** 2
)

print("人为埋入的显著通道:", planted_salient)
print("按权重范数选出的通道:", protect_by_weight)
print("按激活幅度选出的通道:", protect_by_act)
print()
print(f"不保护任何通道:       {err_none:.6f}")
print(f"保护权重范数最大通道: {err_weight:.6f}")
print(f"保护激活幅度最大通道: {err_act:.6f}")

在固定随机种子下,一次结果是:

人为埋入的显著通道: [10 30 77]
按权重范数选出的通道: [83 67 44]
按激活幅度选出的通道: [30 77 10]

不保护任何通道:       0.055171
保护权重范数最大通道: 0.055071
保护激活幅度最大通道: 0.005182

结果非常直观。

按权重范数选择的三列是:

83、67、44

它们没有找到真正的大激活通道。

输出 MSE 几乎没有变化:

0.055171 → 0.055071

而按激活选择准确找到了:

10、30、77

输出 MSE 降到:

0.005182

下降约 90%。

这个实验说明的不是“权重大小永远没用”。

它说明:

当量化误差最终要经过激活传播时,只根据权重范数选择保护对象,可能完全错过最敏感的通道。


8.5 实验二:不保留 FP16,用 AWQ 式缩放

接下来不再恢复任何浮点列。

全部权重都统一进入 INT4 group-wise 量化。

我们计算每个输入通道的平均激活幅度:

act_scale = np.mean(np.abs(X), axis=0)

对每个候选 (\alpha):

[
s_i=a_i^\alpha
]

然后执行:

[
\hat W_\alpha

Q(WD_\alpha)D_\alpha^{-1}
]

代码如下:

def awq_like_quantize(
    W,
    act_scale,
    alpha,
    group_size=32,
):
    """
    简化的 AWQ 式缩放 + group-wise INT4 伪量化。
    """
    scales = np.maximum(act_scale, 1e-4) ** alpha

    # 保留通道之间的相对比例,
    # 同时把整体尺度大致归一到几何中心附近。
    scales = scales / np.sqrt(
        scales.max() * scales.min()
    )

    W_scaled = W * scales[None, :]

    Wq_scaled = quantize_groupwise_int4(
        W_scaled,
        group_size=group_size,
    )

    # 除回通道 scale,得到在原输入 X 下使用的等效权重。
    Wq_effective = Wq_scaled / scales[None, :]

    return Wq_effective, scales

搜索 20 个候选:

Wq_plain = quantize_groupwise_int4(
    W,
    group_size=32,
)

plain_output_mse = np.mean(
    (Y - X @ Wq_plain.T) ** 2
)

records = []

for index in range(20):
    alpha = index / 20

    Wq_awq, scales = awq_like_quantize(
        W,
        act_scale,
        alpha,
        group_size=32,
    )

    output_mse = np.mean(
        (Y - X @ Wq_awq.T) ** 2
    )

    records.append(
        {
            "alpha": alpha,
            "output_mse": output_mse,
            "Wq": Wq_awq,
            "scales": scales,
        }
    )

best = min(
    records,
    key=lambda item: item["output_mse"],
)

print(f"直接 group-wise INT4 MSE:{plain_output_mse:.6f}")
print(f"最佳 alpha:             {best['alpha']:.2f}")
print(f"AWQ 式缩放后 MSE:       {best['output_mse']:.6f}")
print(
    "误差变为原来的:       "
    f"{best['output_mse'] / plain_output_mse:.1%}"
)

一次固定 seed 的输出为:

直接 group-wise INT4 MSE:0.055171
最佳 alpha:             0.40
AWQ 式缩放后 MSE:       0.016408
误差变为原来的:         29.7%

也就是说,完全没有保留 FP16 权重。

仅仅通过:

激活统计
→ 通道缩放
→ group-wise INT4
→ 除回缩放

输出误差就下降了约 70%。


8.6 为什么最佳 (\alpha) 不是 1

在这个实验里,最佳候选是:

alpha = 0.40

而不是:

alpha = 1.00

原因正是前面讲过的权衡。

当 (\alpha) 增大时:

  • 大激活通道获得更强保护;
  • 权重列之间的数值差异也被进一步拉大;
  • 一些 group 的范围可能被放大;
  • 普通通道使用的步长变粗;
  • 总误差可能再次上升。

所以 AWQ 不会简单执行:

scales = act_scale

然后结束。

它需要用真实输出误差搜索一个平衡点。


8.7 再验证一个反直觉现象:权重 MSE 可能变差

比较普通量化和最佳 AWQ 候选的权重 MSE:

best_Wq = best["Wq"]

plain_weight_mse = np.mean(
    (W - Wq_plain) ** 2
)

awq_weight_mse = np.mean(
    (W - best_Wq) ** 2
)

plain_output_mse = np.mean(
    (Y - X @ Wq_plain.T) ** 2
)

awq_output_mse = np.mean(
    (Y - X @ best_Wq.T) ** 2
)

print(f"普通量化的权重 MSE:{plain_weight_mse:.8f}")
print(f"AWQ 量化的权重 MSE:{awq_weight_mse:.8f}")
print()
print(f"普通量化的输出 MSE:{plain_output_mse:.6f}")
print(f"AWQ 量化的输出 MSE:{awq_output_mse:.6f}")

结果为:

普通量化的权重 MSE:0.00004094
AWQ 量化的权重 MSE:0.00005960

普通量化的输出 MSE:0.055171
AWQ 量化的输出 MSE:0.016408

AWQ 的普通权重 MSE反而更大:

0.00004094 → 0.00005960

但是输出 MSE 明显更小:

0.055171 → 0.016408

这组结果特别值得反复体会。

AWQ 不一定让整个权重矩阵在欧氏距离上更接近原矩阵。

它做的是:

  • 减少大激活通道上的误差;
  • 允许小激活通道承担更多误差;
  • 让误差落在模型不那么敏感的位置;
  • 最终使真实层输出更接近原模型。

一句话总结:

好的量化不等于权重误差最平均,而等于误差分布最符合模型的使用方式。


8.8 这个实验验证了什么,又没有验证什么

这个小实验验证了:

  • 通道量化误差会被激活幅度加权;
  • 激活统计可以定位敏感通道;
  • group-wise 共享 scale 是缩放产生收益的必要背景;
  • activation-aware scaling 可以降低输出误差;
  • 最佳缩放需要在显著与非显著通道之间平衡;
  • 输出 MSE 和普通权重 MSE不是一回事。

但它不是完整 AWQ,因为它没有:

  • 真实 Transformer 模块;
  • q、k、v 联合缩放;
  • LayerNorm 或 RMSNorm 参数融合;
  • clipping 搜索;
  • 逐层传播量化误差;
  • 真实 INT4 packing;
  • CUDA、ROCm 或其他低比特 kernel;
  • 语言模型困惑度评测;
  • 下游任务评测。

因此,它适合验证核心原理,不适合直接作为生产量化器。


九、AWQ 与 GPTQ 横向对比

GPTQ 和 AWQ 经常被描述成两种完全相反的哲学:

GPTQ:量化后补偿
AWQ:量化前保护

这个比喻很好记,但不够完整。

先看更准确的对比。

维度 GPTQ AWQ
基本目标 减小量化后层输出重构误差 用激活感知缩放重新分配量化误差
主要信息 校准激活形成的近似二阶信息 通道级激活幅度及候选输出 MSE
核心操作 顺序量化,并更新尚未量化的权重以补偿误差 先做等价通道缩放,再统一量化
是否使用 Hessian 近似
是否需要校准数据
是否需要反向传播 通常不需要训练式反传 不需要
是否做层重构 是,核心就是近似重构 搜索候选输出误差,但不做 GPTQ 式权重补偿
最终权重格式 通常为统一低比特 group-wise 格式 通常为统一低比特 group-wise 格式
是否必须混合精度
主要风险 二阶近似、顺序与实现细节影响结果 缩放过强会抬高 group scale,校准分布也会影响通道统计
能否组合 可以 可以

GPTQ 原论文将其描述为基于近似二阶信息的一次性权重量化方法,通过高效算法把这种补偿扩展到大规模生成模型。(arXiv)

AWQ 则明确避免反向传播和 GPTQ 式回归重构,使用激活统计构造通道缩放,并通过小规模网格搜索最小化模块输出差异。(ar5iv)


9.1 GPTQ 更像“调整误差”

GPTQ 的核心问题是:

当我把当前权重量化到附近格点以后,怎样调整其他还没量化的权重,使整体输出变化最小?

它利用校准输入构造近似 Hessian,并根据二阶信息传播和补偿误差。

因此 GPTQ 更像在量化过程中不断回答:

这里已经产生了误差,
剩余自由度怎样配合才能补回来?

9.2 AWQ 更像“改变误差发生的位置”

AWQ 的核心问题是:

在量化之前,能不能先改变权重的参数化,让量化器天然把更小的有效误差分配给重要通道?

它不直接逐个修改剩余权重来抵消当前误差。

它先改变数值分布,让同一个 group quantizer 产生一种更有利的误差分配。

所以 AWQ 更像在回答:

既然误差不可避免,
怎样让误差优先落在不敏感的位置?

9.3 两者不是严格互斥

一个很重要但经常被忽略的事实是:

AWQ 缩放可以作为其他量化算法之前的重参数化步骤。

原论文在极低比特 INT2 实验中,将 AWQ 与 GPTQ 组合使用,并报告组合方法进一步改善了 GPTQ 的量化结果。(ar5iv)

所以更准确的理解是:

  • GPTQ 是一种误差补偿和重构策略;
  • AWQ 是一种 activation-aware 的通道重参数化策略;
  • 两者作用位置不同;
  • 在某些流水线中可以叠加。

9.4 谁的精度一定更好

没有脱离具体条件的绝对答案。

结果会受到很多因素影响:

  • 模型架构;
  • bit 数;
  • group size;
  • 对称或非对称量化;
  • 是否启用 clipping;
  • GPTQ 是否启用重排;
  • 校准数据;
  • 实现版本;
  • 评测任务;
  • 是否量化 MoE 专家、LM head 等模块。

AWQ 论文在其 LLaMA、Llama 2 和 OPT 等实验设置中报告了优于 RTN、并在多项结果上优于论文所用 GPTQ 基线的表现,但这些结果应理解为特定论文设置下的实验结论,而不是所有现代实现中的永久排名。(ar5iv)

因此,实际选型时不要只问:

AWQ 和 GPTQ 谁更强?

而应该问:

在我的模型、硬件、kernel、group size 和任务上,
哪个格式的精度、速度、显存与兼容性更合适?

9.5 谁量化得更快

从算法结构上看,AWQ 的核心搜索很简单:

  • 统计激活均值;
  • 构造少量 (\alpha) 候选;
  • 做伪量化;
  • 比较输出 MSE。

它不需要 GPTQ 那样的二阶矩阵处理和顺序误差更新。

因此从算法复杂度和实现负担看,AWQ 往往较轻。

但“实际量化多少分钟”仍取决于:

  • 是否整模型同时驻留显存;
  • 是否逐层 offload;
  • 校准 token 数;
  • 模型大小;
  • GPU 和 CPU;
  • 实现是否缓存激活;
  • 是否同时搜索 clipping;
  • 是否进行真实权重打包。

所以最好不要写成:

AWQ 一定比 GPTQ 快多少倍。

更稳妥的说法是:

AWQ 的优化形式通常更轻量,但实际墙钟时间由实现和硬件决定。


9.6 谁推理更快

量化算法决定“权重怎样得到”。

推理速度则主要由另外一组因素决定:

  • 量化格式;
  • 权重 packing 布局;
  • kernel 是否支持;
  • 解包与反量化是否融合;
  • GPU 架构;
  • batch size;
  • prompt 长度;
  • prefill 或 decode;
  • 模型是否受显存带宽限制;
  • 是否发生 CPU offload。

一个 AWQ 模型如果落到不合适的通用 kernel 上,可能并不快。

一个 GPTQ 模型如果使用高度优化的 Marlin、ExLlama 或其他内核,也可能非常高效。

所以:

AWQ 是量化算法,不是单独的速度保证书。


十、从论文原理到工程实现,中间还有哪些细节

10.1 激活仍然是高精度

再强调一次:

AWQ 中的:

x / s

不是在说激活也变成 INT4。

典型 AWQ 是 weight-only 量化。

激活通常仍保持 FP16 或 BF16。

缩放往往被融合到前一个操作中,不需要把每个激活张量永久保存成另一种低比特格式。

因此更准确的描述是:

低比特存权重
高精度保激活
用融合 kernel 完成反量化与计算

10.2 “统一 INT4”也不是所有模型参数一律 4 bit

工程实现中通常不会把模型里的每个浮点数都变成 INT4。

常见的例外可能包括:

  • LayerNorm 或 RMSNorm 参数;
  • bias;
  • embedding;
  • LM head;
  • 很小的辅助模块;
  • 路由器;
  • 某些不受支持的层;
  • 量化 scale 和 zero point;
  • KV cache。

所以“AWQ 全程统一 INT4”应理解为:

被量化的目标线性权重采用统一低比特格式,不需要在同一权重矩阵里稀疏混入 FP16 显著权重。

不是说模型文件中的每一个参数和中间张量都是 4 bit。


10.3 模型大小也不会恰好缩小四倍

理想情况下:

FP16:每权重 16 bit
INT4:每权重 4 bit

权重主体可以缩小到四分之一。

但真实格式还需要保存:

  • 每组 scale;
  • zero point;
  • 张量元数据;
  • 对齐填充;
  • 未量化参数;
  • tokenizer 和配置;
  • 可能的额外 permutation 信息。

因此真实模型大小通常不会严格等于:

原大小 ÷ 4

更准确地说,是:

权重主体接近四倍压缩,最终文件与显存节省会略低于理想值。


10.4 group size 是精度和元数据之间的交换

group size 越小:

  • 每组权重更少;
  • scale 更贴合局部分布;
  • 通常精度更好;
  • scale、zero point 数量更多;
  • 元数据和处理开销增加。

group size 越大:

  • 元数据更少;
  • 格式更紧凑;
  • 更多权重竞争同一个动态范围;
  • 量化误差通常更大;
  • activation-aware scaling 的作用也可能更明显。

因此:

g32
g64
g128

不是简单的“数字越大越高级”。

它们代表不同的精度、压缩和 kernel 权衡。


10.5 校准数据不需要很大,但不能完全乱选

AWQ 只统计通道级激活幅度,并进行低维网格搜索。

相比需要更强层重构的方法,它对校准数据量通常没有那么高的要求。

AWQ 论文在一项 OPT-6.7B、INT3-g128 分析中报告,AWQ 用 16 个序列就取得了优于使用 192 个序列的 GPTQ 结果;论文还在 PubMed 与 Enron 数据分布交叉校准实验中观察到 AWQ 的困惑度变化更小。(ar5iv)

但这不代表校准数据可以毫无关系。

如果你量化的是:

  • 代码模型;
  • 数学模型;
  • 中文模型;
  • 医疗领域模型;
  • 超长上下文模型;
  • 多模态模型;
  • thinking 模型;

最好仍然让校准数据覆盖真实使用中的主要模式。

因为 AWQ 在估计的是:

哪些输入通道在实际分布下经常较大

如果校准分布和部署分布差距极大,这个排序当然可能改变。


10.6 大激活不等于单次离群点

AWQ 通常统计的是:

每个通道在校准数据上的平均绝对幅度

因此它关注的是通道级、分布级现象。

一个通道偶尔出现一次极端值,不一定会成为最显著通道。

一个通道即使没有惊人的单次峰值,但长期保持较大幅度,也可能拥有较高的平均激活尺度。

所以:

AWQ = 处理激活离群值

不够准确。

更完整的说法是:

AWQ 使用通道级激活统计估计权重量化敏感度;激活离群通道可能是其中的重要来源,但二者不是完全同义。


10.7 缩放因子不一定全部大于 1

为了方便解释,我们常说:

重要通道 s > 1

但实际实现可能会对整个 scale 向量做归一化。

例如:

[
s_i
\leftarrow
\frac{s_i}
{\sqrt{s_{\max}s_{\min}}}
]

归一化后:

  • 一部分通道可能大于 1;
  • 一部分通道可能小于 1;
  • 关键是通道之间的相对比例。

所以“保护重要通道”的本质不是:

所有重要通道绝对值必须乘一个大于 1 的数

而是:

重要通道相对于其他通道,在共享量化范围中的权重被提高。


10.8 缩放不是额外存一份运行时向量那么简单

在理论表达中,我们写:

W × s
x ÷ s

工程实现中,最终变换可能被吸收到:

  • RMSNorm 的 gamma;
  • LayerNorm 的 weight 和 bias;
  • 前一个 Linear 的输出行;
  • 后一个 Linear 的输入列;
  • 特殊的 scaled activation 包装;
  • 已经重新参数化的权重文件。

所以打开一个 AWQ 模型时,你不一定会看到每层显式执行:

x = x / awq_scale

scale 可能已经“烘焙”进相邻参数。


10.9 当前工具链需要更新

原稿最后写:

下一篇用 autoawq 跑起来。

这一句在当前环境下需要修改。

AutoAWQ 仓库已于 2025 年 5 月 11 日归档,并明确声明不再维护;其说明将 vLLM 项目的 LLM Compressor 列为替代方向之一。(GitHub)

截至当前,vLLM LLM Compressor 提供了 AWQ 量化示例和文档,用校准数据生成缩放并导出可供 vLLM 使用的量化模型。(GitHub)

因此系列文章的下一篇更建议写成:

下一篇使用当前仍在维护的 AWQ 工具链完成真实模型量化,并验证显存、困惑度和推理兼容性。

可以选择:

  • 原始 mit-han-lab/llm-awq
  • vLLM LLM Compressor;
  • 目标推理框架官方支持的其他 AWQ 工具。

若出于复现旧教程仍使用 AutoAWQ,需要明确标注:

该项目已归档
需要固定兼容版本
不建议作为新的长期生产依赖

十一、常见误区与避坑

误区一:AWQ 最终会把 1% 权重保留成 FP16

不对。

保留少量 FP16 权重是论文用来证明“显著通道确实存在”的动机实验。

最终 AWQ 使用等价缩放,让目标权重统一进入低比特格式。


误区二:AWQ 会先找出固定的 top 1%

不对。

最终 AWQ 通常使用连续通道尺度:

[
s_i=a_i^\alpha
]

不是简单的 top-k 二值选择。

1% 是论文观察,不是固定实现规则。


误区三:AWQ 的“activation-aware”表示激活也被量化

不对。

典型 AWQ 是 weight-only:

W4A16

激活通常仍是 FP16 或 BF16。

“activation-aware”是说量化权重时参考了激活统计。


误区四:看激活意味着完全不看权重

不对。

激活用于构造显著性和缩放候选。

但 group scale、量化误差、clipping、输出 MSE 都和权重有关。


误区五:权重大的通道一定重要

不一定。

真正的输出误差近似取决于:

[
e_{ji}x_i
]

权重大小只会间接影响量化误差。

对应激活是否经常较大,同样关键。


误区六:激活越大,就应该无限放大权重

不对。

缩放过强可能抬高整个 group 的量化步长,使大量普通通道变得更粗糙。

所以 AWQ 要搜索折中点。


误区七:缩放太大会自然把权重 clip 掉

不一定。

如果量化器重新根据缩放后的 min/max 计算范围,主要结果是 scale 变大,而不是自动溢出。

主动 clipping 是另一个经过搜索的操作。


误区八:缩放本身是有损近似

不对。

[
XW^\top

(XD{-1})(WD)\top
]

是严格恒等式。

误差只来自插入的量化函数:

[
Q(WD)
]


误区九:把重要权重放大,相当于增加了 INT4 的 level 数量

不对。

INT4 仍然只有原来的编码数。

AWQ 改变的是不同通道对共享量化范围的相对占用。


误区十:任何量化粒度下缩放都有效

不对。

如果每个被缩放的通道拥有独立 scale,缩放可能被严格抵消:

[
Q(sw)/s=Q(w)
]

AWQ 的收益必须结合 group-wise 共享尺度理解。


误区十一:验证 AWQ 时按每列单独量化就行

不行。

那会把 AWQ 最关键的共享量化网格拿掉。

正确的最小实验应至少实现:

多个输入通道权重共享一个 group scale

误区十二:AWQ 一定会降低普通权重 MSE

不一定。

AWQ 优化的是激活分布下的输出误差。

它可能主动把更多权重误差分配给小激活通道。

因此普通权重 MSE 可能上升,输出 MSE 却下降。


误区十三:AWQ 不需要校准数据

不对。

它需要校准数据来:

  • 收集激活统计;
  • 搜索 (\alpha);
  • 计算候选输出误差;
  • 搜索 clipping。

它属于 PTQ,不是 data-free quantization。


误区十四:AWQ 一定比 GPTQ 更准

不对。

论文结果、现代实现、模型架构、group size 和评测任务都会影响结论。

最可靠的方法仍然是在目标任务上实际评测。


误区十五:AWQ 一定比 FP16 推理快四倍

不对。

四倍是权重 bit 数的理想压缩比例,不是端到端速度保证。

实际速度取决于:

  • kernel;
  • packing;
  • 硬件;
  • batch;
  • prefill/decode;
  • 是否受带宽限制;
  • 框架调度。

十二、怎样用一分钟把 AWQ 讲给别人听

可以直接这样复述:

AWQ 是一种激活感知的低比特权重量化方法。它发现权重的量化误差是否危险,不只取决于权重本身,还取决于这个误差会和多大的输入激活相乘。激活较大的输入通道,会把对应权重列的量化误差明显放大。

直接把这些显著权重保留成 FP16 虽然有效,但会产生硬件不友好的混合精度。AWQ 因此使用等价缩放:把重要通道的权重相对放大,同时把对应激活相对缩小。浮点模型输出完全不变,但在 group-wise INT4 量化中,这会改变各通道对共享量化范围的占用,让重要通道获得更小的有效量化误差。

缩放不是越大越好,因为过强缩放会抬高整个 group 的量化步长,伤害其他通道。所以 AWQ 根据校准激活构造 (s_i=a_i^\alpha),通过少量网格搜索选择输出 MSE 最小的 (\alpha),最后把缩放融合进相邻层,并对权重统一做低比特量化。

如果只能记住五个关键词,就记:

激活决定敏感度
通道级显著性
等价缩放
共享 group scale
输出误差搜索

再压缩成一句话

AWQ 根据激活幅度判断哪些输入通道会放大量化误差,再利用输出等价的通道缩放,重新分配 group-wise INT4 的有限分辨率,使重要通道更精确,而不引入混合精度。


十三、小结

这一篇真正需要吃透的,不是某一行代码,而是下面几层逻辑。

第一,权重量化误差不是最终损失

真正影响模型的是:

[
\Delta Y=XE^\top
]

权重误差会被激活重新加权。


第二,“重要权重”不是完全静态的属性

同一个权重是否敏感,取决于它在真实输入分布下对应的激活通道。

因此,AWQ 用校准数据统计通道级激活幅度。


第三,论文中的 0.1%~1% FP16 是动机实验,不是最终算法

这个实验说明少量激活显著通道承担了不成比例的量化敏感度。

但直接混合 FP16 和 INT4 不利于高效内核。


第四,最终 AWQ 依赖等价缩放

[
XW^\top

(XD{-1})(WD)\top
]

缩放本身完全无损。

只有在中间加入量化函数后:

[
XD{-1}Q(WD)\top
]

它才改变最终量化误差。


第五,AWQ 的收益依赖共享量化尺度

如果每列完全独立量化:

[
Q(sw)/s=Q(w)
]

缩放可能没有任何作用。

AWQ 必须放在 group-wise 量化中理解:

多个通道竞争同一套量化参数,缩放才能重新分配共享分辨率。


第六,缩放过强会伤害其他通道

显著通道得到保护的同时,group scale 可能变大。

所以 AWQ 不直接令:

s = activation

而是构造:

[
s_i=a_i^\alpha
]

再搜索输出误差最小的 (\alpha)。


第七,AWQ 优化的是函数误差,而不是普通权重误差

因此完全可能:

权重 MSE 上升
输出 MSE 下降

这不是异常,而是它成功把误差移动到了不敏感位置。


第八,AWQ 和 GPTQ 不是简单的新旧替代关系

GPTQ 利用近似二阶信息补偿量化误差。

AWQ 通过激活感知缩放改变误差分布。

它们的作用机制不同,甚至可以组合。


AWQ 最漂亮的地方,不只是用了一个小学数学恒等式。

真正漂亮的是它抓住了一个更深的原则:

模型压缩不应该平均地保护所有参数,而应该根据参数在真实数据分布中的作用,分配有限的数值精度。

从这个角度看,AWQ 做的并不是单纯“把某些权重变大”。

它是在回答一个更普遍的问题:

当表示能力有限时,哪些误差值得花资源消除,哪些误差可以被允许存在?

这也是理解低比特量化最重要的一步。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐