MNN对ConvolutionDepthwise Int8量化算子在ARM V8(64位)和ARM V8.2上的性能做了较大的优化,主要优化方法包括改变数据的排列方式以及使用Sdot指令。为了方便叙述,后文统一用DepthwiseConvInt8来表示ConvolutionDepthwiseInt8量化算子。在优化前在安卓手机上测得该量化算子的耗时比使用FP16推理多三倍,严重影响量化模型在端侧的性能。本文从ARM汇编算子的角度出发讲解DepthwiseConvInt8算子的优化方案。
▐ ARM V8 优化数据排列方式
方案介绍
MNN中DepthwiseConvInt8的输入数据格式是,后文简称这种数据排列数据方式为C4),即每4个Channel的数据排列在一起。同理,我们用C16表示每16个Channel的数据连续排列的数据排布方式。下图直观地描述了C4的数据排列方式,假设,图中每一个小正方形代表一个数据,四种颜色分别代表feature map上四个坐标点,正方形内的数字编号表示它们在内存中的排列次序。
一般地,我们会对采取多线程计算,对采取ARM汇编并行计算。本文关注单线程时对维度做性能优化。在ARM汇编中通常会同时计算输出的特征图上多个点的结果,这要求我们尽量能够同时读取输入特征图上的点数据。例如当Depthwise层的Stride参数等于1时,同时计算输出特征图中4个点坐标的数据(一共4(坐标点)*4(channel)=16个数据),我们需要读取上图中编号为的正方形数据。当每个数据占4个字节(32位)时,4个输入数据正好填满一个向量寄存器。但是在Int8量化算子中,每个输入数据占8位(1个字节),如果仍然采取C4的数据排列,则4个数据填不满一个向量寄存器。特别地,当Depthwise层的Stride参数不是1时,采用C4的数据排列方式会导致feature map上多个数据时不能连续读取,进而导致性能损失。所以对于Int8类型的输入数据,在ARM平台上我们首先考虑将C4的数据排列方式改为C16的排列,这样保证可以连续地读取16个Int8类型数据来填满一个向量寄存器。我们用汇编代码来表示这两种数据读取方式的差异。
/* x0: source address, 读取feature map上的4个点 */
/* pack=4, stridex = 2, sizeof(inputData)=1 */
ld1 {v0.s}[0], [x0], #8
ld1 {v1.s}[0], [x0], #8
ld1 {v2.s}[0], [x0], #8
ld1 {v3.s}[0], [x0], #8
/* pack=16, stridex = 2, sizeof(inputData)=1 */
ld1 {v0.4s}, [x0], #32
ld1 {v1.4s}, [x0], #32
ld1 {v2.4s}, [x0], #32
ld1 {v3.4s}, [x0], #32
从代码中可以看到同样是使用4条指令读取数据,C16比C4多读取3倍的数据。所以对于ARM V8平台,我们将pack=4更改为pack=16,虽然该方案额外增加了推理时数据排布的转换时间,但是在ARM汇编内部更好地并行化带来的收益更大。
ARM V8 性能提升结果
华为 Mate40 Pro ARM V8 |
优化前 C4数据排列 |
优化后 C16数据排列 |
Convolution Depthwise Int8 量化算子 |
4.46 ms |
2.78 ms |
▐ ARM V8.2 使用sdot指令提高性能
为什么sdot指令能提高算子性能?
// 3x3kernel 不使用sdot指令,进行9次循环
Loop_Kernel_H3:
Loop_Kernel_W3:
smlal v0.4s, v1.4h, v2.4s // 累加结果存储在v0中
// 3x3kernel 使用sdot指令
sdot v0.4s, v1.16b, v3.16b
sdot v0.4s, v2.16b, v4.16b
smlal v0.4s, v5.4h, v6.4h
使用sdot指令会带来额外开销吗?
// v0.16b : [0,1,2,3, 4,5,6,7, 8,9,10,11, 12,13,14,15]
// v1.16b : [0,1,2,3, 4,5,6,7, 8,9,10,11, 12,13,14,15]
// v2.s[0]=v0.b[0]*v1.b[0]+v0.b[1]*v1.b[1]
// +v0.b[2]*v1.b[2]+v0.b[3]*v1.b[3]
sdot v2.4s, v0.16b, v1.16b // v2.4s: [14,126,366,734]
由代码示例得知,进行累加的4个元素在内存中是连续排列的。上文中已经介绍了算子的输入数据排列是C4,同一个Kernel中的所有元素在内存中肯定不能连续读取。
ARM V8.2 如何进行数据重排
思考重排问题时我们将问题简化,不需要写代码就能得到最高效的解决方案(前提是足够熟悉ARM汇编指令,因为指令集了解地越多,解决方法就越多)。我们把问题抽象为如何高效重排一个向量寄存器中的16个int8_t数据。重排前后的数据排列对比图如下:
ARM V8.2 性能提升结果
|
|
|
|
|
|
|
|
▐ ARM V8.2和ARM V8的优化方案差异总结
在ARM V8.2 上优化DepthwiseConvInt8算子性能的核心是重排输入数据以方便使用sdot指令进行累加。因为Depthwise算子的计算复杂度比Convolution低,所以数据重排的耗时对算子性能影响更大。经过稿纸推演和实际测试,我们确定了耗时最低的数据重排方案,即在输入数据是NC4HW4时,用tbl指令将与3x3kernel中9个数据分成3组,第一组和第二组分别有4个数据连续排列,最后一个数据单独排列。在ARM V8上的优化受限于指令种类数量,目前仅从减少数据读取时间角度优化算子性能。
▐ 总结
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。