大语言模型部署时最直观的瓶颈是显存和带宽。以 7B 参数模型为例,FP16 权重约占用 14GB,再加上激活和 KV cache,单张消费级显卡很难同时跑起较大的 batch 或长文本。量化是把权重和激活从 16 bit 压缩到 8 bit、4 bit 甚至 3 bit 的技术,其中训练后量化(PTQ)不需要重新训练,只需少量校准数据即可完成。当前讨论最多的三种 PTQ 方案分别是 GPTQ、AWQ 和 SqueezeLLM,它们都聚焦于 4 bit 权重,但在误差补偿、重要通道保护和码表设计上走了完全不同的路线。

训练后量化的核心难点
量化本质上把浮点权重映射到有限整数集合。例如把 FP16 权重矩阵 W 按 group 统计 scale 和 zero-point,量化到 int4。反量化时近似为 W' = scale * (q - z)。误差主要来自舍入和离群值。大模型权重分布往往不是均匀的,部分数值绝对值很大,这些离群权重对输出影响显著,如果按全局统一缩放,普通权重会被牺牲很多精度。
因此主流方案很少使用逐张量量化,而是采用 group-wise 量化,按 64 或 128 个通道为一组计算缩放系数。校准数据则用于统计激活范围,避免激活量化时出现严重截断。难点在于:4 bit 只有 16 个表示值,如何在如此有限的表达空间中同时保持输出分布和推理速度;另外,不同层的权重敏感度不同,统一量化策略容易在部分层产生明显误差。
训练后量化的另一个挑战是误差会沿着层传播。前方层的量化噪声可能被后方层放大,单层误差看起来不大,整网累积后可能造成困惑度明显上升。GPTQ、AWQ 和 SqueezeLLM 正是在这些难点上做了不同取舍。
GPTQ 如何用二阶信息补偿误差
GPTQ 的思路来自逐层量化。它先在少量校准数据上做前向,得到每一层的输入 X,并计算近似 Hessian 矩阵 H = 2 X X^T 加阻尼项。然后对权重矩阵按列依次量化。每量化一列,就利用二阶信息把误差补偿到尚未量化的列上。这样后续列的量化值会吸收前面列的舍入误差,显著减小整层的输出偏差。
与原始的 OBQ 相比,GPTQ 将权重列分成块处理,并且固定更新顺序,复杂度大幅下降。它在 4 bit 条件下通常能让 7B 模型的困惑度提升保持在很小的范围内。GPTQ 的缺点是压缩过程依赖二阶矩阵构建,对校准集规模和质量有一定要求;推理侧过去依赖专用内核,不过现在 AutoGPTQ、ExLlama 和 vLLM 等生态已经比较成熟。
下面是用 AutoGPTQ 对一个模型做 4 bit 量化的典型流程:
from transformers import AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
model_name = "meta-llama/Llama-2-7b-hf"
quant_config = BaseQuantizeConfig(bits=4, group_size=128, desc_act=False)
model = AutoGPTQForCausalLM.from_pretrained(model_name, quantize_config=quant_config)
tokenizer = AutoTokenizer.from_pretrained(model_name)
model.quantize(tokenizer)
model.save_quantized("./gptq-4bit")
这里 group_size 设为 128,表示每 128 个通道共享缩放系数。desc_act 表示是否使用激活降序排列,关闭后速度更快,但某些场景可能略增误差。实际使用中可以根据模型和显存预算调整 group_size。
AWQ 通过激活感知缩放保护关键权重
AWQ 的核心判断是权重并非同等重要。一小部分显著权重对激活输出影响极大,但它们未必是数值最大的权重,而是与激活分布高度匹配的通道。如果所有通道按同样方式量化,这些关键通道的误差会被明显放大。AWQ 的做法是统计激活幅值,给重要通道分配更大的 scale,让它们的量化表示范围更宽;推理时再把 scale 乘回来,整体数学结果保持不变。
这种缩放可以合并进矩阵乘算子,不会增加推理开销。相比 GPTQ,AWQ 不需要显式构建 Hessian 矩阵,量化和加载流程更简洁。它在多种模型上能以 4 bit 达到与 GPTQ 相当甚至更好的困惑度,同时在通用 GEMM 上的兼容性更好,很多推理框架已原生支持 AWQ。
下面是一个基于 AutoAWQ 的量化示例:
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_name = "meta-llama/Llama-2-7b-hf"
model = AutoAWQForCausalLM.from_pretrained(model_name, safetensors=True)
tokenizer = AutoTokenizer.from_pretrained(model_name)
quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"}
model.quantize(tokenizer, quant_config=quant_config)
model.save_pretrained("./awq-4bit")
AWQ 的搜索过程主要围绕缩放因子展开,例如根据激活均值计算候选缩放,并在少量数据上快速评估。这个机制让它在工程上非常友好,不需要复杂超参扫描。
SqueezeLLM 的非均匀与稀疏化路线
SqueezeLLM 的目标更激进。它认为均匀量化在 3 bit 或 4 bit 下浪费了大量码本空间,因为权重分布并非均匀。于是它使用加权 K-means 聚类生成非均匀码表,让码字更密集地落在权重多发区域。同时,它引入敏感度计算,对输出影响大的权重分配更细粒度码本,对普通权重使用更粗码本。
另一个重要设计是离群值摘除。对于距离码本中心很远的极端权重,SqueezeLLM 直接将它们从密集矩阵中拆出,用稀疏 FP16 保存。推理时先做密集低比特矩阵乘,再累加稀疏离群项,从而兼顾压缩率和精度。
它的代价在于非均匀码表需要查表反量化,标准 GEMM 内核难以高效支持,通常要配合专用推理代码或自研内核。如果用通用 GPU 推理栈,速度可能不如 4 bit GPTQ 或 AWQ。下面给出一段概念性流程,帮助理解它的计算步骤:
# SqueezeLLM 概念流程 sensitivity = estimate_sensitivity(model, calib_data) codebooks = weighted_kmeans(weight_matrix, sensitivity, num_clusters=16) quantized_weight = quantize_with_codebooks(weight_matrix, codebooks) outlier_index = find_outliers(weight_matrix, threshold) sparse_outlier = extract(weight_matrix, outlier_index)
SqueezeLLM 更适合极限压缩场景,例如 3 bit 部署或对显存极度敏感的边缘设备。接入生态相对小众,需要提前评估是否有可用的高性能推理实现。
三者在精度、速度和部署上的对比
从精度维度看,4 bit GPTQ 和 AWQ 在常见评测集上的困惑度提升通常不大,哪个更好往往取决于模型结构和校准数据。SqueezeLLM 的 4 bit 方案在专用内核下可以接近两者,但一旦进入 3 bit,误差上升会比 4 bit 快得多。显存方面,7B 模型 4 bit 权重约 3.5GB,3 bit 可以进一步降低约四分之一,但稀疏系数和码表会占用额外空间。
| 方法 | 典型位宽 | 误差控制思路 | 推理速度 | 接入难度 | 适合场景 |
|---|---|---|---|---|---|
| GPTQ | 4 bit | 近似二阶补偿 | 较快,需 GPTQ 内核 | 中等 | AutoGPTQ、ExLlama、vLLM |
| AWQ | 4 bit | 激活感知缩放 | 快,通用 GEMM 支持好 | 较低 | vLLM、TensorRT-LLM 等 |
| SqueezeLLM | 3/4 bit | 非均匀聚类+稀疏 | 一般,需 LUT 内核 | 较高 | 极限压缩/边缘设备 |
推理速度不能只看理论计算量。AWQ 和 GPTQ 已经有成熟 kernel,实际吞吐较高;SqueezeLLM 如果只能走自定义查表路径,在小 batch 下可能吃亏。对于在线推理服务,建议优先选择生态支持更完整的 4 bit 方案。
实战选型与接入建议
先确定推理框架比单纯比较算法更重要。如果计划使用 vLLM 做高吞吐服务,AWQ 的原生支持通常更顺畅;如果已经积累了 AutoGPTQ 或 ExLlama 的经验,GPTQ 是稳定选择;只有内存预算压到极致时,再考虑 SqueezeLLM 以及它带来的推理栈改造成本。
量化完成后需要做目标数据集评测,不能只看保存后的模型体积。可以简单跑一段生成,观察输出质量、困惑度和延迟:
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("./awq-4bit", device_map="auto")
tokenizer = AutoTokenizer.from_pretrained("./awq-4bit")
inputs = tokenizer("解释模型量化", return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=64)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
最后需要关注校准数据分布。如果下游任务与校准数据差异很大,4 bit 模型的精度可能比纸面指标更脆弱。建议从真实业务样本中抽取校准集,并在量化前后分别记录困惑度或任务指标。这样才能避免只看压缩比,真正找到适合自己硬件和推理栈的量化方案。