模型量化(3):ONNX 模型的静态量化和动态量化
转自AI Studio,原文链接:模型量化(3):ONNX 模型的静态量化和动态量化 - 飞桨AI Studio1. 引入前面介绍了模型量化的基本原理也介绍了如何使用 PaddleSlim 对 Paddle 模型进行模型动态量化和静态量化这次就继续介绍如下量化使用 ONNXRuntime 对 ONNX 模型进行动态量化和静态量化2. 参考资料官网:onnxruntime.ai官方量化指南:Quan
转自AI Studio,原文链接:模型量化(3):ONNX 模型的静态量化和动态量化 - 飞桨AI Studio
1. 引入
-
前面介绍了模型量化的基本原理
-
也介绍了如何使用 PaddleSlim 对 Paddle 模型进行模型动态量化和静态量化
-
这次就继续介绍如下量化使用 ONNXRuntime 对 ONNX 模型进行动态量化和静态量化
2. 参考资料
3. 量化介绍
3.1 量化概述
-
ONNXRuntime 中的量化是指 ONNX 模型的 8 bit 线性量化。
-
在量化过程中,浮点实数值映射到 8 bit 量化空间,其形式为:
VAL_fp32 = Scale * (VAL_quantized - Zero_point)
-
Scale 是一个正实数,用于将浮点数映射到量化空间,计算方法如下:
-
对于非对称量化:
scale = (data_range_max - data_range_min) / (quantization_range_max - quantization_range_min)
-
对于对称量化:
scale = abs(data_range_max, data_range_min) * 2 / (quantization_range_max - quantization_range_min)
-
-
Zero_point 表示量化空间中的零。
-
重要的是,浮点零值在量化空间中可以精确地表示。这是因为许多 CNN 都使用零填充。
-
如果在量化后无法唯一地表示 0,则会导致精度误差。
-
3.2 量化方式
-
ONNXRuntime 支持两种模型量化方式:
-
动态量化:
-
对于动态量化,缩放因子(Scale)和零点(Zero Point)是在推理时计算的,并且特定用于每次激活
-
因此它们更准确,但引入了额外的计算开销
-
-
静态量化:
-
对于静态量化,它们使用校准数据集离线计算
-
所有激活都具有相同的缩放因子(Scale)和零点(Zero Point)
-
-
方法选择:
- 通常,建议对 RNN 和基于 Transformer 的模型使用动态量化,对 CNN 模型使用静态量化
-
3.3 量化类型
-
ONNXRuntime 支持两种量化数据类型:
-
Int8 (QuantType.QInt8): 有符号 8 bit 整型
-
UInt8 (QuantType.QUInt8): 无符号 8 bit 整型
-
数据类型选择:
-
结合激活和权重,数据格式可以是(activation:uint8,weight:uint8),(activation:uint8,weight:int8)等。
-
这里使用 U8U8 作为 (activation:uint8, weight:uint8) 的简写,U8S8 作为 (activation:uint8, weight:int8) 和 S8U8, S8S8 作为其他两种格式的简写。
-
CPU 上的 OnnxRuntime Quantization 可以运行 U8U8,U8S8 和 S8S8。
-
具有 QDQ 格式的 S8S8 是性能和准确性的默认设置,它应该是第一选择。
-
只有在精度下降很多的情况下,才能尝试U8U8。
-
请注意,具有 QOperator 格式的 S8S8 在 x86-64 CPU 上会很慢,通常应避免使用。
-
GPU 上的 OnnxRuntime Quantization 仅支持 S8S8 格式。
-
在具有 AVX2 和 AVX512 扩展的 x86-64 计算机上,OnnxRuntime 使用 U8S8 的 VPMADDUBSW 指令来提高性能,但此指令会遇到饱和问题。
-
一般来说,对于最终结果来说,这不是一个大问题。
-
如果某些模型的精度大幅下降,则可能是由饱和度引起的。
-
在这种情况下,您可以尝试 reduce_range 或 U8U8 格式,没有饱和度问题。
-
在其他 CPU 架构(使用 VNNI 和 ARM 的 x64)上没有这样的问题。
-
-
3.4 量化格式
-
ONNXRuntime 支持两种量化模型格式:
-
Tensor Oriented, aka Quantize and DeQuantize (QuantFormat.QDQ):
- 该格式使用 DQ (Q (tensor)) 来模拟量化和去量化过程,并且 QuantizeLinear 和DeQuantizeLinear 算子也携带量化参数
-
Operator Oriented (QuantFormat.QOperator):
- 所有量化运算符都有自己的 ONNX 定义,如QLinearConv、MatMulInteger 等
-
4. 量化实践
4.1 安装依赖
-
转换 Paddle 模型至 ONNX 格式需要 Paddle2ONNX 模块
-
量化 ONNX 模型需要依赖 ONNX 和 ONNXRuntime 两个模块
-
使用如下代码进行安装:
In [ ]
!pip install onnxruntime onnx paddle2onnx
4.2 模型准备
-
这里仍然使用 PaddlClas 提供的 MobileNet V1 预训练模型
-
在开始模型量化之前需要先将 Paddle 格式的模型转换为 ONNX 格式
-
这里使用 Paddle2ONNX 的命令行命令进行模型格式转换
-
具体的下载和转换命令如下:
In [ ]
# 下载模型文件
!wget -P models https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/inference/MobileNetV1_infer.tar
# 解压缩模型文件
!cd models && tar -xf MobileNetV1_infer.tar
# 模型转换
!paddle2onnx \
--model_dir models/MobileNetV1_infer \
--model_filename inference.pdmodel \
--params_filename inference.pdiparams \
--save_file models/MobileNetV1_infer.onnx \
--opset_version 12
4.3 动态量化
-
动态量化只转换模型的参数类型,无需额外数据,所以非常简单
-
只需要调用 ONNXRuntime 的 quantize_dynamic 接口即可实现模型动态量化
-
具体的量化代码如下:
In [3]
from onnxruntime.quantization import QuantType, quantize_dynamic
# 模型路径
model_fp32 = 'models/MobileNetV1_infer.onnx'
model_quant_dynamic = 'models/MobileNetV1_infer_quant_dynamic.onnx'
# 动态量化
quantize_dynamic(
model_input=model_fp32, # 输入模型
model_output=model_quant_dynamic, # 输出模型
weight_type=QuantType.QUInt8, # 参数类型 Int8 / UInt8
optimize_model=True # 是否优化模型
)
4.4 静态量化
-
因为需要额外的数据用于校准模型,所以相比动态量化,静态量化更加复杂一些
-
需要先编写一个校准数据的读取器,然后再调用 ONNXRuntime 的 quantize_static 接口进行静态量化
-
具体的量化代码如下:
In [4]
# 解压数据集
!mkdir ~/data/ILSVRC2012
!tar -xf ~/data/data68594/ILSVRC2012_img_val.tar -C ~/data/ILSVRC2012
In [5]
import os
import numpy as np
from PIL import Image
from paddle.vision.transforms import Compose, Resize, CenterCrop, Normalize
from onnxruntime.quantization import CalibrationDataReader, QuantFormat, quantize_static, QuantType, CalibrationMethod
from onnxruntime import InferenceSession, get_available_providers
# 模型路径
model_fp32 = 'models/MobileNetV1_infer.onnx'
model_quant_static = 'models/MobileNetV1_infer_quant_static.onnx'
# 数据预处理
'''
缩放 -> 中心裁切 -> 类型转换 -> 转置 -> 归一化 -> 添加维度
'''
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
val_transforms = Compose(
[
Resize(256, interpolation="bilinear"),
CenterCrop(224),
lambda x: np.asarray(x, dtype='float32').transpose(2, 0, 1) / 255.0,
Normalize(mean, std),
lambda x: x[None, ...]
]
)
# 用于校准的图像数据
'''
读取图像 -> 预处理 -> 组成数据字典
'''
img_dir = 'data/ILSVRC2012'
img_num = 32
datas = [
val_transforms(
Image.open(os.path.join(img_dir, img)).convert('RGB')
) for img in os.listdir(img_dir)[:img_num]
]
# 数据批次读取器
def batch_reader(datas, batch_size):
_datas = []
length = len(datas)
for i, data in enumerate(datas):
if batch_size==1:
yield {'inputs': data}
elif (i+1) % batch_size==0:
_datas.append(data)
yield {'inputs': np.concatenate(_datas, 0)}
_datas = []
elif i<length-1:
_datas.append(data)
else:
_datas.append(data)
yield {'inputs': np.concatenate(_datas, 0)}
# 构建校准数据读取器
'''
实质是一个迭代器
get_next 方法返回一个如下样式的字典
{
输入 1: 数据 1,
...
输入 n: 数据 n
}
记录了模型的各个输入和其对应的经过预处理后的数据
'''
class DataReader(CalibrationDataReader):
def __init__(self, datas, batch_size):
self.datas = batch_reader(datas, batch_size)
def get_next(self):
return next(self.datas, None)
# 实例化一个校准数据读取器
data_reader = DataReader(datas, 32)
# 静态量化
quantize_static(
model_input=model_fp32, # 输入模型
model_output=model_quant_static, # 输出模型
calibration_data_reader=data_reader, # 校准数据读取器
quant_format= QuantFormat.QDQ, # 量化格式 QDQ / QOperator
activation_type=QuantType.QInt8, # 激活类型 Int8 / UInt8
weight_type=QuantType.QInt8, # 参数类型 Int8 / UInt8
calibrate_method=CalibrationMethod.MinMax, # 数据校准方法 MinMax / Entropy / Percentile
optimize_model=True # 是否优化模型
)
2022-05-10 23:41:45.694213577 [W:onnxruntime:, execution_frame.cc:806 VerifyOutputSizes] Expected shape from model of {} does not match actual shape of {1,1} for output linear_1.tmp_1_ReduceMax 2022-05-10 23:41:45.694294753 [W:onnxruntime:, execution_frame.cc:806 VerifyOutputSizes] Expected shape from model of {} does not match actual shape of {1,1} for output linear_1.tmp_1_ReduceMin 2022-05-10 23:41:46.206495944 [W:onnxruntime:, execution_frame.cc:806 VerifyOutputSizes] Expected shape from model of {} does not match actual shape of {1,1} for output flatten_0.tmp_0_ReduceMax 2022-05-10 23:41:46.206616773 [W:onnxruntime:, execution_frame.cc:806 VerifyOutputSizes] Expected shape from model of {} does not match actual shape of {1,1} for output flatten_0.tmp_0_ReduceMin 2022-05-10 23:41:46.210374588 [W:onnxruntime:, execution_frame.cc:806 VerifyOutputSizes] Expected shape from model of {} does not match actual shape of {1,1} for output linear_1.tmp_0_ReduceMax 2022-05-10 23:41:46.210450058 [W:onnxruntime:, execution_frame.cc:806 VerifyOutputSizes] Expected shape from model of {} does not match actual shape of {1,1} for output linear_1.tmp_0_ReduceMin /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/onnx/numpy_helper.py:93: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations if arr.dtype == np.object:
5. 对比测试
5.1 文件大小
-
量化前后的模型文件大小如下表所示:
模型 大小 原始模型 16.3MB 优化模型 16.1MB 动态量化 4.1MB 静态量化 4.1MB -
可以看到量化后的模型文件大小约为原始模型的 1/4
5.2 运行速度
-
这里采用多次前向计算来对比量化前后模型的运行速度
-
可以看出,动态量化模型在运行速度上没有优势
-
而静态量化模型的运行速度在 AIStudio 的 CPU 环境中表现差不多,甚至会有一些下降
-
如果在一些专门为 Int8 优化的设备上,量化模型的表现将会更加优秀
In [6]
import os
import time
import numpy as np
from PIL import Image
from paddle.vision.transforms import Compose, Resize, CenterCrop, Normalize
from onnxruntime import InferenceSession, get_available_providers
# 模型基类
class Session:
def __init__(self, model_onnx):
self.session = InferenceSession(model_onnx, providers=get_available_providers())
def __call__(self, x):
outputs = self.session.run([], {'inputs': x})
return outputs
def benchmark(self, x, warmup=5, repeat=10):
for i in range(warmup):
self(x)
start = time.time()
for i in range(repeat):
self(x)
return time.time() - start
# 图像预处理
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
val_transforms = Compose(
[
Resize(256, interpolation="bilinear"),
CenterCrop(224),
lambda x: np.asarray(x, dtype='float32').transpose(2, 0, 1) / 255.0,
Normalize(mean, std),
lambda x: x[None, ...]
]
)
# 加载模型
dynamic = Session('models/MobileNetV1_infer_quant_dynamic.onnx')
static = Session('models/MobileNetV1_infer_quant_static.onnx')
session = Session('models/MobileNetV1_infer.onnx')
# 加载测试数据
img_dir = 'data/data143470'
imgs = np.concatenate([val_transforms(Image.open(os.path.join(img_dir, img)).convert('RGB')) for img in os.listdir(img_dir)], 0)
# 速度测试
warmup = 5
repeat = 20
time_session = session.benchmark(imgs, warmup, repeat)
time_dynamic = dynamic.benchmark(imgs, warmup, repeat)
time_static = static.benchmark(imgs, warmup, repeat)
# 打印结果
print('原始模型重复 %d 次前向计算耗时:%f s' % (repeat, time_session))
print('动态量化模型重复 %d 次前向计算耗时:%f s' % (repeat, time_dynamic))
print('静态量化模型重复 %d 次前向计算耗时:%f s' % (repeat, time_static))
原始模型重复 20 次前向计算耗时:3.098398 s 动态量化模型重复 20 次前向计算耗时:28.396264 s 静态量化模型重复 20 次前向计算耗时:2.297622 s
5.3 模型精度
- 由于 ImageNet 数据集过大,这里不太好演示
- 所以就简单对量化前后模型输出的结果进行对比
- 可以看到基本上精度表现上会有些许下降
In [7]
outputs_dynamic = dynamic(imgs)[0]
outputs_static = static(imgs)[0]
outputs_session = session(imgs)[0]
argmax_dynamic = outputs_dynamic.argmax(-1)
argmax_static = outputs_static.argmax(-1)
argmax_session = outputs_session.argmax(-1)
MSE = lambda inputs, labels: ((inputs-labels)**2).mean()
mse_dynamic = MSE(outputs_dynamic, outputs_session)
mse_static = MSE(outputs_static, outputs_session)
print('原始模型结果:', argmax_session)
print('动态量化模型结果:', argmax_dynamic)
print('静态量化模型结果:', argmax_static)
print('动态量化 MSE:', mse_dynamic)
print('静态量化 MSE:', mse_static)
原始模型结果: [308 943] 动态量化模型结果: [308 943] 静态量化模型结果: [308 943] 动态量化 MSE: 1.2413246e-05 静态量化 MSE: 8.649646e-05
6. 尾巴
-
模型量化主要用于端侧部署,ONNX 作为通用的模型格式,大部分的端侧硬件和推理框架都支持该格式
-
ONNXRuntime 相比 PaddleSlim 在量化模型的使用上稍微简单和流畅,可以相对轻松的进行部署测试
-
所以 ONNX 搭配 ONNXRuntime 是一个不错的量化工具和格式选择
更多推荐
所有评论(0)