3. PTQ实战:使用PyTorch进行静态量化
好,咱们直接进入正题。今天聊的是PTQ——Post-Training Quantization,也就是训练后量化。说白了,就是模型已经训好了,我们拿过来直接做量化,不需要重新训练。
我个人习惯把PTQ叫做「懒人量化法」。为什么?因为你不需要动训练流程,不需要改loss函数,甚至不需要重新跑一遍数据。但懒人法也有懒人法的坑,今天咱们就一个一个踩过去。
3.1 静态量化 vs 动态量化:先搞清楚区别
很多同学一上来就问:「老师,静态量化和动态量化哪个好?」我的回答是:先搞清楚你用在哪儿。
- 动态量化:权重是int8,但激活值在推理时动态计算scale和zero_point。每次forward都要算一遍,适合CPU上的LSTM、Transformer这类模型。
- 静态量化:权重和激活值都是int8。激活值的scale和zero_point提前通过校准数据集算好,推理时直接查表。适合CNN、需要高吞吐的场景。
我在项目中遇到过一件事:有个同学把动态量化用在ResNet上,结果推理速度反而慢了。为什么?因为动态量化每次都要算激活值的统计量,计算开销比省下来的内存带宽还大。所以,CNN模型老老实实用静态量化。
3.2 PyTorch静态量化三步走
PyTorch的静态量化流程,我总结成三步:
- 准备模型:把模型中的Conv、Linear等算子替换成可量化的版本
- 校准:用一小部分数据跑一遍,收集激活值的统计信息
- 转换:把模型真正量化成int8
嗯,这里要注意:第二步「校准」是最容易出问题的环节。很多同学直接拿训练集跑一遍,结果量化后的模型精度崩了。为什么?因为训练集的分布和实际推理时的分布可能不一样。
3.3 代码实战:从浮点模型到量化模型
咱们直接上代码。假设你有一个训练好的ResNet-18模型:
import torch
import torch.quantization
# 1. 加载浮点模型
model = torch.hub.load('pytorch/vision:v0.10.0', 'resnet18', pretrained=True)
model.eval()
# 2. 准备量化配置
model.qconfig = torch.quantization.get_default_qconfig('fbgemm')
# 如果你在ARM上跑,用'qnnpack'
# 3. 插入观察者(Observer)
model_prepared = torch.quantization.prepare(model, inplace=False)
# 4. 校准:用一小批数据跑一遍
calibration_data = torch.randn(100, 3, 224, 224) # 实际用你的验证集
with torch.no_grad():
for i in range(10):
model_prepared(calibration_data[i*10:(i+1)*10])
# 5. 转换:真正量化
model_quantized = torch.quantization.convert(model_prepared, inplace=False)
print("量化完成!")
这段代码跑完,你就得到了一个int8的ResNet-18。但别高兴太早——精度损失了多少?
3.4 观察精度损失:别被「差不多」骗了
我见过太多人,量化完模型后只看top-1准确率,发现掉了0.5%就觉得「差不多」。但实际部署时,某些类别的精度可能掉了5%甚至10%。
所以我的习惯是:按类别看精度。
def evaluate_per_class(model, dataloader, num_classes=1000):
class_correct = [0] * num_classes
class_total = [0] * num_classes
with torch.no_grad():
for images, labels in dataloader:
outputs = model(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze()
for i in range(len(labels)):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1
for i in range(num_classes):
if class_total[i] > 0:
acc = 100 * class_correct[i] / class_total[i]
if acc < 50: # 精度低于50%的类别
print(f"类别 {i}: 精度 {acc:.2f}%")
我曾经在一个工业检测项目里,量化后整体精度只掉了0.3%,但「裂纹」这个关键类别的精度从92%掉到了78%。如果只看整体指标,这个模型就「安全」上线了——后果不堪设想。
3.5 调整校准策略:核心三板斧
校准策略说白了就是:你用什么数据、用什么方法去算激活值的scale和zero_point。我总结了三个最有效的调整方向:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 1. 校准数据集选择 | 用验证集代替训练集,且覆盖所有类别 | 类别不平衡的数据集 |
| 2. 校准样本数量 | 从100张增加到500-1000张 | 激活值分布复杂时 |
| 3. 量化方法选择 | 从minmax改为percentile或mse | 有离群点时 |
具体到代码,调整量化方法是这样做的:
# 默认是minmax,容易受离群点影响
custom_qconfig = torch.quantization.QConfig(
activation=torch.quantization.MinMaxObserver.with_args(
dtype=torch.quint8, qscheme=torch.per_tensor_affine
),
weight=torch.quantization.MinMaxObserver.with_args(
dtype=torch.qint8, qscheme=torch.per_tensor_symmetric
)
)
# 改用percentile方法,忽略极端值
percentile_qconfig = torch.quantization.QConfig(
activation=torch.quantization.HistogramObserver.with_args(
dtype=torch.quint8, qscheme=torch.per_tensor_affine,
bins=2048, upsample_rate=256
),
weight=torch.quantization.MinMaxObserver.with_args(
dtype=torch.qint8, qscheme=torch.per_tensor_symmetric
)
)
model.qconfig = percentile_qconfig
3.6 避坑指南:我踩过的三个坑
做PTQ这几年,我踩过的坑不少。挑三个最典型的分享给你:
- 坑一:BatchNorm层没融合。PyTorch的量化要求先把BN层融合到Conv层里。如果你忘了做这一步,量化后的模型精度会莫名其妙地掉。用
torch.quantization.fuse_modules()搞定。 - 坑二:校准数据没做预处理。校准数据必须和训练时的预处理完全一致——同样的归一化、同样的resize方式。我曾经因为忘了做归一化,校准出来的scale全是错的。
- 坑三:量化后模型跑在GPU上。PyTorch的量化目前主要针对CPU优化。如果你把量化模型放到GPU上跑,不仅不会加速,反而可能变慢。想加速GPU推理?请用TensorRT。
3.7 知识体系总览
下面这张图是我自己整理的PTQ静态量化知识体系,你可以把它当作一个检查清单:
这张图的核心逻辑就是:准备 → 校准 → 转换 → 评估 → 调整 → 再校准。别想着一步到位,PTQ本身就是一个迭代的过程。
3.8 小结
今天咱们把PTQ静态量化的实战流程走了一遍。从模型准备到校准到转换,每一步都有坑,但每一步也都有对应的解法。我个人觉得,PTQ最考验的不是技术,而是你对模型的理解——你知道模型在哪些类别上容易出错,你知道激活值的分布长什么样,你才能做出好的量化策略。
下次如果你遇到量化后精度崩了的情况,别慌。先看看校准数据对不对,再看看是不是有离群点,最后检查一下BN层有没有融合。这三个地方,解决了90%的问题。