背景介绍
共享内存
共享内存是CUDA中的一种特殊内存,其对应的共享存储器驻留在多核流处理器上。因此与全局内存相比,共享内存具有更高的访问速度和更高的带宽。
共享内存的作用于是一个线程块,即一个线程块中的所有线程都可以访问同一块共享内存。基于这个特点,共享内存也通常用于在线程间进行通信。
图像卷积
图像卷积是图像处理中一个较为重要的操作。卷积的思路可以图示如下(即用一个卷积核和图像上对应一部分图像像素分别相乘然后将结果相加就可以得到卷积结果。):
值得注意的是,在卷积核中存在一些特殊的卷积核可以将其分解成两个独立的卷积核(水平卷积核和竖直卷积核)。通过分解,可以将
N∗M
N
∗
M
次乘法化简成
N+M
N
+
M
次乘法,从而提高运算效率。高斯卷积核就可以分解为竖直卷积核和水平卷积核。
CUDA图像卷积
比较简单的思路是直接将一部分图片拷贝到共享内存中,然后在一个线程块中进行卷积操作,示意图如下(黄色像素所包围区域即为从图像上拷贝到共享内存中的一部分图像信息,紫色像素为卷积核,红色则代表卷积结果):
问题与优化
1、线程优化
在上述方法中存在一些问题,例如当卷积核较大时,空置线程数目就会增加(Apron对应区域)。因为在线程块中一些线程参与了像素拷贝,但是却没有参与卷积操作,这样会导致程序的运行效率降低。下图展示了一种极端情况,卷积核极大,因而只有
19
1
9
的线程参与了运算:
解决方案是使得一个线程同时拷贝多个数据,进而使用更少的线程进行资源拷贝,减少线程的空置率。下图展示了一个线程同时加载3个像素的示意图(每一个线程分别同时在3个黑框里各自读取一个像素),可以发现
13
1
3
的线程参与了运算:
2、卷积核优化
前边已经提到,针对一些特殊的卷积核可以将其分解成竖直卷积核水平卷积两步,并且同时减少计算量。因此在一些情况下,也可以对卷积核进行分解,减少计算量。除此之外,分解卷积核还可以减少空置线程数目。比如说,针对水平卷积核来说,就不需要再在竖直方向上多增加边缘(Apron),进行减少分配的像素。
并且在进行了卷积核优化后还可以对线程进行再一次优化,优化方案如下图所示(即将水平/竖直方向需要处理的像素增加):
代码实现
代码中总共有:
main.cpp//主调用函数,分别调用CPU、GPU版本程序
convolutionSeparable_common.h//函数声明
convolutionSeparable.cu//GPU版本卷积
convolutionSeparable_gold.cpp//CPU版本的卷积实现,主要是为GPU做验证和对比
一下重点解释GPU版本代码
/*
* Copyright 1993-2015 NVIDIA Corporation. All rights reserved.
*
* Please refer to the NVIDIA end user license agreement (EULA) associated
* with this source code for terms and conditions that govern your use of
* this software. Any use, reproduction, disclosure, or distribution of
* this software and related documentation outside the terms of the EULA
* is strictly prohibited.
*
*/
#include <assert.h>
#include <helper_cuda.h>
#include <cooperative_groups.h>
namespace cg = cooperative_groups;
#include "convolutionSeparable_common.h"
////////////////////////////////////////////////////////////////////////////////
// Convolution kernel storage
////////////////////////////////////////////////////////////////////////////////
//使用常量内存保存卷积核信息
__constant__ float c_Kernel[KERNEL_LENGTH];
//将卷积核信息从CPU拷贝到常量内存中
extern "C" void setConvolutionKernel(float *h_Kernel)
{
cudaMemcpyToSymbol(c_Kernel, h_Kernel, KERNEL_LENGTH * sizeof(float));
}
////////////////////////////////////////////////////////////////////////////////
// Row convolution filter
////////////////////////////////////////////////////////////////////////////////
#define ROWS_BLOCKDIM_X 16//线程块宽度
#define ROWS_BLOCKDIM_Y 4//线程块高度
#define ROWS_RESULT_STEPS 8//一个线程同时读取几个像素
#define ROWS_HALO_STEPS 1//边缘像素块,处理边缘
__global__ void convolutionRowsKernel(
float *d_Dst,
float *d_Src,
int imageW,
int imageH,
int pitch
)
{
// Handle to thread block group
cg::thread_block cta = cg::this_thread_block();
//申请共享内存块
//在水平方向想分别在右边和左边添加一个边缘
__shared__ float s_Data[ROWS_BLOCKDIM_Y][(ROWS_RESULT_STEPS + 2 * ROWS_HALO_STEPS) * ROWS_BLOCKDIM_X];
//Offset to the left halo edge
const int baseX = (blockIdx.x * ROWS_RESULT_STEPS - ROWS_HALO_STEPS) * ROWS_BLOCKDIM_X + threadIdx.x;
const int baseY = blockIdx.y * ROWS_BLOCKDIM_Y + threadIdx.y;
d_Src += baseY * pitch + baseX;
d_Dst += baseY * pitch + baseX;
//Load main data
//查了一下unroll这个命令,其实就是将简单的循环部分展开,再编译时不当做循环处理,进而提高程序运行速率
#pragma unroll
for (int i = ROWS_HALO_STEPS; i < ROWS_HALO_STEPS + ROWS_RESULT_STEPS; i++)
{
s_Data[threadIdx.y][threadIdx.x + i * ROWS_BLOCKDIM_X] = d_Src[i * ROWS_BLOCKDIM_X];
}
//Load left halo
#pragma unroll
for (int i = 0; i < ROWS_HALO_STEPS; i++)
{
s_Data[threadIdx.y][threadIdx.x + i * ROWS_BLOCKDIM_X] = (baseX >= -i * ROWS_BLOCKDIM_X) ? d_Src[i * ROWS_BLOCKDIM_X] : 0;
}
//Load right halo
#pragma unroll
for (int i = ROWS_HALO_STEPS + ROWS_RESULT_STEPS; i < ROWS_HALO_STEPS + ROWS_RESULT_STEPS + ROWS_HALO_STEPS; i++)
{
s_Data[threadIdx.y][threadIdx.x + i * ROWS_BLOCKDIM_X] = (imageW - baseX > i * ROWS_BLOCKDIM_X) ? d_Src[i * ROWS_BLOCKDIM_X] : 0;
}
//Compute and store results
//线程同步
//前半部分都是线程拷贝内存的过程,此时同步一次后即可进行计算
cg::sync(cta);
#pragma unroll
for (int i = ROWS_HALO_STEPS; i < ROWS_HALO_STEPS + ROWS_RESULT_STEPS; i++)
{
float sum = 0;
#pragma unroll
for (int j = -KERNEL_RADIUS; j <= KERNEL_RADIUS; j++)
{
sum += c_Kernel[KERNEL_RADIUS - j] * s_Data[threadIdx.y][threadIdx.x + i * ROWS_BLOCKDIM_X + j];
}
d_Dst[i * ROWS_BLOCKDIM_X] = sum;
}
}
extern "C" void convolutionRowsGPU(
float *d_Dst,
float *d_Src,
int imageW,
int imageH
)
{
assert(ROWS_BLOCKDIM_X * ROWS_HALO_STEPS >= KERNEL_RADIUS);
assert(imageW % (ROWS_RESULT_STEPS * ROWS_BLOCKDIM_X) == 0);
assert(imageH % ROWS_BLOCKDIM_Y == 0);
dim3 blocks(imageW / (ROWS_RESULT_STEPS * ROWS_BLOCKDIM_X), imageH / ROWS_BLOCKDIM_Y);
dim3 threads(ROWS_BLOCKDIM_X, ROWS_BLOCKDIM_Y);
convolutionRowsKernel<<<blocks, threads>>>(
d_Dst,
d_Src,
imageW,
imageH,
imageW
);
getLastCudaError("convolutionRowsKernel() execution failed\n");
}
extern "C" void convolutionRowsGPU(
float *d_Dst,
float *d_Src,
int imageW,
int imageH
)
{
assert(ROWS_BLOCKDIM_X * ROWS_HALO_STEPS >= KERNEL_RADIUS);
assert(imageW % (ROWS_RESULT_STEPS * ROWS_BLOCKDIM_X) == 0);
assert(imageH % ROWS_BLOCKDIM_Y == 0);
//减少X方向的线程块数目,但是不减少线程数目,即一个线程处理多个像素
dim3 blocks(imageW / (ROWS_RESULT_STEPS * ROWS_BLOCKDIM_X), imageH / ROWS_BLOCKDIM_Y);
dim3 threads(ROWS_BLOCKDIM_X, ROWS_BLOCKDIM_Y);
convolutionRowsKernel<<<blocks, threads>>>(
d_Dst,
d_Src,
imageW,
imageH,
imageW
);
getLastCudaError("convolutionRowsKernel() execution failed\n");
}
////////////////////////////////////////////////////////////////////////////////
// Column convolution filter
////////////////////////////////////////////////////////////////////////////////
#define COLUMNS_BLOCKDIM_X 16
#define COLUMNS_BLOCKDIM_Y 8
#define COLUMNS_RESULT_STEPS 8
#define COLUMNS_HALO_STEPS 1
__global__ void convolutionColumnsKernel(
float *d_Dst,
float *d_Src,
int imageW,
int imageH,
int pitch
)
{
//列处理与行处理类似
}