注:新手文章,欢迎指正。
cuda编程的时候,占用率(Occupacy)是流处理器(Streaming multiprocessors)中活跃线程束(active warp)数和最大活跃线程束的比值。gpu在内部执行的时候,block对应SM(Streaming multiprocessors),一个SM可以同时并行执行多个block,而一个block只能在一个SM中执行,如juejin.cn/post/731227… 所述:
与 block 对应的硬件级别为 SM,SM 为同一个 block 中的线程提供通信和同步等所需的硬件资源,跨 SM 不支持对应的通信,所以一个 block 中的所有线程都是执行在同一个 SM 上的,而且因为线程之间可能同步,所以一旦 block 开始在 SM 上执行,block 中的所有线程同时在同一个 SM 中执行(并发,不是并行),也就是说 block 调度到 SM 的过程是原子的。SM 允许多于一个 block 在其上并发执行,如果一个 SM 空闲的资源满足一个 block 的执行,那么这个 block 就可以被立即调度到该 SM 上执行,具体的硬件资源一般包括寄存器、shared memory、以及各种调度相关的资源。
如《CUDA C编程权威指南》所述:
当启动一个内核网格时,它的线程块被分布在了可用的SM上来执行。线程块一旦被调度到一个SM上,其中的线程只会在那个指定的SM上并发执行。多个线程块可能会被分配到同一个SM上,而且是根据SM资源的可用性 进行调度的。每个SM都将分配给它的线程块划分到包含32个线程的线程束中,然后在可用的硬件资源上调度执行。一个线程块只能在一个SM上被调度。一旦线程块在一个SM上被调度,就会保存在该 SM上直到执行完成。在同一时间,一个SM可以容纳多个线程块。
- CUDA将SM中的计算资源在该SM中的多个常驻线程块之间进行分配。这种分配形式 导致一些资源成为了性能限制者。
之所以希望占用率保持较高的值,是为了隐藏每个指令的延迟,指令的延迟指的从执行一个指令开始,到指令执行结束,所需要的时间。
如上图所示,在指令延迟期间通过在其他常驻线程束中发布其他指令,可以隐藏每个指令的延迟。
占用率并非越高越好,但是一般而言都希望其保持较高的值。通过block size和grid size,可以很大程度上的影响占用率,这也就影响了GPU的利用率。
通过block size和grid size来计算理论占用率的因素有三点:
- 每个SM中最大的warps数和最大blocks数(The hardware groups threads that execute the same instruction into warps. Several warps constitute a thread block. Several thread blocks are assigned to a Streaming Multiprocessor (SM). Several SM constitute the whole GPU unit (which executes the whole Kernel Grid) 来源:维基百科)。目前而言,对于所有的N卡,一个warp都由32个线程构成。
- 每个SM中的寄存器数量。
- 每个SM中的共享内存大小。
以上三点因素对于GPU的占用率形成木桶效应。在实际计算的时候,计算每个SM中最多可以执行多少个blocks,再将blocks换算成warp数,从而得到活跃线程束数。
xmartlabs.github.io/cuda-calcul… 提供了计算理论GPU占用率的程序,下面以该网页的一个例子为例,举例详细说明如何根据以上三个因素计算理论GPU占用率。
选定CUDA version和计算能力等影响硬件能力的因素后,剩下的可选项就是block size(此处为256)、每个线程需要的寄存器数量(此处为32)、每个block需要的共享内存大小(此处为2048 bytes)。
-
首先考虑第一个影响因素,即每个SM中最大的warps数和最大blocks数:从Threads per Multiprocessor可以看出,该硬件每个SM最多可以执行1536个线程,也就是1536/256=6个blocks(注意block size为256)。这对应了图中Limited by Max Warps / Blocks per Multiprocessor=6。
-
再考虑第二个因素,即每个SM中的寄存器数量:每个block需要256×32=8192个寄存器,注意到Total # of 32-bit registers per Multiprocessor=65536,因此寄存器因素下的最大block数为65536/8192=8个。
-
最后考虑每个SM中的共享内存大小:每个block需要2048+1024=3072 bytes的共享内存(Note: CUDA Runtime uses 1024 bytes of Shared Memory per Thread Block.),图中Shared Memory per Multiprocessor (bytes)=102400,因此该因素下每个SM的最大block数为=33,这对应了图中的 Limited by Shared Memory per Multiprocessor=33,而Shared Memory Allocation unit size代表其分配单位为128 bytes, 由于3072=128*24,因此该因素对此案例无影响。
最后根据木桶效应,取三个因素之中的最小值6作为最后结果,即Active Thread Blocks per Multiprocessor=6,而block size/32=256/32=8 warps,因此最终每个SM执行了6×8=48 warps,注意Warps per Multiprocessor=48,因此Occupancy of each Multiprocessor=48/48=1,为100%。
Active Warps == Stalled Warps + Eligible Warps + Selected Warps
增加active warps可以增加warp scheduler可以调度的warp数,从而更好的实现访存-计算重叠。强烈建议仔细阅读下面链接的文章(不过里面的Active Warps公式是错的,可以看他的图): docs.nvidia.com/gameworks/c…
ncu profile:
上图的ncu profile右列显示该kernel因为shared mem的原因,每个sm只能跑一个block,由于block size为256,即8个warp,而h800 gpu的sm可以容纳64个active warp,因此理论占有率为12.5%。
以下准则摘录于《CUDA C编程权威指南》:
- 保持每个块中线程数量是线程束大小(32)的倍数
- 避免块太小:每个块至少要有128或256个线程
- 根据内核资源的需求调整块大小
- 块的数量要远远多于SM的数量,从而在设备中可以显示有足够的并行
- 通过实验得到最佳执行配置和资源使用情况
参考: