量化一个 Pytorch 模型到 PPL 后端
Quantize a Pytorch model to PPL Backend.
从哪里来,到哪里去?
感谢您跟随教程来到这里,在这里我将为你展示 PPQ 的核心使用方法,在继续下去之前,你需要了解到 PPQ 是一个多平台通用的量化工具,这体现在 PPQ 的核心设计可以拟合不同平台的量化方案,同时其内部表示是独立于后端与前端框架的。截至目前为止,PPQ 原生支持量化来自于 Pytorch, Onnx, Caffe 三种前端的模型,Tensorflow 与 Mxnet 的用户则可以通过 Onnx 模型导出使用 PPQ 的功能。PPQ 在目前的开源版本中只提供 PPL CUDA, PPL DSP, TensorRT, NXP 后端的原生支持,在未来我们可能会扩展针对SNPE, NPU等后端的支持。同时用户也可以自己编写后端导出插件来完成对于其他后端框架的支持,这十分容易。

在本教程中,我们将使用一个Pytorch原生的模型,并尝试将其量化到 PPL CUDA 后端上。请注意本教程只包含量化的内容,如您要了解将网络部署在 PPL Runtime 上的后续步骤请继续阅读后续内容。
在开始之前,您需要准备:
- 一个需要量化的神经网络模型
- 一个小型的校准数据集(calibration dataset),用于校准您的网络
- 一个完整的测试数据集(test dataset),用于测试量化模型的精度
- 一块 Nvidia GPU,必须插在您的主板上并且正常供电;是的,拿在手上的不行
在这里我必须要提醒你校准数据集的要求,PPQ 要求校准数据集必须具有 8~512 个样本数据,与此同时它们不需要带有 标签。然而我需要提醒你请不要使用 calib_dataset = train_dataset[:512] 这样的作法,为了使我们的校准数据尽可能分散,我们希望你使用随机采样的方法从网络训练集或者其他什么地方构建校准集合。
量化我的 Pytorch 模型
这里我们以 pytorch 中的 mobilenet v2 模型为例,向你展示 PPQ 量化网络的完整逻辑:
from typing import Iterable
import torch
import torchvision
from torch.utils.data import DataLoader
from ppq import BaseGraph, QuantizationSettingFactory, TargetPlatform
from ppq.api import export_ppq_graph, quantize_torch_model
BATCHSIZE = 32
INPUT_SHAPE = [3, 224, 224]
DEVICE = 'cuda' # only cuda is fully tested :( For other executing device there might be bugs.
PLATFORM = TargetPlatform.PPL_CUDA_INT8 # identify a target platform for deploy your network.
def load_calibration_dataset() -> Iterable:
return [torch.rand(size=INPUT_SHAPE) for _ in range(32)]
def collate_fn(batch: torch.Tensor) -> torch.Tensor:
return batch.to(DEVICE)
# Load a pretrained mobilenet v2 model
model = torchvision.models.mobilenet.mobilenet_v2(pretrained=True)
model = model.to(DEVICE)
# create a setting for quantizing your network with PPL CUDA.
quant_setting = QuantizationSettingFactory.pplcuda_setting()
quant_setting.equalization = True # require layerwise equalization algorithm.
quant_setting.dispatcher = 'conservative' # require dispatching this network in conservertive way.
# Load some training data for creating a calibration dataloader.
calibration_dataset = load_calibration_dataset()
calibration_dataloader = DataLoader(
dataset=calibration_dataset,
batch_size=BATCHSIZE, shuffle=True)
# quantize your model.
quantized = quantize_torch_model(
model=model, calib_dataloader=calibration_dataloader,
calib_steps=32, input_shape=[BATCHSIZE] + INPUT_SHAPE,
setting=quant_setting, collate_fn=collate_fn, platform=PLATFORM,
onnx_export_file='Output/onnx.model', device=DEVICE, verbose=0)
# Quantization Result is a PPQ BaseGraph instance.
assert isinstance(quantized, BaseGraph)
# export quantized graph.
export_ppq_graph(graph=quantized, platform=PLATFORM,
graph_save_to='Output/quantized(onnx).onnx',
config_save_to='Output/quantized(onnx).json')
不用慌张,它虽然很长,但是逻辑清晰。下面我们将一点一点将你讲述代码内容。
加载你的网络和数据集
加载你的网络和数据集永远是必须的,同时这里还有一些要求:
- 你的网络不能是分布式的,只能以单机方式量化,毕竟你不能把一个分布式的网络部署到终端设备上。
- 你的网络不能在执行过程中发生结构变化,Pytorch 是很灵活的框架,但后端执行框架不是。
- 数据集不要求带有标签,有标签也没用。
- 数据集必须包含8 ~ 512个样本,太少量化不准,太多跑的很慢,而且我还会给你报错。
确保你有符合要求的模型与校准数据集,你就需要告知 PPQ 输入数据的尺寸与 Batchsize,这有助于 PPQ 分析你的网络模型。在我们给出的代码中,第9、10行定义了变量BATCHSIZE与INPUT_SHAPE,第39行以[BATCHSIZE] + INPUT_SHAPE的形式告知了 PPQ 输入的完整尺寸为[32, 3, 224, 224],这一切都是必须的。
在第18行我们定义了函数 collate_fn,用来整理从数据集中读取到的数据;而在第 40 行中,我们告诉 PPQ 使用 collate_fn 函数整理从 calib_dataloader 中读取的数据。该函数使得 PPQ 在从 dataloader 中读取到数据后,将数据发往 cuda,从而完成后续计算。
可能存在的问题-多输入与动态尺寸输入
PPQ 支持量化具有多个输入的神经网络模型,对于具有多个输入的模型,PPQ 支持以 List, Dict 两种方式送入数据,以 List 方式送入数据时,数据的顺序可以通过 print(quantized.inputs.keys()) 获取,以 dict 形式送入数据时,数据格式为 "name of variable": "data",其中 variable 的名字依然可以通过 quantized.inputs.keys() 获取。
如果你的网络模型来自于 onnx 或者 caffe,你可以通过任何神经网络可视化软件打开你的模型,从而观察到数据送入的顺序与 variable 的名字。
下面的代码片段向你展示了如何处理具有多个输入的神经网络:
# 对于具有多个输入的神经网络模型而言,参数 input_shape 已经不能进行表达
# 你需要使用参数 inputs 来实际送入一批样本数据,inputs 的数据类型是 List of Tensors 或 Dict of str -> Tensor
# 下面我们假设模型具有两个输入,尺寸分别为 [32, 3, 224, 224] 和 [32, 3, 96, 96]
# 你可以通过 List 的形式完成输入:
inputs = [torch.zeros(size=[32, 3, 224, 224]), torch.zeros(size=[32, 3, 96, 96])]
# 也可以在知道 variable 名字的情况下使用 Dict 形式完成输入:
inputs = {'INPUT VARIABLE 1': torch.zeros(size=[32, 3, 224, 224]),
'INPUT VARIABLE 1': torch.zeros(size=[32, 3, 96, 96])}
# 使用 inputs 作为输入时,必须将 input_shape 置为 None.
quantized = quantize_torch_model(
model=model, calib_dataloader=calibration_dataloader,
calib_steps=32, inputs=inputs, input_shape=None,
setting=quant_setting, collate_fn=collate_fn, platform=PLATFORM,
onnx_export_file='Output/onnx.model', device=DEVICE, verbose=0)
PPQ 支持量化动态尺寸输入的神经网络,我们此时假设所需量化神经网络能够接受的输入尺寸为[1 ~ 128, 3, 96 ~ 1200, 96 ~ 1200],此时你只需要将 input_shape 置为所有可能输入中,尺寸最小的哪一个即可,即直接置为[1, 3, 96, 96]。
与后端框架不同,PPQ只会使用input_shape来生成一些样本数据,从而对网络结构进行追踪和解析,因此PPQ并不十分在意这些动态尺寸输入这样的特性,这是后端架构所考虑的内容。对于 PPQ 而言,动态尺寸网络与静态尺寸网络并没有本质上的区别。
不过值得你注意的是,PPQ 会将尺寸信息写入到生成的 onnx 文件当中,有一些后端框架会使用该信息对网络进行推理优化,并拒绝接受其他尺寸的输入,在这种情况下可能会导致推理问题。例如上述例子中我们的神经网络可以接受尺寸为[1 ~ 128, 3, 96 ~ 1200, 96 ~ 1200]的任何输入,而PPQ量化时使用[1, 3, 96, 96]对网络结构进行分析追踪,并将此信息写入到生成的onnx文件中,这将导致部分后端推理框架拒绝接受其他尺寸的输入。
确定执行设备
作为量化模拟器而言,PPQ 需要通过计算来确定一些量化参数,因此你需要为 PPQ 指定一个执行设备。很巧 PPQ 使用 Pytorch 框架完成所有量化中的计算,因此在PPQ中执行设备的定义方式与Pytorch是完全相同的。常用的计算设备包括 'cpu', 'cuda', 'cuda:1' 等等。
很不巧的是为了加速软件运行效率,我们手工编写了一些 cuda kernel 辅助 PPQ 量化模拟器的运行,在开源版本中默认它们是启用的状态,你可以通过修改 ppq.core.config.PPQ_CONFIG.USING_CUDA_KERNEL = False 来取消使用这些自定义 kernel。
我们强烈推荐你使用 GPU 完成 PPQ 的量化计算,虽然理论上 PPQ 能够使用 CPU 完成量化,但是相关功能并未经过完善测试,显然存在不可预期的 bug,同时也无法使用 cuda kernel 加速量化计算。
量化代码中第 12 行确定了量化计算执行设备为 'cuda',在第 37 行执行量化逻辑时,我们传入了该字符串到 device 参数用来确定执行设备。一切都有条不紊地进行。
目标量化平台与部署平台
就像我们一开始说到的,PPQ 支持多个平台的量化与部署。因此在 PPQ 的量化逻辑中,你必须指定一个目标量化平台,和一个部署平台。
目标量化平台: 一个由 ppq.core.TargetPlatform 枚举描述的硬件平台代号,PPQ 将根据平台类型调整量化策略,例如目标平台TargetPlatform.PPL_CUDA_INT8,使用逐通道对称线性量化;TargetPlatform.DSP_INT8,使用逐层非对称量化。在 PPQ 中目标量化平台决定了量化策略。
部署平台:一个由 ppq.core.TargetPlatform 枚举描述的硬件平台代号,PPQ 将根据部署平台类型调整网络导出格式,从而适配目标平台后端的部署需求。
从逻辑上来说,PPQ 支持用户选择不一致的目标量化平台与部署平台,比如使用 TensorRT 的量化规则量化一个网络,然后部署到 DSP 上面。我并不能理解这样操作的必要性,但是你的确可以完成上述操作。

下表展示了 PPQ 目标量化平台差异性:
目标平台 | 量化策略 | 量化位宽 | 图融合策略 | 取整策略 | 部署平台 |
---|---|---|---|---|---|
PPL_CUDA_INT8, TensorRT | 逐通道线性对称量化(参数),逐层线性对称量化(激活值) | 8 bit(weight, activation), 32 bit(bias, bias 执行浮点运算) | Conv(Gemm)-Batchnorm 融合,计算节点与激活节点 融合,Conv - Add 融合,跨越非计算节点联合定点,Concat 联合定点 | ROUND_TO_NEAR_EVEN | PPL_CUDA_INT8, TensorRT |
NXP_INT8 | 逐通道线性对称量化(参数,Power-of-2),逐层线性对称量化(激活值,Power-of-2) | 8 bit(weight, activation), 32 bit(bias) | Conv(Gemm)-Batchnorm 融合,计算节点与激活节点 融合,跨越非计算节点联合定点,Concat 联合定点 | ROUND_HALF_UP,对于输入使用 ROUND_HALF_DOWN | NXP_INT8 |
DSP_INT8 | 逐层线性非对称量化 | 8 bit(weight, activation), 32 bit(bias) | Conv(Gemm)-Batchnorm 融合,计算节点与激活节点 融合,跨越非计算节点联合定点,Concat 联合定点 | ROUND_TO_NEAR_EVEN | DSP_INT8, SNPE |
在代码的第 40 行,我们确定了目标量化平台为 PPL_CUDA_INT8,在代码的第 47 行,我们使用相同平台导出了网络。
量化配置信息
在上述代码的 26 ~ 28 行,我们从 QuantizationSettingFactory 中获取了针对于 PPL_CUDA 平台的量化默认配置,并对它做出了修改。
在你自定义 PPQ 的量化逻辑之前,你都可以通过修改量化配置的方法来配置 PPQ 的量化逻辑,包括修改量化的图融合逻辑,算法逻辑,算子调度逻辑以及网络规范化逻辑等等
下表向你展示了目前 PPQ Quantization Setting 中你都可以修改那些内容:
参数名 | 作用 |
---|---|
dispatcher | 子图切分与调度算法,可从 'aggresive', 'conservative', 'PPLNN' 中三选一,不区分大小写 |
ssd_equalization | 是否启动 ssd equalization 算法修正网络参数,equalization 方法是一种有效的降低量化误差的神经网络调整算法,该算法将修改网络权重,使得量化误差降低。ssd equalization 使用运行时的 loss 来负责 equalization 算法取得更好的效果,这将耗费半个小时左右的时间。 |
equalization | 是否启动 equalization 算法修正网络参数,equalization 方法是一种有效的降低量化误差的神经网络调整算法,该算法将修改网络权重,使得量化误差降低。PPQ 使用一种定制化的 equalization 算法来修正网络参数,它通常是立即完成的。 |
quantize_activation | 是否需要对 activation 进行量化,注意虽然我们允许你不量化 activation,但当你导出网络时,我们会向你报告错误(未经量化的tensor不允许导出)。该选项仅为了方便你进行调试。 |
quantize_parameter | 是否需要对 parameter 进行量化,注意虽然我们允许你不量化 parameter,但当你导出网络时,我们会向你报告错误(未经量化的tensor不允许导出)。该选项仅为了方便你进行调试。 |
advanced_optimization | 是否启动 advanced_optim 优化算法,该算法整合了 adaround 和 bias correction 算法的功能,是一种短暂的在线训练方法,能够在10~30分钟内完成网络权重的重新训练。 |
fusion | 是否启动图融合与联合定点。 |
dispatching_table | 用户可以通过该属性来手动调度算子,使用dispatching_table.append(name_of_your_operation, platform) 来确定算子的调度平台。 |
在这里我们只粗略地向你展示了 PPQ 量化配置的使用方法,在每一项大的配置之后,还有诸多小的可配置参数,请参阅相关文档或对应代码段的注释了解它们所对应的功能。