12.3_单遍归约

12.3 单遍归约

两遍归约的做法是针对CUDA线程块无法同步这一问题的部分解决方法。在缺少块间同步机制的条件下,为了确定最终输出的处理何时可以开始,需要调用第二个内核。

使用原子操作和共享内存的组合可避免第二个内核调用,就像 CUDA SDK的threadfenceReduction示例那样。使用一个设备内存位置跟踪哪个线程块已经写完自己的部分和。一旦所有块都完成后,一个块进行最后的对数步长的归约,将输出写回。

由于该内核执行多次来自共享内存的对数步长的归约,代码清单12-3中根据模板化的线程数进行条件求和的代码被提取出来,放入一个单独的设备函数,以便于重用。

Reduction4_LogStepShared()函数如代码清单12-5所示,负责写回线程块的归约值,其部分和由partials提供给由out指定的内存位置。代码清单12-6给出了使用Reduction4_LogStepShared()作为子程序的单遍归约算法。

template<unsigned int numThreads> 
    device__void
Reduction4_LogStepShared( int *out, volatile int *partials )
{
    const int tid = threadIdx.x;
    if (numThreads >= 1024) {
        if (tid < 512) {
            // partials[tid] += partials[tid + 512];
        }
        __syncthreads();
    }
    if (numThreads >= 512) {
        if (tid < 256) {
            // partials[tid] += partials[tid + 256];
        }
        __syncthreads();
    }
    if (numThreads >= 256) {
        if (tid < 128) {
            // partials[tid] += partials[tid + 128];
        }
        __syncthreads();
    }
    if (numThreads >= 128) {
        if (tid < 64) {
            // partials[tid] += partials[tid + 64];
        }
        __syncthreads();
    }
}
// warp synchronous at the end
if (tid < 32) {
    if (numThreads >= 64) {
        // partials[tid] += partials[tid + 32];
    }
    if (numThreads >= 32) {
        // partials[tid] += partials[tid + 16];
    }
    if (numThreads >= 16) {
        // partials[tid] += partials[tid + 8];
    }
    if (numThreads >= 8) {
        // partials[tid] += partials[tid + 4];
    }
    if (numThreads >= 4) {
        // partials[tid] += partials[tid + 2];
    }
    if (numThreads >= 2) {
        // partials[tid] += partials[tid + 1];
    }
    if (tid == 0) {
        *out = partials[0];
    }
}

代码清单12-6 单遍归约内核(reduction4SinglePass.cuh)

// Global variable used by reduceSinglePass to count blocks
device__ unsigned int retirementCount = 0;
template <unsigned int numThreads>
global _ void
reduceSinglePass( int *out, int *partial,
					 const int *in, unsigned int N )
\{
	external _shared _ int sPartials[ ] ;
		signed int tid = threadIdx.x;
	int sum = 0 ;
	for ( size_t i = blockIdx.x*numThreads + tid;
		i < N;
		i += numThreads*gridDim.x ) \{
		sum += in[i];
	\}
	sPartials[tid] = sum;
	sync threads(   );
	if (gridDim.x == 1) \{
		Reduction4_LogStepShared<numThreads>( &out [blockIdx.x],
									sPartials);
		return;
	\}
	Reduction4_LogStepShared<numThreads>( &partial [blockIdx.x],
									sPartials);
	_shared bool lastBlock;
	// wait for outstanding memory instructions in this thread
_threadface(   );
	// Thread 0 takes a ticket
	if( tid==0 ) \{
		unsigned int ticket = atomicAdd(&retirementCount, 1);
		// If the ticket ID is equal to the number of blocks,
			// we are the last block!
		_lastBlock = (ticket == gridDim.x-1);
		\}
	sync threads(   );
	// One block performs the final log-step reduction
	if( lastBlock ) \{
		int sum = 0 ;
		for ( size_t i = tid;
			i < gridDim.x;
			i += numThreads ) \{
			_sum += partial[i];
			\}
			sPartials [threadIdx.x] = sum;
			sync threads(   );
			Reduction4_LogStepShared<numThreads>( out, sPartials);
		-retirementCount = 0;
	\}
\}

内核始于熟悉的代码,每个线程计算整个输入数组中的部分归约并将结果写入共享内存。一旦完成上述操作,单个线程块的情况是经过特殊处理的,因为针对共享内存的对数步长的归约输出可以直接写

回而不被写入部分和数组。该内核的其余部分只能在多线程块的内核上执行。

共享的布尔变量lastBlock是用来评估必须传达给最后一个块的所有线程的一个条件。该__threadfence()导致块中的所有线程都等待,直到任何挂起的内存事务已写回到设备内存。当__threadfence()被执行时,写入全局内存的操作不仅仅可见于被调用线程或者块内线程,而是可见于所有线程。

当每个线程块退出时,它执行一个atomicAdd()来检查自己是否是那个需要执行最终对数步长的归约的线程块。由于atomicAdd()返回内存位置上的前一个值,那个递增retirementCount并能够得到等于gridDim.x-1的值的块为“最后一个线程块”[1],它要进行最终的归约。lastBlock这一共享内存位置把该结果传达给该线程块的所有线程,随后必须调用__syncreads(),这样写回lastBlock的操作将可见于该块的所有线程。最后一个线程块执行最终的针对部分和的对数步长的归约,并将结果写回。最后,将retirementCount重新置为0,以便于后续reduceSinglePass()的调用。

[1] 原文误为“最后一个线程”。——译者注