Quantization

量化

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

量化介绍

量化优势:降低模型大小、推理时延,进而降低内存消耗和功耗。

端到端量化流程通常包括:

  1. 量化准备,二选一
    • 量化训练:云侧,加载浮点模型,插入伪量化算子(量化-反量化、统计、代理梯度),模拟量化带来的权重和激活值变化,微调权重以适应量化,统计权重、激活值min/max。
    • 量化校准,云侧,加载浮点模型,插入统计算子(也可以先加上量化-反量化),基于校准集(少量数据集)进行推理,统计激活值min/max。
  2. 量化转换:云侧,统计权重min/max,计算权重的缩放系数和偏移(scale,offset)并进行量化;计算激活值的(scale,offset);生成量化模型。
  3. 量化推理:端侧,加载量化模型(整型),插入真量化算子(激活值再量化,输入量化、输出反量化),进行量化推理。

浮点数转为低bit,会引入量化误差,减少量化损失有以下几种方案:

  1. 模型参数:有较多/冗余参数的模型量化损失较小。
  2. 量化策略:非对称量化比对称量化损失小,Per Channel(每通道,CinKK)量化比Per Layer(每层,CoutCinKK)量化损失小。
  3. 网络结构:权重量化损失大,激活值量化损失小,可能是由于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对称量化,统计得出w_{scale}。注意,w_{scale}, x_{scale}均采用对称/非对称量化公式统计得出。

Bias量化

Bias通常量化为int32,b_{scale}=w_{scale}*x_{scale},其中输入和权重缩放到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)。注意这里的y_{int32}是量化计算的中间结果。

\begin{align} y&=xw \\ &=x_{scale}(x_{int}-x_{zp})*w_{scale}(w_{int}-w_{zp}) \\ &=(x_{scale}w_{scale})(x_{int}w_{int} - x_{int}w_{zp} - x_{zp}w_{int} + x_{zp}w_{zp}) \\ &=(x_{scale}w_{scale})(y_{int32} - x_{int}w_{zp} - x_{zp}w_{int} + x_{zp}w_{zp}) \\ &=(x_{scale}w_{scale})y_{int32}, if\ zp=0 \end{align}

激活值再量化

激活值再量化是在端侧量化推理时进行的。int8的输入和权重计算后,得到int16的输出。需要将每层int32的输出缩小为int8。再量化的scale如下:

\begin{align} y_{int}&=round(y/y_{scale}+y_{zp}) \\ &=round((x_{scale}w_{scale})y_{int32}/y_{scale} + y_{zp}) \\ &=round((x_{scale}w_{scale}/y_{scale})y_{int32} + y_{zp}) \end{align}

为了提高推理时再量化的效率,可以将(x_{scale}w_{scale}/y_{scale}规约到2^{-n},便于通过右移运算来进行缩放。

量化方式

  1. 训练后动态量化:仅对模型权重进行量化,通过减少每个权重所占用的比特数来压缩原始模型大小,计算时恢复成Float32计算,或者动态对激活值进行量化后,进行整型计算。
  2. 训练后静态量化:对模型权重和激活值均进行量化,使用校准集(如100-1000张图片,数据无偏)统计激活值的数据分布,进行非对称量化。
  3. 量化感知训练:对模型权重和激活值均进行量化,在训练过程中模拟量化带来的效果,统计激活值的数据分布,微调模型。支持从零训练(可用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,最小化量化后的激活值统计分布与原始激活值的统计分布的差异。流程如下:

  1. 计算原始激活值的直方图统计分布P_f
  2. 在给定的min/max搜索空间中(n个候选值),通过min/max对激活值进行量化,计算量化后激活值的直方图统计分布P_q(有n个)。
  3. 计算P_f与n个P_q的差异,取差异最小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。

  1. 对于对称量化:max的搜索空间为[0.8, 1.2, 0.01]*max,共(12 - 8) / 0.1 + 1=41个max候选值。将[0, 2*max](大多数在[0, max],但要考虑离群点)分为150段,统计激活值abs(x)落在每段中的频率。
  2. 对于非对称量化:min/max的搜索空间为[0.8, 1.2, 0.01]*(max-min),共(12 - 8) / 0.1 + 1=41个候选值。将[0, 2*(max-min)](大多数在[0, max-min],但要考虑离群点)分为150段,统计激活值(x - min)落在落在每段中的频率。

统计分布差异性的指标常用方法如下。提示,原始直方图和量化直方图bins数量相同,计算指标依赖概率P_fP_q,与原始和量化数据的类型无关了:

  1. Kullback-Leibler Divergence(KL散度):
    D_{KL}(P_f || P_q) = \sum_{i=1}^{N}P_f(i)*log_2 \frac{P_f(i)}{P_q(i)}

  2. Symmetric Kullback-Leibler Divergence(对称KL散度):因为P_f相对P_q的KL散度与P_q相对P_f的KL散度是不同的,对称KL散度取两者的平均:
    SYM_{KL} = 0.5*(D_{KL}(P_f || P_q) + D_{KL}(P_q || P_f))

  3. Jensen-Shannon Divergence(JS散度),取P_q相对P_f的均值,生成一个新分布P_m,再分别计算P_qP_f相对P_m的KL散度,再取平均:
    P_m = 0.5*(P_f + P_q) \\ D_js = 0.5*(D_{KL}(P_f || P_m) + D_{KL}(P_q || P_m))

校验流程

推理有两种方式,一是不对模型输入做量化处理(如PyTorch, ONNXRuntime),二是对模型输入数据先量化再反量化(MindSpore)。先量化再反量化。

  1. 对权重进行量化-反量化
  2. 推理一遍,确定激活值的min/max搜索空间
  3. 针对搜索空间中的min/max均进行一遍推理,进行直方图统计
  4. 计算分布差异性,选择最优的min/max
  5. 根据min/max计算scale和zp
  6. 根据激活值和权重的scale,进行bias量化(int32)

min不一定是所有batch中的最小值,max未必是所有batch中的最大值。与min_percentile和max_percentile相关。如100张图片的校验集,取max_percentile=0.9,即取第100*(1 - 0.9)=10大的值作为max。具体为:

  1. 第一张图片推理,记录前10大的值
  2. 第二张图片推理,取前10大的值与记录合并排序,记录前10大的值
  3. 以此类推,至校验集遍历完成,选择最终第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的公式如下:

Y = BN(WX + b) = \gamma (WX + b - E[X]) / \sqrt{(var[X] + \epsilon)} + \beta = \gamma (WX + b - \mu) / \sigma + \beta

其中,\mu, \sigma为Batch数据的均值和方差。

假设:
\hat W = \frac {\gamma}{\sigma}W \\ \hat b = \frac {\gamma}{\sigma}(b - \mu) + \beta

Y = \hat WX + \hat b可将Conv+BN进行融合,即BatchNorm被融入Conv中。对于偏置b,可以简化掉:

\hat b = \beta - \frac {\gamma \mu}{\sigma}

由于大部分推理场景中会对BN和卷积进行融合操作,为了更好地模拟推理时的算子融合操作,量化训练时对BN进行Folding处理。几种场景下的计算流程对比如下:

  • 普通训练:Conv -> 计算Moving Average(MA) \mu,\sigma -> BatchNorm -> ReLU6

  • 推理(融合卷积+BN):离线转换时计算\hat W, \hat b -> Conv -> 加Bias -> ReLU6

  • BN Folding训练:第1次Conv(用于计算当前batch的\mu,\sigma) -> 计算Moving Average(MA) \mu,\sigma -> 计算\hat W, \hat b -> 第二次Conv(Folding,不含Bias)-> 加Bias -> ReLU6

  • BN Folding + 量化感知训练:第1次Conv(用于计算当前batch的\mu,\sigma) -> 计算Moving Average(MA) \mu,\sigma -> 计算\hat W, \hat b -> weight quant -> 第二次Conv(Folding)-> 加Bias -> ReLU6 -> act quant

Correction

Bessel Correction:\sigma^2 = \frac n{n-1} \sigma_n^2,n为样本数

BatchNorm Correction:

因为每个Batch计算得到的\mu_B, \sigma_B波动较大,会影响权重参数,所以量化训练时加入校正因子以使用Moving Average得到的\mu, \sigma来与W折叠(对W进行缩放),相当于:

\hat W = \frac {\sigma_B}{\sigma} \times W \times \frac {\gamma}{\sigma_B},令c = \frac {\sigma_B}{\sigma} 为校正因子。

量化训练前期,Conv Folding的输出会除以c,目的是抵消对W的校正,像普通的训练一样,学习准确的BatchNorm参数。到后期模型接近收敛后,冻结BatchNorm,不再更新Moving Average得到的\mu, \sigma,Conv Folding的输出不再除以c,即c = 1

校正保证了:

  1. 权重是经过\frac {\gamma}{\sigma}缩放后量化的,这保证了训练的平滑性,即权重变化较缓,而不是随便mini-batches跳变。
  2. 通过修改Correcttion的值即可在使用\mu_B, \sigma_B\mu, \sigma之间切换,而不用修改BatchNorm折叠的计算。
Delay

提供quant_delay=STEPS接口参数用于控制何时在训练图中插入伪量化节点。

  1. step<quant_delay时,不插入伪量化节点
  2. step<quant_delay时,插入FakeQuantWithMinMaxPerChannel节点,统计最大最小值,通过Float->Quant->Float模拟量化引入的误差。
BN训练图
  • Frozen前:\hat W = \frac {\gamma W}{\sigma_B}, \hat b = -\frac {\gamma \mu_B}{\sigma_B} + \beta
    • Conv2D: Y = WX
    • BatchNormFold:
      • \mu_B = E[Y]; \sigma_B = \sqrt{var[Y]};
      • \mu = 0.9\mu + 0.1\mu_B; \sigma = 0.9\sigma + 0.1\sigma_B
    • CorrectionMul: \hat W = \frac {\sigma_B}{\sigma} \times W
    • MulFold: W = \frac {\gamma \hat W}{\sigma_B}
    • WeightQuant/Not: if step > quant_delay: \hat W = FakeQuantWithMinMaxPerChannel(\hat W)
    • Conv2D: Y = \hat WX
    • ConvMul: Y = \hat W X \div \frac{\sigma_B}{\sigma}
    • CorrectionAdd: Y = Y + 0
    • AddFold: Y = Y + \beta - \gamma \frac{\mu_B}{\sigma_B}
    • ReLU: Y = ReLU(Y)
    • ActQuant/Not:
      • min = EMA(min); max = EMA(max)
      • if step > quant_delay: Y = FakeQuantWithMinMax(Y, min, max)
  • Frozen后:\hat W = \frac {\gamma W}{\sigma}, \hat b = -\frac {\gamma \mu}{\sigma} + \beta
    • Conv2D: Y = WX
    • BatchNormFold:
      • \mu_B = E[Y]; \sigma_B = \sqrt{var[Y]};
      • \mu = 0.9\mu + 0.1\mu_B; \sigma = 0.9\sigma + 0.1\sigma_B
    • CorrectionMul: \hat W = \frac {\sigma_B}{\sigma} \times W
    • MulFold: W = \frac {\gamma \hat W}{\sigma_B}
    • WeightQuant/Not: if step > quant_delay: \hat W = FakeQuantWithMinMaxPerChannel(\hat W)
    • Conv2D: Y = \hat WX
    • ConvMul: Y = \hat W X \div 1
    • CorrectionAdd: Y = Y + \gamma(\frac{\mu_B}{\sigma_B} - \frac{\mu}{\sigma})
    • AddFold: Y = Y + \beta - \gamma \frac{\mu_B}{\sigma_B}
    • ReLU: Y = ReLU(Y)
    • ActQuant/Not:
      • min = EMA(min); max = EMA(max)
      • if step > quant_delay: Y = FakeQuantWithMinMax(Y, min, max)

其中,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]。

注意事项

  1. 不建议从头训练时打开BN折叠,建议Fine-tune阶段使用。若打开精度提升不多,但内存、训练时延会很大。
  2. 权重和激活值可以行不同Bit量化。
  3. CorrectionMul、MulFold两者的系数(乘/除到权重/激活值上的\frac {\sigma_B}{\sigma}, \frac {\gamma}{\sigma}),shape为通道数[C_{out}]
    1. 对于TensorFLow,Conv权重为[H, W, C_{in}, C_{out}],DepthwiseConv权重为[H, W, C_{in}, Multiplier],对于DepthwiseConv节点,先将系数Reshape为[C_{in}, Multiplier]
    2. 对于MindSpore,Conv权重为[C_{out}, C_{in}, H, W],DepthwiseConv权重为[C_{out}, 1, H, W],系数无需做Reshape。
  4. 若最后一层分类数超过255,不建议行MatMul/Dense算子的量化。

训练流程

TensorFlow提供了quant_delay和freeze_bn_delay两个开关,流程通常为:

  1. BatchNorm Folding,训练到接近收敛
  2. 激活值、权重量化(global_step>quant_delay)
  3. Freeze BatchNorm(global_step>freeze_bn_delay)

MindSpore提供了bn_fold, quant_delay, freeze_bn三个开关,流程通常为(以MobileNetV2为例):

  1. 正常训练150Epoch
  2. Fine-tune提高稳定性30Epoch
  3. 激活值量化5Epoch(Fine-tune模式)
  4. 权重量化10Epoch(Fine-tune模式)
  5. BatchNorm Folding 2Epoch(Fine-tune模式)
  6. Freeze BatchNorm 8Epoch(Fine-tune模式)

端侧量化算子

与云侧伪量化算子类似,端侧为支持量化模型的推理,包含3种量化算子:

  • Quant:量化,输入数据由Float32量化为Int8。可采用对称/非对称量化,需提供(scale,zp)给端侧。
  • DeQuant:反量化,Int8张量乘/加之后结果用Int32存储,如果输出/下一层输入需要Float32,则需进行DeQuant。相当于Quant的逆过程,需提供输入和权重的(scale,zp)给端侧。该算子最常用。注意这里的y_{int32}是量化计算的中间结果。
    \begin{align} y&=xw \\ &=x_{scale}(x_{int}-x_{zp})*w_{scale}(w_{int}-w_{zp}) \\ &=(x_{scale}w_{scale})(x_{int}w_{int} - x_{int}w_{zp} - x_{zp}w_{int} + x_{zp}w_{zp}) \\ &=(x_{scale}w_{scale})(y_{int32} - x_{int}w_{zp} - x_{zp}w_{int} + x_{zp}w_{zp}) \end{align}
  • ReQuant:重量化,Int8张量乘/加之后结果用Int32存储,如果输出/下一层输入需要Int8,则需进行Requant。需提供激活值的(scale,zp)给端侧。注意这里的y_{int32}是量化计算的中间结果。
    \begin{align} y&=xw \\ &=x_{scale}(x_{int}-x_{zp})*w_{scale}(w_{int}-w_{zp}) \\ &=(x_{scale}w_{scale})(x_{int}w_{int} - x_{int}w_{zp} - x_{zp}w_{int} + x_{zp}w_{zp}) \\ &=(x_{scale}w_{scale})(y_{int32} - x_{int}w_{zp} - x_{zp}w_{int} + x_{zp}w_{zp}) \end{align}
    这里假设zp=0(采用对称量化时):
    y=(x_{scale}w_{scale})y_{int32}
    根据量化公式有(这里也可将zp置为0)
    \begin{align} y_{int}&=round(y/y_{scale}+y_{zp}) \\ &=round((x_{scale}w_{scale})y_{int32}/y_{scale} + y_{zp}) \\ &=round((x_{scale}w_{scale}/y_{scale})y_{int32} + y_{zp}) \end{align}

常见量化流程如下,主要由输入和输出的类型决定,通常Quant常出现在输入层,DeQuant常出现在输出层,ReQuant出现在中间层:

  1. Float32(Input) -> Quant -> Int8 -> Conv2D -> Int32 -> DeQuant -> Float32(Output)
  2. Float32(Input) -> Quant -> Int8 -> Conv2D -> Int32 -> ReQuant -> Int8
  3. 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或其他框架的模型。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容