转自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 是一个不错的量化工具和格式选择

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐