前言
前段时间因为项目需求,搞了一段时间的前向优化,主要参考了Facebook 18年开源的神经网络加速库QNNPACK。
QNNPACK针对INT8的量化网络,对神经网络的各个算子都提供了非常高效的优化方案,传言其卓越的性能几乎击败了全部已公开的加速算法,不管真假,就优化方案而已个人觉得还是比较新颖,值得好好研究的。到目前为止,QNNPACK已经开源一年多了,除了不多的官方资料之外,民间资料也是最近几个月才陆陆续续出现一些(文章后面会贴出链接),觉得有必要对QNN做一个细致的记录,这里写个QNN专栏方便大家参考交流。
本篇仅作为开篇引言,主要说一些宏观的东西,内容基本都会在官方文档上面找到,短期内会补上对QNN的详细介绍。文章内容会偏重于对QNN源码的分析以及工程的实现,当然在介绍的过程中会结合算法对方案的优势进行说明。详细的宏观算法设计可参考知乎大神的文章神经网络加速库 QNNPACK 实现揭秘;facebook官方文档也可参考,就是比较粗略;前不久QNN的作者也发表文章介绍了qnn加速的核心思想(ps:作者Marat Dukhan从facebook跑路google了,文章署名也是现任公司google...., QNN估计是要停更了)。
QNNPACK初识
为了让这篇文章不会显得毫无价值,接下来会对QNN的优化方案做一个宏观的介绍,让各位心里有个数。QNN的数据流通为UINT8类型,图像内存分布为NHWC模式,内核库包含常见的Conv/Depthwise Conv、Deconv、各种Pooling、Eltwise Sum等网络算子。本文默认读者了解神经网络量化原理,不清楚的也不影响看懂,想了解的话看这篇文章。
每个算子从代码逻辑上可以分为三个阶段:
第一:卷积的数据准备,这一步可以说是精华所在,整个算法实现的基础了。这一阶段涉及输入相关的间接内存缓冲区(Indirext Buffer)的计算以及对权重数据的分块重排;
以卷积为例,QNNPACK 实现了 4×8 的和 8×8 两种计算核(micro kernel),分别用于支持 armv7 和 arm64 指令集的处理器。这两种计算核在原理上区别不大,后者主要利用了更多的寄存器和双发射(Dual Issue)以提高计算的并行度。
图一:间接缓冲区
QNN的一大亮点就是使用了Inter Buffer。常用的Im2col优化算法存在两个问题,第一是占用大量的额外内存,第二是需要对输入进行额外的数据拷贝。这两点如何才能解决呢?间接卷积算法给出的答案是间接缓冲区(Indirect Buffer),如图一右半所示。
Im2col 优化算法首先将输入拷贝到一个矩阵中,如图一中 Input 的相关箭头,然后执行矩阵乘操作。间接卷积优化算法使用的间接缓冲区中存储的其实是指向输入的指针(这也是间接卷积优化算法要求输入内存地址固定的原因)。在代码实现时,第一阶段就需分配好每一层输入的内存空间将Indirect Buffer要指向的区域固定,在第二阶段运行的时候将每一层的输入都放到第一阶段分配的指定内存块中,之后就可以利用Inter Buffer(便于介绍,后文将用二级指针替换Inter Buffer的说法)来对输入数据进行操作了。
第二:运行,在NHWC数据模式的加持下,qnnpack的解决方案使得cache的利用率空前提高,NEON并行原语及NEON汇编也得以大放光彩。
第三:内存清理,就不说了。
文章有关QNNPACK原理的内容部分会参考神经网络加速库 QNNPACK 实现揭秘以及通用矩阵乘(GEMM)优化与卷积计算,感谢两位大佬。文章毕竟是从源码逆向推理,如果存在分析错漏还请各位小伙伴帮忙指正。