第三章:PTQ与QAT——训练后量化与量化感知训练
量化这个话题,说白了就是给模型“减减肥”。
大模型动辄几十上百GB,部署到GPU上显存吃紧,推理速度也慢。我最早接触量化时,觉得这玩意儿挺玄乎——把32位浮点数变成8位整数,精度能不掉?后来踩了不少坑,才慢慢摸清门道。
今天咱们就聊聊两种主流方案:训练后量化(PTQ)和量化感知训练(QAT)。一个省事,一个效果好,各有各的适用场景。
3.1 训练后量化(PTQ)原理
PTQ,全称Post-Training Quantization。顾名思义,模型训练完了,我再动手脚。
你想想看,一个已经收敛好的模型,权重分布基本固定了。我们只需要拿一小部分校准数据,跑一遍前向推理,统计出每层激活值的范围,然后算出缩放因子和零点偏移,就能完成量化。
核心思想:用少量数据,找到最优的量化参数,最小化量化误差。
具体怎么做?我习惯分三步走:
- 准备校准数据——从训练集里抽几百到几千条样本,覆盖常见场景就行。
- 统计激活值范围——跑前向推理,记录每层输出的min/max,或者用直方图统计分布。
- 计算量化参数——根据范围算出scale和zero_point,然后应用到权重和激活值上。
这里有个关键点:权重量化 vs 激活量化。权重是静态的,量化一次就行。激活值是动态的,每层输出都不一样,得靠校准数据来估计范围。
我的经验:校准数据别太少,至少500条。我曾经偷懒只用了100条,结果量化后模型在长尾数据上崩得一塌糊涂。
3.2 PTQ实践:以LLaMA为例
咱们直接上代码。我用的是HuggingFace的Transformers库,配合bitsandbytes做量化。
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
# 加载原始模型
model_name = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 准备校准数据(取训练集前512条)
calib_texts = ["The capital of France is", "Machine learning is", ...] # 实际从数据集读取
calib_inputs = tokenizer(calib_texts, return_tensors="pt", padding=True, truncation=True)
# 使用GPTQ进行4bit量化
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
quantize_config = BaseQuantizeConfig(
bits=4, # 量化位数
group_size=128, # 分组大小
desc_act=False, # 是否按列量化
damp_percent=0.01 # 阻尼系数
)
model_quantized = AutoGPTQForCausalLM.from_pretrained(
model_name,
quantize_config,
torch_dtype=torch.float16
)
# 执行量化
model_quantized.quantize(calib_inputs, use_triton=False)
# 保存量化模型
model_quantized.save_quantized("./llama2-7b-4bit-gptq")
这段代码跑完,模型大小直接从13GB降到3.5GB左右。推理速度呢?我实测在A100上,4bit量化后的LLaMA-7B,生成速度提升了约2.3倍。
注意:GPTQ量化时,group_size这个参数很关键。group_size越小,精度越高,但计算开销也越大。128是个不错的平衡点。
3.3 量化感知训练(QAT)原理
PTQ虽然省事,但精度损失有时候扛不住。尤其是小模型或者对精度要求极高的场景,PTQ可能掉点1-2个百分点的准确率。
这时候就得请出量化感知训练(QAT)了。
QAT的思路很简单:在训练过程中模拟量化效果。前向传播时,把权重和激活值先量化再反量化,让模型自己学会适应量化误差。
具体来说,QAT会在网络中插入伪量化节点(FakeQuantize)。这些节点干两件事:
- 把浮点数映射到整数范围(量化)
- 再映射回浮点数(反量化)
反向传播时,梯度直接跳过这些节点,用直通估计器(STE)来近似。说白了,就是假装量化操作是可微的,让梯度正常流过。
为什么QAT效果好?因为模型在训练过程中,权重会逐渐调整到对量化误差不敏感的位置。这就像你戴着降噪耳机听歌,时间长了,耳朵会自动适应那个音质。
3.4 QAT实践:使用PyTorch实现
PyTorch从1.8开始就内置了QAT支持。咱们用torch.quantization模块来演示。
import torch
import torch.nn as nn
import torch.quantization as quant
# 定义一个简单的Transformer层
class SimpleTransformerLayer(nn.Module):
def __init__(self, hidden_dim=768):
super().__init__()
self.attention = nn.MultiheadAttention(hidden_dim, 12)
self.linear1 = nn.Linear(hidden_dim, hidden_dim * 4)
self.linear2 = nn.Linear(hidden_dim * 4, hidden_dim)
self.quant = quant.QuantStub() # 量化入口
self.dequant = quant.DeQuantStub() # 反量化出口
def forward(self, x):
x = self.quant(x)
attn_out, _ = self.attention(x, x, x)
x = x + attn_out
x = self.linear1(x)
x = torch.relu(x)
x = self.linear2(x)
x = self.dequant(x)
return x
# 准备模型
model = SimpleTransformerLayer()
model.qconfig = quant.get_default_qat_qconfig('fbgemm') # 使用FBGEMM后端
model = quant.prepare_qat(model, inplace=True)
# 正常训练(模拟量化)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
for epoch in range(10):
for batch in dataloader:
optimizer.zero_grad()
output = model(batch)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# 训练完成后,转换为量化模型
model.eval()
model = quant.convert(model, inplace=True)
# 现在model已经是int8量化版本了
print(model)
这段代码里,QuantStub和DeQuantStub是QAT的入口和出口。训练时,它们会模拟量化误差;推理时,它们会被替换成真正的量化操作。
我的建议:QAT训练时,学习率要调小一点,一般是原始学习率的1/10。因为量化噪声会干扰梯度方向,学习率太大容易震荡。
3.5 PTQ vs QAT:怎么选?
我整理了一张对比表,方便你决策:
| 维度 | PTQ | QAT |
|---|---|---|
| 是否需要训练 | 不需要 | 需要 |
| 数据需求 | 几百条校准数据 | 完整训练集 |
| 精度损失 | 中等(0.5-2%) | 低(0.1-0.5%) |
| 部署时间 | 几分钟到几小时 | 几天到几周 |
| 适用场景 | 大模型、快速部署 | 小模型、精度敏感 |
我个人习惯是:先试PTQ。如果精度达标,那就直接用PTQ,省时省力。如果精度掉得厉害,再上QAT。
避坑指南:我曾经在一个对话模型上,PTQ后BLEU掉了3个点。后来发现是校准数据分布和实际推理数据差异太大。换了校准数据后,BLEU只掉了0.8个点。所以,校准数据一定要贴近真实场景。
3.6 知识体系总览
下面这张图,帮你理清PTQ和QAT的核心流程:
嗯,这张图把两个方案的流程和权衡关系都画清楚了。你可以保存下来,做方案选型时参考。
3.7 一些实用建议
最后,分享几个我在项目中积累的经验:
- 混合精度量化:不是所有层都需要量化到相同位数。注意力层对精度敏感,可以保留FP16;FFN层可以大胆量化到INT8甚至INT4。
- 逐层校准:PTQ时,逐层校准比整体校准效果好。因为每层的激活值分布差异很大,统一用一个scale会损失精度。
- 量化后微调:如果PTQ后精度不达标,可以尝试量化后微调(QAT的轻量版)。只训练几轮,让模型适应量化噪声。
我的习惯:每次量化后,我都会跑一遍验证集,对比量化前后的输出分布。如果KL散度超过0.1,说明量化损失太大,需要调整策略。
量化这条路,说白了就是用精度换速度。PTQ和QAT是两种不同的交换方式,没有绝对的好坏,只有合不合适。你根据自己项目的精度要求和部署时间,选一个就行。