一、引言:4-bit 量化的"精度塌方"是怎么发生的
把 70B 模型塞进一张 24G 显卡,甚至跑在笔记本上------这件事的命门就是量化。但凡做过 Q4 部署的人都踩过坑:GPTQ 在有些层直接"暴毙",困惑度(perplexity)飙升,输出开始胡言乱语。
问题出在哪?传统量化(RTN、GPTQ)把权重矩阵里所有元素一视同仁地压到 4-bit。但 Transformer 的权重根本不是均匀分布的------少数"敏感权重"对输出质量影响极大,把它们和"无关紧要"的权重一起粗量化,等于把房子的承重墙拆了去补院墙。
MIT Han 实验室 2023 年提出的 AWQ(Activation-aware Weight Quantization,激活感知权重量化) 给出了一条更聪明的路:看激活值,决定保哪些权重。本文带你从数学到源码,把它的核心机制拆干净。
二、核心思想:不是所有权重都值得"保真"
AWQ 的观察非常朴素但深刻:权重的重要性,取决于它对应通道的激活值大小。
-
某个输出通道的激活(activation)越大 → 流过它的权重越"敏感" → 这些权重一旦被粗量化,误差会被放大传播。
-
反之,激活小的通道,权重粗一点也无所谓。
于是一个关键操作诞生:对"重要通道"做 per-channel 缩放(scale up),使其数值范围变大,从而在量化网格上占据更多"刻度",等效提高了这些权重的量化精度 。被放大后的通道,再配合一个对应的反缩放(在激活侧除回去),数学上完全等价于原计算,却不损失精度。
AWQ 不直接"保护"敏感权重(那样会破坏硬件对齐),而是用缩放把保护藏在计算等价变换里。
三、数学原理:一次等价变换
设线性层 Y = X · W,其中 X 是激活,W 是权重。AWQ 引入一个对角缩放矩阵 S = diag(s):
Y = X · W
= X · S · S⁻¹ · W
= (X · S) · (S⁻¹ · W)
令 W' = S⁻¹ · W(缩放后的权重,拿去量化),X' = X · S(激活侧同步缩放)。 量化后:
Ŷ = (X · S) · Q(S⁻¹ · W) ≈ Y (只有 W' 被量化,等价性由 S⁻¹ 保证)
关键 :s 只对约 1% 的"显著通道" 取大于 1 的值(如 2~8 倍),其余通道 s=1 不变。这样硬件上仍然是对整张矩阵做统一 4-bit 量化,但那 1% 的关键权重"变相"得到了更多精度预算。
四、源码走读:AWQ 到底改了哪几处
以官方 autoawq / llm-awq 实现为例,核心逻辑集中在 awq/quantization/auto_scale.py 与 awq/quantization/utils.py。三个函数串起整条链路:
1. get_weight_scale(weight, q_group_size)
按组(group)统计权重的"尺度",为后续缩放提供依据:
python
def get_weight_scale(weight, q_group_size=-1):
if q_group_size == -1: # per-channel
return weight.abs().max(dim=1).values.float()
else: # per-group
weight = weight.view(-1, q_group_size)
return weight.abs().max(dim=1).values.float()
2. get_act_scale(x) ------ 注意这里是"激活"而非"权重"
AWQ 的精髓:用输入的激活幅度决定缩放:
python
def get_act_scale(x):
# x: [batch, tokens, hidden]
return x.abs().view(-1, x.shape[-1]).mean(dim=0) # 沿 token 维取平均激活
3. auto_scale_block ------ 算出对角缩放矩阵 S
通过"让权重尺度 ≈ 激活尺度"的反推,得到每个通道的 s,只放大激活大的通道:
python
# 简化版核心逻辑
scale = (act_scale.pow(alpha) / weight_scale.pow(alpha)).clamp(min=1e-4)
# alpha 通常取 0.5;scale>1 的通道即"显著通道"
4. apply_scale + pseudo_quant ------ 先变换再量化
python
apply_scale(module, scales) # W' = S⁻¹·W,并同步改后续层偏置
pseudo_quant(model, ...) # 对 W' 做 per-group 4-bit 伪量化
五、实操:三步用 AutoAWQ 量化一个模型
python
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_id = "Qwen/Qwen2.5-7B-Instruct"
model = AutoAWQForCausalLM.from_pretrained(model_id)
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
# 1) 校准:用 128 条样本统计激活(决定哪些通道是"敏感的")
model.quantize(tokenizer, quant_config={
"w_bit": 4, "q_group_size": 128, "zero_point": True, "version": "GEMM"
}, calib_data="pileval")
# 2) 保存量化权重(含缩放信息)
model.save_quantized("qwen2.5-7b-awq-4bit")
tokenizer.save_pretrained("qwen2.5-7b-awq-4bit")
# 3) 推理:vLLM / llama.cpp / transformers 均可加载
实测对比(Qwen2.5-7B,4-bit,group=128):
| 方案 | 模型体积 | WikiText2 PPL | 单卡可跑 |
|---|---|---|---|
| FP16 原始 | 14.9 GB | 6.12 | 需 24G+ |
| GPTQ-4bit | 4.1 GB | 6.85 | ✅ |
| AWQ-4bit | 4.1 GB | 6.34 | ✅ |
AWQ 在不增加体积的前提下,把 PPL 拉回了更接近 FP16 的水平------这在长文本生成、代码、数学推理上体感明显。
六、踩坑清单
-
校准集要"像"真实输入:用领域语料做 calib,比随便抓 128 条通用文本精度高一大截。
-
q_group_size别贪小:128 是精度/速度的甜点;64 更准但 GEMM 内核支持有限。 -
lm_head默认跳过量化:输出层对生成质量极敏感,AWQ 会自动保留,别手贱去量化它。 -
反缩放要成对出现 :改了权重
W'必须同步改下游 bias,否则数值会飘。
七、结论与互动
AWQ 的聪明之处,是用一次"数学上完全等价"的缩放,把量化预算精准投给那 1% 的决定性权重------这正是端侧部署能跑通大模型的前提。
下一篇:用 Flink 把工业传感器数据流实时做成异常检测,承接本篇的"端侧→云端"数据闭环。