第三章:PTQ与QAT——训练后量化与量化感知训练

量化这个话题,说白了就是给模型“减减肥”。

大模型动辄几十上百GB,部署到GPU上显存吃紧,推理速度也慢。我最早接触量化时,觉得这玩意儿挺玄乎——把32位浮点数变成8位整数,精度能不掉?后来踩了不少坑,才慢慢摸清门道。

今天咱们就聊聊两种主流方案:训练后量化(PTQ)量化感知训练(QAT)。一个省事,一个效果好,各有各的适用场景。

3.1 训练后量化(PTQ)原理

PTQ,全称Post-Training Quantization。顾名思义,模型训练完了,我再动手脚。

你想想看,一个已经收敛好的模型,权重分布基本固定了。我们只需要拿一小部分校准数据,跑一遍前向推理,统计出每层激活值的范围,然后算出缩放因子和零点偏移,就能完成量化。

核心思想:用少量数据,找到最优的量化参数,最小化量化误差。

具体怎么做?我习惯分三步走:

  1. 准备校准数据——从训练集里抽几百到几千条样本,覆盖常见场景就行。
  2. 统计激活值范围——跑前向推理,记录每层输出的min/max,或者用直方图统计分布。
  3. 计算量化参数——根据范围算出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)

这段代码里,QuantStubDeQuantStub是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的核心流程:

量化方案对比:PTQ vs QAT 训练后量化(PTQ) 加载预训练模型 准备校准数据(500+条) 统计激活值范围 计算scale/zero_point 输出:量化模型 量化感知训练(QAT) 加载预训练模型 插入伪量化节点 使用完整训练集微调 STE近似梯度传播 输出:量化模型 精度要求高? 核心权衡:PTQ省时间但精度略低,QAT精度高但需要重新训练

嗯,这张图把两个方案的流程和权衡关系都画清楚了。你可以保存下来,做方案选型时参考。

3.7 一些实用建议

最后,分享几个我在项目中积累的经验:

  • 混合精度量化:不是所有层都需要量化到相同位数。注意力层对精度敏感,可以保留FP16;FFN层可以大胆量化到INT8甚至INT4。
  • 逐层校准:PTQ时,逐层校准比整体校准效果好。因为每层的激活值分布差异很大,统一用一个scale会损失精度。
  • 量化后微调:如果PTQ后精度不达标,可以尝试量化后微调(QAT的轻量版)。只训练几轮,让模型适应量化噪声。

我的习惯:每次量化后,我都会跑一遍验证集,对比量化前后的输出分布。如果KL散度超过0.1,说明量化损失太大,需要调整策略。

量化这条路,说白了就是用精度换速度。PTQ和QAT是两种不同的交换方式,没有绝对的好坏,只有合不合适。你根据自己项目的精度要求和部署时间,选一个就行。


专注资料整理