假设二维卷积内核接收五个参数: 输入数组 N 的指针; 滤波器 F 的指针; 输出数组 P 的指针; 方形滤波器的半径 r; 输入输出数组的宽度; 输入和输出数组的高度。如下图所示,一个简单的并行方式是网格中的每个线程计算与自身坐标相同的输出像素。对应的内核函数代码如下,浮点计算与全局内存访问的比仅为 0.25 OP/B (每加载 8 字节执行 2 次运算)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
__global__ voidconvolution_2D_basic_kernel(float *N, float *F, float *P, int r, int width, int height) { int outCol = blockIdx.x * blockDim.x + threadIdx.x; int outRow = blockIdx.y * blockDim.y + threadIdx.y; int Pvalue = 0.0f; for (int fRow = 0; fRow < 2*r+1; fRow++) { for (int fCol = 0; fCol < 2 * r + 1; fCol++) { int inRow = outRow - r + fRow; int inCol = outCol - r + fCol; if (inRow > 0 && inRow < height && inCol > 0 && inCol < width) { Pvalue += P[inRow * width + inCol] * F[fRow * r + fCol]; } } } P[outRow * width + outCol] = Pvalue; }
7.3 Constant Memory and Caching
可以发现卷积核 F 通常很小,在整个卷积内核的执行过程中不会改变,所有线程都以相同的顺序访问其元素。因此我们可以考虑将其存储在常量内存里,之前说过它和全局内存的区别是线程不能修改常量内存变量的值并且常量内存非常小,目前为 64 KB. 假设已经在主机代码里分配好 F_h 的内存,可以通过 cudaMemcpyToSymbol() 将其从主机内存传输到设备常量内存中。内核函数以全局变量的形式访问常量内存变量。因此,它们的指针不需要作为参数传递给内核函数。
__global__ voidconvolution_tiled_2D_constant_mem_kernel_2( // OUT_TILE_DIM^2 threads per block float* N, float* P, int width, int height){
// Upper left output tile coord int col = blockIdx.x * OUT_TILE_DIM + threadIdx.x; int row = blockIdx.y * OUT_TILE_DIM + threadIdx.y;
// Each thread may need to load multiple elements into shared memory __shared__ float N_s[IN_TILE_DIM][IN_TILE_DIM]; for (int i = threadIdx.y; i < IN_TILE_DIM; i += OUT_TILE_DIM) { for (int j = threadIdx.x; j < IN_TILE_DIM; j += OUT_TILE_DIM) { int in_col = blockIdx.x * OUT_TILE_DIM + j - FILTER_RADIUS; int in_row = blockIdx.y * OUT_TILE_DIM + i - FILTER_RADIUS;