5.1 模型量化
深入理解模型量化的原理、主流 PTQ 方法(INT8/INT4/GPTQ/AWQ)与 QAT,掌握精度-速度-显存的权衡
为什么需要量化?
在部署大语言模型时,显存(GPU memory)是最核心的瓶颈。让我们用一个简单的例子来理解这个问题:
显存估算经验公式:一个参数量为 的模型,以 FP16(16 bit = 2 bytes)存储权重,需要的显存约为 bytes。例如,Qwen3-8B 的 FP16 权重需要约 。
但实际推理时,显存需求远不止权重本身:
以 Qwen3-8B(FP16)为例:
| 组成部分 | 估算大小 | 说明 |
|---|---|---|
| 模型权重(FP16) | ~16 GB | 参数 |
| KV 缓存(2048 tokens,batch=1) | ~1-2 GB | 随序列长度线性增长 |
| 激活值与中间结果 | ~1-2 GB | 取决于 batch size |
| 框架开销 | ~1-2 GB | CUDA context 等 |
| 合计 | ~20-22 GB | 单张 A100-40G 可容纳,但余量不大 |
如果是 7B 模型(如 Qwen3-8B 的前身 Qwen2.5-7B),FP16 权重约 14 GB——在 16GB 的 T4 GPU 上几乎无法运行推理。
量化的核心思想:将模型权重从高精度(FP16/BF16,16 bit)压缩到低精度(INT8 为 8 bit,INT4 为 4 bit),从而将显存占用减少到原来的 1/2 甚至 1/4,同时尽可能保持模型质量。
数值精度基础
在深入量化方法之前,先回顾常见的数值精度格式:
| 精度格式 | 位宽 | 指数位 | 尾数位 | 动态范围 | 典型用途 |
|---|---|---|---|---|---|
| FP32 | 32 bit | 8 | 23 | 传统训练精度 | |
| BF16 | 16 bit | 8 | 7 | 现代训练首选(与 FP32 同范围) | |
| FP16 | 16 bit | 5 | 10 | 推理常用 | |
| INT8 | 8 bit | — | — | 量化推理 | |
| INT4 | 4 bit | — | — | 极致压缩 | |
| NF4 | 4 bit | — | — | 非均匀分布 | QLoRA 专用 |
BF16 vs FP16:BF16 保留了与 FP32 相同的指数位(8位),因此动态范围与 FP32 一致,不容易溢出。FP16 的动态范围小得多,在训练中容易出现梯度溢出(gradient overflow)。现代 LLM 训练几乎都使用 BF16。
量化的数学原理
最基本的均匀量化(Uniform Quantization)公式:
其中 是缩放因子(scale), 是零点(zero-point)。反量化时:
对称量化(Symmetric Quantization)令 :
其中 是目标位宽。例如 INT8 对称量化中,。
训练后量化(PTQ)方法详解
训练后量化(Post-Training Quantization, PTQ)是在模型训练完成后,不需要重新训练即可进行的量化方法。以下介绍四种主流方案。
方法一:INT8 动态量化(bitsandbytes LLM.int8())
核心思想:Dettmers 等(2022)发现,Transformer 模型的激活中存在少量异常值特征(outlier features)——某些隐藏维度的激活值远大于其他维度。如果直接用 INT8 量化,这些异常值会严重损害精度。
LLM.int8() 的解决方案:混合精度分解(Mixed-precision Decomposition)。
检测异常值:在激活矩阵中,找出绝对值超过阈值(默认 6.0)的特征维度
分离计算:将矩阵乘法分为两部分——异常值维度保持 FP16 计算,其余维度用 INT8 计算
合并结果:将 FP16 和 INT8 的计算结果合并,得到最终输出
效果:几乎无精度损失(<0.1% 性能下降),显存减少约 50%。
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
# INT8 量化配置
quantization_config = BitsAndBytesConfig(
load_in_8bit=True, # 启用 INT8 量化
)
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-8B",
quantization_config=quantization_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
# 查看显存占用
print(f"模型显存: {model.get_memory_footprint() / 1024**3:.2f} GB")
# 预期:约 8-9 GB(相比 FP16 的 ~16 GB 减少约一半)方法二:INT4 NormalFloat(bitsandbytes NF4)
核心思想:Dettmers 等(2023)在 QLoRA 论文中提出了 NormalFloat(NF4) 量化格式。其关键洞察是:预训练语言模型的权重近似服从零均值正态分布。
NF4 的量化步骤:
归一化:将每个量化块(通常 64 个权重)除以该块的绝对值最大值,映射到
最优分位数映射:将 区间按标准正态分布 的分位数划分为 个区间,每个区间对应一个量化值
查表量化:将归一化后的权重映射到最近的量化值(4-bit 索引)
其中 是标准正态分布的逆累积分布函数。
双重量化(Double Quantization):QLoRA 还对量化的缩放因子本身再做一次量化(FP32 → FP8),进一步节省约 0.5 bit/参数。
# INT4 NF4 量化配置(QLoRA 所用)
quantization_config = BitsAndBytesConfig(
load_in_4bit=True, # 启用 4-bit 量化
bnb_4bit_quant_type="nf4", # 使用 NormalFloat 格式
bnb_4bit_use_double_quant=True, # 启用双重量化
bnb_4bit_compute_dtype=torch.bfloat16, # 计算时用 BF16
)
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-8B",
quantization_config=quantization_config,
device_map="auto",
)
print(f"模型显存: {model.get_memory_footprint() / 1024**3:.2f} GB")
# 预期:约 5-6 GB(相比 FP16 的 ~16 GB 减少约 3/4)NF4 是 QLoRA 的基石:QLoRA = NF4 量化 + LoRA 微调。在 INT4 量化的模型上附加 LoRA 适配器,使得 7B 模型可以在单张 16GB GPU 上进行微调——这是推动 LLM 微调民主化的关键技术。
方法三:GPTQ — 基于校准的最优量化
核心思想:Frantar 等(2023)将量化视为一个逐层优化问题。对于每一层的权重矩阵 ,找到量化权重 ,使得量化误差最小:
其中 是该层在校准数据上的输入激活。
GPTQ 算法步骤:
准备校准数据:收集 128-256 条代表性文本,送入模型获取每层的输入激活
计算 Hessian 矩阵:,衡量每个权重对输出的影响程度
逐列量化:按 Hessian 对角元素排序,依次量化每个权重,并用 Hessian 信息将量化误差最优地分摊到尚未量化的权重上
分组量化:将权重分成若干组(group_size,通常 128),每组使用独立的缩放因子
# GPTQ 量化通常使用 auto-gptq 或 optimum 库
# 以下为使用预量化 GPTQ 模型的示例
from transformers import AutoModelForCausalLM, AutoTokenizer
# 加载社区提供的 GPTQ 量化模型
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-8B-GPTQ-Int4", # 预量化的 GPTQ INT4 模型
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B-GPTQ-Int4")
# 自行量化的流程(需要 auto-gptq 库)
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
quantize_config = BaseQuantizeConfig(
bits=4, # 量化到 4 bit
group_size=128, # 每 128 个权重共享一组量化参数
desc_act=True, # 按激活值大小排序量化顺序
)
# 量化需要校准数据集
model = AutoGPTQForCausalLM.from_pretrained(
"Qwen/Qwen3-8B",
quantize_config=quantize_config,
)
model.quantize(calibration_dataset) # 校准 + 量化
model.save_quantized("Qwen3-8B-GPTQ-Int4")GPTQ 量化需要离线校准:与 bitsandbytes 的即时量化不同,GPTQ 需要提前准备校准数据并运行量化过程(约需 1-2 小时),但生成的量化模型在推理时更快、质量更高。
方法四:AWQ — 激活感知权重量化
核心思想:Lin 等(2024)观察到,在权重量化中,并非所有权重同等重要——少数与大激活值对应的权重通道(channels)远比其他权重重要。
AWQ 的关键洞察:
其中 是第 个输入通道的平均激活幅度。激活幅度大的通道,量化误差会被放大。
AWQ 的解决方案:对重要通道的权重先乘以一个缩放因子 (使其更易量化),再对缩放后的权重做量化:
缩放因子 通过在校准数据上搜索最优值获得。
AWQ vs GPTQ 对比:
| 特性 | GPTQ | AWQ |
|---|---|---|
| 量化粒度 | 逐列量化 + 误差补偿 | 通道级缩放 + 均匀量化 |
| 校准数据量 | 128-256 条 | 128 条 |
| 量化速度 | 较慢(逐列优化) | 较快(仅需搜索缩放因子) |
| 推理速度 | 快 | 更快(权重格式更规整) |
| 质量 | 优秀 | 优秀,稍优于 GPTQ |
| vLLM 支持 | 支持 | 支持(推荐) |
# AWQ 量化通常使用 autoawq 库
from awq import AutoAWQForCausalLM
model = AutoAWQForCausalLM.from_pretrained("Qwen/Qwen3-8B")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM",
}
# 量化(需要校准数据)
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized("Qwen3-8B-AWQ")量化感知训练(QAT)
与 PTQ 不同,量化感知训练(Quantization-Aware Training, QAT)在训练过程中模拟量化效果:
前向传播:在计算中插入伪量化节点(fake quantization),模拟量化/反量化的舍入误差
反向传播:使用直通估计器(Straight-Through Estimator, STE),将量化函数的梯度近似为 1
收敛后导出:训练完成后,将伪量化替换为真实量化操作
QAT 的优缺点:
| 优点 | 缺点 |
|---|---|
| 量化质量最高 | 需要重新训练 |
| 适合极低精度(INT2/INT3) | 训练成本高 |
| 模型自适应量化误差 | 需要训练数据 |
在实际应用中,PTQ 是主流选择——因为 GPTQ 和 AWQ 在 4-bit 量化下已经能达到接近 FP16 的质量。QAT 通常只在需要 2-3 bit 极致压缩时才值得投入。
精度-速度-显存权衡总表
以 Qwen3-8B(约 80 亿参数)在单张 A100-40G 上为例:
| 精度方案 | 显存占用 | 相对 FP16 | tokens/s | 首 token 延迟 | GSM8K 准确率 | 推荐场景 |
|---|---|---|---|---|---|---|
| FP16 | ~16 GB | 1.0x | 基准 | 基准 | 基准 (~80%) | 质量优先、显存充足 |
| INT8 (LLM.int8()) | ~9 GB | 0.56x | ~0.9x | ~1.1x | ~79.5% | 显存受限但需高质量 |
| INT4 (NF4) | ~5.5 GB | 0.34x | ~1.1x | ~1.0x | ~77-78% | QLoRA 微调 |
| GPTQ-Int4 | ~5 GB | 0.31x | ~1.3x | ~0.8x | ~78% | 生产部署 |
| AWQ-Int4 | ~5 GB | 0.31x | ~1.4x | ~0.7x | ~78.5% | 生产部署(推荐) |
量化对推理能力的影响最大:实验表明,量化对简单任务(如文本分类、情感分析)影响很小,但对需要精确计算的任务(如数学推理、复杂逻辑)影响较大。INT4 量化可能导致 GSM8K 准确率下降 2-3 个百分点。
完整代码示例:多精度模型加载对比
import torch
import time
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_name = "Qwen/Qwen3-8B"
def load_model_with_precision(model_name, precision="fp16"):
"""以不同精度加载模型并测量显存"""
torch.cuda.reset_peak_memory_stats()
start_time = time.time()
if precision == "fp16":
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto",
)
elif precision == "int8":
quantization_config = BitsAndBytesConfig(load_in_8bit=True)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quantization_config,
device_map="auto",
)
elif precision == "int4":
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quantization_config,
device_map="auto",
)
else:
raise ValueError(f"不支持的精度: {precision}")
load_time = time.time() - start_time
memory_gb = torch.cuda.max_memory_allocated() / 1024**3
print(f"[{precision.upper()}] 加载时间: {load_time:.1f}s | 显存: {memory_gb:.2f} GB")
return model, memory_gb, load_time
# 依次加载不同精度的模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
for precision in ["fp16", "int8", "int4"]:
model, mem, t = load_model_with_precision(model_name, precision)
# 简单推理测试
inputs = tokenizer("请解释量子计算的基本原理", return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=100)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(f" 回复预览: {response[:100]}...")
del model
torch.cuda.empty_cache()量化方法选择指南
根据你的使用场景,选择合适的量化方法:
开发调试
使用 bitsandbytes INT8/INT4,零配置即时量化,适合快速原型开发
微调训练(QLoRA)
使用 bitsandbytes NF4 + 双重量化,显存节省最大化
生产部署
使用 AWQ 或 GPTQ 预量化模型,配合 vLLM 部署,推理速度最优
边缘设备
使用 GGUF 格式 + llama.cpp,支持 CPU/混合推理,适合无 GPU 环境
本节小结
- 量化的本质是用更少的 bit 表示权重,以显存换质量的权衡
- PTQ 四大方法各有适用场景:LLM.int8()(通用)、NF4(QLoRA)、GPTQ(高质量部署)、AWQ(最佳性能部署)
- 量化对推理和数学任务影响最大,对通用指令跟随影响较小
- 生产环境推荐 AWQ + vLLM 组合,开发环境推荐 bitsandbytes 即时量化
- QAT 在极低精度(2-3 bit)下有优势,但在 4-bit 及以上时 PTQ 已足够好