SM中的执行资源包括寄存器、共享内存、线程块插槽和线程插槽。这些资源被动态分区并分配给线程,以支持其执行。在第3章,可扩展并行执行中,我们看到Fermi一代设备有1536个线程插槽。这些线程插槽在运行时被分区并分配给线程块。如果每个线程块由512个线程组成,则1536个线程插槽被分区并分配给三个块。在这种情况下,由于线程插槽的限制,每个SM最多可容纳三个线程块。
如果每个线程块包含256个线程,则对1536个线程插槽进行分区并分配给6个线程块。**在线程块之间动态分区线程插槽的能力使SM变得多才多艺。他们可以执行许多线程块,每个线程很少,也可以执行几个线程块,每个线程有很多线程。**这与固定分区方法形成鲜明对比,在这种方法中,每个块都会接收固定数量的资源,无论其实际需求如何。当一个块线程很少,并且无法支持需要比固定分区允许的更多线程插槽的块时,固定分区会导致线程插槽被浪费。
**资源的动态分区可能导致资源限制之间的微妙相互作用,这可能导致资源利用不足。**这种交互可以在块插槽和线程插槽之间发生。例如,如果每个块有128个线程,则1536个线程插槽可以分区并分配给12个块。然而,由于每个SM只有8个区块插槽,因此只允许8个区块。这意味着最终只会使用1024个线程插槽。因此,要充分利用块插槽和线程插槽,每个块中至少需要256个线程。
正如我们在第4章“内存和数据局部性”中提到的,在CUDA kernel中声明的自动变量被放入寄存器中。一些内核可能会使用大量自动变量,而另一些内核可能会使用很少的自动变量。因此,人们应该期望一些内核需要很多寄存器,而一些需要更少的寄存器。通过在块之间动态分区寄存器,如果需要很少的寄存器,SM可以容纳更多的块,如果需要更多的寄存器,则可以容纳更多的块。然而,人们确实需要意识到寄存器限制和其他资源限制之间的潜在相互作用。
在矩阵乘法示例中,假设每个SM有16,384个寄存器,内核代码每个线程使用10个寄存器。如果我们有16×16个线程块,每个SM可以运行多少个线程?我们可以通过首先计算每个块所需的寄存器数量来回答这个问题,即101616 = 2560。六个区块所需的寄存器数量为15,360,低于16,384个限制。添加另一个块需要17,920个寄存器,这超过了限制。因此,寄存器限制允许六个总共有1536个线程的块在每个SM上运行,这也符合8个块插槽和1536个线程插槽的限制。
现在假设程序员在内核中声明了另外两个自动变量,并将每个线程使用的寄存器数量增加到12个。假设相同的16×16块,每个块现在需要121616 = 3072寄存器。六个块所需的寄存器数量现在为18,432个,这超过了一些CUDA硬件的寄存器限制。CUDA运行时系统通过将分配给每个SM的块数量减少一个来处理这种情况,从而将所需的寄存器数量减少到15,360个。然而,这减少了在SM上运行的线程数量从1536个减少到1280个。也就是说,通过使用两个额外的自动变量,该程序看到每个SM的warp并行性减少了1/6。这有时被称为“性能悬崖”,资源使用量的轻微增加可能导致并行性和性能的显著下降[RRS 2008]。
共享内存是在运行时动态分区的另一种资源。**tile算法通常需要大量的共享内存才能有效。**不幸的是,大量共享内存的使用可以减少在SM上运行的线程块数量。正如我们在第5.3节中讨论的,减少线程并行性会对DRAM系统内存访问带宽的利用产生负面影响。减少的内存访问吞吐量反过来会进一步降低线程执行吞吐量。这是一个陷阱,可能会导致tile算法的性能令人失望,应该小心避免。
读者应该清楚,所有动态分区资源的约束以复杂的方式相互作用。准确确定每个SM中运行的线程数量可能很困难。阅读器参考CUDA占用计算器[NVIDIA],这是一个可下载的Excel工作表,根据内核对资源的使用情况,计算每个SM上运行的线程的实际数。