1.简介
1.1目的
在过去的一段时间里,对基于INT8量化技术的模型加速方案的算法进行了一系列的实现和实验,特别有引入的INT8矩阵相乘的方法替代原本caffe中MKL运算库FLOAT32矩阵相乘的方法,两者相比,INT8量化技术几乎不能实现模型加速的需求,但是在gpu上,由于gpu硬件对INT8矩阵乘法的支持,引入INT8技术后,在gpu上模型前向的时间是可以得到3倍左右的提升的。所以本文档针对上述的实验结果和现象进行分析和总结。
1.2范围
本文档描述的代码修改以及实验方法都是基于caffe框架进行的,主要的加速策略是模型INT8量化,并引入的INT8*INT8=INT32的矩阵操作,所有的实验都是基于Mnist数据集的Lenet网络。
1.3定义、首字母缩写词和缩略语
序号 | 术语或缩略语 | 说明性定义 |
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 |
1.4 参考资料
《int8量化代码评审.ppt》
2.实验的方法
本文档中的实验是基于caffe框架进行的,修改了其中的源码,使得这个框架可以按照制定的模型加速测绿,并修改对应参数层中前向和后向算法将对应可训练的参数的基于MKL的FLOAT32矩阵乘法替换为基于INT8矩阵乘法。
2.1整体流程
GOOGLE把INT8量化用于语音识别,是因为它迫切需要降低运算和储存/传输的压力。如果把经典深度神经网络(如CNN)看做一张网,那么用于语音识别的RNN就是一堆网叠加在一块,彼此互联。因为语音是一个时序数据,每一个时间点的信号都需要一整张网来模拟。可想而知数据流之大。论文中也说明了可以显著的减少储存和运算的需求,我们这里是将INT8量化技术用到CNN网络中。
如果把神经网络分解到最后,绝大部分都会对应FMA操作。其中FMA操作如公式(1)所示。
y=WX+B | (1) |
INT8量化的操作也是在FMA操作的过程中进行的,如图1所示。
图1 量化和恢复过程
其中W表示的已经量化过的矩阵,对X的Q(.)操作是在Mult(.)之前迅速的完成的。INT8进行Mult(.)操作之后会进行R(.)操作恢复到float32,并保持float32的表示完成Add(.)操作以及F(.)激活操作。
2.2量化策略
2.2.1论文中给出理论上的量化策略
- Quantizing
给定一组FLOAT型的数据V=Vx,一个量化的尺度S (例如INT8,则S=256),通过计算一个因子Q,得到量化后的结果V'=0≤Vx'≤S。具体的过程如下:给定R=Vminmax,其中Vmax是给定的一组float型数据中最大的数,Vmin为最小的数。论文中因子Q的计算如下:Q=SR,最终的量化表达式为:Vx'=Q*Vx-Vmin()。
- Recovery
同样的,量化后的值可以恢复到量化前高精度值得近似值,具体的恢复表达式为:Vx=Vx'*Q-1+Vmin,其中Q-1=RS。
- Quantization error and bias
整个的量化过程是两种loss损失的过程,第一种就是输入的值和quantization-recovery操作后的值之间的loss损失,即精度损失。第二种是在quantization-recovery操作计算数值的过程中引入偏差带来的损失,即偏置损失。第一种错误不可避免但是平均上看,带来的影响很小。而第二种偏置错误,如果更注意的去量化是可以避免的,所以花费特别的精力去消除第二种错误是非常必要的。
- Eliminating bias error
FMA的操作为就是去执行一系列的如下操作
Vc=Va*Vb | (2) |
根据之前量化和恢复的方法可以令Vx''=Vx'+QVmin,那么就有:Vx=Vx''Q。那么量化恢复后的(2)式可以改写成:
Vc=Va''*Vb''Qa*Qb | (3) |
可以看出每个Vx''已经是一个整数类型,Vx'也已经是一个整型类型,而QVmin却是一个FLOAT型将要近似为整型,这样就引入一个误差:
E=floatQVmin()QVmin() | (4) |
那么为了避免这种误差,就要求在量化过程中执行的方式和这个公式是一致的,所以就引入一个近似的操作round(.),即有:
Vx'=roundQVx-roundQVmin() | (5) |
因为这些错误在量化和乘积操作中是一致的,并且可以相互抵消。同样的在量化过程中有:
Vx=Vx'+roundQVmin()Q | (6) |
- Efficient implementation
上述将FLOAT32量化成INT8,通过引入SIMD,降低了数据的内存带宽,这样就可以增加cpu对数据的吞吐量。以avx指令集为例子,其指令长度为256bit,如果存储FLOAT32,可以存储有8个,而存储INT8可以存储32个,对于数据的吞吐量是前者的四倍。
2.2.2实际在工程实践上使用的量化策略
为了方便在工程上的实验,这里对上述的量化策略做了一些无损的修改,(语音识别组宋亚楠和刘迪源提供的思路)。
- Quantizing
将映射空间从0-255变成-127-128,即上述的float2uchar变成float2char的方法,这里就有新的量化方法如公式(7)所示:
Q=S>>1maxabsVmax,absVmin | (7) |
最终的量化方法如公式(8)所示:
Vx'=Q*Vx | (8) |
- Recovery
相应的量化恢复的方法也发生了变化公式(9)和公式(10)所示。
Q-1=maxabsVmax ,absVmin S>>1 | (9) |
Vx=Vx'*Q-1 | (10) |
3.实验结果及其分析
针对上述方法修改完毕之后的caffe框架,做了两组对比实验。第一组实验主要是观察不同的卷积层进行INT8量化对模型的性能的影响。第二组实验主要是观察不同卷积层进行INT8量化对模型前向耗时影响。所有的实验都是基于Mnist数据集在LeNet网络上进行的,具体的实验结果以及实验分析如下所示。
3.1 性能效果
第一层卷积是否(int8)量化 | 第二层卷积是否(int8)量化 | accuracy | loss |
否 | 否 | 0.9901 | 0.0294976 |
否 | 是 | 0.9902 | 0.29693 |
是 | 是 | 0.9891 | 0.0322802 |
是 | 否 | 0.9892 | 0.0321049 |
表1 不同层量化的模型压缩效果
不同的卷积层进行INT8量化后的性能效果如表1所示。其中第一条记录表示的是LeNet网络中两层卷积层都没有量化的基线结果。第二条记录可以看出仅仅对LeNet的第二层卷积层进行INT8量化,对模型的输出准确度几乎没有影响。第三条记录可以看出当对LeNet两个卷积层都进行量化后,模型的的输出准确度出现了下降的趋势,可能的原因是直接对模型的输入数据进行INT8量化会对模型的准确度是有一定的影响的。第四条记录可以看出仅仅对LeNet中的第一个卷积层进行量,出现了模型输出准确度的下降,从而证明了上述假设。
总体上,就LeNet网络而言,对其做INT8量化之后对模型的准确度效果影响不大。
3.2 模型前向加速的效果
第一层卷积是否(int8)量化 | 第二层卷积是否(int8)量化 | Mean_forward_cpu_time(us) (10000次取平均值) |
否 | 否 | 493.146 |
否 | 是 | 654.282 |
是 | 是 | 722.201 |
是 | 否 | 565.444 |
表2 不同层量化的前向算法耗时
不同的卷积层进行INT8量化后的前向算法耗时情况如表2所示,所有的实验都是设置openmp线程数为1的,即OMP_NUM_THREADS=1的,INT8量化的矩阵相乘的方法是基于AVX2指令集加速的。表中的第一条记录是LeNet网络两个卷积层使用MKL库FLOAT32矩阵乘法的基线前向耗时。第二条记录表示的是仅仅对LeNet网络中第二个卷积层进行INT8量化的前向耗时情况,可以看出前向的耗时要比基线要多,速度变慢,说明引入INT8量化之后整体的耗时是增加的。第三条记录可以看出,当对LeNet网络中的两个卷积层同时进行INT8量化之后,网络的前向耗时变得更大了,说明INT8量化之后网络相比原本的MKL,变慢了,前向耗时增加了。第四条记录也是一样的结论。
总体上可以看出,通过引入INT8矩阵乘法之后,相比原先caffe中的MKL库的FLOAT32的矩阵相乘方法,模型的前向耗时并没有得到加速的效果,下面会对具体的矩阵乘法的耗时情况进行对比。
3.3 矩阵乘法耗时效果对比
因为效果不佳,这里对LeNet网络中的两个卷积层矩阵乘法的耗时情况进行单独测试,结果如表3所示。
Release(-02) OMP_NUM_THREADS=1 | Conv1 | Conv2 | ||
Avx2int8 | MKLfloat32 | Avx2int8 | MKLfloat32 | |
100000mean(us) | 43.7489 | 20.2298 | 70.3936 | 76.4201 |
表3 MKL库FLOAT32矩阵乘法和基于AVX2指令集INT8矩阵乘法的耗时对比
从表中发现,虽然LeNet网络中第一个卷积层中MKL库的FLOAT32矩阵乘法较快,但是在第二层卷积层中INT8矩阵乘法的耗时是比MKL库中FLOAT32矩阵乘法要小的。其中第一层卷积层中矩阵乘法的尺寸是(576*32,32*20),第二个卷积层中的卷积乘法的尺寸是(64*512,512*50),矩阵尺寸的形式是(N*K,K*M),这说明矩阵的尺寸大小可能对矩阵运算速度上有影响。于是下面对固定N和M对不同的K的取值做了一系列的实验。特别的解释int8avx2表示的是基于avx2指令集的INT8矩阵乘法耗时情况,mkl表示的基于MKL库的FLOAT32矩阵乘法耗时情况,eigen表示的是基于Eigen3库的FLOAT32矩阵乘法耗时情况,floatavx2表示的是基于avx2指令集的FLOAT32矩阵乘法的耗时情况。
图2 100_K_100尺寸的四种矩阵乘法耗时随K变化的趋势情况对比
图2中表示的是N=100 ,M=100 ,K=32,64,96,...10240 。可以看出,随着K的增大int8avx2方法是最快的,其次是mkl,eigen,最后是floatavx2的方法。并且有mkl的耗时和int8avx2的方法很接近。
图3 1000_K_1000尺寸的四种矩阵乘法耗时随K变化的趋势情况对比
图3中表示的是N=1000 ,M=1000 ,K=32,64,96,...10240 。可以看出,随着K的增大mkl方法是最快的,其次是int8avx2,eigen,最后是floatavx2的方法。可以看出int8avx2的方法的优势已经没有了。
从图2和图3中可以得到结论是当N和M的取值相比K的取值较小的时候,随之K的增大,int8avx2相比mkl是有一定优势,但是随着M和N的值和K为同一个量级或者差距很小的时候,这种优势就没有了。图4和图5分别对比了int8avx2,floatavx2,mkl在不同的M和N取值上随着K变化的耗时比。
图4 100_K_100尺寸的int8avx2,floatavx2以及mkl矩阵乘法随K变化的耗时比
理论上INT8矩阵乘法要比FLOAT32矩阵乘法要快4倍。在我们的实验中在图4和图5中可以看出,随着K的增大,int8avx2的速度会比floatavx2快3倍还多,这个结论和ocr组实验得到的结论是一致的,这样就验证了在实际应用中同样使用avx2指令集INT8要比FLOAT32要快3倍多。然而int8avx2和mklfloat的速度几乎没有什么差距。图4中的黑线基本上维持在1附近,图5中的黑线几乎都是低于1的,这说明了在实际的应用中经过avx2优化后的INT8矩阵方法相比于mkl的FLOAT32矩阵乘法基本上没有什么加速效果。
图5 1000_K_1000尺寸的int8avx2,floatavx2以及mkl矩阵乘法随K变化的耗时比
特别的我们还做了基于Eigen库的float矩阵乘法和char矩阵乘法的耗时对比。在实验之前我们推测,Eigen的char的矩阵乘法应该比Eigen的float型的矩阵乘法要快上四倍左右,但是实际上,在测试过程中前者比后要慢很多,耗时对比情况如图6所示。
图6 100_K_100尺寸的Eigenchar和Eigenfloat矩阵乘法随K变化的耗时比
基于Eigen的char矩阵乘法竟然会比基于Eigen的float矩阵乘法的速度慢上七倍还多,这说明寄希望于Eigen库的INT8矩阵乘法加速的方法也是不可行的。
3.4 gpu上矩阵乘法耗时效果对比
前面做的都是cpu上FLOAT32矩阵乘法和INT8矩阵乘法上的时间对比情况,同样,我们也验证了在gpu上两个矩阵乘法的时间对比情况。英伟达公司在最新的GPU卡上已经支持了INT8矩阵乘法,需要调用的函数为cublasGemmEx ,本次的实验是P4机器以及人脸组集群机器P30G24进行的,我们分别对cublasGemm(普通的cublas支持的FLOAT32矩阵乘法)、cublasGemmEx(CUDA_R_32,新的接口支持的FLOAT32矩阵乘法)以及cublasGemmEx(CUDA_R_8I,新的接口支持INT8矩阵乘法)。所有的矩阵都是方阵,实验的结果如下:
Matrix size | P4(time ms) | P30G24(time ms) | ||||
Sgemm (float32) | sgemmEx (float32) | sgemmEx (int8) | Sgemm (float32) | sgemmEx (float32) | sgemmEx (int8) | |
512 | 0.094034 | 0.087107 | 0.029559 | 0.071754 | 0.066665 | 0.021427 |
1024 | 0.545898 | 0.550913 | 0.159189 | 0.269500 | 0.267248 | 0.095028 |
2048 | 3.822290 | 3.917560 | 1.141507 | 1.634034 | 1.63391 | 0.521268 |
表4 P4卡和P30G24卡FLOAT32矩阵乘法和INT8矩阵乘法的耗时对比
从表4可以看出,在P4和P40的卡上,INT8矩阵乘法的速度比FLOAT32矩阵乘法的速度要快上3倍多,所以如果对FLOAT32的数据进行INT8量化后再进行矩阵相乘,确实可以得到加速效果。特别的,这里使用gpu测试时间的函数是cudaEventRecord相关的函数,普通的计时函数好像测试GPU的时间不准。
4.总结
OCR模型之所以可以给出CPU上使用INT8矩阵乘法要比FLOAT32矩阵乘法要快3倍多的效果的原因是,他们组的FLOAT32矩阵乘法baseline的速度就比较慢,对比的对像是用SSE指令集自己实现的FLOAT32和INT8矩阵乘法的对比,对于我们要替换caffe中的MKL矩阵乘法库的需求来说没有参考意义,在我们的实验中也确实可以复现OCR组的实验结果,但是在和MKL库FLOAT32矩阵乘法对比的实验中INT8本身优势已经荡然无存了。综上所述,MKL库在矩阵乘法的优势非常的明显,之后基于INT8量化的思路方向有两个:一是等MKL2018的版本,据官方论坛说,会在这个版本中添加支持INT8计算的MKL矩阵乘法;二是学习一下tensorflow中对Eigen库矩阵乘法优化的方法,据吕亚飞他们说,谷歌工程师在tensorflow中的Eigen库的矩阵运算方面做了很大优化工作,这个是他们组研究计划,可以经常和他们保持沟通学习。
在GPU上已经有高性能的卡片(如P4和P40卡)可以支持INT8矩阵乘法的运算,并且对比FLOAT32可以得到3倍多的速度提升,所以在GPU上INT8量化进行模型加速的方案还是可行的,只是目前来看,这种GPU都比较昂贵,实际大规模投入使用更多成本比较高,以后也会关注gpu方面的加速方案。