这篇论文主要讨论如何针对CNN做一些GPU矩阵计算的优化。传统CNN计算主要开销是在convolutions, activation function, pooling.
首先,我们看convolution的操作过程:
参数表:
O是输出input feature map,F是filter, D0是input feature map. 从公式看到如果用循环操作,需要7次循环,n,k,p,q4次可独立循环,c,r,s是累加操作的循环。
其次,我们看convolution在GPU上如何实现,文中介绍了三种方法:
第一,最直观的方法是直接实现如上公式,但是这种实现呢需要处理许多的corner case。 文中介绍cuda-convnet2是实现了该种方法,该种方法在不同取值的卷积参数空间效率不一,比如batch size > 128,效率很高,但是如果batch size < 64, 效率比较低。
第二,采用快速傅里叶变换fft,具体怎么做,参见论文Fast training of convolutional networks through ffts(待读). 该种方法效率非常高,但是由于filter需要扩大到和input一样大小,占用了大量内存,特别是CNN的前几层filter 大小远小于input大小。第二,当striding 参数>1, fft效率也不高。facebook最近开源了fbfft的实现,参见论文fast convolutional nets with fbfft: a gpu performance evaluation(待读).
第三,也就是现在caffe实现的方法。将卷积操作转换为密集矩阵相乘。将input data组装成大小为CRS x NPQ的矩阵,这使得内存相对原始input data的大小扩大了之多RS倍,举个例子,对AlexNet的conv1:
Fm: 96 x 363
Dm: 363 x 387200(设double 类型, 需要临时分配内存1GB)
D: 128 x 3 x 227 x 227 = 19787136.
Dm 是D的内存大小的7倍。
具体卷积是如何转换为矩阵的呢?看如下图:
对照caffe的代码就是im2col_gpu, caffe_gpu_gemm, caffe_gpu_gemm 会调用cublasSgemm.
这种方法使用扩大临时内存方法换取密集矩阵计算的便利。
密集矩阵相乘为什么好呢,值得我们如此牺牲内存代价?因为 it has a high ratio of floating-point operations per byte of data transferred. 当矩阵越大,ratio越高,我们看到该方法的矩阵大小还是可以的,CRS乘机相对比较大。该方法的缺陷是: Dm太大,因此需要分片materiialize Dm,我们看caffe代码实现,在每次batch,遍历了N次的im2col_gpu, caffe_gpu_gem。但是这种循环方法使得要多次读写Dm和读取D, 相对第一种方案需要更多memory traffic. 因此,本文采用了另一种实现:
对A x B = C, 分块加载A和B从off-chip memory to on-chip caches, 同时计算C的一部分。这样减少了数据传输带来的延迟。对于Dm,我们是在on-chip上转换成Dm,而不是在off-chip上转换.
三种方法性能的比较: