AI移动端优化之Im2Col+Pack+Sgemm

AI移动端优化之Im2Col+Pack+Sgemm

转自:https://blog.csdn.net/just_sort/article/details/108412760

这篇文章是基于NCNN的Sgemm卷积为大家介绍Im2Col+Pack+Sgemm的原理以及算法实现,希望对算法优化感兴趣或者做深度学习模型部署的读者带来帮助。

1. 前言

最近空闲时候在给MsnhNet贡献Arm端的代码,地址详见:https://github.com/msnh2012/Msnhnet 。对于卷积运算一种最常见的实现方法就是Im2Col加上Sgemm,这样可以把复杂的卷积运算转换成两个矩阵直接相乘,在OpenBlas中就提供了矩阵相乘的多种算法如Sgemm。所以这篇文章的目的就是讲清楚如何对卷积层进行Im2Col将其转换为矩阵乘法,以及为了进一步加速矩阵乘法的计算过程使用了NCNN中的Pack策略,最后再使用Neon对主要的计算过程进行优化。

2. Im2Col+Sgemm计算卷积原理

相信大家对于卷积的概念都非常熟悉了,这里就不再重复提起了,我大概说一下我了解的卷积的计算方式有哪些吧。首先是暴力计算,这是最直观也是最简单的,但是这种方式访存很差,效率很低。其次是我在基于NCNN的3x3可分离卷积再思考盒子滤波介绍过的手工展开某些特定的卷积核并且一次处理多行数据,这样做有个好处就是我们规避掉了一些在行方向进行频繁切换导致的Cache Miss增加,并且在列方向可以利用Neon进行加速。再然后就是比较通用的做法Im2Col+Sgemm,这种方式可以使得访存更好。其它常见的方法还有Winograd,FFT,Strassen等等。

本文的重点是尝试讲清楚Im2Col+Sgemm的算法原理及其实现思路,以及在Im2Col的基础上进行数据Pack进一步改善矩阵乘法的计算中由于Cache Miss带来的性能下降。

首先,让我们来考虑一个单通道的长宽均为 444 的输入特征图,并且假设卷积核的大小是 3×33\times 33×3,那么它经过Im2Col之后会变成什么样呢?如下图所示:

在这里插入图片描述

其实变换的方式非常简单,例如变化的第一步如下图所示:

在这里插入图片描述

直接在卷积核滑动的范围内,把所有元素顺序排列成一列即可,其它位置处同理即可获得Im2Col的结果。多通道和单通道的原理一样,如下图所示:

在这里插入图片描述

其次,让我们来考虑一下卷积核经过Im2Col操作后应该是什么样子的,对于多通道如下图所示(单通道就是其中一种颜色):

在这里插入图片描述

最后,输入特征图和卷积核都进行了Im2Col之后其实都变成了一个二维的矩阵,我们就可以使用矩阵乘法来直接计算结果了。下图展示了一个 4×4×34\times 4\times 34×4×3 的特征图和一个 3×3×33\times 3\times 33×3×3 的卷积核经过Im2Col之后之后使用Sgemm进行计算的过程,其中每一种颜色都代表一个输出通道。

在这里插入图片描述

相信看了上面几个图之后,大家对Im2Col和Sgemm结合用来做卷积的过程有所了解,为了更加严谨,下面还是以公式化的形式再来总结一下:

假设输入的特征图维度是 (1,D,H,W)(1,D,H,W)(1,D,H,W) ,表示Batch为1,通道数为 DDD,高为 HHH ,宽为 WWW,卷积核的维度是 (Dout,D,K,K)(D_{out},D,K,K)(Dout,D,K,K),表示输出通道数为 DoutD_{out}Dout ,卷积核大小是 K×KK\times KK×K,则输入特征图的Im2Col过程如下所示,窗口从左到右上到下的顺序在每个输入通道同步滑动,每个窗口内容按行展开成一列,然后再按通道顺序接上填到 im2colim2colim2col buffer对应的列,并且 im2colim2colim2col buffer按照从左到右的顺序填写。

在这里插入图片描述

由于这个地方没有padding并且步长是1,那么卷积的输出特征图的高为 Hout=(H+2∗p−K)/Stride+1=H−K+1H_{out}=(H+2*p-K)/Stride+1=H-K+1Hout=(H+2pK)/Stride+1=HK+1,同理宽为 W−K+1W-K+1WK+1。所以最后 im2colim2colim2col buffer的宽维度为 (H−K+1)∗(W−K+1)(H - K +1) * (W - K + 1)(HK+1)(WK+1),高维度则是 K∗K∗DK * K * DKKD ,最后再把权值Im2Col成 (Dout,D∗K∗K)(D_{out},D*K*K)(Dout,DKK),这样卷积就可以直接变成两个二维矩阵的乘法了。

最后再获得了乘积矩阵之后只需要按照通道进行顺序排列就可以获得输出特征图了,这个过程就是Im2Col的逆过程,学名叫作Col2Im,也比较简单就不再赘述了。

3. 如何实现Im2Col

这里我们先做一些约定,变量 src 表示输入特征图的数据,输入特征图的维度是 [1,inChannel,inHeight,inWidth][1,inChannel,inHeight,inWidth][1,inChannel,inHeight,inWidth],输入卷积核的维度是$ [1,outChannel,KernelH,kernelW]$,最后卷积操作执行完之后的输出特征图维度就是 [1,outChannel,outHeight,outWidth][1,outChannel,outHeight,outWidth][1,outChannel,outHeight,outWidth]

3.1 对输入特征图进行Im2Col

首先我们对输入特征图进行Im2Col操作,按照之前的介绍可知,我们要得到的结果矩阵的维度是 [inChannel∗kernelH∗kernelW,outHeight∗outWidth][inChannel*kernelH*kernelW,outHeight*outWidth][inChannelkernelHkernelW,outHeightoutWidth],这个过程只需要窗口从左到右上到下的顺序在每个输入通道同步滑动,每个窗口内容按行展开成一列,然后再按通道顺序接上填到 im2colim2colim2col buffer对应的列,并且 im2colim2colim2col buffer按照从左到右的顺序填写。不难写出这部分的代码实现,这里的 src_im2col 等价于 im2colim2colim2col buffer:

// 1. im2colfloat *src_im2col = new float[outWidth * outHeight * kernelH * kernelW * inChannel];const int Stride = kernelW * kernelH * outHeight * outWidth;//const int inSize = inHeight * inWidth;const int outSize = outHeight * outWidth; const int kernelSize = kernelH * kernelW;// inCahnnel x kW x kH// outWidth x outHeightfor(int cc = 0; cc < inChannel; cc++){const float *src0 = src + cc * kernelH * kernelW * inChannel;int dst_idx = Stride * cc;for(int i = 0; i < kernelH; i++){for(int j = 0; j < kernelW; j++){for(int x = 0; x < outHeight; x++){for(int y = 0; y < outWidth; y++){int row = x * StrideH + i;int col = y * StrideW + j;int ori_idx = row * inWidth + col;src_im2col[dst_idx] = src0[ori_idx];dst_idx++;      }}}}}

3.2 对卷积核进行Im2Col

按照我们第二节的介绍,我们同样可以轻易写出对卷积核进行Im2Col的操作,代码如下:

		    int Stride = 0;for(int cc = 0; cc < outChannel; cc++){int c = cc;const float* k0 = kernel + c * inChannel * kernelSize;Stride =  kernelSize * inChannel;float* destptr = dest + c * Stride;for(int i = 0; i < inChannel * kernelSize; i++){destptr[0] = k0[0];destptr += 1;k0 += 1;}}

3.3 优化点之Pack策略

按照第二节介绍的思路,在获取了输入特征图和卷积核的Im2Col变换矩阵之后其实就可以利用Sgemm计算出卷积的结果了。

但是如果直接使用矩阵乘法计算,在卷积核尺寸比较大并且输出特征图通道数也比较大的时候,我们会发现这个时候Im2Col获得矩阵是一个行非常多列非常少的矩阵,在做矩阵乘法的时候访存会变得比较差,从而计算效率边地。这是因为当代CPU架构的某些原因,导致程序在行方向上的处理始终比列方向慢一个档次,所以我们如果能想个办法使得行方向的元素变少,列方向的元素变多这样就可以有效改善缓存达到加速的目的,另外列方向上元素增多更容易让我们发挥Neon指令集的作用。所以,这就是本文将要提到的第一个优化技巧,数据打包(Pack)。

具体来说,对于卷积核我们进行 4×44\times 44×4 的Pack(所谓 4×44 \times 44×4 的Pack就是在Im2Col获得的二维矩阵的高维度进行压缩,在宽维度进行膨胀,不知道我这种说法是否合适,参考一下下面的图应该很好理解),如下图所示:

在这里插入图片描述

这是一个 3×33\times 33×3 的卷积核并且输出通道数为 444,它经过Im2Col之后首先变成上图的上半部分,然后经过Pack 4×44\times 44×4 之后变成了上图的下半部分,即变成了每个卷积核的元素按列方向交织排列。

这部分的代码也比较容易实现,另外一个技巧是,每次在执行卷积计算时,对于Image的Im2col和Pack每次都会执行,但对于卷积核,Im2col和Pack在任意次只用做一次,所以我们可以在模型初始化的时候提前把卷积核给Pack好,这样就可以节省卷积核Im2Col和Pack耗费的时间,代码实现如下:

void ConvolutionLayerSgemm::convolutionTransformKernel(float *const &kernel, const int &kernelW, const int &kernelH, float* &dest, const int &inChannel,const int &outChannel){int kernelSize = kernelH * kernelW;int ccOutChannel = 0;int ccRemainOutChannel = 0;int Stride = 0;ccOutChannel = outChannel >> 2;ccRemainOutChannel = ccOutChannel << 2;for(int cc = 0;  cc < ccOutChannel; cc ++){int c = cc << 2;const float* k0 = kernel + c * inChannel * kernelSize;const float* k1 = kernel + (c + 1) * inChannel * kernelSize;const float* k2 = kernel + (c + 2) * inChannel * kernelSize;const float* k3 = kernel + (c + 3) * inChannel * kernelSize;Stride = 4 * kernelSize * inChannel;float* destptr = dest + (c / 4) * Stride;for(int i = 0; i < inChannel * kernelSize; i++){destptr[0] = k0[0];destptr[1] = k1[0];destptr[2] = k2[0];destptr[3] = k3[0];destptr += 4;k0 += 1;k1 += 1;k2 += 1;k3 += 1;}}for(int cc = ccRemainOutChannel; cc < outChannel; cc++){int c = cc;const float* k0 = kernel + c * inChannel * kernelSize;Stride = 4 * kernelSize * inChannel;float* destptr = dest + (c / 4 + c % 4) * Stride;for(int i = 0; i < inChannel * kernelSize; i++){destptr[0] = k0[0];destptr += 1;k0 += 1;}}}

注意这个地方如果Pack的时候有拖尾部分,也就是说outChannel不能被4整除时,那么直接按照原始顺序排列即可,后面在Sgemm方法计算的时候需要注意一下。下图展示了一个 5×4×35\times 4\times 35×4×3 的卷积核经过Im2Col再经过Pack 4×44\times 44×4 之后获得的结果,可以看到拖尾那一行是直接复制的,不够的长度直接置0即可。

在这里插入图片描述

接下来我们继续讲一下对于输入数据的Pack,我这里以对输入数据进行8x8的Pack为例子来讲解输入数据Pack之后应该是什么样子,以及如何和刚才已经4x4Pack好的卷积核执行矩阵乘法以完成整个卷积过程。至于为什么这里要使用Pack8x8而不是Pack4x4,因为考虑到MsnhNet后面的版本中需要支持Armv8的实现并且无论如何Pack也基本不影响计算的逻辑。下面我们举个例子来加以说明。

下面展示了一个输入维度为 [1,1,7,4][1,1,7,4][1,1,7,4] 的特征图使用Im2Col进行展开并Pack8x8之后获得结果(卷积核维度为 [1,1,3,3][1,1,3,3][1,1,3,3]),其中左边代表的是Im2Col后的结果,右边是将其进一步Pack8x8后获得的结果。这个地方由于 outHeight×outWidthoutHeight\times outWidthoutHeight×outWidth 不能被8整除,所以存在拖尾部分无法进行Pack,即结果图中的第二和第三列,直接顺序放就可以了。

在这里插入图片描述

这部分的代码实现也比较简单,如下所示:

// pack 8x8// preapareconst int packChannel = outSize / 8 + outSize % 8;const int packHeight = inChannel;    const int packWidth = 8 * kernelSize;int kernelPackChannel = outChannel / 4 + outChannel % 4;const int kernelPackHeight = inChannel;const int kernelPackWidth = 4 * kernelSize;float *src_im2col_pack = new float[packHeight * packWidth * packChannel];// pack startint colCount = outSize >> 3;int remainColCount = colCount << 3;#if USE_OMP#pragma omp parallel for num_threads(OMP_THREAD)
#endiffor(int i = 0; i < colCount; i++){int newi = i << 3;const float *src0 = src_im2col;src0 += newi;float *packptr = src_im2col_pack + i * packHeight * packWidth;for(int j = 0; j < inChannel * kernelSize; j ++){
#if USE_NEON
#if __aarch64__throw Exception(1, "Error: armv8 temporarily not supported!", __FILE__, __LINE__, __FUNCTION__);
#elseasm volatile("pld        [%0, #256]          \n""vld1.f32   {d0-d3}, [%0]       \n""vst1.f32   {d0-d3}, [%1]       \n": "=r"(src0),  // %0"=r"(packptr) // %1: "0"(src0),"1"(packptr): "memory", "q0", "q1");
#endif#elsepackptr[0] = src0[0];packptr[1] = src0[1];packptr[2] = src0[2];packptr[3] = src0[3];packptr[4] = src0[4];packptr[5] = src0[5];packptr[6] = src0[6];packptr[7] = src0[7];
#endifpackptr += 8;src0 += outSize;}}// pack tail#if USE_OMP#pragma omp parallel for num_threads(OMP_THREAD)
#endiffor(int i = remainColCount; i < outSize; i++){const float *src0 = src_im2col;src0 += i;float *packptr = src_im2col_pack + (i / 8 + i % 8) * packHeight * packWidth;for(int j = 0; j < inChannel * kernelSize; j++){packptr[0] = src0[0];packptr += 1;src0 += outSize;}}

3.4 优化点之带Pack的矩阵乘法

现在我们已经获得了输入特征图和卷积核的Im2Col+Pack操作后的矩阵,那么我们就可以使用矩阵乘法来计算出最后的结果了,即Sgemm算法,但这里因为Pack的原因不能使用OpenBlas标准库。这里介绍一下如何手写一个Sgemm算法。首先Sgemm的接口大概是:

sgemm (int M, int N, int K, float *A, float *B, float *C)

其中输入矩阵A的特征维度是 M×KM\times KM×K ,输入矩阵B的特征维度是 K×NK\times NK×N,输出矩阵 CCC 的特征维度是 M×NM\times NM×N ,不考虑任何优化的情况下复杂度是 O(N×K×N)O(N\times K \times N)O(N×K×N) 。因为我们这里Pack了数据,所以访存相比于原始版本会变好一些,但计算量实际上还是没变的。除此之外,由于行方向的数据增多我们可以更好的在行方向上进行利用Neon优化使得整个计算过程的效率更好。

所以就目前的情况来说,矩阵A就是我们的卷积核经过Im2Col+Pack4x4获得的输出矩阵,矩阵B就是我们的输入特征图经过Im2Col+Pack8x8之后获得的输出矩阵,然后矩阵C就是经过矩阵乘法之后获得的输出特征图了。在实现矩阵乘法的时候,以矩阵A为基准一次处理4行,并且在列方向分别处理4或者8个元素,这个可以结合上面的输入特征图的Im2Col+Pack8x8的示意图来想。最后,对于矩阵A不够4的拖尾部分,进行暴力处理即可。这个地方考虑的是当代典型的CNN架构一般通道数都是 4 的倍数,暂时没有实现拖尾部分的Neon优化,下面先看一下这个算法实现的整体部分,复刻NCNN。

这部分的代码实现如下:

// sgemm (int M, int N, int K, float *A, float *B, float *C)
// A (M x K)
// B (K x N)
// C (M x N)//int M = outChannel;int N = outHeight * outWidth;int K = kernelSize * inChannel;int ccOutChannel = outChannel >> 2;int ccRemainOutChannel = ccOutChannel << 2;#if USE_OMP#pragma omp parallel for num_threads(OMP_THREAD)
#endiffor(int cc = 0; cc < ccOutChannel; cc++){int c = cc << 2;float *destptr0 = dest + c * outSize;float *destptr1 = dest + (c + 1) * outSize;float *destptr2 = dest + (c + 2) * outSize;float *destptr3 = dest + (c + 3) * outSize;int i = 0;// N = outHeight*outWidthfor(; i + 7 < N; i = i+8){const float *ptrB = src_im2col_pack + (i / 8) *  packHeight * packWidth;
#if __aarch64__throw Exception(1, "Error: armv8 temporarily not supported!", __FILE__, __LINE__, __FUNCTION__);
#elseconst float *ptrA = kernel_im2col_pack + (c / 4) * kernelPackHeight * kernelPackWidth;
#endif#if USE_NEON#if __aarch64__throw Exception(1, "Error: armv8 temporarily not supported!", __FILE__, __LINE__, __FUNCTION__);
#elseasm volatile();
#endif#elsefloat sum0[8] = {0};float sum1[8] = {0};float sum2[8] = {0};float sum3[8] = {0};int j = 0;// K = kernelSize * inChannel// 同时计算4行,同时在每一列计算8个输出for(; j + 7 < K; j = j + 8){for(int n = 0; n < 8; n++){sum0[n] += ptrA[0] * ptrB[n];sum1[n] += ptrA[1] * ptrB[n];sum2[n] += ptrA[2] * ptrB[n];sum3[n] += ptrA[3] * ptrB[n];ptrA += 4;sum0[n] += ptrA[0] * ptrB[n + 8];sum1[n] += ptrA[1] * ptrB[n + 8];sum2[n] += ptrA[2] * ptrB[n + 8];sum3[n] += ptrA[3] * ptrB[n + 8];ptrA += 4;sum0[n] += ptrA[0] * ptrB[n + 16];sum1[n] += ptrA[1] * ptrB[n + 16];sum2[n] += ptrA[2] * ptrB[n + 16];sum3[n] += ptrA[3] * ptrB[n + 16];ptrA += 4;sum0[n] += ptrA[0] * ptrB[n + 24];sum1[n] += ptrA[1] * ptrB[n + 24];sum2[n] += ptrA[2] * ptrB[n + 24];sum3[n] += ptrA[3] * ptrB[n + 24];ptrA += 4;sum0[n] += ptrA[0] * ptrB[n + 32];sum1[n] += ptrA[1] * ptrB[n + 32];sum2[n] += ptrA[2] * ptrB[n + 32];sum3[n] += ptrA[3] * ptrB[n + 32];ptrA += 4;sum0[n] += ptrA[0] * ptrB[n + 40];sum1[n] += ptrA[1] * ptrB[n + 40];sum2[n] += ptrA[2] * ptrB[n + 40];sum3[n] += ptrA[3] * ptrB[n + 40];ptrA += 4;sum0[n] += ptrA[0] * ptrB[n + 48];sum1[n] += ptrA[1] * ptrB[n + 48];sum2[n] += ptrA[2] * ptrB[n + 48];sum3[n] += ptrA[3] * ptrB[n + 48];ptrA += 4;sum0[n] += ptrA[0] * ptrB[n + 56];sum1[n] += ptrA[1] * ptrB[n + 56];sum2[n] += ptrA[2] * ptrB[n + 56];sum3[n] += ptrA[3] * ptrB[n + 56];ptrA -= 28;}ptrA += 32;ptrB += 64;}// K = kernelSize * inChannel * 4// 如果是pack4x4那么末尾一定是4的倍数for(; j < K; j++){for(int n = 0; n < 8; n++){sum0[n] += ptrA[0] * ptrB[n];sum1[n] += ptrA[1] * ptrB[n];sum2[n] += ptrA[2] * ptrB[n];sum3[n] += ptrA[3] * ptrB[n];}ptrA += 4;ptrB += 8;}for(int n = 0; n < 8; n++){destptr0[n] = sum0[n];destptr1[n] = sum1[n];destptr2[n] = sum2[n];destptr3[n] = sum3[n];}#endifdestptr0 += 8;destptr1 += 8;destptr2 += 8;destptr3 += 8;}

3.5 优化点之Neon的应用

上面的列方向一次处理多个元素的累乘和累加过程可以进一步使用Neon优化,将上面的一次处理4行,每列一次计算8个输出的代码翻译为Neon汇编代码即可,代码实现以及简要的注释如下:

asm volatile("veor       q1, q0, q0           \n""vdup.f32   q8,    d2[0]         \n""vdup.f32   q9,    d2[0]         \n""vdup.f32   q10,   d2[0]         \n""vdup.f32   q11,   d2[0]         \n""vdup.f32   q12,   d2[0]         \n""vdup.f32   q13,   d2[0]         \n""vdup.f32   q14,   d2[0]         \n""vdup.f32   q15,   d2[0]         \n"// r4 = K >> 2"lsr         r4, %12, #2        \n"// 如果nn等于0,使用beq进行循环跳转,即跳转到循环1 "cmp         r4, #0             \n""beq         loop1              \n"// for(; nn != 0; nn--) && nn = K >> 2"loop0:                         \n" // kernel q0-q3"pld        [%5, #512]          \n""vldm       %5!, {d0-d7}        \n" // input  q4-q7"pld        [%4, #512]          \n""vldm       %4!, {d8-d15}       \n"//calc// sum0[n] += ptrA[0] * ptrB[n];"vmla.f32   q8, q4, d0[0]       \n"// sum1[n] += ptrA[1] * ptrB[n];"vmla.f32   q9, q5, d0[0]       \n"// sum0[n] += ptrA[0] * ptrB[n + 8];"vmla.f32   q10, q4, d0[1]      \n"// sum1[n] += ptrA[1] * ptrB[n + 8];"vmla.f32   q11, q5, d0[1]      \n"// sum0[n] += ptrA[0] * ptrB[n + 16];"vmla.f32   q12, q4, d1[0]      \n" // sum1[n] += ptrA[1] * ptrB[n + 16];"vmla.f32   q13, q5, d1[0]      \n"// sum0[n] += ptrA[0] * ptrB[n + 24];"vmla.f32   q14, q4, d1[1]      \n" // sum1[n] += ptrA[1] * ptrB[n + 24];"vmla.f32   q15, q5, d1[1]      \n"// sum2[n] += ptrA[2] * ptrB[n];"vmla.f32   q8, q6, d2[0]       \n" // sum3[n] += ptrA[3] * ptrB[n];"vmla.f32   q9, q7, d2[0]       \n"// sum2[n] += ptrA[2] * ptrB[n + 8];"vmla.f32   q10, q6, d2[1]      \n" // sum3[n] += ptrA[3] * ptrB[n + 8];"vmla.f32   q11, q7, d2[1]      \n"// sum2[n] += ptrA[2] * ptrB[n + 16];"vmla.f32   q12, q6, d3[0]      \n" // sum3[n] += ptrA[3] * ptrB[n + 16];"vmla.f32   q13, q7, d3[0]      \n"// sum2[n] += ptrA[2] * ptrB[n + 24];"vmla.f32   q14, q6, d3[1]      \n" // sum3[n] += ptrA[3] * ptrB[n + 24];"vmla.f32   q15, q7, d3[1]      \n"// ptrA += 4x4"pld        [%4, #512]          \n""vldm       %4!, {d8-d15}       \n"// sum0[n] += ptrA[0] * ptrB[n + 32];"vmla.f32   q8, q4, d4[0]       \n" // sum1[n] += ptrA[1] * ptrB[n + 32];"vmla.f32   q9, q5, d4[0]       \n"// sum0[n] += ptrA[0] * ptrB[n + 40];"vmla.f32   q10, q4, d4[1]      \n" // sum1[n] += ptrA[1] * ptrB[n + 40];"vmla.f32   q11, q5, d4[1]      \n"// sum0[n] += ptrA[0] * ptrB[n + 48];"vmla.f32   q12, q4, d5[0]      \n" // sum1[n] += ptrA[1] * ptrB[n + 48];"vmla.f32   q13, q5, d5[0]      \n"// sum0[n] += ptrA[0] * ptrB[n + 56];"vmla.f32   q14, q4, d5[1]      \n" // sum1[n] += ptrA[1] * ptrB[n + 56];"vmla.f32   q15, q5, d5[1]      \n"// sum2[n] += ptrA[2] * ptrB[n + 32];"vmla.f32   q8, q6, d6[0]       \n" // sum3[n] += ptrA[3] * ptrB[n + 32];"vmla.f32   q9, q7, d6[0]       \n"// sum2[n] += ptrA[2] * ptrB[n + 40];"vmla.f32   q10, q6, d6[1]      \n" // sum3[n] += ptrA[3] * ptrB[n + 40];"vmla.f32   q11, q7, d6[1]      \n"// sum2[n] += ptrA[2] * ptrB[n + 48];"vmla.f32   q12, q6, d7[0]      \n"// sum3[n] += ptrA[3] * ptrB[n + 48];"vmla.f32   q13, q7, d7[0]      \n"// sum2[n] += ptrA[2] * ptrB[n + 56];"vmla.f32   q14, q6, d7[1]      \n" // sum3[n] += ptrA[3] * ptrB[n + 56];"vmla.f32   q15, q7, d7[1]      \n""subs        r4, r4, #1         \n"// 第一个for循环的结束,nn>0"bne         loop0             \n" // 开始写第二个for循环"loop1:                         \n"// K = kernelSize * inChannel * 4// K >> 2 == inChannel>>2 = inChannel & 3// 计算完之后进行第三个for循环进行最后的赋值"and         r4, %12, #3        \n""cmp         r4, #0             \n""beq         loop3              \n""loop2:                         \n" // kernel q0 && ptrA += 4// q0 = [d0, d1] = [ptrA[0], ptrA[1], ptrA[2], ptrA[3]]"pld        [%5, #128]          \n""vld1.f32   {d0-d1}, [%5]!      \n"// input q4, q5 && ptrB += 8// q4, q5 = [d8, d9, d10, d11] = [ptrB[0], ..., ptrB[7]]"pld        [%4, #256]          \n""vld1.f32   {d8-d11}, [%4]!     \n"// for(int n = 0; n < 8; n++){//    sum0[n] += ptrA[0] * ptrB[n];//    sum1[n] += ptrA[1] * ptrB[n];//    sum2[n] += ptrA[2] * ptrB[n];//    sum3[n] += ptrA[3] * ptrB[n];// }"vmla.f32   q8, q4, d0[0]       \n" "vmla.f32   q9, q5, d0[0]       \n""vmla.f32   q10, q4, d0[1]      \n""vmla.f32   q11, q5, d0[1]      \n""vmla.f32   q12, q4, d1[0]      \n" "vmla.f32   q13, q5, d1[0]      \n""vmla.f32   q14, q4, d1[1]      \n" "vmla.f32   q15, q5, d1[1]      \n""subs        r4, r4, #1         \n""bne         loop2             \n"// 完成赋值"loop3:                         \n" "vst1.f32    {d16-d19}, [%0]    \n""vst1.f32    {d20-d23}, [%1]    \n""vst1.f32    {d24-d27}, [%2]    \n""vst1.f32    {d28-d31}, [%3]    \n": "=r"(destptr0), // %0"=r"(destptr1), // %1"=r"(destptr2), // %2"=r"(destptr3), // %3"=r"(ptrB),      // %4"=r"(ptrA)       // %5: "0"(destptr0),"1"(destptr1),"2"(destptr2),"3"(destptr3),"4"(ptrB),"5"(ptrA),"r"(K)      // %12: "cc", "memory", "r4", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15");

4. 优化效果

下面给出一些测试数据来验证一下此算法的有效性,注意算法的正确性笔者已经预先做过一些测试,基本可以保证,不久后此代码也将合并到MsnhNet的下一个发行版本中。

  • 下面的测试结果都默认开启了Neon优化,单核A53,Armv7a架构。

  • 并且所有的优化方法代码实现都在:https://github.com/msnh2012/Msnhnet 的Arm部分。

输入特征图大小特征图输入通道数卷积核通道数卷积核大小步长优化方法速度
14x1451210243x313x3s1手工优化430ms
14x1451210243x31Im2col+Pack+Sgemm315ms
14x1451210243x323x3s2手工优化120.49ms
14x1451210243x32Im2col+Pack+Sgemm93ms

可以看到这里Im2Col+Pack+Sgemm的速度明显快于之前的手动展开版本,在stride为1时优化了100+ms。

下面我们再测一组更大的特征图看一下此算法的表现,一般特征图比较大那么通道数就比较小,我们对应修改一下:

输入特征图大小特征图输入通道数卷积核通道数卷积核大小步长优化方法速度
112x112641283x313x3s1手工优化542ms
112x112641283x31Im2col+Pack+Sgemm588.75ms
112x112641283x323x3s2手工优化97.43ms
112x112641283x32Im2col+Pack+Sgemm138.90ms

什么鬼?竟然降速了,这是为什么呢?

有一个直观的猜测就是当输入通道数和卷积核尺寸的乘积也就是卷积核Im2Col获得的矩阵的宽度比较大时,本文介绍的Im2Col+Pack+Sgemm能带来加速效果。在输入特征图通道数较小并且特征图较大时甚至会比手工优化更慢,因为这个时候对访存的优化效果很小,因为这个时候Im2Col的矩阵行数和列数差距并不大,这个时候手工优化带来的Cache Miss并不会对卷积的计算带啦很大的影响。因此,此算法并非所有情况适用,需要灵活判断。

本实验进行得比较有限,关于何时切换Sgemm策略进行卷积可以关注MsnhNet的后续实现。

5. 后记

最近花了比较久的时间陆陆续续写完这篇文章并在MsnhNet上实现了这个代码,希望能给想了解Im2Col以及NCNN的Pack优化策略以及如何手动实现一个类Sgemm算法并对其进行优化的读者一些帮助。完整代码可以见:https://github.com/msnh2012/Msnhnet 的Arm端实现部分。另外提供了一个单个OP测试工程,地址为:https://github.com/BBuf/ArmNeonOptimization,目前支持Conv3x3s1,Conv3x3s2,BatchNorm,ConvSgemm 的测试。

6. 致谢

  • https://github.com/Tencent/ncnn

  • https://github.com/msnh2012/Msnhnet

7. 广告时间

MsnhNet是一款基于纯c++的轻量级推理框架,本框架受到darknet启发。

项目地址:https://github.com/msnh2012/Msnhnet ,欢迎一键三连。

本框架目前已经支持了X86、Cuda、Arm端的推理(支持的OP有限,正努力开发中),并且可以直接将Pytorch模型(后面也会尝试接入更多框架)转为本框架的模型进行部署,欢迎对前向推理框架感兴趣的同学试用或者加入我们一起维护这个轮子。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/532446.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

elementui的upload组件怎么获取上传的文本流、_抖音feed流直播间引流你还不会玩?实操讲解...

本文由艾奇在线明星优化师写作计划出品在这个全民惊恐多灾多难且带有魔幻的2020&#xff0c;一场突如其来的疫情改变了人们很多消费习惯&#xff0c;同时加速了直播电商的发展&#xff0c;现在直播已经成为商家必争的营销之地&#xff0c;直播虽然很火&#xff0c;但如果没有流…

FFmpeg 视频处理入门教程

FFmpeg 视频处理入门教程 转自&#xff1a;https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html 作者&#xff1a; 阮一峰 日期&#xff1a; 2020年1月14日 FFmpeg 是视频处理最常用的开源软件。 它功能强大&#xff0c;用途广泛&#xff0c;大量用于视频网站和商业软件&…

checkbox wpf 改变框的大小_【论文阅读】倾斜目标范围框(标注)的终极方案

前言最常用的斜框标注方式是在正框的基础上加一个旋转角度θ&#xff0c;其代数表示为(x_c,y_c,w,h,θ)&#xff0c;其中(x_c,y_c )表示范围框中心点坐标&#xff0c;(w,h)表示范围框的宽和高[1,2,7]。对于该标注方式&#xff0c;如果将w和h的值互换&#xff0c;再将θ加上或者…

彻底理解BP之手写BP图像分类你也行

彻底理解BP之手写BP图像分类你也行 转自&#xff1a;https://zhuanlan.zhihu.com/p/397963213 第一节&#xff1a;用矩阵的视角&#xff0c;看懂BP的网络图 1.1、什么是BP反向传播算法 BP(Back Propagation)误差反向传播算法&#xff0c;使用反向传播算法的多层感知器又称为B…

梯度下降法和牛顿法计算开根号

梯度下降法和牛顿法计算开根号 本文将介绍如何不调包&#xff0c;只能使用加减乘除法实现对根号x的求解。主要介绍梯度下降和牛顿法者两种方法&#xff0c;并给出 C 实现。 梯度下降法 思路/步骤 转化问题&#xff0c;将 x\sqrt{x}x​ 的求解转化为最小化目标函数&#xff…

汇博工业机器人码垛机怎么写_全自动码垛机器人在企业生产中的地位越来越重要...

全自动码垛机器人在企业生产中的地位越来越重要在智能化的各种全自动生产线中&#xff0c;全自动码垛机器人成了全自动生产线的重要机械设备&#xff0c;在各种生产中发挥着不可忽视的作用。全自动码垛机器人主要用于生产线上的包装过程中&#xff0c;不仅能够提高企业的生产率…

小说中场景的功能_《流浪地球》:从小说到电影

2019年春节贺岁档冒出一匹黑马&#xff1a;国产科幻片《流浪地球》大年初一上映后口碑、票房双丰收&#xff1a;截至9日下午&#xff0c;票房已破15亿&#xff0c;并获得9.2的高评分。著名导演詹姆斯卡梅隆通过社交媒体对我国春节期间上映的科幻影片《流浪地球》发出的祝愿&…

线性回归与逻辑回归及其实现

线性回归与逻辑回归及其实现 回归与分类 预测值定性分析&#xff0c;即离散变量预测时&#xff0c;称之为分类&#xff1b;预测值定量分析&#xff0c;即连续变量预测时&#xff0c;称之为回归。 如预测一张图片是猫还是狗&#xff0c;是分类问题&#xff1b;预测明年的房价…

hbase 页面访问_HBase

HBase 特点 海量存储 Hbase 适合存储 PB 级别的海量数据&#xff0c;在 PB 级别的数据以及采用廉价 PC 存储的情况下&#xff0c;能在几十到百毫秒内返回数据。这与 Hbase 的极易扩展性息息相关。正式因为 Hbase 良好的扩展性&#xff0c;才为海量数据的存储提供了便利。 2&…

深入理解L1、L2正则化

深入理解L1、L2正则化 转自&#xff1a;【面试看这篇就够了】L1、L2正则化理解 一、概述 正则化&#xff08;Regularization&#xff09;是机器学习中一种常用的技术&#xff0c;其主要目的是控制模型复杂度&#xff0c;减小过拟合。正则化技术已经成为模型训练中的常用技术&a…

机器学习中的概率模型

机器学习中的概率模型 转自&#xff1a;https://zhuanlan.zhihu.com/p/164551678 机器学习中的概率模型 概率论&#xff0c;包括它的延伸-信息论&#xff0c;以及随机过程&#xff0c;在机器学习中有重要的作用。它们被广泛用于建立预测函数&#xff0c;目标函数&#xff0c;以…

max std value 宏_Rust Macro/宏 新手指南

Rust语言最强大的一个特点就是可以创建和利用宏/Macro。不过创建 Rust宏看起来挺复杂&#xff0c;常常令刚接触Rust的开发者心生畏惧。这片文章 的目的就是帮助你理解Rust Macro的基本运作原理&#xff0c;学习如何创建自己的 Rust宏。相关链接&#xff1a;在线学编程 - 汇智网…

农林资金 大数据审计案例_大数据审计:现状与发展

大数据审计&#xff1a;现状与发展【摘要】传统手工环境下&#xff0c;审计人员常用的审计方法包括检查法、观察法、重新计算法、外部调查法、分析法、鉴定法等。随着信息技术的发展&#xff0c;被审计单位的运行越来越依赖于信息化环境。信息化环境下审计工作发生了巨大的变化…

angularjs sill 创建项目_开源项目——博客项目MyBlogs.Core,基于.NET 5

个人博客站项目源码&#xff0c;高性能低占用的博客系统&#xff0c;这也许是我个人目前写过的性能最高的web项目了 。目前日均处理请求数80-120w次&#xff0c;同时在线活跃用户数30-100人&#xff0c;数据量累计已达到100多万条&#xff0c;数据库Redis网站主程序同时运行在一…

怀旧服推荐配置_【怀旧服】狂暴战P4毕业装备推荐

在怀旧服开启P4阶段之后&#xff0c;狂暴战玩家的输出也得到了进一步的提升。当然&#xff0c;狂暴战想要打出足够的伤害离不开对应的装备&#xff0c;现在就给大家介绍下狂暴战P4阶段的BIS装备。散件装备狂暴战在这一阶段依旧有非常不错的散件装备&#xff0c;个人建议玩家入手…

高斯混合模型GMM及EM迭代求解算法(含代码实现)

高斯混合模型GMM及EM迭代求解算法&#xff08;含代码实现&#xff09; 高斯分布与高斯混合模型 高斯分布 高斯分布大家都很熟悉了&#xff0c;下面是一元高斯分布的概率密度函数&#xff08;Probability Density Function&#xff0c;PDF&#xff09;&#xff1a; P(x)N(μ,…

十个模块_专栏 | ABAQUS Part模块的十个小技巧

作者介绍星辰_北极星2012年开始从事Abaqus仿真相关工作&#xff0c;服务大小课题逾百项; 主要仿真领域&#xff1a;石油工程、岩土工程和金属加工工艺&#xff1b; 重点研究方向&#xff1a;ABAQUS GUI二次开发、固体力学、断裂以及损伤等。Abaqus有部件(Part)和装配体(Assembl…

深度学习时代的视频理解综述

深度学习时代的视频理解综述 本文为b站bryanyzhu老师四期视频理解相关论文解读的汇总图文笔记。 我们先精读深度学习时代视频理解领域最为重要的两篇论文&#xff1a;双流网络和 I3D。它们分别是领域内两大类方法双流&#xff08;利用光流&#xff09;网络和 3D CNN 网络的代…

typec扩展坞hdmi没反应_typec扩展坞转hdmi/vga多功能网口usb转换器苹果华为电脑matebook6元优惠券券后价26.8元...

★typec扩展坞转hdmi/vga多功能网口usb转换器苹果华为电脑matebook&#xff0c;6元拼多多优惠券★券后价26.8元★★★typec扩展坞转hdmi/vga多功能网口usb转换器苹果华为电脑matebook&#xffe5;26.8元&#xffe5;32.8元已拼5097件点击抢购猜你喜欢[速发]喵喵机P1热敏打印机手…

NLP任务概览

NLP任务概览 本文为台湾大学李宏毅老师视频课程笔记。本课程介绍了 &#xff08;2020年&#xff09;NLP 领域常见的 17 种任务。本文只会从输入输出的角度概览多种 NLP 任务&#xff0c;并简介它们的常见做法&#xff0c;并不会细致地介绍每个任务模型的具体细节。 两种模式与…