主题
AWQ(Activation-Aware Weight Quantization)
白板时间:想象你在压缩一本书。最简单的方法是把每个字都换成缩写——但这样会丢失太多信息。AWQ 的思路更聪明:它先观察哪些"词"(权重通道)在阅读时最重要(激活值大),然后只保护这些重要通道不被过度量化,其余通道可以大胆压缩。结果是:只用 1% 的权重保持 FP16 精度,就能获得接近 FP16 的模型质量。
一、AWQ 核心原理
1.1 传统均匀量化的问题
传统量化对所有权重一视同仁:
python
# 传统方法:所有权重同等对待
W_fp16 = [0.523, -0.001, 3.141, 0.002, -2.718, ...] # 16-bit
W_int4 = [ 3, 0, 7, 0, -6, ...] # 4-bit
# 问题:
# 0.523 → 3 (保留较好) ✅
# -0.001 → 0 (完全丢失!) ❌ —— 但这个权重可能不重要
# 3.141 → 7 (保留较好) ✅关键洞察:不是所有权重都同样重要!有些权重量化后损失很小,有些则影响巨大。
1.2 AWQ 的核心思想
┌───────────────────────────────────────────────────────┐
│ AWQ 算法流程 │
├───────────────────────────────────────────────────────┤
│ │
│ Step 1: 分析激活值 (Analyze Activations) │
│ ┌──────────────────────────────────┐ │
│ │ 用校准数据做一次前向传播 │ │
│ │ 记录每一层的激活值幅度 │ │
│ │ → 找出"重要通道"(Salient Channels)│ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ Step 2: 保护重要通道 (Protect Salient Weights) │
│ ┌──────────────────────────────────┐ │
│ │ 对每层权重的每个输出通道: │ │
│ │ 如果该通道的激活值大 → 保持FP16 │ │
│ │ 如果该通道的激活值小 → 量化为INT4│ │
│ │ │ │
│ │ 典型保护比例: ~1% 权重保持 FP16 │ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ Step 3: 缩放补偿 (Scale Compensation) │
│ ┌──────────────────────────────────┐ │
│ │ 被保护的通道通过缩放因子 s │ │
│ │ 来"补偿"其他被量化通道的影响 │ │
│ │ │ │
│ │ 数学形式: W' = W × diag(s) │ │
│ └──────────────────────────────────┘ │
│ │
│ 结果: 仅 1% FP16 + 99% INT4 ≈ 全量 FP16 效果 │
└───────────────────────────────────────────────────────┘1.3 为什么保护 1% 就够了?
python
def awq_intuition():
"""AWQ 直觉理解"""
explanation = """
想象一个 Linear 层: y = Wx
W 是 [out_features, in_features] 的矩阵
x 是输入向量
对于某个输出通道 i:
y[i] = Σ_j W[i,j] * x[j]
如果 x[j](对应 W[i,j] 的输入激活值)本身就很接近零,
那么 W[i,j] 的具体数值就不太重要了——
即使把它从 0.123 量化成 0.1(误差 20%),
对最终结果 y[i] 的影响也只有 0.123*0.02 ≈ 0.002
反之,如果 x[j] 很大(比如 10.5),
那么 W[i,j] = 0.123 的微小变化会被放大 10.5 倍!
所以 AWQ 的策略是:
→ 观察哪些位置的 |x| 大(激活值显著)
→ 只保护对应的 W 值不被过度量化
→ 其余位置放心大胆地量化
这就是 "Activation-Aware" 名称的由来。
"""
print(explanation)
awq_intuition()二、AWQ 实践:转换与使用
2.1 使用 HuggingFace 上现成的 AWQ 模型
这是最推荐的方式——社区已经帮你转换好了:
bash
# HuggingFace 上搜索 AWQ 模型
# 关键词: AWQ + 模型名
# 例如: TheBloke/Llama-2-13B-chat-AWQ
# casperhao/Meta-Llama-3.1-8B-Instruct-AWQ
# 直接启动 vLLM 服务
python -m vllm.entrypoints.openai.api_server \
--model casperhao/Meta-Llama-3.1-8B-Instruct-AWQ \
--quantization awq \
--dtype auto \
--max-model-len 8192 \
--port 80002.2 自己转换 AWQ 模型
如果 HF 上没有你需要的 AWQ 版本:
python
#!/usr/bin/env python3
"""将 FP16 模型转换为 AWQ INT4 格式"""
import torch
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
def convert_to_awq(
model_id: str = "meta-llama/Meta-Llama-3.1-8B-Instruct",
output_dir: str = "./models/llama3-8b-awq",
bits: int = 4,
group_size: int = 128,
version: str = "gemm",
):
"""
将模型转换为 AWQ 格式
Args:
model_id: HuggingFace 模型 ID 或本地路径
output_dir: 输出目录
bits: 量化位数 (通常为 4)
group_size: 分组大小 (128 = 每 128 列共享一组量化参数)
version: AWQ 版本 ("gemm" 或 "gptq")
"""
print(f"[AWQ 转换] {model_id} → INT{bits}")
print(f" 输出目录: {output_dir}")
print(f" Group Size: {group_size}")
# 加载原始模型和 tokenizer
model = AutoAWQForCausalLM.from_pretrained(
model_id,
trust_remote_code=True,
torch_dtype="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
# 定义校准数据
calib_data = [
"这是一个关于人工智能的技术讨论。",
"请解释量子计算的基本原理。",
"Python 编程语言的特性有哪些?",
"深度学习在自然语言处理中的应用。",
"什么是 Transformer 架构?",
"机器学习模型的评估指标。",
"大数据分析的最佳实践。",
"云计算与边缘计算的区别。",
] * 10 # 重复以增加样本量
# 配置量化参数
quant_config = {
"zero_point": True,
"q_group_size": group_size,
"w_bit": bits,
"version": version,
}
# 执行量化
print("[1/3] 加载模型完成")
print(f"[2/3] 开始量化 ({len(calib_data)} 条校准数据)...")
model.quantize(
tokenizer,
quant_config=quant_config,
calib_data=calib_data,
)
print("[3/3] 保存量化后的模型...")
# 保存模型
model.save_quantized(output_dir, safetensors=True, shard_size="5GB")
tokenizer.save_pretrained(output_dir)
# 验证保存的文件
import os
files = os.listdir(output_dir)
total_size = sum(
os.path.getsize(os.path.join(output_dir, f))
for f in files if f.endswith(('.safetensors', '.bin'))
) / 1024**3
print(f"\n[完成] ✅")
print(f" 输出目录: {output_dir}")
print(f" 文件数: {len(files)}")
print(f" 模型大小: {total_size:.2f} GB")
print(f"\n启动命令:")
print(f" python -m vllm.entrypoints.openai.api_server \\")
print(f" --model {output_dir} \\")
print(f" --quantization awq \\")
print(f" --port 8000")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="AWQ 模型转换工具")
parser.add_argument("--model", type=str, required=True, help="源模型路径或HF ID")
parser.add_argument("--output", type=str, default="./models/awq-output", help="输出目录")
args = parser.parse_args()
convert_to_awq(model_id=args.model, output_dir=args.output)运行方式:
bash
# 安装 AWQ 工具
pip install autoawq
# 转换模型
python convert_to_awq.py \
--model meta-llama/Meta-Llama-3.1-8B-Instruct \
--output ./models/llama3-8b-awq
# 转换完成后启动 vLLM
python -m vllm.entrypoints.openai.api_server \
--model ./models/llama3-8b-awq \
--quantization awq \
--port 8000三、AWQ 模型质量评估
3.1 自动化评估脚本
比如下面的程序对比 FP16 和 AWQ INT4 在标准基准上的表现:
python
import time
from vllm import LLM, SamplingParams
from dataclasses import dataclass
@dataclass
class EvalResult:
"""评估结果"""
model_name: str
precision: str
avg_latency_s: float
throughput_tps: float
vram_gb: float
sample_outputs: list
def evaluate_model(model_path: str, quantization: str = None,
test_prompts: list = None):
"""评估单个模型的性能和质量"""
if test_prompts is None:
test_prompts = [
"用一句话解释机器学习的定义。",
"列出 Python 的三个核心特性。",
"PagedAttention 和传统 Attention 的主要区别是什么?",
"写一个快速排序算法。",
"将以下中文翻译成英文:人工智能正在改变世界。",
"解释量子纠缠现象。",
"什么是 RESTful API?",
"比较 SQL 和 NoSQL 数据库。",
]
import torch
start = time.time()
llm = LLM(
model=model_path,
quantization=quantization,
dtype="auto",
gpu_memory_utilization=0.90,
)
load_time = time.time() - start
sp = SamplingParams(
temperature=0.0,
max_tokens=256,
)
start = time.time()
outputs = llm.generate(test_prompts, sp)
infer_time = time.time() - start
total_tokens = sum(
len(o.outputs[0].token_ids) for o in outputs
)
avg_latency = infer_time / len(test_prompts)
throughput = total_tokens / infer_time
vram = torch.cuda.max_memory_allocated() / 1024**3
samples = [o.outputs[0].text.strip()[:100] for o in outputs[:3]]
del llm
torch.cuda.empty_cache()
return EvalResult(
model_name=model_path.split("/")[-1],
precision=quantization or "fp16",
avg_latency_s=avg_latency,
throughput_tps=throughput,
vram_gb=vram,
sample_outputs=samples,
)
def compare_fp16_vs_awq():
"""对比 FP16 vs AWQ INT4"""
models_to_compare = [
{
"name": "Llama 3.1 8B",
"fp16": "meta-llama/Meta-Llama-3.1-8B-Instruct",
"awq": "casperhao/Meta-Llama-3.1-8B-Instruct-AWQ",
},
]
for config in models_to_compare:
print(f"\n{'='*70}")
print(f"对比: {config['name']}")
print(f"{'='*70}")
r_fp16 = evaluate_model(config["fp16"], None)
print(f"\n[FP16] 加载耗时: {r_fp16.avg_latency_s:.2f}s")
print(f" 吞吐: {r_fp16.throughput_tps:.0f} tokens/s")
print(f" 显存: {r_fp16.vram_gb:.1f} GB")
print(f" 示例输出:")
for i, s in enumerate(r_fp16.sample_outputs):
print(f" [{i+1}] {s[:80]}...")
r_awq = evaluate_model(config["awq"], "awq")
print(f"\n[AWQ INT4] 吞吐: {r_awq.throughput_tps:.0f} tokens/s")
print(f" 显存: {r_awq.vram_gb:.1f} GB")
print(f" 示例输出:")
for i, s in enumerate(r_awq.sample_outputs):
print(f" [{i+1}] {s[:80]}...")
print(f"\n{'='*70}")
print("对比总结")
print(f"{'='*70}")
mem_reduction = (1 - r_awq.vram_gb / r_fp16.vram_gb) * 100
speedup = r_awq.throughput_tps / max(r_fp16.throughput_tps, 0.001)
print(f" 显存节省: {mem_reduction:.1f}% ({r_fp16.vram_gb:.1f}G → {r_awq.vram_gb:.1f}G)")
print(f" 吞吐提升: {speedup:.2f}x")
compare_fp16_vs_awq()典型结果:
======================================================================
对比: Llama 3.1 8B
======================================================================
[FP16] 显存: 16.2 GB, 吞吐: 1850 tokens/s
[AWQ INT4] 显存: 5.1 GB, 吞吐: 2680 tokens/s
======================================================================
对比总结
======================================================================
显存节省: 68.5% (16.2G → 5.1G)
吞吐提升: 1.45x四、AWQ 最佳实践
4.1 校准数据选择
python
def calibration_data_guide():
"""校准数据选择指南"""
guide = """
══════════════════════════════════════════════════
AWQ 校准数据选择指南
══════════════════════════════════════════════════
核心原则: 校准数据应与你实际使用的场景相似
✅ 推荐做法:
├── 数量: 64-512 条即可(不需要大量数据)
├── 来源: 与目标领域相关的文本
│ ├── 通用模型 → Wikipedia / C4 / RedPajama
│ ├── 代码模型 → GitHub 代码 / LeetCode
│ ├── 医疗模型 → PubMed / 医学文献
│ └── 法律模型 → 判例文书 / 法律条文
├── 长度: 接近实际 prompt 的长度 (建议 256-2048 tokens)
└── 多样性: 覆盖不同的主题和风格
❌ 避免:
├── 使用训练数据(可能造成数据泄露)
├── 太少的校准数据 (< 32 条)
├── 与目标场景差异太大的数据
└── 全是短文本 (< 50 tokens)
📦 快速方案(不想自己准备):
├── Pile 数据集的随机采样
├── C4 (Common Crawl) 子集
└── vLLM 内置默认校准数据(如 PTB / WikiText)
"""
print(guide)
calibration_data_guide()4.2 Group Size 选择
python
def group_size_guide():
"""Group Size 参数说明"""
info = """
Group Size 决定了多少列权重共享同一组量化参数:
┌──────────┬──────────┬──────────┬────────────────────┐
│ Group Size │ 显存开销 │ 精度 │ 适用场景 │
├──────────┼──────────┼──────────┼────────────────────┤
│ 128 │ 最小 │ 较好 │ 默认推荐 ⭐ │
│ 64 │ 稍大 │ 更好 │ 高精度需求 │
│ 32 │ 更大 │ 最好 │ 对质量要求极高 │
│ -1 (per-channel) │ 最大 │ 理论最优 │ 不推荐(显存太大) │
└──────────┴──────────┴──────────┴────────────────────┘
推荐: group_size=128 是质量和效率的最佳平衡点
"""
print(info)
group_size_guide()4.3 常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
ModuleNotFoundError: 'awq' | 未安装 autoawq | pip install autoawq |
| 量化后质量严重下降 | 校准数据不匹配 | 使用领域相关文本重新校准 |
| vLLM 加载 AWQ 报错 | 模型格式不兼容 | 确认使用 save_quantized(safetensors=True) |
| 显存没有明显减少 | 可能加载了 FP16 副本 | 检查是否有 .index.json 文件 |
| 速度反而变慢 | 小模型量化收益不明显 | 7B 以下模型建议直接用 FP16 |
五、总结
本节深入学习了 AWQ 量化技术:
| 主题 | 核心要点 |
|---|---|
| 核心思想 | 保护激活值大的重要通道(~1% 权重保持 FP16),其余大胆量化 |
| vs GPTQ | 同等精度下 AWQ 通常效果更好;AWQ 量化速度更快 |
| 使用方式 | ① 直接下载 HF 上的 AWQ 模型;② 用 autoawq 自己转换 |
| 关键参数 | bits=4, group_size=128, zero_point=True |
| 校准数据 | 64-512 条领域相关文本即可,不需要大量数据 |
| 典型收益 | 显存降 65-75%,吞吐升 1.3-1.5x,质量损失 < 2% |
| 适用场景 | 几乎所有生产部署(除非对数学推理有极致要求) |
核心要点回顾:
- AWQ 的精髓是"差异化处理"——不是所有权重都同等重要,保护关键的 1% 就能保住大部分质量
- 校准数据的质量比数量更重要——64 条高质量领域文本 > 10000 条无关文本
- HuggingFace 上已有大量预转换的 AWQ 模型——优先使用现成的,省去转换时间
- Group Size=128 是黄金标准——在显存开销和量化精度之间取得最佳平衡
- 小模型(< 7B)量化收益有限——优先考虑更大的模型 + 量化,而非小模型 + 量化
下一节我们将学习 GPTQ 及其他量化方案,了解不同方法的权衡取舍。