6. 量化说明

6.1 量化介绍

6.1.1 量化定义

模型量化是指将深度学习模型中的浮点参数和操作转换为定点表示,如FLOAT32转换为INT8等。量化能够降低内存占用,实现模型压缩和推理加速,但会造成一定程度的精度损失。

6.1.2 量化计算原理

以线性非对称量化为例,浮点数量化为有符号定点数的计算原理如下:

[ x_{\text{int}} = \text{clamp}\Big( \left\lfloor \frac{x}{s} \right\rfloor + z; , -2^{b-1},, 2^{b-1} - 1 \Big) \tag{6-1} ]

其中( x )为浮点数,( x_{\text{int}} )为量化定点数,( \lfloor \cdot \rfloor )为四舍五入运算,( s )为量化比例因子,( z )为量化零点,( b )为量化位宽,如INT8数据类型中( b )为8;( \text{clamp} )为截断运算,具体定义如下:

[ \text{clamp}(x; a, c) = \begin{cases} a, & x < a, \ x, & a \leq x \leq c, \ c, & x > c, \end{cases} \tag{6-2} ]

从定点数转换为浮点数称为反量化过程,具体定义如下:

[ x \approx \hat{x} = s(x_{\text{int}} - z) \tag{6-3} ]

设量化范围为( (q_{\text{min}}) ),截断范围为( (c_{\text{min}}) ),量化参数( s )和( z )的计算公式如下:

[ s = \frac{q_{\text{max}} - q_{\text{min}}}{c_{\text{max}} - c_{\text{min}}} = \frac{q_{\text{max}} - q_{\text{min}}}{2^b - 1} \tag{6-4} ]

[ z = c_{\text{max}} - \left\lfloor \frac{q_{\text{max}}}{s} \right\rfloor \quad \text{或} \quad z = c_{\text{min}} - \left\lfloor \frac{q_{\text{min}}}{s} \right\rfloor \tag{6-5} ]

其中截断范围是根据量化的数据类型决定,例如INT8的截断范围为( (-128, 127) );量化范围根据不同的量化算法确定,具体可参考6.1.6 量化算法章节。

6.1.3 量化误差

量化会造成模型一定程度的精度丢失。根据公式(6-1)可知,量化误差来源于舍入误差和截断误差,即 ( \lfloor \cdot \rfloor ) 和 clamp 运算。四舍五入的计算方式会产生舍入误差,误差范围为 ( (-\frac{1}{2}s, \frac{1}{2}s) )。当浮点数 ( x ) 过大,比例因子 ( s ) 过小时,容易导致量化定点数超出截断范围,产生截断误差。理论上,比例因子 ( s ) 的增大可以减小截断误差,但会造成舍入误差的增大。因此为了权衡两种误差,需要设计合适的比例因子和零点,来减小量化误差。

6.1.4 线性对称量化和线性非对称量化

线性量化中定点数之间的间隔是均匀的,例如INT8线性量化将量化范围均匀等分为256个数。线性对称量化中零点是根据量化数据类型确定并且零点 ( z ) 位于量化定点数范围上的中心对称点,例如INT8中零点为0。线性非对称量化中零点根据公式(6-5)计算确定并且零点 ( z ) 一般不在量化定点数范围上的中心对称点。

对称量化是非对称量化的简化版本,理论上非对称量化能够更好的处理数据分布不均匀的情况,因此实践中大多采用非对称量化方案。 图6-1 线性对称量化和线性非对称量化

6.1.5 Per-Layer量化和Per-Channel量化

Per-Layer量化将网络层的所有通道作为一个整体进行量化,所有通道共享相同的量化参数。Per-Channel量化将网络层的各个通道独立进行量化,每个通道有自己的量化参数。Per-Channel量化更好的保留各通道的信息,能够更好的适应不同通道之间的差异,提供更好的量化效果。

图6-2 Per-Layer量化和Per-Channel量化 注:RKNN-Toolkit2中的Per-Channel量化中只针对权重进行Per-Channel量化,激活值和中间值仍为Per-Layer量化。

6.1.6 量化算法

量化比例因子( s )和零点( z )是影响量化误差的关键参数,而量化范围的求解对量化参数起到决定性作用。本章节介绍三种关于量化范围求解的算法,Normal,KL-Divergence和MMSE。

Normal量化算法是通过计算浮点数中的最大值和最小值直接确定量化范围的最大值和最小值。从6.1.2 量化计算原理可知,Normal量化算法不会产生截断误差,但对异常值很敏感,因为大异常值可能会导致舍入误差过大。

[ q_{\text{min}} = \min , \mathbf{V} \tag{6-6} ]

[ q_{\text{max}} = \max , \mathbf{V} \tag{6-7} ]

其中( \mathbf{V} )为浮点数Tensor。

KL-Divergence量化算法计算浮点数和定点数的分布,通过调整不同的阈值来更新浮点数和定点数的分布,并根据KL散度最小化两个分布的相似性来确定量化范围的最大值和最小值。KL-Divergence量化算法通过最小化浮点数和定点数之间的分布差异,能够更好地适应非均匀的数据分布并缓解少数异常值的影响。

[ \underset{q_{\text{min}}, q_{\text{max}}}{\arg \min} , H\big( \Psi(\mathbf{V}), , \Psi(\mathbf{V}_{\text{int}}) \big) \tag{6-8} ]

其中( H(\cdot, \cdot) )为KL散度计算公式,( \Psi(\cdot) )为分布函数,将对应数据计算为离散分布,( \mathbf{V}_{\text{int}} )为量化定点数Tensor。

MMSE量化算法通过最小化浮点数与量化反量化后浮点数的均方误差损失,确定量化范围的最大值和最小值,在一定程度上缓解大异常值带来的量化精度丢失问题。由于MMSE量化算法的具体实现是采用暴力迭代搜索近似解,速度较慢,内存开销较大,但通常会比Normal量化算法具有更高的量化精度。

[ \underset{q_{\text{min}}, q_{\text{max}}}{\arg \min} \left\lVert \mathbf{V} - \hat{\mathbf{V}}(q_{\text{min}}, q_{\text{max}}) \right\rVert_F^2 \tag{6-9} ]

其中( \hat{\mathbf{V}}(q_{\text{min}}, q_{\text{max}}) )为( \mathbf{V} )的量化、反量化形式,( \left\lVert \cdot \right\rVert_F )为F范数。

6.2 量化配置

6.2.1 量化数据类型

RKNN-Toolkit2支持的量化数据类型为INT8。

6.2.2 量化算法建议

Normal量化算法运行速度快,适用于一般场景。
KL-Divergence量化算法运行速度慢于Normal量化算法,对于存在非均匀分布的部分模型能够改善量化精度,部分场景下能够缓解少数异常值造成的量化精度丢失问题。
MMSE量化算法运行速度较慢,内存消耗大,相比KL_Divergence量化算法能够更好的缓解异常值造成的量化精度丢失问题。对于量化友好的模型可尝试使用MMSE量化算法来提高量化精度,因为在多数场景下MMSE量化精度要高于Normal和KL-Divergence量化算法。
默认情况下使用Normal量化算法,当遇到量化精度问题时可尝试使用KL-Divergence和MMSE量化算法。

6.2.3 量化校正集建议

量化校正集用于计算激活值的量化范围,在选择量化校正集时应覆盖模型实际应用场景的不同数据分布,例如对于分类模型,量化校正集应包含实际应用场景中不同类别的图片。一般推荐量化校正集数量为20-200张,可根据量化算法的运行时间适当增减。需要注意的是,增加量化校正集数量会增加量化算法的运行时间但不一定能提高量化精度。

6.2.4 量化配置方法

RKNN-Toolkit2中量化的配置方法在rknn.config()rknn.build()接口实现。其中量化方法配置由rknn.config()接口实现,量化开关和校正集路径的选择由rknn.build()接口实现。

rknn.config()接口包含以下相关量化配置项:

  1. quantized_dtype:选择量化类型,目前仅支持线性非对称的INT8量化,默认为asymmetric_quantized-8

  2. quantized_algorithm:选择量化算法,包括Normal,KL-Divergence和MMSE量化算法。可选值为normalkl_divergencemmse,默认为normal

  3. quantized_method:选择Per-Layer和Per-Channel量化。可选值为layerchannel,默认为channel

rknn.build()接口包含以下相关量化配置项:

  1. do_quantization:是否开启量化,默认为False

  2. dataset:量化校正集的路径,默认为空。

目前支持文本文件格式,用户可以把用于校正的图片(jpgpng格式)或npy文件路径放到一个.txt文件中。文本文件里每一行为一条路径信息,如:

a.jpg
b.jpg

如有多个输入,则每个输入对应的文件用空格隔开,如:

a0.jpg a1.jpg
b0.jpg b1.jpg

6.3 混合量化

混合量化对模型不同层采用不同的量化数据类型,将不适合量化的层使用较高精度的数据类型表达,以此缓解模型量化精度损失的问题,但混合精度量化会增加额外开销,并且需要用户确定不同层的量化数据类型。

6.3.1 混合量化用法

为了在性能和精度之间做更好的平衡,RKNN-Toolkit2提供了混合量化功能,用户可以通过精度分析的输出结果来手动指定各层是否进行量化。

目前混合量化功能支持如下用法:

  1. 将指定的量化层改成非量化层,如用FLOAT16进行计算。(因NPU上非量化算力较低,所以推理速度会有一定降低)。

  2. 每一层的量化参数也可以进行修改。(量化参数不建议修改)

6.3.2 混合量化使用流程

使用混合量化功能时,具体分四步进行。

  1. 加载原始模型,生成量化配置文件、临时模型文件和数据文件。具体的接口调用流程如下:

图6-3 混合量化第一步

  1. 修改第一步生成的量化配置文件。

第一步调用混合量化接口hybrid_quantization_step1完成后会在当前目录下生成名为{model_name}.quantization.cfg的配置文件。配置文件格式如下:

custom_quantize_layers:
  Conv__350:0: float16
  Conv__358:0: float16
  ...
quantize_parameters:
  ...
  FeatureExtractor/MobilenetV2/Conv/Relu6:0:
    qtype: asymmetric_quantized
    qmethod: layer
    dtype: int8
    min: -0.0
    max: -6.0
    scale: -0.023529411764705882
    zero_point: -128
  ...

custom_quantize_layers下每一行可按照tensor名: 量化类型的格式添加自定义量化层,该tensor对应层的运算类型即改为指定运算类型。目前量化数据类型可选择float16

quantize_parameters下是模型中每个tensor的量化参数。每个tensor的量化参数按照tensor名: 量化属性和参数的格式呈现。其中min/max代表量化范围的最小最大值,tensor名可根据精度分析输出结果查看或使用Netron打开临时模型文件{model_name}.model查看对应输出tensor名。
3. 生成 RKNN 模型。具体的接口调用流程如下: 图6-3 混合量化第三步

  1. 使用第三步生成的 RKNN 模型进行推理。
    注:RKNN-Toolkit2工程中examples/functions/hybrid_quant目录下提供了一个混合量化的例子,具体可以参考该例子对模型进行混合量化。

6.4 量化感知训练

6.4.1 QAT简介

量化感知训练(英文名称 Quantization-aware Training,简称 QAT)是一种量化训练方式,该方式旨在解决低比特量化的精度损失问题。低比特量化会掉精度,是因为值域从浮点数调整到定点数时会有精度损失,QAT训练时会将量化误差计入训练的损失函数,训练出一个带量化参数的模型。

与 RKNN-Toolkit2 工具提供的后训练量化(英文名称 Post Training Quantization,简称 PTQ)对比,两种量化方法的特点如下:

量化方法 基于原始框架二次训练 数据集 权重参数是否被调整 损失函数 性能
后训练量化(PTQ) 不需要 少量未标注数据 无关 最优
量化感知训练(QAT) 需要 完整的训练数据集 量化损失计入训练损失函数 存在算子不支持QAT时,性能略弱于 PTQ

6.4.2 QAT原理

量化感知训练时,所有权重的存放格式、算子的计算单元都是按照浮点数进行的,这是为了保证反向传播功能可以正常生效、模型的训练可以正常进行。与训练浮点模型不同,在模型可被量化的位置上,量化感知训练会插入FakeQuantize模块进行伪量化操作,模拟浮点数调整到定点数的精度损失,使其能被损失函数识别、优化,最终使模型转为定点模型时仍可以保持准确的推理结果。

量化感知训练目前已被广泛使用,各主流推理框架皆有实现,可参考下文所列链接获取更详细的使用说明:

6.4.3 QAT使用依据

由于QAT需要增加额外的训练代码,且部分开源仓库的代码可能与QAT功能存在冲突,推荐在同时满足以下两种情况时,考虑使用QAT训练进行模型量化:

  1. 参考章节7进行量化精度排查,确认RKNN的 PTQ 功能不满足精度要求。

  2. 尝试章节6.3进行混合量化,确认RKNN的混合量化功能不满足精度、性能要求。

6.4.4 QAT实现简例及配置说明

以下是各框架 QAT 功能的说明文档,实际使用请以官方文档为准:

这里我们以 Pytorch 为例,说明实现 QAT 的流程及一些需要注意的地方。

# for 1.10 <= torch <= 1.13
import torch

class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = torch.nn.Conv(3, 8, 3, 1)

    def forward(self, x):
        x = self.conv(x)
        return x

# initialize a floating point model
float_model = M().train()

from torch.quantization import quantize_fx, QConfig, FakeQuantize, MovingAverageMinMaxObserver, \
    MovingAveragePerChannelMinMaxObserver

qconfig = QConfig(activation=FakeQuantize.with_args(observer=
    MovingAverageMinMaxObserver,
    quant_min=0,
    quant_max=255,
    reduce_range=False),  # reduce_range 默认是True
    weight=FakeQuantize.with_args(observer=
    MovingAveragePerChannelMinMaxObserver,
    quant_min=-128,
    quant_max=127,
    dtype=torch.qint8,
    qscheme=torch.per_channel_affine,  # 参数 qscheme 默认是 torch.per_channel_symmetric
    reduce_range=False))
qconfig_dict = {"": qconfig}
model_qat = quantize_fx.prepare_qat_fx(float_model, qconfig_dict)

# define the training loop for quantization aware training
def train_loop(model, train_data):
    model.train()

    for image, target in data_loader:
        ...

# Run training
train_loop(model_qat, train_data)

model_qat = quantize_fx.convert_fx(model_qat)

以上流程代码中,除了qconfig的配置针对RKNN硬件做了部分调整,其余操作均按照官方代码的指引实现。qconfig中主要有以下两处改动:
activation量化配置指定reduce_rangeFalsereduce_rangeFalse时,有效量化数值范围为-128~127reduce_rangeTrue时,有效量化数值范围为-64~63,量化效果较差。RKNN 硬件支持reduce_rangeFalse
weight量化配置指定qschemetorch.per_channel_affine。默认的torch.per_channel_symmetric会限制zero_point固定为0,RKNN硬件支持zero_point非0,故选择torch.per_channel_affine

6.4.5 QAT支持的算子

以 Pytorch 为例,使用 QAT 量化的过程中,先对带有权重参数的 ConvLinear 采取 QAT 规则量化,再检验其他算子,若符合常规量化规则,则对其采用常规量化。在算子不符合 QAT 或常规量化规则的情况下,算子会维持 FP32 的计算规则。

不同框架的支持情况存在差异,用户可以参考以下链接,根据使用的框架及版本,查寻更详细的量化算子支持情况。

6.4.6 QAT模型中浮点算子的处理

QAT模型中,当算子无法量化,会采用浮点进行计算。这类算子在转为RKNN模型时,分为两种情况讨论。

  1. 前后为可量化算子: ![](images/RKNN_SDK_高级用户指南/QAT OP前后为可量化算子状态.png) 图6-5 QAT OP前后为可量化算子状态 如上图左边所示,图中的 gelu 算子在原模型中为浮点算子,其前后的 conv 为量化算子,RKNN-Toolkit2 在加载模型时,会将两个已量化算子中间的浮点算子转为量化算子,提升推理性能,这个操作不会影响精度。转为 RKNN 模型后,结构如上图右边所示。

  2. 前后存在非量化算子: ![](images/RKNN_SDK_高级用户指南/QAT OP前后为非量化算子状态.png) 图6-5 QAT OP前后为非量化算子状态 如上图左边所示,图中的 gelusoftmax 算子在原模型中为浮点算子,其前后的 conv 为量化算子,RKNN-Toolkit2 在加载模型时,由于 gelusoftmax 中间的量化参数缺失,gelusoftmax 仍保持浮点类型。转为 RKNN 模型后结构如上图右边所示,图中 RKNN 模型在 gelu 的前面插入反量化算子,softmax 后面插入量化算子,这些插入的量化、反量化算子都会增加额外的耗时。

6.4.7 QAT经验总结

  1. QAT配置

根据不同硬件的特性,QAT训练往往需要调整配置才能达到更好的效果。对于RKNP而言,建议参考6.4.4的代码说明配置qconfig参数。

  1. 模型中保存的量化参数可能需要二次调整

sigmoid为例子,模型中sigmoid算子记录的量化参数可能不是实际推理时使用的量化参数。在官方的代码(https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/quantized/cpu/qsig_moid.cpp )中,sigmoid的量化参数在推理时会进行调整,将min置为0,max置为1。

针对这类算子,RKNN-Toolkit2在转换QAT模型时,会在模型转换阶段调整其量化参数,使推理结果和Pytorch原始推理结果更接近。