CUDA 卷积计算及其优化——以一维卷积为例

《大规模并行处理器编程实战》学习,其他章节关注专栏 CUDA C

初次接触 CUDA C 编程不建议直接阅读,友情链接:

建议阅读:在卷积优化前,熟悉核函数的组织形式有利于更好的位置映射-CUDA编程入门(一):以图片运算看线程的组织和核函数的使用

  • 纯C++/CUDA 编写的卷积神经网络实现项目

对于输入数据为N[Width],卷积核大小为M[Mask_Width]的卷积运算,进行不同程度的优化(这里的卷积指滤波/内积,而不需要旋转),输出为P[Width]。

1.常规的一维卷积

常规的一维卷积比较简单,线程数为Width,每个线程负责一个输出值得Mask_Width宽度的卷积运算,即:
CUDA卷积计算及其优化——以一维卷积为例_卷积

2.利用共享存储器的卷积优化(使用光环元素的分块一维卷积)

由于直接卷积时,相邻线程在数据读取时都需要访问N,N在全局存储器上,这样会造成不断的访问全局存储器,因此可以利用共享存储器进行优化,先将数据放在共享存储器上,再不断的访问共享存储器,提高效率。
shared 变量声明的共享存储器对线程块是共享的,因此在使用分块卷积时才有优化的效果。可对于每一线程块先加载其整个线程块中的线程用到的数据N到共享存储器,然后再利用共享存储器进行计算,以减少线程块中线程对全局存储器的不断访问。
假如卷积核长度为5,数据长度为16,则分块卷积时,各个块内使用到的数据N如下:
CUDA卷积计算及其优化——以一维卷积为例_卷积_02

分块0中的空元素称为幽灵元素,分块0中的2,3被分块1重复使用,分块1中的4,5同样被分块0使用,这些被多个块重复使用的元素称为光环元素/边缘元素。其余元素称为内部元素。
对于每个分块,建立一个共享存储器,将该分块用到的元素都放进去:
CUDA卷积计算及其优化——以一维卷积为例_加载_03

内部元素的加载比较简单,其映射与前面直接读取是一致的。对于光环元素的加载,采用不同的方式。如上图,分3步对共享存储器进行加载,n表示MASK_Width/2,即光环元素的长度:

  1. 第一步,利用线程块中后n个线程加载前面的光环元素(n个)。为什么要用后面的加载前面的,如上图所示,在分块1中,6,7的位置与分块0中2,3的位置是一致的,因此只需要在利用threadidx和blockidx计算N中的对应位置时将blockidx-1,即可从对6,7的映射变到对2,3的映射,这样更容易计算前n个光环元素在N中的位置,具体如下:
    CUDA卷积计算及其优化——以一维卷积为例_加载_04

  2. 第二步,加载内部元素
    CUDA卷积计算及其优化——以一维卷积为例_卷积_05

  3. 第三步,加载后n个光环元素,同样利用前n个块中线程进行加载:
    CUDA卷积计算及其优化——以一维卷积为例_分块_06

加载到共享存储器后,进行正常的运算即可:
CUDA卷积计算及其优化——以一维卷积为例_加载_07

3.利用通用高速缓存

对于光环元素, 由于两个分块都有读取,因此不一定需要加载到共享存储器上。因为分块0从全局存储器读取该光环元素后,分块1可以直接在通用高速缓存中读取,无须加载进共享内存。所有分块只需要把内部元素加载到共享存储器即可。
CUDA卷积计算及其优化——以一维卷积为例_分块_08