4. PTQ(训练后量化)实战:使用PyTorch进行静态量化

好,咱们今天来聊聊PTQ,也就是训练后量化。说白了,就是模型训练完了,我们直接拿它来做量化,不用再重新训练一遍。这在工程落地中非常实用,尤其是当你手里只有预训练好的模型,或者训练资源有限的时候。

我个人习惯把PTQ分成两种:动态量化和静态量化。很多新手容易搞混,我刚开始接触时也踩过坑。咱们先把这个概念理清楚。

动态量化 vs 静态量化:到底差在哪?

动态量化,它只在推理时对权重做量化,而激活值(就是每层输出的中间结果)还是用浮点数算。你想想看,这样虽然省了点存储,但计算时还得来回转换,速度提升有限。

静态量化就不一样了。它会在推理前,先跑一批校准数据,统计出激活值的分布范围,然后直接把激活值也量化成int8。这样一来,整个计算图都是int8的,推理速度能快不少。

我举个例子你就明白了:

对比项 动态量化 静态量化
权重 量化(int8) 量化(int8)
激活值 浮点(float32) 量化(int8)
校准数据 不需要 需要少量校准数据
推理速度 中等提升 大幅提升
适用场景 LSTM、Transformer等 CNN等计算密集型模型

嗯,这里要注意:动态量化虽然速度提升有限,但它有个好处——不需要校准数据。如果你手头没有合适的校准集,或者模型结构对激活值分布特别敏感,动态量化反而是个稳妥的选择。

我的经验: 在部署语音模型时,我遇到过激活值分布特别不均匀的情况。静态量化后精度掉了2个点,换成动态量化反而只掉了0.5个点。所以别一味追求静态量化,得看实际效果。

Fuse操作与BN合并:为什么必须做?

做静态量化前,有个关键步骤叫fuse(融合)。说白了,就是把一些相邻的层合并成一个层。最常见的,就是把Conv2d、BatchNorm2d、ReLU这三兄弟合并成一个Conv2d。

为什么会这样?因为量化时,每一层都要单独计算量化参数。如果层数太多,量化误差会累积。合并后,层数变少,误差自然就小了。

我记得有一次,一个同事没做fuse就直接量化,结果模型精度掉了5个点。他查了半天,最后发现是BN层没合并。合并后,精度只掉了0.3个点。差距就这么大。

PyTorch里做fuse很简单,就一行代码:

import torch.quantization as quant

# 定义模型
model = MyModel()

# 设置量化配置
model.qconfig = quant.get_default_qconfig('fbgemm')

# 准备fuse
model = quant.fuse_modules(model, [['conv1', 'bn1', 'relu1'],
                                   ['conv2', 'bn2', 'relu2']])

# 准备量化
model = quant.prepare(model, inplace=True)

# 校准(跑几批数据)
for data in calibration_loader:
    model(data)

# 转换
model = quant.convert(model, inplace=True)

你看,代码其实不多。但有几个坑我得提醒你:

避坑指南: 我曾经在fuse时,把Conv2d和BN的层名写错了。结果fuse没成功,但也没报错。最后量化完精度不对,排查了半天才发现。所以建议你fuse后打印一下模型结构,确认层确实合并了。

实战:完整静态量化流程

好,咱们来走一遍完整的流程。假设你有一个训练好的ResNet-18模型,想部署到x86服务器上。

第一步,加载模型并设为eval模式:

model = torchvision.models.resnet18(pretrained=True)
model.eval()

第二步,设置量化后端。fbgemm是给x86用的,qnnpack是给ARM用的:

model.qconfig = quant.get_default_qconfig('fbgemm')

第三步,fuse。ResNet里有很多BasicBlock,每个Block里都有Conv-BN-ReLU的结构:

model = quant.fuse_modules(model, 
    [['conv1', 'bn1', 'relu'],
     ['layer1.0.conv1', 'layer1.0.bn1', 'layer1.0.relu1'],
     ['layer1.0.conv2', 'layer1.0.bn2'],
     # ... 其他层类似
    ])

第四步,prepare。这一步会插入Observer,用来统计激活值的分布:

model = quant.prepare(model, inplace=True)

第五步,校准。用几百张图片跑一下前向推理,让Observer收集数据:

with torch.no_grad():
    for images, _ in calibration_loader:
        model(images)

第六步,convert。把Observer去掉,换成真正的量化计算:

model = quant.convert(model, inplace=True)

好了,现在模型已经量化好了。你可以用torch.jit.script把它导出,或者直接用PyTorch推理。

关键点: 校准数据不需要太多,一般500-1000张就够了。但一定要覆盖模型可能遇到的各种情况。我见过有人只用猫的图片校准,结果部署时遇到狗就识别不准了。

量化后的精度验证

量化完,第一件事就是跑精度验证。别急着部署,先看看精度掉了多少。

一般来说,静态量化后精度损失在0.5%以内是正常的。如果超过1%,就得排查原因了:

  • 是不是fuse没做好?
  • 校准数据分布是否合理?
  • 有没有某些层的激活值范围特别大?

我记得有一次,量化后精度掉了3个点。查了半天,发现是某个卷积层的输出范围在[-100, 100]之间,而其他层都在[-10, 10]之间。这个异常值把量化参数拉偏了。后来我单独给这层设置了量化范围,精度就回来了。

嗯,今天就先聊到这。静态量化是PTQ里最常用的技术,掌握了它,大部分部署场景都能应付。下一章咱们聊聊更进阶的量化感知训练(QAT),看看怎么把精度损失降到最低。