2. 基于PTQ的8位激活量化
任务目标
对已训练好的浮点模型实施PTQ(训练后量化)中的8位激活量化:使用少量校准数据统计激活值分布,据此确定量化范围(scale和zero_point),将激活从FP32映射到INT8,并对比量化前后模型输出的数值差异,验证量化误差在可接受范围内。
环境与依赖
本案例使用PyTorch 2.0+和torchvision,无需额外安装。量化采用PyTorch官方torch.quantization API实现,校准数据使用CIFAR-10训练集前128个batch(每个batch 64张图)。
完整实现
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from torch.quantization import prepare_qat, convert
from torch.quantization import get_default_qconfig, QConfig
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx
from torch.ao.quantization import MinMaxObserver, PerChannelMinMaxObserver
from torch.ao.quantization.observer import HistogramObserver
import copy
# 1. 定义一个小型CNN(与CIFAR-10匹配)
class SmallCNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 16, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(16, 32, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(32 * 8 * 8, 64),
nn.ReLU(),
nn.Linear(64, 10),
)
def forward(self, x):
return self.classifier(self.features(x))
# 2. 准备校准数据(CIFAR-10验证集前128个batch)
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])
calib_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=transform)
calib_loader = torch.utils.data.DataLoader(
calib_dataset, batch_size=64, shuffle=False,
sampler=torch.utils.data.SubsetRandomSampler(range(128 * 64)))
# 3. 加载预训练模型(此处用随机权重演示流程,实际应加载训练好的权重)
model_fp32 = SmallCNN()
model_fp32.eval()
# 4. 配置量化:MinMaxObserver确定激活范围
qconfig = QConfig(
activation=MinMaxObserver.with_args(dtype=torch.quint8, qscheme=torch.per_tensor_affine),
weight=PerChannelMinMaxObserver.with_args(dtype=torch.qint8, qscheme=torch.per_channel_symmetric)
)
# 5. 使用FX图模式量化(支持更灵活的量化范围统计)
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx
model_to_quantize = copy.deepcopy(model_fp32)
qconfig_dict = {"": qconfig}
model_prepared = prepare_fx(model_to_quantize, qconfig_dict)
# 6. 校准:统计激活值min/max
print("开始校准(统计激活量化范围)...")
with torch.no_grad():
for i, (images, _) in enumerate(calib_loader):
model_prepared(images)
if i % 32 == 0:
print(f" 校准进度: {i}/{len(calib_loader)}")
print("校准完成")
# 7. 转换为量化模型
model_quantized = convert_fx(model_prepared)
print("量化模型转换完成")
# 8. 验证:比较量化前后输出差异
def get_outputs(model, loader, num_batches=5):
"""返回前num_batches个batch的模型输出(已排序,便于比较)"""
outputs = []
with torch.no_grad():
for i, (images, _) in enumerate(loader):
if i >= num_batches:
break
outputs.append(model(images))
return torch.cat(outputs, dim=0)
# 使用独立的验证集(非校准数据)
val_loader = torch.utils.data.DataLoader(
torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=transform),
batch_size=64, shuffle=False)
print("计算量化前输出...")
out_fp32 = get_outputs(model_fp32, val_loader, num_batches=5)
print("计算量化后输出...")
out_int8 = get_outputs(model_quantized, val_loader, num_batches=5)
# 9. 计算差异指标
abs_diff = (out_fp32 - out_int8).abs()
cos_sim = nn.functional.cosine_similarity(out_fp32, out_int8, dim=1).mean()
print("\n===== 量化前后输出差异 =====")
print(f"最大误差: {abs_diff.max().item():.6f}")
print(f"平均误差: {abs_diff.mean().item():.6f}")
print(f"余弦相似度: {cos_sim.item():.6f}")
# 10. 查看量化参数(激活的scale和zero_point)
print("\n===== 激活量化参数(第1个卷积层后) =====")
for name, module in model_quantized.named_modules():
if hasattr(module, 'activation_post_process'):
obs = module.activation_post_process
print(f" {name}: scale={obs.scale.item():.6f}, zero_point={obs.zero_point.item()}")
break # 只显示第一个
运行方式
- 直接运行脚本:
python ptq_activation_quant.py(首次运行会自动下载CIFAR-10数据集,约170MB) - 若已有训练好的模型权重,将
model_fp32替换为加载权重的模型即可
验证方法与判定标准
运行后观察终端输出,满足以下条件即判定量化有效:
- 余弦相似度 > 0.99:表明量化前后输出方向基本一致,信息未严重丢失
- 平均误差 < 0.1:在CIFAR-10的10类logits输出上,0.1以内的平均偏差属于正常PTQ误差范围
- 激活scale值合理:打印出的scale应在
1e-3 ~ 1e-1量级(取决于激活值范围),zero_point通常接近128(对于ReLU后的非负激活)
若余弦相似度低于0.95,说明量化范围统计不准确,可尝试:增加校准batch数、改用HistogramObserver(百分位截断)替代MinMaxObserver,或对模型做BN折叠。
关键说明
- 本案例使用FX图模式量化(
prepare_fx/convert_fx),相比旧版torch.quantization.prepare能自动处理ReLU等算子的融合,且校准统计更精确 - 校准数据仅用于统计激活值的min/max,不参与梯度更新,因此PTQ无需训练
- 示例代码使用随机权重模型仅用于演示量化流程;实际使用时请替换为训练好的权重,量化误差会更有参考意义
- 若需部署到实际硬件,可进一步导出为ONNX或使用
torch.jit脚本化模型