跳转到内容

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 8000

2.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'未安装 autoawqpip 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%
适用场景几乎所有生产部署(除非对数学推理有极致要求)

核心要点回顾

  1. AWQ 的精髓是"差异化处理"——不是所有权重都同等重要,保护关键的 1% 就能保住大部分质量
  2. 校准数据的质量比数量更重要——64 条高质量领域文本 > 10000 条无关文本
  3. HuggingFace 上已有大量预转换的 AWQ 模型——优先使用现成的,省去转换时间
  4. Group Size=128 是黄金标准——在显存开销和量化精度之间取得最佳平衡
  5. 小模型(< 7B)量化收益有限——优先考虑更大的模型 + 量化,而非小模型 + 量化

下一节我们将学习 GPTQ 及其他量化方案,了解不同方法的权衡取舍。

基于 MIT 许可发布