我们知道,在SM中,线程块不是线程的最小集合。SM会把线程块划分为若干个线程束,每个线程束由32个连续的线程组成。在一个线程束中 ,所有的线程按照SIMT的方式执行。也就是说,在线程束里的所有线程都会执行相同的指令,每个线程都在该线程束上的私有数据进行操作。
我们知道,线程块是拥有维度的。块最多可以有三维。我们之前又学过如何得到线程的全局索引,也就是在一个块中,线程的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
可以看到分支率是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选项。
这里我们发现,mathKernel1和mathKernel3的分支效率都不能达到100%,是因为nvcc没有进行优化的缘故。
我们还可以使用nvprof来获得分支和分化分支的事件计数器,命令如下:
nvprof --events branch,divergent_branch "3-1 simpleDivergence.exe"
其运行结果如下:
当一个核函数的divergent_branch为0的时候,分支执行效率为100%。这个时候线程束的活跃性最高,但在一般比较大型的核函数里都达不到这一点或需要通过nvcc进行优化。