量化
Papers
- Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference: 1712.05877.pdf
- Quantizing deep convolutional networks for efficient inference: A whitepaper: 1806.08342.pdf
量化介绍
量化优势:降低模型大小、推理时延,进而降低内存消耗和功耗。
端到端量化流程通常包括:
- 量化准备,二选一
- 量化训练:云侧,加载浮点模型,插入伪量化算子(量化-反量化、统计、代理梯度),模拟量化带来的权重和激活值变化,微调权重以适应量化,统计权重、激活值min/max。
- 量化校准,云侧,加载浮点模型,插入统计算子(也可以先加上量化-反量化),基于校准集(少量数据集)进行推理,统计激活值min/max。
- 量化转换:云侧,统计权重min/max,计算权重的缩放系数和偏移(scale,offset)并进行量化;计算激活值的(scale,offset);生成量化模型。
- 量化推理:端侧,加载量化模型(整型),插入真量化算子(激活值再量化,输入量化、输出反量化),进行量化推理。
浮点数转为低bit,会引入量化误差,减少量化损失有以下几种方案:
- 模型参数:有较多/冗余参数的模型量化损失较小。
- 量化策略:非对称量化比对称量化损失小,Per Channel(每通道,CinKK)量化比Per Layer(每层,CoutCinKK)量化损失小。
- 网络结构:权重量化损失大,激活值量化损失小,可能是由于BN/ReLU等算子本身限定了激活值的范围,具有一定的量化效果。
量化算法
Quantization scheme: (Per Layer, Per Channel)与(Symetric, Affine)的组合。
对于Per Layer/Channel量化,通常:
- 对权重行对称量化:权重分布对称,如[-1, 1],对称量化即可。但实际测试发现大部分权重数据还是有偏的,需采用非对称量化。
- 对激活值行非对称量化:经过ReLU6等算子的Activation分布常非对称,如[0, 6],采用对称量化会导致
1.数据密集的区间离散程度加剧,精度损失较大;
2.数据稀疏的区间空间浪费,如[-128, 0]区间并无激活值分布。
对称量化
类型关系:float = scale x int
q = round(x/scale),其中q为量化后的Signed Int8类型,x为Float类型,scale为Float类型(待计算的参数)。对称量化的zero_point为0(简写为zp,无需计算的参数)。
scale = 2max(|x_min|, x_max) / (q_max - q_min),其中:
- x_min/x_max为量化前最小/最小大值,因为采用对称量化,故取正负对称的区间。可以人为设定x_min/x_max,对于超出范围的值进行饱和式截断,即超出范围的值被设置为边界值。
- q_min/q_max为量化后的最小/最大值,即量化后的值的范围,也是对称的,如[-128, 127]。
非对称量化
类型关系:float = scale x (uint - zp)
q = round(x/scale) + zp,其中q为量化后的Unsigned Int8类型,x为Float类型,scale为Float类型,zp为Int32类型,对应Float数值的0.0,可用在卷积的padding等过程中。
scale = (x_max - x_min) / (q_max - q_min)
zp = q_min - round(x_min/scale),或q_max - round(x_max/scale)
- x_min/x_max为量化前最小/最小大值。可以人为设定x_min/x_max,对于超出范围的值进行饱和式截断,即超出范围的值被设置为边界值。
- q_min/q_max为量化后的最小/最大值,即量化后的值的范围,q_min=0, q_max=2^n
- zero_point为0。(一些工作使用offset = -zero_point)
权重量化
直接进行int8对称量化,统计得出。注意,
均采用对称/非对称量化公式统计得出。
Bias量化
Bias通常量化为int32,,其中输入和权重缩放到int8,相乘后Bias一般缩放到int16,in32足够。
模型输入量化
- TFLite等,未在输入节点后插入伪量化算子,需要人为统计数据集的min/max并计算(scale,zp),如scale=1/128,zp=128。
- scale = (x_max - x_min) / (q_max - q_min)
- zp = q_min - round(x_min/scale)
- Pytorch支持在输入节点后插入QuantStub算子,统计x_min/x_max
- MindSpore在输入节点后插入伪量化算子,统计x_min/x_max
模型输出反量化
- 对于分类模型,输出整型数据也可用于argmax计算,无需反量化。
- 对于需要浮点输出的模型(如检测、回归),需进行反量化。
- 量化感知训练、训练后静态量化(部分方案)过程中需要量化-反量化的过程。
反量化需要最后一层输入x和权重w的(scale,zp)。注意这里的是量化计算的中间结果。
激活值再量化
激活值再量化是在端侧量化推理时进行的。int8的输入和权重计算后,得到int16的输出。需要将每层int32的输出缩小为int8。再量化的scale如下:
为了提高推理时再量化的效率,可以将规约到
,便于通过右移运算来进行缩放。
量化方式
- 训练后动态量化:仅对模型权重进行量化,通过减少每个权重所占用的比特数来压缩原始模型大小,计算时恢复成Float32计算,或者动态对激活值进行量化后,进行整型计算。
- 训练后静态量化:对模型权重和激活值均进行量化,使用校准集(如100-1000张图片,数据无偏)统计激活值的数据分布,进行非对称量化。
- 量化感知训练:对模型权重和激活值均进行量化,在训练过程中模拟量化带来的效果,统计激活值的数据分布,微调模型。支持从零训练(可用quant_delay指定量化起始step)和预训练。
多数框架都支持训练后量化,如TensorFlow Lite(模型被量化,但输入输出保持Float)、Pytorch、ONNXRuntime、OCS(Outlier Channel Splitting)。
训练后动态量化
Post Training Dynamic Quantization。或称动态范围量化,仅权重量化。使用量化工具,针对已训练好的模型,统计Per Layer/Channel的最大值/最小值,通过量化公式对权重进行量化转换。在推理时,内存初始化期间将权重反量化为Float(一次反量化并缓存),进行浮点计算。或者动态对激活值进行量化后(影响时延),进行整型计算。
训练后静态量化
Post Training Static Quantization。或称全整形量化,训练后校准量化(Post Training Calibration Quantization)。
权重量化
权重不做校准,进行对称或非对称量化,Per Channel量化。
激活值量化
基于统计分布确定激活值的min/max,最小化量化后的激活值统计分布与原始激活值的统计分布的差异。流程如下:
- 计算原始激活值的直方图统计分布
- 在给定的min/max搜索空间中(n个候选值),通过min/max对激活值进行量化,计算量化后激活值的直方图统计分布
(有n个)。
- 计算
与n个
的差异,取差异最小min/max用于激活值量化。
涉及的超参:直方图bin个数,min/max搜索空间,统计分布差异性指标。
直方图bin个数:由于量化后,数据会离散到256个点上,所以bin个数要小于256。若过大,大部分bin上没有值。
min/max搜索空间:通过{search_start_scale, search_end_scale, search_step}和candidate确定。例如:
max_candidate=10, search_start_scale=0.8, search_end_scale=1.2, search_step=0.01, bin=150。
- 对于对称量化:max的搜索空间为
,共
个max候选值。将
(大多数在
,但要考虑离群点)分为150段,统计激活值
落在每段中的频率。
- 对于非对称量化:min/max的搜索空间为
,共
个候选值。将
(大多数在
,但要考虑离群点)分为150段,统计激活值
落在落在每段中的频率。
统计分布差异性的指标常用方法如下。提示,原始直方图和量化直方图bins数量相同,计算指标依赖概率和
,与原始和量化数据的类型无关了:
Kullback-Leibler Divergence(KL散度):
Symmetric Kullback-Leibler Divergence(对称KL散度):因为
相对
的KL散度与
相对
的KL散度是不同的,对称KL散度取两者的平均:
Jensen-Shannon Divergence(JS散度),取
相对
的均值,生成一个新分布
,再分别计算
和
相对
的KL散度,再取平均:
校验流程
推理有两种方式,一是不对模型输入做量化处理(如PyTorch, ONNXRuntime),二是对模型输入数据先量化再反量化(MindSpore)。先量化再反量化。
- 对权重进行量化-反量化
- 推理一遍,确定激活值的min/max搜索空间
- 针对搜索空间中的min/max均进行一遍推理,进行直方图统计
- 计算分布差异性,选择最优的min/max
- 根据min/max计算scale和zp
- 根据激活值和权重的scale,进行bias量化(int32)
min不一定是所有batch中的最小值,max未必是所有batch中的最大值。与min_percentile和max_percentile相关。如100张图片的校验集,取max_percentile=0.9,即取第100*(1 - 0.9)=10大的值作为max。具体为:
- 第一张图片推理,记录前10大的值
- 第二张图片推理,取前10大的值与记录合并排序,记录前10大的值
- 以此类推,至校验集遍历完成,选择最终第10大的值作为max
量化感知训练
Quantizatin Aware Training。训练过程中,在权重算子内/后插入伪量化算子(Fake Quantization OP),用来统计数据(权重、激活值)流经该算子时的最大/最小值,并进行伪量化规约。伪量化算子参与前向计算,模拟量化损失,但不参与反向计算(梯度更新对精度要求高)。
量化训练完成后,使用量化工具,根据伪量化算子统计到的参数计算量化参数(scale,zp),基于量化公式对权重进行量化转换。
在推理时,使用量化后的权重,行量化推理和激活值再量化。
前向传播
插入伪量化算子,统计数据最大/最小值,模拟量化损失,过程如下:
统计数据分布:[x_min, x_max]
饱和式截断:clamp(x) = min(max(x, x_min), x_max)
先把Float转为Int,再把Int转为Float,引起精度损失:q(x) = int((clamp(x) - x_min)/scale) x scale + x_min
反向传播
根据前向传播时的量化规约公式,反向传播时导数处处为0。故反向传播时,只进行截断处理,量化规约操作不参与计算。即:
- x < x_min or x > x_max时,导数为0
- x_min < x < x_max时,导数为1
更新Min和Max
类似BatchNorm算子,分为Running和Moving两种计算方式,建议前期用Runing,后期用Moving:
Running minimum: x_min = {min(X) if x_min == None, min(x_min, min(X)) otherwise}
Running maximum: x_max = {max(X) if x_max == None, max(x_max, max(X)) otherwise}
Moving minimum: x_min = {min(X) if x_min == None, (1-b)x_min + bmin(X)) otherwise}
Moving maximum: x_max = {max(X) if x_max == None, (1-b)x_max + bmax(X)) otherwise}
BatchNorm折叠
原理
卷积算子等价于经过img2col处理后的全连接算子,所Conv+BN的公式如下:
其中,为Batch数据的均值和方差。
假设:
则可将Conv+BN进行融合,即BatchNorm被融入Conv中。对于偏置
,可以简化掉:
由于大部分推理场景中会对BN和卷积进行融合操作,为了更好地模拟推理时的算子融合操作,量化训练时对BN进行Folding处理。几种场景下的计算流程对比如下:
普通训练:Conv -> 计算Moving Average(MA)
-> BatchNorm -> ReLU6
推理(融合卷积+BN):离线转换时计算
-> Conv -> 加Bias -> ReLU6
BN Folding训练:第1次Conv(用于计算当前batch的
) -> 计算Moving Average(MA)
-> 计算
-> 第二次Conv(Folding,不含Bias)-> 加Bias -> ReLU6
BN Folding + 量化感知训练:第1次Conv(用于计算当前batch的
) -> 计算Moving Average(MA)
-> 计算
-> weight quant -> 第二次Conv(Folding)-> 加Bias -> ReLU6 -> act quant
Correction
Bessel Correction:,n为样本数
BatchNorm Correction:
因为每个Batch计算得到的波动较大,会影响权重参数,所以量化训练时加入校正因子以使用Moving Average得到的
来与W折叠(对W进行缩放),相当于:
,令
为校正因子。
量化训练前期,Conv Folding的输出会除以,目的是抵消对W的校正,像普通的训练一样,学习准确的BatchNorm参数。到后期模型接近收敛后,冻结BatchNorm,不再更新Moving Average得到的
,Conv Folding的输出不再除以
,即
。
校正保证了:
- 权重是经过
缩放后量化的,这保证了训练的平滑性,即权重变化较缓,而不是随便mini-batches跳变。
- 通过修改Correcttion的值即可在使用
和
之间切换,而不用修改BatchNorm折叠的计算。
Delay
提供quant_delay=STEPS
接口参数用于控制何时在训练图中插入伪量化节点。
-
step<quant_delay
时,不插入伪量化节点 -
step<quant_delay
时,插入FakeQuantWithMinMaxPerChannel节点,统计最大最小值,通过Float->Quant->Float模拟量化引入的误差。
BN训练图
- Frozen前:
- Conv2D:
- BatchNormFold:
- CorrectionMul:
- MulFold:
-
WeightQuant/Not: if step > quant_delay:
- Conv2D:
-
ConvMul:
-
CorrectionAdd:
- AddFold:
- ReLU:
-
ActQuant/Not:
- if step > quant_delay:
- Conv2D:
- Frozen后:
- Conv2D:
- BatchNormFold:
- CorrectionMul:
- MulFold:
-
WeightQuant/Not: if step > quant_delay:
- Conv2D:
-
ConvMul:
-
CorrectionAdd:
- AddFold:
- ReLU:
-
ActQuant/Not:
- if step > quant_delay:
- Conv2D:
其中,CorrectionMul和MulFold可以融合。
验证图
- Conv2D
- BatchNormFold
- MulFold
- FakeQuantWithMinMaxPerChannel
- Conv2D
- AddFold
Pattern匹配
改变数据且不会和后续算子融合的算子,其后都需插入FakeQuant。
- 权重后插入FakeQuant
- 激活层后插入FakeQuant
- 无激活层则在Conv后插入FakeQuant
- 模型的输入、输出后插入FakeQuant
- 不改变数据的算子后不插入FakeQuant,如Reshape, Transform
- 改变数据的算子后均插入FakeQuant,如Add, Sub, Mul, Div
权值量化:包括正向、反向,直接记录输入数据的min, max,初始值为[-6, 6]。
激活值量化:包括正向、反向,使用指数滑动平均(EMA)记录输入数据的min, max,初始值为[0, 6]。
其他值量化:包括正向、反向,使用指数滑动平均(EMA)记录输入数据的min, max,初始值为[-6, 6]。
注意事项
- 不建议从头训练时打开BN折叠,建议Fine-tune阶段使用。若打开精度提升不多,但内存、训练时延会很大。
- 权重和激活值可以行不同Bit量化。
- CorrectionMul、MulFold两者的系数(乘/除到权重/激活值上的
),shape为通道数
,
- 对于TensorFLow,Conv权重为
,DepthwiseConv权重为
,对于DepthwiseConv节点,先将系数Reshape为
。
- 对于MindSpore,Conv权重为
,DepthwiseConv权重为
,系数无需做Reshape。
- 对于TensorFLow,Conv权重为
- 若最后一层分类数超过255,不建议行MatMul/Dense算子的量化。
训练流程
TensorFlow提供了quant_delay和freeze_bn_delay两个开关,流程通常为:
- BatchNorm Folding,训练到接近收敛
- 激活值、权重量化(global_step>quant_delay)
- Freeze BatchNorm(global_step>freeze_bn_delay)
MindSpore提供了bn_fold, quant_delay, freeze_bn三个开关,流程通常为(以MobileNetV2为例):
- 正常训练150Epoch
- Fine-tune提高稳定性30Epoch
- 激活值量化5Epoch(Fine-tune模式)
- 权重量化10Epoch(Fine-tune模式)
- BatchNorm Folding 2Epoch(Fine-tune模式)
- Freeze BatchNorm 8Epoch(Fine-tune模式)
端侧量化算子
与云侧伪量化算子类似,端侧为支持量化模型的推理,包含3种量化算子:
- Quant:量化,输入数据由Float32量化为Int8。可采用对称/非对称量化,需提供(scale,zp)给端侧。
- DeQuant:反量化,Int8张量乘/加之后结果用Int32存储,如果输出/下一层输入需要Float32,则需进行DeQuant。相当于Quant的逆过程,需提供输入和权重的(scale,zp)给端侧。该算子最常用。注意这里的
是量化计算的中间结果。
- ReQuant:重量化,Int8张量乘/加之后结果用Int32存储,如果输出/下一层输入需要Int8,则需进行Requant。需提供激活值的(scale,zp)给端侧。注意这里的
是量化计算的中间结果。
这里假设zp=0(采用对称量化时):
根据量化公式有(这里也可将zp置为0)
常见量化流程如下,主要由输入和输出的类型决定,通常Quant常出现在输入层,DeQuant常出现在输出层,ReQuant出现在中间层:
- Float32(Input) -> Quant -> Int8 -> Conv2D -> Int32 -> DeQuant -> Float32(Output)
- Float32(Input) -> Quant -> Int8 -> Conv2D -> Int32 -> ReQuant -> Int8
- Int8 -> Conv2D -> Int32 -> ReQuant -> Int8
量化模型表示格式
不同框架的量化模型表示格式(quantization representation format)不同,这导致了量化模型的相互转化比较困难:
- TensorFlow Lite,以普通算子构成模型,算子的属性中加入融合的算子、量化参数、量化后的权重/偏执。
- PyTorch,以量化算子构成模型,如:
M(
(quant): Quantize(scale=tensor([0.0353]), zero_point=tensor([62]), dtype=torch.quint8)
(conv): QuantizedConvReLU2d(1, 1, kernel_size=(1, 1), stride=(1, 1), scale=0.016702720895409584, zero_point=0)
(bn): Identity()
(relu): Identity()
(dequant): DeQuantize()
)
- ONNXRuntime:
- Operator-oriented (QOperator):所有的量化算子有自己的ONNX定义,如QLinearConv, MatMulInteger。
- Tensor-oriented (QDQ; Quantize and DeQuantize):在原始算子之间插入DeQuantizeLinear(QuantizeLinear(tensor))。QuantizeLinear和DeQuantizeLinear算子携带量化参数。以下量化模型是QDQ格式,后两者可用ONNXRuntime直接运行:
- 以quant_format=QuantFormat.QDQ 方式进行训练后静态量化的模型。
- 转换自TensorFlow或由PyTorch导出的Quantization-Aware training (QAT)模型。
- 转换自TFLite或其他框架的模型。