本篇记录学习cuda的执行模式,主要有几个方面的内容:
理解jetson nano上的gpu架构;
理解线程束执行的本质;
分支化与避免分支化;
展开循环;
其他;
一、理解jetson nano的gpu架构
这块内容在上一篇笔记中有学习到一些,本篇就进行一个补充和复习。
jetson nano上的gpu是Maxwell架构的,整个gpu上面只有一个SM(流多处理器)。该SM上包含128个处理核心,这128个计算核心被划分为4个部分,每一个部分包含32个计算核心,这和warp调度有着密切联系,因为32个计算核心刚好能够同时执行一个warp。
另外,我在网上查询数据后,了解到jetson nano上总共的内存只有4G,我本以为这4G是cpu和gpu共享的内存,从而不需要进行显示的数据传输(使用cudaMalloc,cudaFree,cudaMemcopy等api),但上手操作了一下,发现仍然需要进行显示传输。
二、理解线程束执行的本质
我们在编写一个kernel后,会根据需要处理的数据量的大小来组织线程,分配合理的线程数量来执行核函数。线程数量管理和组织可以被理解为一个层次结构,即grid、block、thread。
dim3 gridsize(2,3)
dim3 blocksize(16,16)
上面两行代码表示将所有线程组织为一张2行3列的网格,该网格由线程块组成,所以整个网格包含2*3=6个线程块;往下一个层次走,每一个线程块被组织为了16行16列,即表示一个线程块有16*16=256个线程。经过这样划分,那么整个网格就包含6*256=1536个线程。最后就表示经过这样组织,这个核函数最多可以使用计算核心执行1536次运算。
很明显,jetson nano上只有一个SM,并且只有128个计算核心。对于一个SM上一次只能装载一个block,每个SM同时最多只能执行4个warp,那么整个执行过程是怎样的呢?
上一篇已经学习过了warp的概念,其实就是两句话:一个warp由32个线程组成,这32个线程同时执行,同时结束;warp是gpu执行的最小单位。
很多其他架构的gpu不止一个SM,加入有n和SM,那么多个block可以同时装载到n个SM上进行调度。但本篇是针对jetson nano进行分析,针对只有一个SM的架构。
所以,在jetsonnano上,不管你网格是怎么组织的,所有组织好的线程块都会由硬件来进行调度,一个block会被装载到SM上,后面的block就需要进行等待。按照上面的例子,一个block有256个线程,但SM上只有128个线程,怎么进行调度的呢?这个时候就需要利用到warp的概念了。因为SM被划分为了4个组,每一个包含32个计算核心,所以被调度到SM上的block所包含的256个线程会被划分为256/32=8个warp,这8个warp就会被调度到这4个计算核心组上进行计算。
三、线程束分化、避免分化
一个warp当中的线程是并行的,并且是SIMD执行的,所以一个warp当中的线程执行的是相同的指令。但是,如果在kernel中出现了if判断等分支语句的导师一个warp中的线程出现了多种执行路线,导致同一时刻部分线程执行的不是同一个指令怎么办呢?这其实就是线程束分化。
线程束分化其实就是同一个warp中的线程执行了不同的指令。这样会导致一中情况,并行变串行,性能下降。
假设一个warp内的线程前16个1线程执行+1操作,后16个线程执行-1操作,那么就会导致线程束分化。这就会造成前16个线程先去执行+1操作,而后16个线程等待,等到前16个线程执行完毕后,后16个线程执行-1操作,等到后16个线程执行完毕后,整个warp才调度出去。
所以就导致了性能降低,原因是不分县城从并行变为了串行,没有充分使用到整个warp的32个计算核心。
其实解决线程束分化的方法也很简单,就是让处于一个分支的线程出于一个warp中。但具体处理数据需要具体分析,怎样让执行相同分支的在一个warp需要用到不同的方法。
四、循环展开
和之前学习arm neon 时一样,在对循环进行展开时,其实是为了减少检查判断的省略。在汇编语言层面,会将展开的循环语句也按照汇编语言展开,这样就减少了循环的判断和跳转。
五、其他
补充记录一下关于sm。
在jetson nano上面的资源非常有限,该平台上只有一个sm,通过一下代码可以获取到该sm上运行线程和线程块的信息。
int device;
cudaGetDevice(&device);
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, device);
int maxBlocksPerSM = prop.maxThreadsPerMultiProcessor / prop.maxThreadsPerBlock;
printf("maxThreadsPerMultiProcessor:%d\n",prop.maxThreadsPerMultiProcessor);
printf("maxThreadsPerBlock:%d\n",prop.maxThreadsPerBlock);
printf("maxBlocksPerSM:%d\n",maxBlocksPerSM);
可以看到,其实一个sm上能够运行的线程和线程块其实是有限的,在jetson nano上一个sm最多只能装载两个block,即最多有两个block在并行执行,而一个block最多包含1024个线程,所以我们可以在组织线程的时候,可以根据最大限制充分利用gpu进行计算。
当然,影响执行效率的因素还有很多,比如寄存器和共享内存的使用,当核函数内部使用加多寄存器时,或者说使用共享内存使用加多时,都会导致装载到sm上的线程块减少。