Quantization Compression

2021-10-17 | [MLSys]
Keywords: #Quantization #Deep Learning
Table of Contents

本文参考此博客

量化有若干相似的术语。低精度(Low precision)可能是最通用的概念。常规精度一般使用 FP32(32位浮点,单精度)存储模型权重;低精度则表示 FP16(半精度浮点),INT8(8位的定点整数)等等数值格式。不过目前低精度往往指代 INT8。

量化一般指 INT8 。不过,根据存储一个权重元素所需的位数,还可以包括:

  • 二值神经网络:在运行时权重和激活只取两种值(例如 +1,-1)的神经网络,以及在训练时计算参数的梯度。
  • 三元权重网络:权重约束为+1,0和-1的神经网络。
  • XNOR网络:过滤器和卷积层的输入是二进制的。 XNOR 网络主要使用二进制运算来近似卷积。

工业界最终选择了 INT8 量化—— FP32 在推理(inference)期间被 INT8 取代,而训练(training)仍然是 FP32。TensorRTTensorFlowPyTorchMxNet 和许多其他深度学习软件都已启用(或正在启用)量化。

通常,可以根据 FP32 和 INT8 的转换机制对解决方案进行分类。一些框架简单地引入了 QuantizeDequantize 层,当从卷积或全链接层送入或取出时,它将 FP32 转换为 INT8 或相反。在这种情况下,如图的上半部分所示,模型本身和输入/输出采用 FP32 格式。深度学习框架加载模型,重写网络以插入QuantizeDequantize 层,并将权重转换为 INT8 格式。

其他一些框架将网络整体转换为 INT8 格式,因此在推理期间没有格式转换,如图的下半部分。该方法要求算子(Operator)都支持量化,因为运算符之间的数据流是INT8。对于尚未支持的那些,它可能会回落到 Quantize/Dequantize 方案。下文的讨论都基于这种方式。

由于 INT8 使用的比特数只有 FP32 的 25% ,在 INT8 和 FP32 之间转换数值的方法非常重要,因为它会显着影响预测精度。

量化的算术

量化过程可以分为两部分:将模型从 FP32 转换为 INT8,以及使用 INT8 进行推理。本节说明这两部分背后的算术原理。如果不了解基础算术原理,在考虑量化细节时通常会感到困惑。

定点和浮点

从事计算机科学的人很少了解算术运算的执行方式。由于量化桥接了固定点(fixed point)和浮点(floating point),在接触相关研究和解决方案之前,有必要先了解它们的基础知识。

定点和浮点都是数值的表示(representation),它们区别在于,将整数(integer)部分和小数(fractional)部分分开的点,点在哪里。 定点保留特定位数整数和小数,而浮点保留特定位数的有效数字(significand)和指数(exponent)

上图给出了定点和浮点表示的格式和示例。对于定点,II 表示整数,FF 表示 IIIII.FFFFF中的分数。对于浮点数,base分别为二进制、十进制和十六进制格式的 2、10 和 16 。定点和浮点的数值示例在图中是一一对应的。

在指令集(Instruction Set Architecture)的内置数据类型中,定点是整数,浮点是二进制格式。一般来说,指令集层面的定点是连续的,因为它是整数,且两个邻近的可表示数字的间隙是 1 。另一方面,浮点代表实数,其数值间隙由指数确定,因而具有非常宽的值域(32 位数值最大整数是 $2^{31}−1$,而浮点值域为 $(2−2^{-23})×2^{127}$),值越接近零就越准确。一个观察结果是,在给定指数时,浮点在不同范围内拥有数值数量相同数量,如下图。例如,$[1,2)$中浮点值的数量与 $[0.5,1)、[2,4]、[4,8]$等相同。

浮点运算可以由整数运算组成,在计算机发展的早期,浮点计算都是用软件在定点硬件上模拟的。下面的等式展示了如何将浮点乘法用定点乘法和加法表示。加法的表示方法要复杂得多,这里不做进一步讨论,有需求的可以参考计算机体系结构相关资料。

$$\begin{aligned} z &=x \times y \\ z_{\text {significand }} \times b a s e^{z_{\text {exponent }}} &=\left(x_{\text {significand }} \times \text { base }^{x_{exponent }}\right) \times\left(y_{\text {significand }} \times \text { base }^{y_{{exponent }}}\right) \\ &=\left(x_{\text {significand }} \times y_{\text {significand }}\right) \times\left(\text { base }^{x_{exponent }} \times \text { base }^{y_{exponent}}\right) \\ &=\left(x_{\text {significand }} \times y_{\text {significand }}\right) \times \text { base }^{x_{exponent}+y_{exponent }} \end{aligned}$$

实际上,在上面有效数字的整数乘法之后,当乘法结果相对于表示范围太大时,通常需要重新缩放,如下图。重新缩放移动将有效数字结果的一部分转移到指数,并以最近舍入方法舍入剩余的有效数字。图的右半部分是一个例子。由于部分数字被舍弃,浮点乘法会丢失一些信息

量化浮点

神经网络由浮点运算构成。如定点与浮点所述,FP32 和 INT8 的值域是$[(2−2^{-23})×2^{127},(2^{23}-2)×2^{127}]$ 和 $[−128,127]$,而取值数量大约分别为 $2^{32}$ 和 $2^8$ 。因此,将网络从 FP32 转换为 INT8 并不像数据类型转换截断那样简单

幸运的是,神经网络权重的值分布范围很窄,非常接近零。下图给出了 MobileNetV1 中十层(拥有最多值的层)的权重分布。

当值落在 $(−1,1)$ 中,量化浮点使用类似$x_{float}=x_{scale}\times x_{quantized}$ 的方法将 FP32 映射到 INT8,其中 $x_{float}$ 表示 FP32 权重,$x_{quantized}$ 表示量化的 INT8 权重,$x_{scale}$ 是映射因子(缩放因子)。有时我们不希望将 FP32 零映射到 INT8 零,即数字信号处理中的均一量化和如下等式 。

$$x_{float}=x_{scale}\times (x_{quantized}−x_{zero\_point})$$

大多数情况下量化选用无符号整数,那么 INT8 值域为 $[0,255]$ 。zero_point 在这种情况下更有意义。具体而言,如下面的 等式所示,量化浮点值可以分为两个步骤

  1. 通过在权重张量(Tensor)中找到 minmax 值从而确定 $x_{scale}$ 和$x_{zero_point}$。
  2. 将权重张量的每个值从 FP32 转换为 INT8 。

$$ x_{\text {float }} \in\left[x_{\text {float }}^{\min }, x_{\text {float }}^{\max }\right] $$ $$ x_{\text {scale }} =\frac{x_{\text {float }}^{\max }-x_{\text {float }}^{\text {min }}}{x_{\text {quantized }}^{\max }-x_{\text {quantized }}^{\min }} $$

$$ x_{\text {zeropoint }} =x_{\text {quantized }}^{\max }-x_{\text {float }}^{\max } \div x_{\text {scale }} $$

$$ x_{\text {quantized }} =x_{\text {float }} \div x_{\text {scale }}+x_{\text {zeropoint }} $$

注意,当浮点运算结果不等于整数时,需要额外的舍入步骤。例如将 FP32 值域 $[−1,1]$ 映射到 INT8 值域 $[0,255]$,有 $x_{scale}=\frac{2}{255}$,而$x_{zero_point}=255−\frac{255}{2}\approx 127$。

量化过程中存在误差是不可避免的,就像数字信号处理中量化一样。下图显示了数字信号处理的量化和误差

Implementation

Background

量化并不是什么新知识,我们在对图像做预处理时就用到了量化。回想一下,我们通常会将一张 uint8 类型、数值范围在 0~255 的图片归一成 float32 类型、数值范围在 0.0~1.0 的张量,这个过程就是反量化。类似地,我们经常将网络输出的范围在 0.0~1.0 之间的张量调整成数值为 0~255、uint8 类型的图片数据,这个过程就是量化。所以量化本质上只是对数值范围的重新调整,可以「粗略」理解为是一种线性映射。(之所以加「粗略」二字,是因为有些论文会用非线性量化,但目前在工业界落地的还都是线性量化,所以本文只讨论线性量化的方案)。

不过,可以明显看出,反量化一般没有信息损失,而量化一般都会有精度损失。这也非常好理解,float32 能保存的数值范围本身就比 uint8 多,因此必定有大量数值无法用 uint8 表示,只能四舍五入成 uint8 型的数值。量化模型和全精度模型的误差也来自四舍五入的 clip 操作。

我们用 $r$ 表示浮点实数,$q$ 表示量化后的定点整数浮点和整型之间的换算公式为:

$$r=S(q-Z)$$ $$q=round(\frac{r}{S}+Z)$$

其中,$S$ 是 scale,表示实数和整数之间的比例关系,$Z$ 是 zero point,表示实数中的 0 经过量化后对应的整数,它们的计算方法为:

$$S=\frac{r_{max}-r_{min}}{q_{max}-q_{min}}$$ $$Z = round(q_{max}-\frac{r_{max}}{S})$$

$r_{max}, r_{min}$ 分别是 $r$ 的最大值和最小值;$q_{min}, q_{max}$ 同理。需要强调的一点是,定点整数的 zero point 就代表浮点实数的 0,二者之间的换算不存在精度损失。 这么做的目的是为了在 padding 时保证浮点数值的 0 和定点整数的 zero point 完全等价,保证定点和浮点之间的表征能够一致。

矩阵运算的量化

由于卷积网络中的卷积层和全连接层本质上都是一堆矩阵乘法,因此我们先看如何将浮点运算上的矩阵转换为定点运算。 假设 $r_1,r_2$ 是浮点实数上的两个 $N\times N$ 的矩阵,$r_3$ 是 $r_1,r_2$ 相乘后的矩阵 $$r_3^{i,k}=\sum_{j=1}^N r_1^{i,j}r_2^{j,k}$$ 假设 $S_1,Z_1$ 是 $r_1$ 矩阵对应的 scale 和 zero point,$S_2,Z_2,S_3,Z_3$ 同理,那么可以推出: $$S_3(q_3^{i,k}-Z_3)=\sum_{j=1}^NS_1(q_1^{i,k}-Z_1)S_2(q_2^{i,k}-Z_2)$$ 整理得到 $$q_3^{i,k}=\frac{S_1S_2}{S_3}\sum_{j=1}^N (q^{i,j}_1-Z_1)(q^{i,j}_2-Z_2)+Z_3$$

除了 $\frac{S_1S_2}{S_3}$ ,其他都是定点整数运算。那如何把 $\frac{S_1S_2}{S_3}$ 也变成定点运算呢?这里要用到一个 trick。假设 $M=\frac{S_1S_2}{S_3}$ ,由于 $M$ 通常都是 $(0, 1)$之间的实数 (这是通过大量实验统计出来的),因此可以表示成 $M=2^{-n}M_0$,其中 $M_0$ 是一个定点实数。注意,定点数并不一定是整数,所谓定点,指的是小数点的位置是固定的,即小数位数是固定的。因此,如果存在 $M=2^{-n}M_0$,那我们就可以通过 $M_0$ 的 bit 位移操作实现 $M=2^{-n}M_0$ ,这样整个过程就都在定点上计算了。

很多刚接触量化的同学对这一点比较疑惑,下面我就用一个简单的示例说明这一点。我们把 $M=\frac{S_1S_2}{S_3}$ 代入可以得到:$$q_3^{i,j}=M\sum_{j=1}^N (q_1^{i,j}-Z_1)(q_2^{i,j}-Z_2)+Z_3=MP+Z_3$$

这里面 $P$ 是一个在定点域上计算好的整数

假设 $P = 7091,M = 0.0072474273418460$ ($M$ 可以通过 $S$ 事先计算得到),那下面我们就是要找到一个 $M_0$ 和 $n$ ,使得 $M P =2^{-n}M_0 P$ 成立。我们可以用一段代码来找到这两个数:

M = 0.0072474273418460
P = 7091

def multiply(n, M, P):
    result = M * P
    Mo = int(round(2 ** n * M)) # 这里不一定要四舍五入截断,因为python定点数不好表示才这样处理

    approx_result = (Mo * P) >> n
    print("n=%d, Mo=%d, approx=%f, error=%f"%\
          (n, Mo, approx_result, result-approx_result))

for n in range(1, 16):
    multiply(n, M, P)

可以看到,在 $n=11、M_0=15$ 的时候,误差就已经在 1 以内了。因此,只要 $M_0P$ 的数值范围在 21(32-11) 个 bit 内,就可以通过对 $M_0P$ 右移 $n$ 个 bit 来近似 $MP$ 了,而这个误差本身在可以接受的范围内。这样一来,就可以完全通过定点运算来计算,即我们实现了浮点矩阵乘法的量化

卷积网络的量化

有了上面矩阵乘法的量化,我们就可以进一步尝试对卷积网络的量化。假设一个这样的网络:

这个网络只有三个模块,现在需要把 conv、fc、relu 量化。

假设输入为 $x$,可以事先统计样本的最大值和最小值,然后计算出 $S_x$ (scale) 和 $Z_x$ (zero point)。 同样地,假设 conv、fc 的参数为 $w_1, w_2$,以及 scale 和 zero point 为 $S_{w1},Z_{w1},S_{w2},Z_{w2}$ 。中间层的 feature map 为 $a_1,a_2$ ,事先统计出它们的 scale 和 zero point 为 $S_{a1},Z_{a1},S_{s2},Z_{a2}$。

卷积运算和全连接层的本质都是矩阵运算,因此我们可以把卷积运算表示成 (这里先忽略加 bias 的操作,这一步同样可以量化,不过中间有一些 trick,我们在之后的文章再仔细研究): $$a_1^{i,k}=\sum_{j=1}^N x^{i,j}w_1^{i,j}$$ $$q_{a1}^{i,k}=M\sum_{j=1}^N (q_x^{i,j}-Z_x)(q_{w1}^{i,j}-Z_{w1})+Z_{a1}$$ 其中 $M=\dfrac{S_{w1}S_{x}}{S_{a1}}$。得到 conv 的输出后,不用反量化回 $a_1$ ,直接用 $q_{a1}$ 继续后面的计算即可。

对于量化的 RuLU 来说,计算公式不再是 $q_{a2}=\max(q_{a1},0)$,而是 $q_{a2}=\max(q_{a1},Z_{a1})$,并且 $S_{a1}=S_{a2},Z_{a1}=Z_{a2}$。在部署的时候,ReLU 或者 BN 通常是会整合到 conv 中一起计算的。

得到 $q_{a2}$ 之后,我们可以计算 fc 层。假设网络输出为 $y$ ,对应的 scale 和zero_point 为 $S_y,Z_y$ ,则量化之后的 fc 层可以用如下公式来计算: $$ q_{y}^{i, k}=M \sum_{j=1}^{N}\left(q_{a 2}^{i, j}-Z_{a 2}\right)\left(q_{w 2}^{j, k}-Z_{w 2}\right)+Z_{y} $$

然后通过公式 $y=S_y(q_y-Z_y)$ 把结果反量化回去,就可以得到近似原来全精度模型的输出了。

可以看到,上面整个流程都是用定点运算实现的。我们在得到全精度的模型后,可以事先统计出 weight 以及中间各个 feature map 的 min、max,并以此计算出 scale 和 zero point,然后把 weight 量化成 int8/int16 型的整数后,整个网络便完成了量化,然后就可以依据上面的流程做量化推理了。

Model Size

  • 网络的大小取决于Parameter的数量,如果是float32,就是$\text{Parameters}\times 4$