NVIDIA GPU 架构通过多线程流式多处理器 (SM: Streaming Multiprocessors) 可扩展阵列构建。当主机 CPU 上的 CUDA 程序调用内核网格时,网格的块被枚举并分发到具有可用执行能力的多处理器。一个线程块的线程在一个 SM 上同时执行,同时多个线程块也可以在一个 SM 上同时执行。当线程块终止时,新块在空出的 SM 上启动。
SM 旨在同时执行数百个线程。为了管理如此大量的线程,SM 采用了一种称为 SIMT(Single-Instruction, Multiple-Thread: 单指令多线程)的独特架构,在 SIMT 架构中进行了描述。这些指令流水线运行,并利用了单个线程内的指令级并行性,以及通过并行硬件多线程实现的广泛的线程级并行,如硬件多线程中详述。与 CPU 内核不同,SM 按顺序发射,没有分支预测或推测执行。
SIMT 架构 和 硬件多线程 描述了所有设备通用的流式多处理器的架构特性。 Compute Capability 3.x、Compute Capability 5.x、Compute Capability 6.x 和 Compute Capability 7.x 分别提供了计算能力 3.x、5.x、6.x 和 7.x 的设备的详细信息。
NVIDIA GPU 架构使用小端模式。
SM 以 32 个并行线程组(称为 Warp)的形式来创建、管理、调度和执行线程。组成 Warp的每个线程从同一程序地址一起开始,但每个线程都有自己的指令地址计数器和寄存器状态,因此可以自由地分支并独立执行。Warp一词源于 weaving,这是第一种线程并行技术。half-warp 表示前半部分或后半部分的 Warp。quarter-warp 表示第一、第二、第三或第四个四分之一 Warp。
当一个 SM 执行一个或多个线程块时,它将线程块拆分为 Warp,每个 Warp 都由 Warp 调度程序来调度执行。每个块被分割成 Warp 的方式是一样的;每个 warp 包含连续的线程,这些线程 ID 是不断增加的,第一个 Warp包含线程0。线程层次结构 描述了线程 ID 如何对应到块中的线程索引。
一个 Warp 一次执行一条公共指令,因此当一个 Warp 中的 32 个线程都执行相同的执行路径时,Warp 效率才完全发挥出来。如果 Warp 中的线程由于数据依赖而发生条件分支发散,则 warp 会执行每个需要的分支路径,同时禁用不在该路径执行的线程。分支发散只发生在一个 Warp 内;不同 Warp 之间独立执行,无论是执行带有分叉的还是不带有分叉的代码路径。
SIMT 体系结构类似于 SIMD(单指令多数据)向量组成 (Organization),其中由单指令控制多个处理元素。一个关键区别是 SIMD 向量组成对软件公开了 SIMD 宽度,而 SIMT 指令则可以指定单个线程的执行和分支行为。与 SIMD 向量机相比,SIMT 使程序员能够为独立的标量线程编写线程级并行代码,以及为协调线程编写数据并行代码。为了正确起见,程序员基本上可以忽略 SIMT 行为;但是,通过减少 Warp 中的线程发散,可以显著提升性能。在实践中,这类似于传统代码中 Cache Line 的作用:对于程序正确性来说,可以安全地忽略 Cache Line 的大小,但在考虑峰值性能时,必须将其考虑到代码结构中。另一方面,向量架构需要软件将负载合并到向量中并手动管理分支发散。
在 Volta 之前,Warp 使用了一个在 Wrap 中 32 个线程之间共享的程序计数器,并使用可一个在 Warp 中指定活动线程的活动掩码。结果,来自不同区域或不同执行状态的 Warp 中的线程不能相互发送信号或交换数据,同时需要由锁或互斥锁保护的细粒度数据共享的算法很容易导致死锁,死锁取决于竞争线程来自哪个 Warp。
从 Volta 架构开始,独立线程调度 (Independent Thread Scheduling) 允许线程之间的完全并发,而不管 Warp。使用独立线程调度时,GPU 会维护每个线程的执行状态,包括程序计数器和调用堆栈,并可以在线程粒度上进行执行,从而更好地利用执行资源,或允许线程等待数据产生。由调度优化器决定如何将来自同一个 Warp 的活动线程组合成 SIMT 单元。这保留了与先前 NVIDIA GPU 一样的 SIMT 高吞吐量执行,同时提升了灵活性:线程现在可以在 sub-warp 的粒度上进行分散和重新聚合。
如果开发人员是基于先前硬件架构的
注意:
执行当前指令的 Warp 中的线程称为活动线程,而未执行当前指令的线程是非活动的(禁用)。线程可能由于多种原因而处于非活动状态,包括比 warp 中的其他线程更早退出、执行与 warp 不同的分支路径、或线程数不是 Warp 大小的倍数的块中的最后一些线程。
如果 Warp 执行了一个非原子指令,该指令是让 Warp 中的多个线程写入全局或共享内存中的同一位置,则该位置发生的序列化写入次数取决于设备的计算能力(参见 Compute Capability 3.x、Compute Capability 5.x、Compute Capability 6.x 和 Compute Capability 7.x),哪个线程会最后写入是不确定的。
如果 Warp 执行了一个原子指令,该指令是让 Warp 中的多个线程读取、修改和写入全局内存中的同一位置,则对该位置的每次读取/修改/写入都会发生并且它们都被序列化,但是它们发生的顺序是不确定的。
SM 执行的每个 Warp 的执行上下文(程序计数器、寄存器等)在 Warp 的整个生命周期内都保存在芯片上。因此,从一个执行上下文切换到另一个执行上下文是没有切换成本的,并且在每个指令发出时,Warp 调度程序都会选择一个线程已经准备好执行下一条指令 (Warp 的活动线程) 的 Warp,并将指令发射给 Warp 中的这些线程.
特别是,每个 SM 都有一组32位寄存器,这些寄存器被划分到不同的 Warp 之间,同时并行数据缓存 (parallel data cache) 和共享内存 (shared memory) 被划分到不同的线程块之间.
对于给定内核,可以在 SM 上驻留和处理的块和 Warp 的数量取决于内核使用的寄存器和共享内存的数量以及 SM 上可用的寄存器和共享内存的数量。每个 SM 也有驻留块的最大数量和驻留 Warp 的最大数量。这些限制以及 SM 上可用的寄存器数量和共享内存是设备计算能力的函数,在附录计算能力中给出。如果每个 SM 中的寄存器或共享内存数量无法支撑至少一个块的运行,则内核将无法启动。
一个块中的warp总数如下: $$ ceil(\frac{T}{W_{size}},1) $$
- T 是每个块中的线程数
-
$W_{size}$ 是 Warp 的大小,默认为32 -
$ceil(x,y)$ 是 x 四舍五入到 y 的整数倍。
为每个块分配的寄存器总数以及共享内存总量记录在 CUDA 工具包提供的 CUDA Occupancy Calculator中。