卷积神经网络在ARM-CPU上的推断计算综述

摘要


  深度学习在计算机视觉领域大放异彩,许多在传统方法下无法解决的问题正在被一一攻克。然而,高昂的计算成本也极大地限制了深度学习的使用,在移动端设备、嵌入式设备等计算资源比较拮据的平台上其计算密集的特性尤为突出。本文对现阶段,工业界所使用的深度学习推断阶段的软件操作库、计算框架等,做了充分调研,由底向上勾勒出深度学习推断阶段的技术轮廓。本文主要工作如下:
  1. 总结深度学习推断阶段的主要操作,指出其性能瓶颈所在;
  2. 从软件库层面总结现阶段可用的开源库;
  3. 对比各层次的软件库,做出总结。

深度学习推断的主要操作


  对于大部分的卷积神经网络而言,卷积层是最消耗时间的部分,而全连接层则是参数量最多的部分[2]。 如下图所示[10]为 2012 年获得imagenet冠军的深度神经网络结构Alexnet分别在GPU和CPU进行推断的性能benchmark,由图可以看出,在CPU上卷积层和全连接层占用了95%的计算时间,而在CPU上卷积层和全连接层占用了89%的时间,如何高效地进行卷积层和全连接层的计算成为提升深度学习推断性能的关键点。

  此处我们总结了业界现在进行卷积层计算和全连接层计算较为主流的方法,分别是 im2col + GEMM(Image to Column + GEneral Matrix Mutiplication),FFT(Fast Fourier Transforms),Winograd Transform。由于卷积层和全连接层的计算实现原理相仿,故此处以卷积层的计算为示例。

  首先是 im2col + GEMM,这是处理卷积层计算较为直观的方法,它的核心思想是将计算转换成两个矩阵的乘法:1. 使用 im2col 将图像转换成一个矩阵;2. 使用 im2col 将卷积核转换成一个矩阵;3. 对前两步所得矩阵进行相乘操作。如下图所示:

  im2col 原来是 Matlab 中提供的一个函数,在处理神经网络的三通道图像输入时,它的操作就是将一个 3D 的数组转换成一个 2D 的数组,这样我们才能把图像当成一个矩阵来处理。在卷积计算中,每个卷积核的运算对象都是输入的 3D 数据中的一个个小立方体,所以 im2col 在处理图像时会根据 stride 将一个个的小立方体中的数据拷贝成矩阵中的一行。我们对卷积核进行相同的转换操作,再将得到的卷积核矩阵进行一下转置,就可以进行卷积层的运算操作了。这里 k 是每个卷积核和输入立方体的数值个数,那么假设我们要处理 1 x 3 x 160 x 160 (N x C x H x W)的一张图像,经过一个 3 x 3 x 3 x 16 (H x W x I x O) 的卷积层,stride = 1padding = "SAME",则需要进行如下的矩阵运算:

$$\begin{equation}\begin{split} A_{160 \times 160,3 \times 3 \times 3} \times B_{3 \times 3 \times 3,16} = C_{160 \times 160, 16} \\   \text{      where  A  is  input  matrix  and  B  is  kernel  matrix} && \end{split}\end{equation}$$

  或许你已经注意到了,如果 stride < kernel size,那么将会有大量的重复像素被包含到转换之后的矩阵之中,这对于内存而言是一个很大的消耗。这是 im2col + GEMM 进行卷积运算的一个明显缺点,不过相比起能够利用多年来科学计算工程师对大矩阵相乘优化的成果,这个缺点就显得微不足道了。im2col + GEMM 方案被很多计算框架所采用,例如贾杨清博士编写的 Caffe 框架就是这么实现的,具体请参考这篇文章:在 Caffe 中如何计算卷积?。全连接层的运算利用 im2col + GEMM 实现较为容易理解,限于篇幅所限我们这里不展开讨论。

  第二个方法是基于快速傅里叶变换的卷积法(Fast Fourier Transforms)[11][12][13]。使用 FFT 进行卷积的计算,其背后的数学原理是将时域中的卷积运算转换成频域中的乘积运算,从而将运算量减少。使用 FFT 变换进行卷积计算的定义如下:

  对于定义在整数Z^2上的二元函数fg,二者的离散卷积操作定义如下(此处函数fg是图像像素位置到像素值之间的一个映射):

$$(f * g)(x, y) = \sum^{\infty}_{u=-\infty}\sum^{\infty}_{v=-\infty}f(u, v)g(x - u, y - v) $$

  当 fg 的支撑集为有限长度 UV 时,上式会变成有限和(即 UV 将决定卷积核进行计算的大小):

$$(f * g)(x, y) = \sum^{U}_{u=-U}\sum^{V}_{v=-V}f(u, v)g(x - u, y - v) $$

  由卷积定理我们知道,两个离散信号在时域做卷积的离散傅里叶变换相当于这两个信号的离散傅里叶变换在频域做相乘,具体地,先将信号从时域转成频域:

$$\begin{split} F(f) = DFT(f(x, y)) \\ F(g) = DFT(g(x, y)) \end{split}$$

  则有(o 为矩阵逐元素相乘,经过离散傅里叶变换后得到的矩阵大小与原来的相同):

$$y(x, y) = f(x, y) * g(x, y) \leftrightarrow F(y) = DFT(y) = F(f) \circ F(g) = DFT(f(x, y)) \circ DFT(g(x, y))$$

  最后,我们再做一次傅里叶逆变换,将频域信号转回时域,就完成了卷积的计算:

$$y(x, y) = IDFT(F(y)) = IDFT(DFT(f(x, y)) \circ DFT(g(x, y)))$$

  上述过程总共进行2次DFT(离散傅里叶变换)和1次IDFT(逆离散傅里叶变换),DFT和IDFT的运算可以采用FFT。要在频域中对一副图像进行滤波,滤波器的大小和图像的大小必须要匹配,这样两者的相乘才容易。因为一般卷积核的大小比图像要小,所以我们需要拓展我们的kernel,让它和图像的大小一致[13],所以需要使用循环填充的方式将卷积核进行扩展,以便最后两个信号相乘时能够大小一致。

  采用上述方式进行卷积的计算,其优点显而易见——大大减少在时域中进行直接卷积运行的计算量。这种方法被一些神经网络运算库所采用,如facebook的NNPACK。但是由于现代的卷积神经网络常使用stride = 2 / 3 / ...的卷积(上述方法为stride = 1,所以其对卷积的方式有限制性,无论stride值为多少,都会进行stride = 1的操作,不如im2col + GEMM方式通用,而且当卷积核足够小、stride值足够大时,im2col + GEMM 的计算量将比FFT方法更少。各个厂商在实现其神经网络库的卷积操作所采取的方法各不相同,都有其所考虑的侧重点,如NVIDIA的cuDNN就撅弃了FFT这种方式,具体可参考Chetlur, Sharan, et al. “cudnn: Efficient primitives for deep learning.” arXiv preprint arXiv:1410.0759 (2014).

  第三种方法是基于Winograd Transform。其核心思想是采用Winograd’s minimal filtering algorithms,针对3 x 3的小卷积核和较小的 batch size能达到很高的计算速度。具体可参考 Lavin, Andrew, and Scott Gray. “Fast algorithms for convolutional neural networks.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2016.

可用的开源库


  本文的重点叙述对象平台是ARM架构的CPU,这是因为目前移动端常用的系统中,各个厂商之间产品的性能差异主要体现在基于ARM架构的CPU平台之上。

  NEON[5]——在现代的软件系统中,当需要在32位微处理器上处理16位数据(如语音)或8位数据(如图片)时,有部分计算单位无法被用到。基于SIMD(单指令多数据)的并行计算模型可在这种情况下提高计算性能,如本来的一个32位数加法指令,可同时完成4个8位数的加法指令。如下图所示,为ARMv6 UADD8 R0, R1, R2指令 ,其利用32位通用寄存器同时进行4个8位数的加法,这样的操作保证了4倍的执行效率而不需要增加额外的加法计算单元。从 ARMv7 架构开始,SIMD计算模型便通过一组在特定的64位、128位向量寄存器(不同于通用寄存器)上进行操作的指令得到扩展,这组指令便称为NEON,NEON技术已经在ARM Cortex-A系列处理器上得到支持。NEON指令由ARM/Thumb指令流进行执行,相比需要额外加速设备的加速方法,NEON简化了软件的开发、调试和集成。

  如下图为VADD.I16 Q0, Q1, Q2指令对存储在向量寄存器 Q1, Q2 中的128数据以16位为单位进行并行加法。

  NEON指令支持8、16、32、64位有符号/无符号整型数,支持32位单精度浮点数,使用VCVT指令可进行数据类型的转换。NEON指令的寄存器组由32位/64位寄存器组成,在NEON指令的眼中,寄存器组可被看成16个128位的4字寄存器,既Q0 - Q15,也可被看成32个64位的双字寄存器,既D0 - D31,如下图所示,这种不同的寄存器组视图不需要通过特定的指令进行切换,而是由 NEON 指令来决定执行时需要的寄存器组视图。

  要使用最新的NEON指令,需要使用最新的GNU、RealView编译工具,这两支编译器均支持NEON指令集。要使用NEON指令,最直接的方式是使用汇编语言,如下代码分别为使用GNU assembler(Gas) 和RVCT(RealView Compilation Tools)汇编调用NEON指令的函数,其参数的传递和返回均通过NEON寄存器。

  在代码中使用intrinsic函数和数据类型是使用NEON指令的另一种方式,这种方式还会提供如类型检查、自动的寄存器分配等特性。intrinsic函数的使用类似于C/C++函数接口的使用,但在编译时,intrinsic函数会被更低层次的指令序列代替,这也意味着工程师能使用高级语言描述更低层次的架构行为,编译器能在编译阶段帮助工程师进行性能上的优化,如下代码实现了上述汇编代码相同的功能。

  还有一种使用NEON指令的方式,那便是由编译器进行自动向量化。由于不使用NEON的汇编指令和intrinsics,所以这种方法能够保持代码的可移植性。由于C/C++语言并不能显示地描述代码的并行行为,所以工程师需要给编译器一些提示,以便编译器能够使用NEON指令,如下所示代码。

  Eigen[6][7][8]——正如前面所说,神经网络的推断计算本质上就是矩阵的运算,所以一个性能突出的 BLAS 库将成为计算的利器。Eigen 是 C/C++ 的高性能线性代数运算库,提供常用的矩阵操作,目前主流的深度学习框架如 TensorFlow,Caffe2等都选择 Eigen 作为 BLAS 库。Eigen 官方对业界常用的 BLAS 库做了 benchmark,比较了 Eigen3, Eigen2, Intel MKL, ACML, GOTO BLAS, ATLAS 的运算性能,在单线程情况下,最重量级的矩阵乘法性能对比如下图所示。由 Eigen 的官方 benchmark 可以看出,在大多数操作上Eigen的优化已经逼近MKL,甚至一些操作超过了 MKL。Eigen 支持多个 SIMD 指令集,包括 ARM 的 NEON 指令集。也就是说,如果目标是 ARM 架构的芯片,那么使用 Eigen 将从 NEON 指令集获得性能增益。

压博娱乐登入开户压博国际娱乐手机客户端 压博娱乐官网平台Eigen supports SSE, AVX, AVX512, AltiVec/VSX (On Power7/8 systems in both little and big-endian mode), ARM NEON for 32 and 64-bit ARM SoCs, and now S390x SIMD (ZVector). With SSE, at least SSE2 is required. SSE3, SSSE3 and SSE4 are optional, and will automatically be used if they are enabled. Of course vectorization is not mandatory – you can use Eigen on any CPU. Note: For S390x SIMD, due to lack of hardware support for 32-bit vector float types, only 32-bit ints and 64-bit double support has been added.

  • model name : Intel(R) Core(TM)2 Quad CPU Q9400 @ 2.66GHz ( x86_64 )
  • compiler: c++ (SUSE Linux) 4.5.0 20100604 [gcc-4_5-branch revision 160292]

  ACL(ARM-Compute Library)[3][4]——专为 ARM CPU & GPU 优化设计的计算机视觉和机器学习库,基于 NEON & OpenCL 支持的 SIMD 技术。作为 ARM 自家的加速库,CPU 端基于 NEON 指令集做了许多高性能的接口,包括许多常用的图像处理函数、矩阵运算函数、神经网络操作函数等,如下图为 ComputeLibrary/arm_compute/runtime/NEON/NEFunctions.h 文件所提供的函数一览,位操作、直方图均衡化、矩阵乘法、卷积、池化、BN应有尽有,接口粒度有粗有细。

  使用ARM-Compute Library进行推断网络的搭建很方便,如下代码构建了一个conv1: 3x3 -> BatchNorm -> relu的小网络。在LG NEXUS 5平台上,这个网络进行一次推断的时间为8ms,而使用 Caffe2 进行推断的时间为4.8ms。由于ARM-Compute Library现在还处于开发完善阶段,很多操作如MobileNets中使用的Depthwise Seperable Convolution(Group Convolution)还没有得到支持。

  NNPACK[14]——NNPACK由facebook开发,是一个加速神经网络推断计算的加速包,NNPACK可以在多核CPU平台上提高卷积层计算性能。NNPACK采用的快速卷积算法基于Fourier transform算法和Winograd transform算法。下表是NNPACK在官网上展出的跟caffe的性能比较[14],由表可以看出,在常见网络结构的卷积操作中,NNPACK都有一个很大算力提升(Forward propagation performance on Intel Core i7 6700K vs BVLC Caffe master branch as of March 24, 2016)。NNPACK对Fast Fourier transform,Winograd transform,Matrix-matrix multiplication(GEMM),Matrix-vector multiplication (GEMV),Max-pooling做了特别的优化。NNPACK已经被许多深度学习框架用于底层加速,包括facebook自家的Caffe2。

Library Caffe NNPACK NNPACK NNPACK
Algorithm im2col + sgemm FFT-8x8 FFT-16x16 Winograd F(6x6, 3x3)
AlexNet:conv2 315 ms 129 ms 86 ms N/A
AlexNet:conv3 182 ms 87 ms 44 ms 70 ms
AlexNet:conv4 264 ms 109 ms 56 ms 89 ms
AlexNet:conv5 177 ms 77 ms 40 ms 64 ms
VGG-A:conv1 255 ms 303 ms 260 ms 404 ms
VGG-A:conv2 902 ms 369 ms 267 ms 372 ms
VGG-A:conv3.1 566 ms 308 ms 185 ms 279 ms
VGG-A:conv3.2 1091 ms 517 ms 309 ms 463 ms
VGG-A:conv4.1 432 ms 228 ms 149 ms 188 ms
VGG-A:conv4.2 842 ms 402 ms 264 ms 329 ms
VGG-A:conv5 292 ms 141 ms 83 ms 114 ms
OverFeat:conv2 424 ms 158 ms 73 ms N/A
OverFeat:conv3 250 ms 69 ms 74 ms 54 ms
OverFeat:conv4 927 ms 256 ms 272 ms 173 ms
OverFeat:conv5 1832 ms 466 ms 524 ms 315 ms

  NNPACK 主要支持下列的神经网络操作:

  • Convolutional layer
    • Training-optimized forward propagation (nnp_convolution_output)
    • Training-optimized backward input gradient update (nnp_convolution_input_gradient)
    • Training-optimized backward kernel gradient update (nnp_convolution_kernel_gradient)
  • Inference-optimized forward propagation (nnp_convolution_inference)
  • Fully-connected layer
    • Training-optimized forward propagation (nnp_fully_connected_output)
    • Inference-optimized forward propagation (nnp_fully_connected_inference)
  • Max pooling layer
    • Forward propagation, both for training and inference, (nnp_max_pooling_output)
  • ReLU layer (with parametrized negative slope)
    • Forward propagation, both for training and inference, optionally in-place, (nnp_relu_output)
    • Backward input gradient update (nnp_relu_input_gradient)
  • Softmax layer
    • Forward propagation, both for training and inference, optionally in-place (nnp_softmax_output)

  NCNN[15]——NCNN由腾讯研发开源,是一个专门为移动端进行优化的神经网络推断计算框架,其不依赖于其他第三方库,网络操作如卷积、池化等均由框架自己实现,这是NCNN相对于Caffe2、TensorFlow等所不同的一个特点。目前,NCNN只支持对深度学习训练框架 Caffe[16]导出的模型进行解析。

  使用Caffe进行模型的训练,当训练完成时,框架会导出一个存有训练得到的网络各层的参数文件*.caffemodel,配合描述网络结构的文件deploy.prototxt,即可调用Caffe的推断接口生成对应的推断器,在输入图像即可进行推断,如下为使用 Caffe 的 Python 接口进行推断的代码示意[16]:

  NCNN并不提供网络训练的相应操作,其起到的作用与上述代码一致——读入网络参数与结构,进行前向推断运算。由于Caffe模型与NCNN模型并不完全兼容,所以在使用NCNN之前需要先对Caffe模型进行转换,这个过程由NCNN所提供的转换工具caffe2ncnn完成,NCNN在本地进行编译时在ncnn/build/tools下会生成一系列的工具程序。除了模型转换程序,NCNN还提供模型的加密程序ncnn2mem,用于对网络结构文件和网络参数文件进行加密。读取加密模型与读取非加密模型需要使用不同的接口,如下代码所示:

  NCNN的推断过程很方便,在 ncnn/examples/squeezenet.cpp下有例程,核心操作如下代码所示。相比ACL、NNPACK等网络操作库,使用NCNN不同再重新定义推断的网络,这提高了Caffe所导出模型的通用性,在对模型进行修改后不用对推断的代码进行修改。

  TVM[18]——TVM提供了一个Tensor的中间描述层,围绕着这个中间描述层,TVM提供了操作Tensor的计算接口,并能向下生成各个平台的运行代码,从而向上提供给各类深度学习框架使用。TVM的工作流程分为如下几个步骤:1. 描述Tensor和计算规则;2. 定义计算规则的运算方式;3. 编译出所需平台的运行代码;4. 集成生成函数。

  TVM使用类似于TensorFlow的计算图模型来定义计算规则,如下代码所示定义了Tensor A,B,C进行向量加的计算规则,其使用lamda表达式来描述Tensor中各个元素之间的计算关系。在此阶段不会有任何具体的运算发生:

  定义完计算规则后,计算规则本身是可以有多种不同的实现的。如上述的向量加,由于向量各个元素之间的运算并不存在依赖关系,所以可以使用并行的方式来进行计算,但是这在不同的设备上,其实现的代码则千差万别。TVM要求工程师为计算规则定义一个对应的schedule,用来描述计算规则的运算方式,以便能针对不同的计算平台生成相应的代码,我们可以把schedule理解为计算规则的转换方式,如下代码定义了一个schedule,默认情况下,schedule将以串行的方式来对计算规则进行运算。此处我们使用split函数来对串行的运算方式进行分解,使之能以factor个元素为单位对运算方式进行分解:

  当定义完 schedule 后,就可以对特定计算平台进行绑定,编译出所需的代码,如下代码将schedule所返回的两个迭代子与NVIDIA GPU的CUDA 计算模型进行绑定,bxs的第一层迭代子,其对应CUDA中的线程块(BLOCK),在上述配置下,将有ceil(n / 64)个线程块运行在GPU上;txs的第二层迭代子,每次迭代将对一个C的元素进行计算,在GPU上每次迭代将由一个线程(THREAD)来负责计算,即每个线程块中将配置 64 个线程。绑定完成后,我们可以调用build函数生成对应的TVM函数,build函数会接收schedule、输入输出的 tensor、目标语言、目标 host等作为参数,生成一个指定语言的函数接口(默认为Python)。此处生成的fadd_cuda是CUDA核函数的一个warpper,使用它便可将运算在 NVIDIA GPU上执行。

  生成函数接口之后,我们可以使用任意语言来调用该函数接口,此处我们仍然是用Python来进行调用:1. 先声明一个GPU context, 使之能与我们CPU端的线程形成对应关系;2.tvm.nd.array将数据拷贝到GPU之上;3. 执行运算。

  我们可以将生成的CUDA代码输入示意fadd_cuda.imported_modules[0].get_source():

  而当我们希望使用OpenCL的计算设备时,则可以选择生成OpenCL所需的代码,并是用fadd_cl.imported_modules[0].get_source()查看其生成的OpenCL代码:

  正如TVM对自身的介绍所说,其希望能搭建深度学习框架与后端计算设备之间的桥梁。这是一项很有雄心,同时也特别繁杂的工作。

总结


  本文总结了现阶段工业界在移动端设备进行卷积神经网络推断的方法,在软件的每个层次上都总结了具有代表性的解决方案,下图是这些解决方案的一个层次结构图。由于深度学习的训练阶段所采用的技术方案实际上已经被NVIDIA所垄断,所以我们给出了NVIDIA提供的技术栈作为横向的比较对象。

参考链接


卷积神经网络在 ARM-CPU 上的推断计算综述

发布者

默默

默默码农

《卷积神经网络在ARM-CPU上的推断计算综述》上有2条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注