虽然CUDA寄存器和共享内存在减少对全局内存的访问次数方面非常有效,但必须注意保持在这些内存的容量范围内。这些内存是线程执行所需的资源形式。每个CUDA设备提供有限的资源,从而限制了给定应用程序可以同时驻留在SM中的线程数量。通常,每个线程需要的资源越多,每个SM中可以驻留的线程就越少,同样,在整个设备中并行运行的线程也就越少。
为了说明内核的寄存器使用与设备可以支持的并行性水平之间的交互,假设在当前一代设备D中,每个SM可以容纳多达1536个线程和16,384个寄存器。虽然16,384是一个大数字,但考虑到每个SM中可以驻留的线程数量,每个线程只允许使用非常有限的寄存器。为了支持1536个线程,每个线程只能使用16,384/1536 = 10个寄存器。如果每个线程使用11个寄存器,则可以在每个SM中同时执行的线程数量将减少。这种减少发生在块粒度上;例如,如果每个块包含512个线程,则通过一次减少512个线程来实现线程的减少。因此,从1536开始,下一个较小的线程数量将是1024,这表明可以同时驻留在每个SM中的线程减少1/3。该程序可以大幅减少可用于调度的warp数量,从而降低处理器在存在长延迟操作的情况下找到有用工作的能力。
每个SM可用的寄存器数量因设备而异。应用程序可以动态确定所用设备的每个SM中可用的寄存器数量,并选择使用适合该设备的寄存器数量的内核版本。寄存器的数量可以通过调用cudaGetDeviceProperties函数来确定,该函数在第3.6节中进行了讨论。假设变量&dev_prop传递给设备属性的函数,字段dev_prop.regsPerBlock生成每个SM中可用的寄存器数量。对于设备D,此字段的返回值应为16,384。然后,应用程序可以将这个数字除以驻留在每个SM中的目标线程数,以确定可以在kerel中使用的寄存器数量。
共享内存使用也可以限制分配给每个SM的线程数量。我们可以假设同一设备D有16,384(16K)字节的共享内存,在每个SM中分配给线程块。我们也可以假设D中的每个SM最多可容纳8个block。要达到这个最大值,每个块不得使用超过2K字节的共享内存;否则,每个SM中可以驻留的块数量将减少,使这些块使用的共享内存总量不超过10K字节。例如,如果每个块使用5K字节的共享内存,则每个SM的分配不能超过三个块。
对于矩阵乘法示例,共享内存可以成为限制因素。对于16×16的tile尺寸,每个块需要16×16×4=1K字节的存储Mds。(请注意,每个元素都是foat类型,为4字节。)Nds还需要1KB。因此,每个块使用2K字节的共享内存。16K字节的共享内存允许8个块同时驻留在SM中。由于这与线程硬件允许的最大值相同,共享内存不是此tile大小的限制因素。在这种情况下,真正的限制是线程硬件限制,每个SM只允许1536个线程。此约束将每个SM中的块数限制为六个。因此,将仅使用6*2KB=12KB的共享内存。这些限制从一个设备到另一个设备,但可以在运行时通过设备查询确定。
每个SM中共享内存的大小也可能因设备而异。每一代或型号的设备在每个SM中可以有不同数量的共享内存。内核通常希望能够根据硬件中的可用量使用不同数量的共享内存。我们可能想要一个主机代码来动态确定共享内存的大小,并调整内核使用的共享内存量,这可以通过调用cuda-GetDeviceProperties函数来完成。我们假设变量&dev_prop传递给函数,而字段dev_prop.sharedMemPerBlock gqives每个SM中可用的寄存器数量。然后,程序员可以确定每个块应该使用的共享内存量。
不幸的是,图4.16中的内核不支持这一点。图4.16 中使用的声明。将其共享内存使用大小硬连接到编译时常数:
__shared__ float Mds[TILE_WIDTH][TILE_WIDTH];
__shared__ float Nds[TILE_WIDTH][TILE_WIDTH];
也就是说,Mds和Nds的大小设置为TILE_WIDTH2元素,无论编译时TILE_WIDTH的值如何。为了说明,假设该文件包含
#define TILE_WIDTH 16.
Mds和Nds都将有256个元素。如果我们想更改Mds和Nds的大小,我们更改TILE_WIDTH的值并重新编译代码。如果不重新编译,内核无法在运行时轻松调整其共享内存使用情况。
我们可以在CUDA中通过不同风格的声明来启用这种调整。我们可以在共享内存声明前面添加一个“C extern”关键字,并在声明中省略数组的大小。以这种方式,Mds和Nds的声明读作
extern __shared__ Mds[];
extern __shared__ Nds[];
请注意,数组现在是一维的。我们需要使用基于垂直和水平索引的线性索引。
当我们启动内核时,我们可以根据设备查询结果动态确定要使用的共享内存量,并将其作为第三个配置参数提供给内核启动。修订后的内核可以通过以下语句启动:
其中size_tis是用于声明变量的内置类型,用于保存动态分配数据结构的大小信息。大小以字节表示。在我们的矩阵乘法示例中, 16 × 16 tile,我们的大小为16 × 16 x 4=1024字节。省略了在运行时设置大小值的计算细节。