MEC:Memory-efficient Convolution for Deep Neural Network 深度神经网络中内存高效的卷积算法MEC 论文详解 ICML 2017

论文:MEC: Memory-efficient Convolution for Deep Neural Network 深度神经网络中内存高效的卷积算法

作者:Minsik Cho,Daniel Brand

来源:ICML 2017

论文链接:https://arxiv.org/abs/1706.06873

Github代码链接:无

目前,卷积的计算大多采用间接计算的方式,主要有以下三种实现方式:

  • im2col + GEMM。 caffe等很多框架中都使用了这种计算方式,原因是将问题转化为矩阵乘法后可以方便的使用很多矩阵运算库(如MKL、openblas、Eigen等)。
  • FFT变换。 时域卷积等于频域相乘,因此可将问题转化为简单的乘法问题。
  • Winograd。在GPU上效率更高。NNPACK就是FFT和Winograd方法的结合。

上面三种方法执行效率都还不错,但对内存占用比较高,因为需要存储中间结果或者临时辅助变量。

本文的方法主要改进了im2col + GEMM的策略,目的主要是减少内存消耗的同时顺便提升点速度。由于同样可以利用目前成熟的矩阵运算库,因此算法的实现难度并不大。

1 相关介绍

本文针对直接卷积太过简单,性能较差,基于im2col,FFT,Winorgrad等的卷积计算方法存在很大的内存开销而使得性能下降并且在性能和内存消耗直接很难达到一个平衡的问题,提出了内存高效的卷积MEC。

MEC减少了内存开销并且加速了卷积计算过程。MEC使用一种简单而新颖的方法以高度紧凑的方式降低输入矩阵,同时仍然用高度优化的库例如BlAS去加速矩阵乘法,并且通过并行的方式执行很多小矩阵的矩阵乘法。MEC减少了输入矩阵的内存占用,提升了内存子系统的效率(也就是提升了cache的局部性)从而MEC能够在不损失精度的情况下加速卷积的计算。

通过利用CPU/GPU在mobile和server平台进行了实验,证明了MEC是一种非常高效的算法,能够应用到各种内存有限制的系统上。

2 Preliminaries

标记

  • 利用C语言表示张量和矩阵都是row-major顺序的,但是如果使用一些数学库,比如cuBLAS,它是column-major顺序的。文中仍然用row-major顺序表示,但是将所有的矩阵理解为转置的。
  • s h , s w s_{h}, s_{w} sh,sw表示卷积核的步长
  • lowered matrix L L L:inputs转换成矩阵的中间存储表示
  • 为便于解释,假设输入 I I I已经应用了任何带0的填充。输出矩阵 O O O的维度为:

o h , w = i h , w − k h , w s h , w + 1 (1) \tag{1} o_{h, w}=\frac{i_{h, w}-k_{h, w}}{s_{h, w}}+1 oh,w=sh,wih,wkh,w+1(1)

相关工作

  • im2col-based convolution:将输入矩阵转换成Toeplitz矩阵,然后使用高优化了的线性代数库BLAS进行快速矩阵乘法
  • FFT-based convolution:卷积可以在频域进行简单计算。但是要求所有卷积核都必须填充,使得和input矩阵有同样的size,这样就增大了内存开销。卷积核比输入矩阵小得越多,内存开销越大。
  • Winograd-based convolution:基于Coppersmith-Winograd算法,它展示了如何以更多的加法的代价和大量中间乘积来减少乘法计数。有论文指出,基于winograd的卷积对于GPU上的小内核是有效的。

3 MEC算法

  • 对于给定的内存大小,MEC可以使用更大的模型进行训练或推理
  • MEC允许在训练期间使用更大的mini-batch size来加速每个周期的延迟
  • MEC可以通过提高内存子系统的效率(例如增加缓存命中次数)来加速计算

3.1 动机

  • 直接卷积简单直接,没有内存开销,但是计算不高效
  • im2col卷积将卷积转换成一个卷积乘法来计算,可以使用高度优化了的库如BLAS库来计算,计算更高效。但是增加了内存占用(因为转换了的input矩阵比原始input矩阵更大)

im2col的 lowered matrix L L L维度为

i n o h o w × k h k w k c (2) \tag{2} i_{n} o_{h} o_{w} \times k_{h} k_{w} k_{c} inohow×khkwkc(2)

3.2 MEC 算法初级版本

MEC可以解决im2col内存占用的问题。MEC过程如图2所示:

  • 图1中im2col输入矩阵转换后维度为25 × 9,而MEC方法转换后的维度为5 × 21,小了54%,并且最后的输出矩阵都和直接卷积一样是相同的
  • MEC的计算将转换后的矩阵分成5个部分 P , Q , R , S , T {P, Q, R, S, T } P,Q,R,S,T分别和卷积核相乘

这种乘法在BLAS的gemm接口中有三种计算方式:

  • 首先, k h × k w k_{h} \times k_{w} kh×kw的矩阵 K K K解释为一个 k h k w × 1 k_{h} k_{w} \times 1 khkw×1的矩阵
  • 其次,分区 P , Q , R , S , T {P, Q, R, S, T } P,Q,R,S,T通过提供指向初始元素的指针和 l d = i h k w l d=i_{h} k_{w} ld=ihkw来指定, l d = i h k w l d=i_{h} k_{w} ld=ihkw L L L的一整行的长度
  • 最后,每一行 O O O P , Q , R , S , T {P, Q, R, S, T} P,Q,R,S,T K K K之间的5个单独的gemm调用组成。尽管gemm调用的数量增加了,但是mult/add操作的总数仍然与基于im2col的卷积相同,保持计算复杂度不变

和im2col相比,总的计算量保持不变,但是减少了内存开销。

  • 整个过程算法1所示,其中 i n = i c = k c = 1 i_{n}=i_{c}=k_{c}=1 in=ic=kc=1
  • 第4行的循环并行地从输入矩阵复制 k w k_w kw个连续的元素到中间矩阵 L L L中,所有复制可以并行执行
  • 第10行的循环并行地计算输出矩阵,其中的每一个矩阵乘法计算都通过一个gemm调用执行,这些矩阵乘法计算可以并行地执行

3.3 MEC 算法高级版本

为了能够处理通道( i k 和 i c i_k和i_c ikic)和mini-batches( i n i_n in)将算法1扩展到算法2。

  • 输入矩阵 I I I的维度扩展为 i n × i h × i w × i c i_{n} \times i_{h} \times i_{w} \times i_{c} in×ih×iw×ic,格式为 n − h − w − c n-h-w-c nhwc
  • 输出矩阵 O O O的维度扩展为 O h × i n O w k c O_{h} \times i_{n} O_{w} k_{c} Oh×inOwkc,在整个mini-batch中执行 O h O_{h} Oh个乘法,这导致了输出格式为 h − n − w − c h-n-w-c hnwc,和输入格式不同

在DNNs网络中是可接受的,因为在DNNs中,每个卷积层后面都跟着一个池化层,这个池化层期待得到的输入格式是 h − n − w − c h-n-w-c hnwc并且生成标准的 n − h − w − c n-h-w-c nhwc格式的输出(输入输出格式和卷积层相反)。但是在一个所有层都期望输入和输出 n − h − w − c n-h-w-c nhwc格式的网络中,就会很麻烦。图3提供了两种方案来处理与格式相关的问题。

Solution A (Lines 9 to 19 of Algorithm 2)
首先对算法1进行直接的扩展(算法2中9-13 行),然后得到一个 h − n − w − c h-n-w-c hnwc格式的输出 O O O。然后,将 O O O转换成 n − h − w − c n-h-w-c nhwc格式(算法2中14-19行)。 L L L作为辅助空间。

Solution B (lines 21 to 25 of Algorithm 2)
在算法2中,21行分别处理mini-batch中的 i n i_n in个样本,用更小的inputs生成 i n O h i_{n} O_{h} inOh个parallel/batched的gemm调用,而不是使用更大的inputs进行 O h O_{h} Oh个调用。这样将直接生成一个格式为 n − h − w − c n-h-w-c nhwc的输出 O O O

就复杂度而言,这两个解决方案执行相同数量的浮点乘法。然而,在实践中,子矩阵的大小会影响性能,尤其是在实现敏感的平台上,比如GPU。因此,MEC试图在第8行中使用可调参数 T T T在解决方案A和B之间找到一个好的平衡。(此外,只有 L L L可以作为辅助空间,即至少和 O O O一样大时,解决方案A是可用的)。 T T T是一个与平台相关的参数(例如,CPU与GPU,或GPU计算的能力),目前发现对于最新的GPU的一个不错的阈值大约为100。

3.4 分析

MEC中的 lowered matrix L L L的size为

i n o w i h k w k c (3) \tag{3} i_{n} o_{w} i_{h} k_{w} k_{c} inowihkwkc(3)
im2col中的 lowered matrix L L L的size为
i n o h o w × k h k w k c (2) \tag{2} i_{n} o_{h} o_{w} \times k_{h} k_{w} k_{c} inohow×khkwkc(2)
可见,和im2col比MEC多了一个因子 k h k_h kh

看看两个方法的不同 R R R(公式3减公式2)

R = i n k c ( o h o w k h k w − o w i h k w ) = i n k c o w k w ( o h k h − i h ) = i n k c o w k w ( i h − k h s h k h + k h − i h ) = i n k c o w k w ( i h − k h ) ( k h s h − 1 ) (4) \tag{4} \begin{aligned} R &=i_{n} k_{c}\left(o_{h} o_{w} k_{h} k_{w}-o_{w} i_{h} k_{w}\right) \\ &=i_{n} k_{c} o_{w} k_{w}\left(o_{h} k_{h}-i_{h}\right) \\ &=i_{n} k_{c} o_{w} k_{w}\left(\frac{i_{h}-k_{h}}{s_{h}} k_{h}+k_{h}-i_{h}\right) \\ &=i_{n} k_{c} o_{w} k_{w}\left(i_{h}-k_{h}\right)\left(\frac{k_{h}}{s_{h}}-1\right) \end{aligned} R=inkc(ohowkhkwowihkw)=inkcowkw(ohkhih)=inkcowkw(shihkhkh+khih)=inkcowkw(ihkh)(shkh1)(4)

  • 一旦 k h > s h k_{h}>s_{h} kh>sh(即卷积核之间没有重叠),那么由于 i h > k h i_{h}>k_{h} ih>kh,MEC总能减少内存占用
  • k h ≤ s h k_{h} \leq s_{h} khsh,则没有冗余信息

4 实验结果

实验设置

  • 使用32位的单精度实验
  • 使用多线程的OpenBLAS, OpenMP, and cuBLAS在CPU/GPU上实现MEC(C++)
  • 使用相同的库在CPU/GPU上用完全并行地方法实现基于im2col的卷积
  • 用其他C++的开源卷积库作为对比,比较内存开销和性能:如开源的基于FFT的卷积、开源的基于Winograd的卷积

卷积算法的一些描述

  • Conv.cpu:用openBLAS/openMP实现的基于im2col的卷积
  • Conv.gpu:用openBLAS实现的基于im2col的卷积
  • Wino.cpu:基于Winograd F ( 2 × 2 , 3 × 3 ) F(2×2,3×3) F(2×2,3×3)的卷积(仅当 k h = k w = 3 k_{h}=k_{w}=3 kh=kw=3时可用)
  • Wino.gpu:除了使用GPU外其他同上
  • FFT.gpu:在GPU上使用cuFFT库卷积
  • MEC.cpu:用openBLAS/openMP实现的基于MEC的卷积
  • MEC.gpu

benchmarks包含12个卷积层:

实验平台

  • Mobile:ARM7 (MSM8960)的安卓手机,mini-bath size=1
  • Server:Linux server with Intel CPU (E5-2680) and Nvidia GPU (P100) for inference and training (mini-bath size=32)

实验结果

  • (a)MEC.cpu比 Conv.cpu比在Server-CPU上的第一层卷积 v 1 v_1 v1性能和内存占用都更优;可以清楚地观察到,随着比率 k / s k/s k/s的增加,内存开销和运行时都得到了改善(见公式4)
  • (b)可以看出MEC.cpu的内存占用率最低
  • (c)(d)可以看出MEC.cpu运行得最快
  • (e)可以看出MEC.gpu需要的内存开销最少,Wino.gpu由于内存限制只能在 c v 6 − c v 12 cv6-cv12 cv6cv12层测试
  • (f)可以看出MEC.gpu最快
  • MEC.cpu减少到近三分之一的内存开销,运行时间提高20%

5 MEC的简单实现

图片中的简单2维的例子代码如下:

#include <iostream>
using namespace std;
#define Ih 7
#define Iw 7
#define Kh 3
#define Kw 3
#define Oh 5
#define Ow 5

int * getCol(int a  [Ih][Iw],int start,int end)
{
    int size=Ih*Kw;
    int * temp=new int[size];
    int i=0;
    for(i=0;i<size;i++){
        for(int j=start;j<=end;j++){
            temp[i]=a[i/Kw][j];
        }
    }
    int count=0;
    for(int i=0;i<Ih;i++){
        for(int j=start;j<=end;j++)
            temp[count++]=a[i][j];
    }
    return temp;
}

void GEMM(int L[Oh][Ih*Kh],int  K2 [Kh*Kw]){

    int O[Oh][Ow];
    for(int i=0;i<Oh;i++){
        for(int j=0;j<Ow;j++){
            int sum=0;
            for(int m=0;m<Kh*Kw;m++){
                sum+=K2[m]*L[i][j*Kw+m];
            }
            O[j][i]=sum;
        }
    }

    cout<<"O:"<<endl;
    for(int i=0;i<Oh;i++){
        for(int j=0;j<Ow;j++){
            cout<<O[i][j]<<" ";
        }
        cout<<endl;
    }


}

void vanillaMEC(int  I[Ih][Iw]  ,int  K [Kh][Kw] ){
    int * K2=new int [Kh*Kw];
    int  L [Oh][Ih*Kw];


    //Show  elements of I
    cout<<"I:"<<endl;
    for(int i=0;i<Ih;i++){
        for(int j=0;j<Iw;j++)
            cout<<I[i][j]<<" ";
        cout<<endl;
    }
    for(int i=0;i<Ow;i++){
        int *temp=getCol(I,i,Kw+i-1);
        for(int j=0;j<Ih*Kw;j++){
            L[i][j]=temp[j];
        }
    }

    //Show  elements of L
    cout<<"L:"<<endl;
    for(int i=0;i<Ow;i++){
        for(int j=0;j<Ih*Kw;j++){
           cout<<L[i][j]<<" ";
        }
        cout<<endl;
    }

    int count=0;
    for(int i=0;i<Kh;i++){
        for(int j=0;j<Kw;j++){
            K2[count++]=K[i][j];
        }
    }

    //Show  elements of K
    cout<<"K2:"<<endl;
    for(int i=0;i<Kh*Kw;i++){
        cout<<K2[i]<<" ";
    }
    cout<<endl;

    GEMM(L,K2);

}
int main() {
    int I[Ih][Iw]={
            {0,0,0,0,0,0,0},
            {0,2,2,1,1,2,0},
            {0,2,0,1,1,0,0},
            {0,2,0,1,2,0,0},
            {0,1,1,1,1,1,0},
            {0,0,0,1,0,2,0},
            {0,0,0,0,0,0,0}
    };

    int K[3][3]={
   
   {1,0,0},{1,1,1},{1,0,-1}};
//    int Ih= sizeof(I)/ sizeof(I[0]); //row
//    int Iw=sizeof(I[0])/sizeof(int);//col

    vanillaMEC(I,K);

    return 0;
}

输出

I:
0 0 0 0 0 0 0
0 2 2 1 1 2 0
0 2 0 1 1 0 0
0 2 0 1 2 0 0
0 1 1 1 1 1 0
0 0 0 1 0 2 0
0 0 0 0 0 0 0
L:
0 0 0 0 2 2 0 2 0 0 2 0 0 1 1 0 0 0 0 0 0
0 0 0 2 2 1 2 0 1 2 0 1 1 1 1 0 0 1 0 0 0
0 0 0 2 1 1 0 1 1 0 1 2 1 1 1 0 1 0 0 0 0
0 0 0 1 1 2 1 1 0 1 2 0 1 1 1 1 0 2 0 0 0
0 0 0 1 2 0 1 0 0 2 0 0 1 1 0 0 2 0 0 0 0
K2:
1 0 0 1 1 1 1 0 -1
O:
4 6 3 5 4
2 6 2 4 4
1 5 3 4 4
2 4 3 3 4
0 2 2 4 3

猜你喜欢

转载自blog.csdn.net/yyl424525/article/details/102649364