CUDA学习——Chapter 3(7)归约问题(1)相邻归约及并行归约

第三章

归约问题

首先来介绍一个并行计算的案例:
有一个长度为n的数组L,求他们的和。
如果按顺序叠加,那么时间复杂度就是O(n)。
伪代码就是:

count←0
for i←1 to n
	count+=L[i]
return count

那还有一种在串行是O(n),在并行上可以是log2n\left \lceil log_2n \right \rceil的算法,称为并行归约算法。这种算法是基于二分法的思想来做的,其基本思路就是,把输入的数组划分成更小的基本单位,直到计算每一个基本单位的和都只需要一个加法指令,然后自底向上的合并这些结果,称为归约。而并行计算的优势在于并行,因此最底层的基本单位的和的计算就可以使用并行来处理,同时,得到的基本单位的和组成的集合可以就地保存在输入向量中,不需要有额外的空间开销。

那么这些基本单位的划分可以分为两种:相邻配对和交错配对。

还是拿上面的L来说,相邻配对指的是相邻的两个元素组成一个基本单位,比如L[0]和L[1],L[2]和L[3],如果数组有奇数个元素,那么最后就剩一个单独的L[n-1]做基本单位。

交错配对指的是,以n/2为分界线,左右两边各自重新分序号,序号相同的组在一起。比如L[0]和L[4],L[1]和L[5](如果L的长度为8),直到L[3]和L[7]。

下列的示意图应该可以更好地帮助大家理解(3-19为相邻配对,3-20为交错配对):
相邻配对
交错配对

如何实现并行归约?

首先,思路就是,每次都是线程全部并行计算完之后,再开始下一步的线程并行计算,直至活跃的线程只有一个。这个思路既可以是循环的,也可以是递归的。在Chapter 3(5)里面我曾经介绍过一个设备端的API:__syncthreads,可以使用这个API来使得线程块内的活跃线程全部到达同一个点。

特别需要注意的一点就是:线程块之间是无法等待的,所以需要把每个线程块得到的结果通过cudaMemcpy拷贝回主机端进行串行计算。

其设备端代码如下:

__global void reduceNeighbored(int *g_idata,int *g_odata,unsigned int n)
{
	//set thread ID
	unsigned int tid=threadIdx.x;
	unsigned int idx=blockIdx.x*blockDim.x+threadIdx.x;
	int *idata=g_idata+blockIdx.x*blockDim.x;
	//boundary check
	if (idx>=n) return;
	//in-place reduction in global memory
	for(int stride=1;stride<blockDim.x;stride *=2){
	if((tid%(2*stride)==0){
		idata[tid]+=idata[tid+stride];
	}
	// synchronize within block
	__syncthreads();
	}
	// write result for this block to global mem
	if (tid==0) g_odata[blockIdx.x]=idata[0];

我们来分析一下这些代码:

unsigned int tid=threadIdx.x;
unsigned int idx=blockIdx.x*blockDim.x+threadIdx.x;

第一条代码得到线程在当前行中的唯一ID,第二条代码得到该线程在当前块中的唯一ID。

int *idata=g_idata+blockIdx.x*blockDim.x;

因为g_idata是指向整个矩阵的指针,而blockIdx.x*blockDim.x可以得到当前块与矩阵对应的行,从而得到偏移量。(blockIdx.x*blockDim.x也就是走过了多少个线程,记住,一个线程对应矩阵里的一个元素)。

for(int stride=1;stride<blockDim.x;stride *=2)

blockDim.x是x方向上块的最大线程数(也就是x方向上一行最多能有多少个线程)。传入的idata是一行的输入向量。

if((tid%(2*stride)==0){
		idata[tid]+=idata[tid+stride];
	}

stride一开始是1,也就是说会由tid为偶数的线程(0、2、4、6)来完成相加操作。然后stride*2,就会由tid为4的倍数的线程(0、4)完成下一步操作。
因为每一次的for循环都意味着本次基本单位求和已结束。所以在开始下一次for循环之前要使用__syncthreads()函数来对线程进行同步。

	// write result for this block to global mem
	if (tid==0) g_odata[blockIdx.x]=idata[0];

因为最后执行该行求和任务的是ID为0的thread,所以只让ID为0的thread执行该操作,往g_odata的blockIdx.x的位置写入结果。
为什么是blockIdx.x而不是其它的数?因为blockIdx.x对应矩阵内的行数。
我们再来看看主机端的代码(部分):

int main(int argc, char **argv)
{
	// initialization
    int size = 1 << 24; // total number of elements to reduce
    // execution configuration
    int blocksize = 512;   // initial block size
    dim3 block (blocksize, 1);
    dim3 grid  ((size + block.x - 1) / block.x, 1);
    // allocate host memory
    size_t bytes = size * sizeof(int);
    int *h_idata = (int *) malloc(bytes);
    int *h_odata = (int *) malloc(grid.x * sizeof(int));
    // initialize the array
    for (int i = 0; i < size; i++)
    {
        // mask off high 2 bytes to force max number to 255
        h_idata[i] = (int)( rand() & 0xFF );
    }
    int gpu_sum = 0;
    // allocate device memory
    int *d_idata = NULL;
    int *d_odata = NULL;
    CHECK(cudaMalloc((void **) &d_idata, bytes));
    CHECK(cudaMalloc((void **) &d_odata, grid.x * sizeof(int)));
    // kernel 1: reduceNeighbored
    CHECK(cudaMemcpy(d_idata, h_idata, bytes, cudaMemcpyHostToDevice));
    CHECK(cudaDeviceSynchronize());
    reduceNeighbored<<<grid, block>>>(d_idata, d_odata, size);
    CHECK(cudaDeviceSynchronize());
    CHECK(cudaMemcpy(h_odata, d_odata, grid.x * sizeof(int),
                     cudaMemcpyDeviceToHost));
    int gpu_sum = 0;
    for (int i = 0; i < grid.x; i++) gpu_sum += h_odata[i];
    // 验证代码略……
    return 0;
}

我们还是先比较细致地分析一下部分的代码:

	// initialization
    int size = 1 << 24; // total number of elements to reduce
    // execution configuration
    int blocksize = 512;   // initial block size
    dim3 block (blocksize, 1);
    dim3 grid  ((size + block.x - 1) / block.x, 1);

这段代码声明了一个长度为512的一维块向量。而整个矩阵的大小有1<<24,所以声明了一个1<<24/512+1长度的块(思考一下,矩阵的大小是1<<24,而长度为512,那么其宽度不就是1<<24/512,为了防止小数不能被包含,向上取整,所以+1)。

	CHECK(cudaMemcpy(h_odata, d_odata, grid.x * sizeof(int),
                     cudaMemcpyDeviceToHost));
    int gpu_sum = 0;
    for (int i = 0; i < grid.x; i++) gpu_sum += h_odata[i];

h_odata里面储存着每行求和完后的代码,因为线程块之间不可同步,所以每行的和的求和需要在主机端上进行。而线程块的个数对应的变量就是grid.x,所以循环的长度为grid.x。

验证代码其实就是在主机端使用O(n2)O(n^2)的矩阵求和代码算一遍就可以了,然后把这个结果和GPU计算的结果相比对。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章