Streaming MultiProcessor、Register、Shared-Memory对CUDA线程块尺寸的影响

简介

线程块中线程总数的大小除了受到硬件中Max Threads Per block的限制,同时还要受到Streaming Multiprocessor、RegisterShared Memory的影响。这些条件的共同作用下可以获得一个相对更合适的block尺寸。当block尺寸太小时,将无法充分利用所有线程;当block尺寸太大时,如果线程需要的资源总和过多,CUDA将通过强制减少block数量来保证资源供应,同样无法利用所有线程。而grid的尺寸通常越大越好,当grid中的线程总数超过一次所能启动的并发线程总数时,过多的线程将以线程块为单位由CUDA进行新的调用,当然启动数量够用就可以了,以免浪费资源。

但具体最多我们可以定义的grid尺寸是多大跟计算能力倒是关系不大,所以你会发现消费级GTX970与专业级Tesla P4的计算能力分别是5.2和6.1,但可以定义的grid尺寸却相同。在我的测试中,同一段代码,一维网格和block的情况下,Block尺寸都为1024时,GTX1080的grid在win10系统桌面由显卡驱动的情况下,最多可以是78(WIN10系统。而linux下的Tesla P4和GTX970的grid最大值都是680。
然而二维grid和block的时候,block尺寸虽然有1024的限制,但grid尺寸可以大到你的int放不下,当然我们并没有这么多的流处理器在运算,只是过多的block在排着队等着被执行。
而CUDA的调度中为了隐藏内存访问带来的latency,一个block中的线程数量越多越好(确切来说是wrap数量)。

本文需要通过NVIDIA官方提供的一个非常有用的工具C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v7.0\tools\CUDA_Occupancy_Calculator.xls进行辅助计算来获得合适的block大小。这个excle表中的Help表单有详细的使用说明,以下分析将基于这个工具。
可以通过在编译时添加--ptxas-options=-v参数在编译时输出当前程序中核心函数所使用的RegisterShared Memory等大小,或者使用nvcc --resource-usage kernel.cu来获取,注意此处获取到的值只是nvcc在编译期为核函数分配的相应空间,如果通过动态的方式分配过shared memory,则需要单独加上你手动动态分配的shared memory才是总是shared memory。
输出的ptxas info可能包含的信息表示如下:
registers: 寄存器
lmem: local memory 局部内存
smem: shared memory 共享内存
cmem: constant memory 常量内存
其中cmem又可以分以一下一些情况(不全):
cmem[0] kernel arguments
cmem[2] user defined constant objects
cmem[16] compiler generated constants (some of which may correspond to literal constants in the source code)

参考:https://devtalk.nvidia.com/default/topic/493425/ptxas-info-unexplained-what-is-cmem-n-/

显卡信息可以由官方示例中的deviceQuery得出,当然也可以查官方说明。
看着这个表挺复杂,其实只需要记住我们所要设定的线程块尺寸只要能够保证SM满占即可。

SM的限制对block尺寸的影响

Multiprocessor(以下所有的Multiprocessor都是指Streaming Multiprocessor,即SM,具体SM在GPU中的结构组织参见http://notes.maxwi.com/2015/06/11/CUDA-study-notes/基本概念中的图示)中thread数量的限制主要影响工作状态的线程是否能够占满当前的整个Multiprocessor。为了使工作状态的线程能够占满整个Multiprocessorblock中的thread的总数应当不小于Max Threads Per Multiprocessor / Max Thread Blocks Per Multiprocessor,对于该表,也就是使Occupancy of each Multiprocessor100%即可。Max Thread Blocks Per Multiprocessor无法由deviceQuery得到,表中会给出,其实也很容易计算得到Max Threads Per Multiprocessor / 32(因为一个block一次最小也要启动一个wrap,即32个线程),同一计算能力的显卡这些基本信息都是一样的。

下图所示为CUDA_Occupancy_Calculator给出的计算能力为5.0的GPU的参数。
Physical Limits for GPU Compute Capability

此时线程块的中线程数量应当不小于2048/32 = 64。当线程块的数量小于64时,由于每个Multiprocessor中可以含有的最大线程块只有32个,所以此时32个线程块乘以这个小于64的数字必然要小于2048,也就是无论怎样都无法使线程填满整个Multiprocessor,导致SM中会有空闲的Streaming Processor。所以根据要填满SM,则要求每个线程块中至少需要有64个线程。如果将线程块尺寸设置为1024,则此时根据SM总线程的限制2048,每个SM正好可以启动2个线程块,如果我们每个线程块上的资源都够用,那这就是一个最佳尺寸。然而通常资源是最大限制,所以下面借助官方工具来综合考虑资源占用下的线程块尺寸。

注意:1). 当前计算能力5.0的设备每个线程块中线程数量上限Maximum Thread Block Size=1024、2). CUDA中线程组织单位为Wrap,此处Threads per Warp=32,所以线程块中的线程数量应为32的倍数。3). 各维都有大小限制,计算能力5.0的三维分别为1024,1024,64

CUDA_Occupancy_Calculator的使用

下图为CUDA_Occupancy_Calculator计算结果,由于此处只讨论了SM中线程及线程块的限制对block尺寸的影响,所以这里只需要使曲线的红三角在波峰即可。注意第一张图中的1.)、1.b)需要根据自己的硬件情况进行选择;2).需要自己填入的程序参数(可以通过2.2的方法获得),3).工具计算出的GPU使用率,显示此时占用为100%,也就是SM中的活动线程束为64个,即64*32=2048,SM处于满占状态。其他各图是其对应的曲线,由第二幅图可以看出在registershared memory固定的情况下,block尺寸设置为256, 512, 1024时都可以占满SM,block尺寸越小,同一个block中可使用的Shared Memory和每个线程中可使用Register就越大,因为同一个SM中的block共享这些资源。。它们都有以下特点:1). 大于64,满足不小于Max Threads Per Multiprocessor / Max Thread Blocks Per Multiprocessor; 2). 32的整数倍,满足线程束的最小单位Wrap; 3). 总线程数可以被2048整除,因为Max Threads Per Multiprocessor=2048
CUDA Occupancy Calculator
Impact of Varying Block Size
Impact of Varying Shared Memory Usage Per Block
Impact of Varying Register Count Per Thread

Register对block尺寸的影响

计算能力为5.0的设备Registers per Multiprocessor=65536,Max Registers per Thread=256。也就是说一个SM总共也就只有65536个register,一个thread最多能定义256大小的register,根据上图可以看出,显然当Register Per Thread大于32时性能就要开始降低了。因为当前情况下SM中的所有线程都被占满了(此处占满意思是所有线程都为活动线程),也就是说在这种block参数配置下,一个SM最多可以启动2048个线程(注意是最多可以,并不是说一定要,比如我一共就1000个数据,当然就启动1000个线程就可以了),由于SM中能使用的register最多只有65536,当SM中的资源不够用时,SM就会强制减少block,所以Register Per Thread应该不大于Registers per Multiprocessor / Max Threads per Multiprocessor,也就是65536 / 2048 = 32,如何降低register占用是个很难的调整阶段,只能慢慢多调

Shared Memory对block尺寸的影响

计算能力为5.0的设备Shared Memory per Multiprocessor (bytes)=65536,Max Shared Memory per Block=49152。也就是说block中的smem(Shared Memory)必须要小于49152,要想使得SM中的线程全部占满,那么整个SM中占用的smem必须小于65536。由于smem是以block为单位进行分配,所以当smem不够用时也就会以block为单位进行减少线程。所以Shared Memory Per Block (bytes)应该不大于Shared Memory per Multiprocessor / Active Thread Blocks per Multiprocessor = Shared Memory per Multiprocessor / (Max Threads per Multiprocessor / Threads Per Block),当block尺寸设置为256时,Shared Memory Per Block (bytes)最大值为65536 / (2048/256) = 8192,单位是bytes。

总结

block最佳尺寸应该满足:
1). 不小于Max Threads Per Multiprocessor / Max Thread Blocks Per Multiprocessor
2). 32的整数倍;
3). 可以被Max Threads Per Multiprocessor整除。
4). Register Per Thread应该不大于Registers per Multiprocessor / Max Threads per Multiprocessor,否则根据CUDA_Occupancy_Calculator.xls参考调节block尺寸以获得最佳性能。

5). Shared Memory Per Block (bytes)最大值应该不大于Shared Memory per Multiprocessor / (Max Threads per Multiprocessor / Threads Per Block),否则根据CUDA_Occupancy_Calculator.xls参考调节block尺寸以获得最佳性能。
一句话总结:保证每个SM中可以启动的线程总数达到最大值的情况下block中的线程数越大越好。

如果原理都懂了,也可以直接使用CUDA 6.5之后带的一个API,自动帮你计算Grid尺寸和block尺寸:cudaOccupancyMaxPotentialBlockSize(),该API在cuda_runtime.h头文件中,必须采用NVCC编译才能使用,其定义如下:

1
2
3
4
5
6
7
8
9
10
template<class T>
__inline__ __host__ CUDART_DEVICE cudaError_t cudaOccupancyMaxPotentialBlockSize(
int *minGridSize,
int *blockSize,
T func,
size_t dynamicSMemSize = 0,
int blockSizeLimit = 0)
{
return cudaOccupancyMaxPotentialBlockSizeVariableSMem(minGridSize, blockSize, func, __cudaOccupancyB2DHelper(dynamicSMemSize), blockSizeLimit);
}

参数意义:

1
2
3
4
minGridSize     = Suggested min grid size to achieve a full machine launch.
blockSize = Suggested block size to achieve maximum occupancy.
func = Kernel function.
dynamicSMemSize = Size of dynamically allocated shared memory. Of course, it is known at runtime before any kernel launch. The size of the statically allocated shared memory is not needed as it is inferred by the properties of func.

参考:https://stackoverflow.com/questions/9985912/how-do-i-choose-grid-and-block-dimensions-for-cuda-kernels

注意:

  1. 没有标明单位的量其单位都为个,例如register。
  2. CUDA_Occupancy_Calculator给出了registershared memory的分配单位,精确优化时应该非常有用。
    Register allocation unit size 256
    Register allocation granularity warp
    Shared Memory allocation unit size 256
    Warp allocation granularity 4