cuda中循环展开和并行归约的代码理解及可视化辅助理解

参考

 首先列出参考文献:

代码部分:会了么的个人空间-会了么个人主页-哔哩哔哩视频 (bilibili.com)

图片及部分理解部分:Cuda C编程权威指南1.并行规约分化+循环展开-CSDN博客


背景

cuda的执行模型

逻辑概念上,grid>block>thread

也就是,网格>线程块>线程

每个核函数的启动都对应着一个grid,grid中的所有block共享全局内存,每个block又是由许多线程构成的(block内的线程共享共享内存)。

CUDA中SIMD的基本单位是一个warp(线程束一般是由32个线程组成,共享寄存器)。

threadIdx.x:当前线程在线程块中的索引

blockIdx.x:当前线程块的索引

blockDim.x:每个block的线程数

一般在main函数中用下面代码定义:

    // initialization
    int size = 1 << 24; // total number of elements to reduce
    printf("    with array size %d  ", size);

    // execution configuration
    int blocksize = 1024;   // initial block sizeif(argc > 1)
    {
        blocksize = atoi(argv[1]);   // block size from command line argument
    }

    dim3 block (blocksize, 1);
    dim3 grid  ((size + block.x - 1) / block.x, 1);
    printf("grid %d block %d\n", grid.x, block.x);

以上代码每个block的thread数初始值是1024,但是可以在终端设置大于1的数。

此外,

tid:当前thread在这个block中的id索引

idx:当前thread在全局中的索引

ideta:每一个block计算的部分data的数据指针起点;进行了循环展开后相当于间隔八个data块处理一次

在核函数定义:

//set thread ID
	unsigned int tid = threadIdx.x;
	unsigned int idx = blockDim.x*blockIdx.x*8+threadIdx.x;
	//boundary check
	if (tid >= n) return;
	//convert global data pointer to the
	int *idata = g_idata + blockIdx.x*blockDim.x*8;

相邻配对和交错配对

提示:有的也叫邻域并行计算和间域并行计算

邻域:

// Neighbored Pair Implementation with divergence
__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;

    // convert global data pointer to the local pointer of this block
    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 threadblock
        __syncthreads();
    }

间域:

__global__ void reduceInterleaved (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;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x;

    // boundary check
    if(idx >= n) return;

    // in-place reduction in global memory
    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1)
    {
        if (tid < stride)
        {
            idata[tid] += idata[tid + stride];
        }

        __syncthreads();
    }

上面两种是基础情况,比较容易理解(尤其看着图)


循环展开

这点东西我看了一整天还是晕乎乎

由于核函数中的每个线程块都只处理对应数据块的数据内容,那么可以通过代码让一个block处理多个data块。

也就是在数据求和前,使用每个thread做一次加法,从多个(示例是8个)data块中取数据

最后再归约相加

循环展开,先贴个代码:(8倍block)

__global__ void reduceUnrolling8 (int *g_idata, int *g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x * 8;

    // unrolling 8
    if (idx + 7 * blockDim.x < n)
    {
        int a1 = g_idata[idx];
        int a2 = g_idata[idx + blockDim.x];
        int a3 = g_idata[idx + 2 * blockDim.x];
        int a4 = g_idata[idx + 3 * blockDim.x];
        int b1 = g_idata[idx + 4 * blockDim.x];
        int b2 = g_idata[idx + 5 * blockDim.x];
        int b3 = g_idata[idx + 6 * blockDim.x];
        int b4 = g_idata[idx + 7 * blockDim.x];
        g_idata[idx] = a1 + a2 + a3 + a4 + b1 + b2 + b3 + b4;
    }

    __syncthreads();

   

示意图长这样:

一个block处理8个data的数据,比如256个线程处理512*8个数据(用一个block将8个data求和成一个data)

然后对每个block里的data的求和操作:(示意图如下)

①首先是最简单的情况,确定了blocksize是512,不需要再写条件语句

	//in-place reduction in global memory
	for (int stride = blockDim.x/2; stride>32; stride >>=1)
	{
		if (tid <stride)
		{
			idata[tid] += idata[tid + stride];
		}
		//synchronize within block
		__syncthreads();
	}
	//write result for this block to global mem
	

这里注意三种情况都需要用__syncthreads();来同步指令,这是因为每一层的操作都是并行的,不同速度的thread要进行等待来进行block内的同步,防止出现层之间的交叉导致逻辑不合理。

②如果想在终端传入参数

此时可使用blockDim书写条件语句

//in-place reduction in global memory
	if(blockDim.x>=1024 && tid <512)
		idata[tid]+=idata[tid+512];
	__syncthreads();
	if(blockDim.x>=512 && tid <256)
		idata[tid]+=idata[tid+256];
	__syncthreads();
	if(blockDim.x>=256 && tid <128)
		idata[tid]+=idata[tid+128];
	__syncthreads();
	if(blockDim.x>=128 && tid <64)
		idata[tid]+=idata[tid+64];
	__syncthreads();

③也可以使用模版函数

在核函数前加一条语句

template <unsigned int iBlockSize>

在核函数中加入,根据Switch的传入的值,分别计算idata的值

//in-place reduction in global memory
	if(iBlockSize>=1024 && tid <512)
		idata[tid]+=idata[tid+512];
	__syncthreads();
	if(iBlockSize>=512 && tid <256)
		idata[tid]+=idata[tid+256];
	__syncthreads();
	if(iBlockSize>=256 && tid <128)
		idata[tid]+=idata[tid+128];
	__syncthreads();
	if(iBlockSize>=128 && tid <64)
		idata[tid]+=idata[tid+64];
__syncthreads();

在main函数里需要加一个Switch函数,根据在终端传入的size参数值分别处理

//kernel 4:reduceCompleteUnroll
	CHECK(cudaMemcpy(idata_dev, idata_host, bytes, cudaMemcpyHostToDevice));
	CHECK(cudaDeviceSynchronize());
	iStart = cpuSecond();
	switch(blocksize)
	{
		case 1024:
			reduceCompleteUnroll <1024><< <grid.x/8, block >> >(idata_dev, odata_dev, size);
		break;
		case 512:
			reduceCompleteUnroll <512><< <grid.x/8, block >> >(idata_dev, odata_dev, size);
		break;
		case 256:
			reduceCompleteUnroll <256><< <grid.x/8, block >> >(idata_dev, odata_dev, size);
		break;
		case 128:
			reduceCompleteUnroll <128><< <grid.x/8, block >> >(idata_dev, odata_dev, size);
		break;
	}

----------------

以上第2、3种情况,都需要对[0,32]之间的thread进行代码展开

if(tid < 32) {
    volatile int *vmem = idata;
    vmem[tid] *= vmem[tid + 32];
    vmem[tid] *= vmem[tid + 16];
    vmem[tid] *= vmem[tid + 8];
    vmem[tid] *= vmem[tid + 4];
    vmem[tid] *= vmem[tid + 2];
    vmem[tid] *= vmem[tid + 1];
  }

从64个数合并到一个数,首先将前32个数,按照步长为32,进行并行加法,前32个tid得到64个数字的两两和,存在前32个数字中
然后这32个数加上步长为16的变量,得到16个数,这16个数的和就是最后这个块的归约结果

当执行 tid+32的时候,这32个线程都在执行这步,而不会有任何本线程束内的线程会进行到下一句

使用volatile修饰data就是为了防止编译器优化全局内存或共享内存的读取,防止一层求和完成后下一层求和会出现不同的thread读同一位置的情况。

OK结束。

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值