在实际编写调用CUDA的内核函数中,对线程块和网格的划分往往是十分困难的,不同的划分方法经常会造成很大的性能差异,其划分与具体的硬件以及算法kernel的实现有很大关系,属于性能调优中一个比较重要的环节。
线程块划分
线程块划分其实质就是划分每个线程块包含的线程数量,以及线程块数量,与硬件有很大关系。
首先看一个线程块包含多少个线程合适,《CUDA中的一些基本概念》文章中了解到一些基本的GPU硬件知识,再次搬出下图:
GPU运行的线程的最小单位就是线程束,线程束数目代表的是一个SM内的硬件core的数量即一个SM的实际并发的线程数。
一个线程块只能在一个SM进行调度,不可能存在一个线程块在多个SM上同时执行的场景,这是由硬件调度决定的,后面再介绍为什么会设计这样的调度策略。在理想状态下,或当做指令只需要一次访问内存,然后将获取到的指令广播到这个线程束中的所有硬件core中即SP。这种模式比CPU模式更加高效,CPU是通过获取单独的执行流来支持任务级的并行。
既然一个线程块只能在一个SM上执行,线程块中的线程数量应该尽可能大的发挥出全部硬件特性,所以线程块中的线程数量划分第一个原则:一个线程块中的线程数量应该线程束的整数倍,如果不是整数倍,最后SM调度中,将会有core处于闲置状态,从而浪费性能。
为了减少内存读取延迟造成的GPU失速问题,一个SM应该尽可能多的 线程束即线程数量,这样才能用较多的指令来隐藏内存延迟问题,尽最大可能提高GPU利用率。一般在程序中, 192是线程块包含的最少的线程数目。
一个SM中的线程数量并不是无限大,每个SM最多能处理的线程数量都是有限制的,在费米架构硬件上,每个SM每次最多能执行1536个线程,而在G80硬件上,只能执行768个线程。
在实际设置线程块包含的线程数目是,往往并不是设置成线程束的整数倍,实际运行就能够调用最大core数量,还受到编写的kernel函数使用的硬件资源数目,尤其是寄存器数目。每个架构的GPU硬件,每个SM内的寄存器都是有一定数量的限制虽然与CPU相比,GPU的寄存器较多但也不是无限多,如果kernel中的寄存器使用数量超过SM数量,则即使按照线程束的整数倍进行设置,运行过程中也不可能调用最大core数量,因为硬件资源受到限制,比如一个SM的线程束目为32,但是实际kernel使用硬件资源过多,SM调度过程中可能一次性只能并发执行30个或者更少,剩余的线程在运行完成之后才会被调度到,实际运行效果将会比较差,这时就要优化kernel函数,尽可能使用少的硬件资源使能够调用到32个线程并发,或者将该kernel进一步拆分成两步进行计算。
网格划分
一个内核函数的网格划分,其实质就是划分成若干个线程块,每个线程块都可以是一维、二维或者三维,划分网格即线程块时尽量要做到线程与内存一一映射,这样才能避免同步以及内存不一致性问题。
如上所图为一个常见的二维数据划分的网格,每个X轴和Y轴都网格都包含若干个线程块,每个线程块在X轴或Y轴都包含若干个线程。
以一个32*16维的数组大小为例,共有32*16=512个元素,根据设计原则尽量将数据与线程做到一一对应,每个线程处于一个元素数据,共需要512个线程。假设使用的GPU的线程束大小为32,线程块包含的线程数目应该为线程束的整数倍,此时选择线程块中的线程数目为128个(当然实际硬件中可能一个线程块就可以搞定),需要的线程块数目为4个,剩下的工作就是网格的划分,将相应的数据划分到相应的数据块中,网格的划分有多种形式,可是是条纹形状,也可以为方框形状。
第一种网格划分方法,如下图所示:
数组每行共32个元素,每个元素大小为一个字节, 按照列划分线程块,每行可以分成四等分量,在每行中每块划分成8个线程,上幅图中按照列划分成分。该划分方法虽然线程0~7之前在访问内存时可以进行内存合并,一次性最多只能合并8个元素,在线程7和8直接访问的内存不连续无法进行内存合并,整个线程块可以将内存访问合并成16次。但是在英伟达GPU中,如果内存最大按照256个字节进行访问,显然该该方法只能合并成64个字节,并不能充分发挥其特性,在内存存储效率并不是很高。一般在网格划分中并不建议按照列划分,很容易造成内存访问不连续,使程序的性能指数级地下降。
进一步对网格划分进行优化,按照行划分线程块:
将32*16数据按照行划分,在同一个线程0~31中访问的内存都是连续的,其内存完全可以合并成一个读内存操作,相比较列划分方案,其内存读取效率要成倍提高。
第三种分法,线程块中即包含部分列也包含部分行:
第三种分法在有些GPU内存是按照128个字节分配,其内存合并效率与第二种方法一样,如果是有些GPU内存是按照256个字节读取则内存合并效率并没有第二种方法高。如果数据量为64*16,则第三种分支也是可以和第二种分发有相同的效率。
网格划分的两个原则:内存和线程最好建立一一映射,内存合并效率要尽量最高