CUDA学习——Chapter 3(3)线程束

第三章

线程束和线程块

我们知道,在SM中,线程块不是线程的最小集合。SM会把线程块划分为若干个线程束,每个线程束由32个连续的线程组成。在一个线程束中 ,所有的线程按照SIMT的方式执行。也就是说,在线程束里的所有线程都会执行相同的指令,每个线程都在该线程束上的私有数据进行操作。
CUDA学习——Chapter 3(3)线程束_第1张图片

我们知道,线程块是拥有维度的。块最多可以有三维。我们之前又学过如何得到线程的全局索引,也就是在一个块中,线程的idx可以用以下式子来表示

idx=threadIdx.y*blockDim.x+threadIdx.x

那对于一个三维块来说,线程的idx满足:

idx=threadIdx.z*blockDim.y*blockDim.x+threadIdx.y*blockDim.x+threadIdx.x

一个线程块到底会被划分成几个线程束呢?那就是一个比较简单的数学公式了

w a r p   p e r   b l o c k = ⌈ t h r e a d   p e r   b l o c k s i z e   o f   w a r p ⌉ warp\ per\ block= \lceil \frac{thread\ per\ block}{size\ of\ warp} \rceil warp per block=size of warpthread per block

我们举个例子,假设一个块由下式定义:

dim3 block(40,2);

那么这个块就会有80个线程,而80/32=2余16,所以会被划分成3个线程束,其中一个线程束只有一半是活跃的。虽然最后一个线程束不活跃,但是它依然会占着SM的资源。

线程束分化

我们假设核函数里面有这样的语句

if(cond){
   ...
} else {
   ...
}

假设一个线程束内有16个线程cond为true,有16个线程cond为false。这种在同一个线程束中执行不同的指令的行为,称之为线程束分化。因为在一个线程束内的线程是必须要并行地执行同一条指令的,这似乎就形成了一个悖论。

为了解决这一问题,线程束会连续执行每一个分支路径而禁用不执行这一路径的线程。比如说,线程束会执行满足cond的线程而禁用不满足cond的线程。那么如果if…else…越多,分支就会越多,并行性就越差。

下面是一个展示线程束分化的例子:

__global__ void mathKernel1(float *c) {
    int tid=blockIdx.x * blockDim.x + threadIdx.x;
    float a,b;
    a=b=0.0f;
    if (tid % 2 ==0) {
        a = 100.00f;
    } else {
        b = 200.00f;
    }
    c[tid] = a + b;
}

这段代码是以线程的全局索引作为分化条件的。它会使在一个线程束内的至少一半的线程被禁用。

正如threadIdx、blockIdx这些对于每个线程确定的常量来说,对于线程束来说,也有一个warpSize常量用于输出线程束的大小。那么我们可以以线程束大小为基本单位,来消除线程束分化的影响。

__global__ void mathKernel2(void){
    int tid=blockIdx.x*blockDim.x+threadIdx.x;
    float a,b;
    a=b=0.0f;
	if((tid/warpSize)%2==0){
		a=100.0f;
	} else {
		b=200.0f;
	}
	c[tid]=a+b;
}

那么,通篇的代码如下:
Example 3-1:

#include 
#include 
#include 
#include 
#include 

/*
 * simpleDivergence demonstrates divergent code on the GPU and its impact on
 * performance and CUDA metrics.
 */

#define CHECK(call) \
{ \
    const cudaError_t error=call; \
    if(error!=cudaSuccess) \
      { \
        printf("Error: %s:%.5f, ", __FILE__, __LINE__); \
        printf("code:%.5f, reason: %s\n",error,cudaGetErrorString(error)); \
        exit(-10*error); \
	} \
}

__global__ void mathKernel1(float *c)
{
	int tid = blockIdx.x * blockDim.x + threadIdx.x;
	float ia, ib;
	ia = ib = 0.0f;

	if (tid % 2 == 0)
	{
		ia = 100.0f;
	}
	else
	{
		ib = 200.0f;
	}

	c[tid] = ia + ib;
}

__global__ void mathKernel2(float *c)
{
	int tid = blockIdx.x * blockDim.x + threadIdx.x;
	float ia, ib;
	ia = ib = 0.0f;

	if ((tid / warpSize) % 2 == 0)
	{
		ia = 100.0f;
	}
	else
	{
		ib = 200.0f;
	}

	c[tid] = ia + ib;
}
__global__ void warmingup(float *c)
{
	int tid = blockIdx.x * blockDim.x + threadIdx.x;
	float ia, ib;
	ia = ib = 0.0f;

	if ((tid / warpSize) % 2 == 0)
	{
		ia = 100.0f;
	}
	else
	{
		ib = 200.0f;
	}

	c[tid] = ia + ib;
}


int main(int argc, char **argv)
{
	// set up device
	int dev = 0;
	cudaDeviceProp deviceProp;
	CHECK(cudaGetDeviceProperties(&deviceProp, dev));
	printf("%s using Device %d: %s\n", argv[0], dev, deviceProp.name);

	// set up data size
	int size = 64;
	int blocksize = 64;

	if (argc > 1) blocksize = atoi(argv[1]);

	if (argc > 2) size = atoi(argv[2]);

	printf("Data size %d ", size);

	// set up execution configuration
	dim3 block(blocksize, 1);
	dim3 grid((size + block.x - 1) / block.x, 1);
	printf("Execution Configure (block %d grid %d)\n", block.x, grid.x);

	// allocate gpu memory
	float *d_C;
	size_t nBytes = size * sizeof(float);
	CHECK(cudaMalloc((float**)&d_C, nBytes));

	// run a warmup kernel to remove overhead
	LARGE_INTEGER ta, tb, tc;
	QueryPerformanceFrequency(&tc);
	QueryPerformanceCounter(&ta);
	CHECK(cudaDeviceSynchronize());
	warmingup <<<grid, block >>> (d_C);
	CHECK(cudaDeviceSynchronize());
	QueryPerformanceCounter(&tb);
	printf("warmup      <<< %4d %4d >>> elapsed %.5f sec \n", grid.x, block.x,
		(tb.QuadPart - ta.QuadPart)*1.0 / tc.QuadPart);
	CHECK(cudaGetLastError());

	// run kernel 1
	QueryPerformanceCounter(&ta);
	mathKernel1 <<<grid, block >>> (d_C);
	CHECK(cudaDeviceSynchronize());
	QueryPerformanceCounter(&tb);
	printf("mathKernel1 <<< %4d %4d >>> elapsed %.5f sec \n", grid.x, block.x,
		(tb.QuadPart - ta.QuadPart)*1.0 / tc.QuadPart);
	CHECK(cudaGetLastError());

	// run kernel 2
	QueryPerformanceCounter(&ta);
	mathKernel2 <<<grid, block >>> (d_C);
	CHECK(cudaDeviceSynchronize());
	QueryPerformanceCounter(&tb);
	printf("mathKernel2 <<< %4d %4d >>> elapsed %.5f sec \n", grid.x, block.x,
		(tb.QuadPart - ta.QuadPart)*1.0 / tc.QuadPart);
	CHECK(cudaGetLastError());

	// free gpu memory and reset divece
	CHECK(cudaFree(d_C));
	CHECK(cudaDeviceReset());
	return EXIT_SUCCESS;
}

运行结果如下:
线程束分化例子
可以看到,在warmup核函数消除启动所导致的时间误差之后,有线程束分化的mathKernel1是比mathKernel2花销大的,但是两者差别很小。

我们将命令行切换到该程序目录下,输入

nvprof --metrics branch_efficiency simpleDivergence.exe

可以得到如下结果
CUDA学习——Chapter 3(3)线程束_第2张图片

可以看到分支率是100%,即没有分化的分支数。这个结果是源于CUDA的编译器nvcc将该程序优化的结果。它将短的、有条件的代码段的断定指令取代了分支指令。

但是这种断定指令的替换是有条件的,只有当条件语句的指令数不太多的时候,才会有这样的分支预测。当条件语句太多的时候,必然会导致线程束分化。

按照下面的代码,重写mathKernel1,使得内核代码的分支预测直接显示:

//使用原书代码写出来的mathKernel3核函数的分支效率为100%,与原书不符
__global__ void mathKernel3(float *c){
	int tid=blockIdx.x*blockDim.x+threadIdx.x;
	float ia,ib;
	ia=ib=0.0f;
	bool ipred=(tid%2==0);
	if(ipred){
		ia=100.0f;
	}
	else if(!ipred){ //原书这里写的是if(!ipred),如果这样写,根本就没有if-else代码块,也就不需要分支预测了
		ib=200.0f;
	}
	c[tid]=ia+ib;
}

使用该命令编译,可以强制CUDA编译器不利用分支预测去优化内核

nvcc -ccbin "D:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.15.26726\bin\HostX86\x64" -g -G "3-1 simpleDivergence.cu" -o "3-1 simpleDivergence.exe"

visual studio的安装目录根据你自己的实际情况而定。如果不确定,刻意去项目属性那里看一下命令行–ccbin后面的参数指向的是哪里,原样抄下来就可以了。

在visual studio中,点击项目——属性——CUDA C/C++——Device和Host都选上Generate Device/Host Debug Information,这样就可以加上-g -G选项。

使用nvprof分析如下:
CUDA学习——Chapter 3(3)线程束_第3张图片

这里我们发现,mathKernel1和mathKernel3的分支效率都不能达到100%,是因为nvcc没有进行优化的缘故。

我们还可以使用nvprof来获得分支和分化分支的事件计数器,命令如下:

nvprof --events branch,divergent_branch "3-1 simpleDivergence.exe"

其运行结果如下:

当一个核函数的divergent_branch为0的时候,分支执行效率为100%。这个时候线程束的活跃性最高,但在一般比较大型的核函数里都达不到这一点或需要通过nvcc进行优化。

你可能感兴趣的:(CUDA,CUDA,C,并行计算)