模型量化小结

针对深度学习算法模型参数大,部署后会占用内存、计算量增加及能耗巨大。不利于后期的一个维护,是因为每一个任务单独进行。多任务学习可以有效的缓解以上问题。但多任务学习可能会带来一些问题,相似任务之间出现混乱。难训练,可以从另外一个角度出发,对模型进行量化:其主要是通过减少原始模型参数的数量或比特数来实现对内存和计算需求的降低,从而进一步降低能耗。目前性能最稳定的就是INT8的模型量化技术,相对于原始模型的FP32计算相比,INT8量化可将模型大小减少 4 倍,并将内存带宽要求减少 4 倍,对 INT8 计算的硬件支持通常快 2 到 4 倍。 值得注意的是量化主要是一种加速前向推理的技术,并且绝大部分的量化算子仅支持前向传递。

1.应用范围

数据类型:

weight的8 bit量化 :data_type = qint8,数据范围为[-128, 127]
activation的8 bit量化:data_type = quint8,数据范围为[0, 255]
bias一般是不进行量化操作的,仍然保持float32的数据类型,还有一个需要提前说明的,weight在浮点模型训练收敛之后一般就已经固定住了,所以根据原始数据就可以直接量化,然而activation会因为每次输入数据的不同,导致数据范围每次都是不同的,所以针对这个问题,在量化过程中专门会有一个校准过程,即提前准备一个小的校准数据集,在测试这个校准数据集的时候会记录每一次的activation的数据范围,然后根据记录值确定一个固定的范围。

支持后端:

具有 AVX2 支持或更高版本的 x86 CPU:fbgemm
ARM CPU:qnnpack

2. 量化方法分类

Post Training Dynamic Quantization:这是最简单的一种量化方法,Post Training指的是在浮点模型训练收敛之后进行量化操作,其中weight被提前量化,而activation在前向推理过程中被动态量化,即每次都要根据实际运算的浮点数据范围每一层计算一次scale和zero_point,然后进行量化;

Post Training Static Quantization:第一种不是很常见,一般说的Post Training Quantization指的其实是这种静态的方法,而且这种方法是最常用的,其中weight跟上述一样也是被提前量化好的,然后activation也会基于之前校准过程中记录下的固定的scale和zero_point进行量化,整个过程不存在量化参数_(_scale和zero_point)的再计算;

Quantization Aware Training:对于一些模型在浮点训练+量化过程中精度损失比较严重的情况,就需要进行量化感知训练,即在训练过程中模拟量化过程,数据虽然都是表示为float32,但实际的值的间隔却会受到量化参数的限制。 至于为什么不在一开始训练的时候就模拟量化操作是因为8bit精度不够容易导致模型无法收敛,甚至直接使用16bit进行from scrach的量化训练都极其容易导致无法收敛。

3.以onnx模型量化为例

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,则会导致精度误差。

量化方式:ONNXRuntime 支持两种模型量化方式:

动态量化:

对于动态量化,缩放因子(Scale)和零点(Zero Point)是在推理时计算的,并且特定用于每次激活。
因此它们更准确,但引入了额外的计算开销

静态量化:

对于静态量化,它们使用校准数据集离线计算
所有激活都具有相同的缩放因子(Scale)和零点(Zero Point)

方法选择:

通常,建议对 RNN 和基于 Transformer 的模型使用动态量化,对 CNN 模型使用静态量化

量化类型: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)上没有这样的问题。

量化格式:ONNXRuntime 支持两种量化模型格式:

Tensor Oriented, aka Quantize and DeQuantize (QuantFormat.QDQ)

该格式使用 DQ (Q (tensor)) 来模拟量化和去量化过程,并且 QuantizeLinear 和DeQuantizeLinear 算子也携带量化参数

Operator Oriented (QuantFormat.QOperator)

所有量化运算符都有自己的 ONNX 定义,如QLinearConv、MatMulInteger 等。

4.onnx量化脚本,分割模型量化为例,亲测可用

# fp_32 to int8
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType
#需要量化的onnx模型
model_fp32 = 'input.onnx'
#量化后的模型
model_quant = 'output1.onnx'
quantized_model = quantize_dynamic(model_fp32, model_quant, weight_type=QuantType.QUInt8)


# QAT quantization  QAT量化
import onnx
from onnxruntime.quantization import quantize_qat, QuantType
#需要量化的onnx模型
model_fp32 = 'input.onnx'
#量化后的模型
model_quant = 'output2.onnx'
quantized_model = quantize_qat(model_fp32, model_quant)
#fp_32 to fp_16
import onnx
from onnxconverter_common import float16
model=onnx.load('your.onnx')
model_fp16=float16.convert_float_to_float16(model)
onnx.save(model_fp16,'your_result.onnx')

如果量化后模型表现不佳,可考虑部分量化,也即是mixed precision。

import onnx
from onnxconverter_common import float16
model=onnx.load('your.onnx')
model_fp16=auto_convert_mixed_precision(model,test_data,rtol=0.01,atol=0.001,keep_io_types=Ture)
onnx.save(model_fp16,'your_result.onnx')

猜你喜欢

转载自blog.csdn.net/hasque2019/article/details/129952687