选自,作者:MARATDUKHAN、YIMINGWU、HAOLU,机器之心编译。
为了将最新的计算机视觉模型部署到移动设备中,Facebook开发了一个用于低密度卷积的优化函数库——QNNPACK,用在最佳神经网络中。
QNNPACK的全称是QuantizedNeuralNetworkPACKage(量化神经网络包),是Facebook应用的一部分,已经被部署到全球数十亿台移动设备中。这个新库可以执行高级计算机视觉任务,如在手机上实时运行MaskR-CNN和DensePose或在性能受限的移动设备中用100ms以内的时间实施图像分类。
Facebook开源QNNPACK,为优化推理提供全方位的支持,作为构建平台的一部分。QNNPACK借助Caffe2模型表征即刻可用,Facebook正在开发实用程序,将PyTorch的Python前端模型导出到图表征中。他们还在其他平台上优化这些运算,而不仅限于移动设备。
由于移动设备的计算力仅仅是数据中心服务器的十分之一到千分之一,运行当前最佳人工智能应用需要作出一些调整,压缩来自硬件的所有可用性能。QNNPACK通过提供量化张量上的卷积、解卷积及全连接运算高性能实现来做到这一点。在QNNPACK之前,几个常见的神经网络基元(分组卷积、扩张卷积)缺乏良好的开源实现;因此,ResNeXt、CondenseNet和ShuffleNet等颇有前景的研究模型没有得到充分利用。
移动设备前沿AI技术新优化
两年前,Facebook开始在手机上部署神经网络,多数计算机视觉架构随着大型内核被部署到卷积运算中。这些运算因计算强度高而饱受诟病:直接实现涉及每个加载元素的许多乘-加运算。Caffe2Go使用的是一种叫做NNPACK的内核库,该库实现基于Winograd变换或快速傅立叶变换的渐近快速卷积算法,以减少卷积计算中的乘-加运算。例如,3×3卷积比1×1卷积运算慢两倍,但使用直接算法要慢9倍。
计算机视觉领域发展迅猛,然而,这种新的神经网络架构使用的是几种无法从快速卷积算法中获益的卷积,即1×1卷积、分组卷积、转置卷积、空洞卷积和深度卷积。这些类型的卷积计算强度相对较低,因此可以通过利用低精度计算从内存降低的带宽中受益。
用于计算机视觉的神经网络将多数推理时间用在卷积和全连接算子中。这些算子与矩阵相乘紧密相关:全连接算子和1×1卷积直接映射到矩阵相乘,具有较大内核的卷积可以分解成一种名为im2col的内存布局转换和矩阵相乘的组合。因此,卷积神经网络中的有效推理问题很大程度上可以看做矩阵乘法的有效实现问题——在线性代数库中也称为GEMM。
实现矩阵相乘
不直接在科学计算或者深度学习软件上工作的软件工程师可能不熟悉库是如何实现矩阵相乘的,所以在详细介绍QNNPACK之前,会有一个总体介绍。
在以下示例中,A是输入,B是权重,C是输出。在推理过程中,B从不变化,也因此不需要消耗时间就能迁移到任何方便的存储配置中。
MxK矩阵A与KxN矩阵B相乘得到MxN矩阵C。C中的每个元素都可以认为是A行与对应B列的点积。
在点积基元上实现整个矩阵相乘是可能的,但这样的实现过于低效。在一个点积中,每一个乘-加运算需要上传两个元素,在当前的处理器上,这一实现会受到内存和缓存带宽,而不是乘-加单元计算力的限制。但一个小小的修改——同时计算几行A和几行B的点积——却使得性能大大提升。
修改后的基元加载A的MR及B的NR元素,实施MRxNR乘积累加运算。MR和NR的最大值受到整数个数和处理器架构其它细节的限制。但在多数现代系统中,这些最大值足够大,可以使运算只受计算限制,所有高性能矩阵乘法实现都建立在这个基元上,该基元通常被称为PDOT(paneldotproduct)微内核。
神经网络中的优化及QNNPACK如何提高效率
PyTorch及其它深度学习框架在训练期间通常利用浮点数来表示权重和神经网络的神经元。模型训练完成之后,浮点数及运算就会显得过分:许多类型的模型可以在调整后使用推理用的低精度整数运算,不会出现明显的准确率损失。低精度整数表征在单精度、甚至是半精度浮点上提供一些益处:内存占用减小2/1或3/4,有助于将神经网络模型保存在移动处理器的小缓存中;提高内存带宽受限的运算性能;提高能源利用率;在许多类型的硬件上提高计算吞吐量。
QNNPACK使用与安卓神经网络API兼容的线性量化方案。它假设量化值q[i]表示为8位无符号整数,并且它们与实值表示r[i]相关,公式如下:
r[i]=scale*(q[i]–zero_point)
公式中的scale是一个正浮点数,zero_point是一个无符号的8位整数,就像q[i]一样。
由于移动架构的局限,MR和NR不超过8。因此即使是在有1024个通道的最大模型中,整个内存块在PDOT微内核中的读取速度也只能达到16KB,即使在超低端移动内核上也能适用于一级缓存。这标志着QNNPACK和其他GEMM实现之间的一个重要区别:虽然其它库重新打包A和B矩阵以更好地利用缓存层次结构,希望在大量计算中分摊打包开销,但QNNPACK针对A和B的面板适用于一级缓存的情况进行了优化。因此,它的目的是删除所有计算非必需的内存转换。
在量化矩阵-矩阵乘法中,8位整数的乘积通常会被累加至32位的中间结果中,随后重新量化以产生8位的输出。常规的实现会对大矩阵尺寸进行优化——有时K太大无法将A和B的面板转入缓存中。为了有效利用缓存层次结构,传统的GEMM实现将A和B的面板沿K维分割成固定大小的子面板,从而每个面板都适应L1缓存,随后为每个子面板调用微内核。这一缓存优化需要PDOT为内核输出32位中间结果,最终将它们相加并重新量化为8位整数。
由于ONNPACK对于面板A和B总是适应L1缓存的移动神经网络进行了优化,因此它在调用微内核时处理整个A和B的面板。而由于无需在微内核之外积累32位的中间结果,QNNPACK会将32位的中间结果整合进微内核中并写出8位值,这节省了内存带宽和缓存占用。
使整个A、B面板适配缓存帮助实现了QNNPACK中的另一个优化:取消了矩阵A的重新打包。矩阵B包含静态权重,可以一次性转换成任何内存布局,但矩阵A包含卷积输入,每次推理运行都会改变。因此,重新打包矩阵A在每次运行时都会产生开销。尽管存在开销,传统的GEMM实现还是出于以下两个原因对矩阵A进行重新打包:缓存关联性及微内核效率受限。如果不重新打包,微内核将不得不读取被潜在的大跨距隔开的几行A。如果这个跨距恰好是2的许多次幂的倍数,面板中不同行A的元素可能会落入同一缓存集中。如果冲突的行数超过了缓存关联性,它们就会相互驱逐,性能也会大幅下降。幸运的是,当面板适配一级缓存时,这种情况不会发生,就像QNNPACK优化的模型一样。
打包对微内核效率的影响与当前所有移动处理器支持的SIMD向量指令的使用密切相关。这些指令加载、存储或者计算小型的固定大小元素向量,而不是单个标量(scalar)。在矩阵相乘中,充分利用向量指令达到高性能很重要。在传统的GEMM实现中,微内核把MR元素重新打包到向量暂存器里的MR线路中。在QNNPACK实现中,MR元素在存储中不是连续的,微内核需要把它们加载到不同的向量暂存器中。越来越大的暂存器压力迫使QNNPACK使用较小的MRxNR拼贴,但实际上这种差异很小,而且可以通过消除打包开销来补偿。例如,在32位ARM架构上,QNNPACK使用4×8微内核,其中57%的向量指令是乘-加;另一方面,gemmlowp库使用效率稍高的4×12微内核,其中60%的向量指令是乘-加。
微内核加载A的多个行,乘以B的满列,结果相加,然后完成再量化并记下量化和。A和B的元素被量化为8位整数,但乘积结果相加到32位。大部分ARM和ARM64处理器没有直接完成这一运算的指令,所以它必须分解为多个支持运算。QNNPACK提供微内核的两个版本,其不同之处在于用于乘以8位值并将它们累加到32位的指令序列。
默认微内核
NEON是ARM架构上的向量扩展(vectorextension),它包含很多不寻常的指令。QNNPACK中的默认微内核广泛使用了两种NEON特定类型的指令:「长」指令,产生的元素向量是其输入的两倍宽;向量暂存器与另一向量暂存器中的元素相乘。微内核加载8位整数(无正负之分)的向量,将其扩展到16位,并使用向量x标量+长指令(/SMLAL2inAArch64)的结果与累加到32位的16位元素相乘。
ARMNEON提供了一条指令(/USUBL2onAArch64)来减去8位整数的向量并产生16位整数结果的向量,在大多数ARM微架构中,这条指令和简单的整数扩展指令(/UMOVL2onAArch64)一样快。作为额外的优化,微内核结合了A和B矩阵元素零点的减法和从8位整数到16位整数的扩展。
双发射微内核(Dual-issuemicrokernel)
默认微内核使用最少的命令,因此它在低端核上的性能最优,但每个周期默认微内核仅能执行一个NEON命令。类似地,高端Cortex-A内核也是每个周期仅能执行一次NEON整数乘法命令,但是它至少能够并行执行NEON整数乘法和NEON整数加法命令。因此理论上通过精心写并行执行两个命令的汇编代码可以改进性能:vectormultiplylong(,UMULLinAArch64)乘8-bit元素得到16-bit乘积;向量成对相加(vectorpairwiseadd)(,UADALPinAArch64)加邻近的16-bit乘积得到32-bit结果。假设向量相乘(vectormultiply)和向量成对相加命令的调度完美,则双发射微内核每个周期可输出8个乘加结果,是默认微内核的2倍。
在高端Cortex-A内核上实际利用双发射能力较为复杂,原因如下:一,在高端Cortex-A内核上的双发射能力并不完美,可以维持两个周期内执行三个命令的速度;二,NEON不支持8-bit整数向量的vector-by-scalar乘法,因此研究中使用的是向量乘法以及额外的命令(,EXTonAArch64),以旋转矩阵A中的向量;三,在8-bit元素上执行乘法,则无法在乘法之前减去零点(减去后结果的宽度是9bit),需要预计算A的行的总和以在重新量化之前调整累加的32-bit结果。
尽管上述因素导致了一些开销,但在Cortex-A75核上,利用双发射能力的微内核对于较大的通道数(K64)速度提升了15%到20%。
从矩阵相乘到卷积
简单的1×1卷积可直接映射到矩阵相乘,但对于具备较大卷积核、padding或子采样(步幅)的卷积而言则并非如此。但是,这些较复杂的卷积能够通过记忆变换im2col映射到矩阵相乘。对于每个输出像素,im2col复制输入图像的图像块并将其计算为2D矩阵。由于每个输出像素都受KHxKWxC输入像素值的影响(KH和KW分别指卷积核的高度和宽度,C指输入图像中的通道数),因此该矩阵的大小是输入图像的KHxKW倍,im2col给内存占用和性能都带来了一定的开销。和Caffe一样,大部分深度学习框架转而使用基于im2col的实现,利用现有的高度优化矩阵相乘库来执行卷积操作。
Facebook研究者在QNNPACK中实现了一种更高效的算法。他们没有变换卷积输入使其适应矩阵相乘的实现,而是调整PDOT微内核的实现,在运行中执行im2col变换。这样就无需将输入张量的实际输入复制到im2col缓存,而是使用输入像素行的指针设置indirectionbuffer,输入像素与每个输出像素的计算有关。研究者还修改了矩阵相乘微内核,以便从indirectionbuffer加载虚构矩阵(imaginarymatrix)A的行指针,indirectionbuffer通常比im2colbuffer小得多。此外,如果两次推断运行的输入张量存储位置不变,则indirectionbuffer还可使用输入张量行的指针进行初始化,然后在多次推断运行中重新使用。研究者观察到具备indirectionbuffer的微内核不仅消除了im2col变换的开销,其性能也比矩阵相乘微内核略好(可能由于输入行在计算不同输出像素时被重用)。
QNNPACK和深度卷积
分组卷积(groupedconvolution)将输入和输出通道分割成多组,然后对每个组进行分别处理。在有限条件下,当组数等于通道数时,该卷积就是深度卷积,常用于当前的神经网络架构中。深度卷积对每个通道分别执行空间滤波,展示了与正常卷积非常不同的计算模式。因此,通常要向深度卷积提供单独实现,QNNPACK包括一个高度优化版本3×3深度卷积。
深度卷积的传统实现是每次都在卷积核元素上迭代,然后将一个卷积核行和一个输入行的结果累加到输出行。对于一个3×3的深度卷积,此类实现将把每个输出行更新9次。在QNNPACK中,研究者计算所有3×3卷积核行和3×3输入行的结果,一次性累加到输出行,然后再处理下个输出行。
QNNPACK实现高性能的关键因素在于完美利用通用暂存器(GPR)来展开卷积核元素上的循环,同时避免在hotloop中重新加载地址寄存器。32-bitARM架构将实现限制在14个GPR。在3×3深度卷积中,需要读取9个输入行和9个卷积核行。这意味着如果想完全展开循环必须存储18个地址。然而,实践中推断时卷积核不会发生变化。因此Facebook研究者使用之前在CxKHxKW中的滤波器,将它们封装进[C/8]xKWxKHx8,这样就可以仅使用具备地址增量(addressincrement)的一个GPR访问所有滤波器。(研究者使用数字8的原因在于,在一个命令中加载8个元素然后减去零,在128-bitNEON暂存器中生成8个16-bit值。)然后使用9个输入行指针,指针将滤波器重新装进10个GPR,完全展开滤波器元素上的循环。64-bitARM架构相比32-bit架构,GPR的数量翻了一倍。QNNPACK利用额外的ARM64GPR,一次性存储3×5输入行的指针,并计算3个输出行。
QNNPACK的性能优势
测试结果显示出QNNPACK在端到端基准上的性能优势。在量化当前最优MobileNetV2架构上,基于QNNPACK的Caffe2算子的速度大约是TensorFlowLite速度的2倍,在多种手机上都是如此。除了QNNPACK之外,Facebook还开源了Caffe2quantizedMobileNetv2模型,其top-1准确率比相应的TensorFlow模型高出1.3%。
Caffe2quantizedMobileNetv2模型开源地址:
MobileNetV1
MobileNetV1架构在使用深度卷积(depthwiseconvolution)使模型更适合移动设备方面具备开创性。MobileNetV1包括几乎整个1×1卷积和3×3卷积。Facebook研究者将量化MobileNetV1模型从TensorFlowLite转换而来,并在TensorFlowLite和QNNPACK的32-bitARM设备上对MobileNetV1进行基准测试。二者运行时均使用4线程,研究者观察到QNNPACK的运行速度几何平均值是TensorFlowLite的1.8倍。
MobileNetV2
作为移动视觉任务的当前最优架构之一,MobileNetV2引入了瓶颈构造块和瓶颈之间的捷径连接。研究者在MobileNetV2分类模型的量化版上对比基于QNNPACK的Caffe2算子和TensorFlowLite实现。使用的量化Caffe2MobileNetV2模型已开源,量化TensorFlowLite模型来自官方库:。下表展示了二者在常用测试集上的top1准确率:
Facebook研究者利用这些模型建立了FacebookAI性能评估平台()的基准,该基准基于32-bitARM环境的大量手机设备。对于TensorFlowLite线程设置,研究者尝试了一到四个线程,并报告了最快速的结果。结果显示TensorFlowLite使用四线程的性能最优,因此后续研究中使用四线程来对比TensorFlowLite和QNNPACK。下表展示了结果,以及在典型智能手机和高端机上,基于QNNPACK的算子速度比TensorFlowLite快得多。
展望
QNNPACK已经帮助Facebook的app在全世界的移动设备中部署人工智能。研究者正在尝试进一步提升QNNPACK的性能,包括FP16格式的低精度计算,利用NEON点积(VDOT)和16-bit累积(16-bitaccumulation)来使移动设备上的AI更加轻便。
Facebook期待通过PyTorchAPI提供QNNPACK算子支持,以及为移动开发者提供工具。Facebook希望QNNPACK能够通过提升模型的移动性能惠及AI研究者和开发者。