CUDA C++编程指南(4)——硬件实现
AI-安全-功耗 CUBE 博客目录导读
目录
4.1. SIMT架构
4.2. 硬件多线程
NVIDIA GPU架构围绕可扩展的多线程流式多处理器(SMs)阵列构建。当主机CPU上的CUDA程序调用内核网格时,网格的块会被枚举并分配给具有可用执行能力的多处理器。线程块的线程在一个多处理器上并发执行,多个线程块可以在一个多处理器上同时执行。当线程块终止时,新的块会在空闲的多处理器上启动。
多处理器设计用于同时执行数百个线程。为了管理如此大量的线程,它采用了一种独特的架构称为SIMT(单指令多线程)。指令通过流水线执行,既利用了单个线程内的指令级并行性,也通过硬件多线程中详述的同步硬件多线程实现了广泛的线程级并行性。与CPU核心不同,这些指令是按顺序发出的,且没有分支预测或推测执行。
SIMT架构和硬件多线程描述了流式多处理器对所有设备通用的架构特性。
NVIDIA GPU架构采用小端字节序表示。
4.1. SIMT架构
多处理器以32个并行线程为一组创建、管理、调度和执行线程,这种线程组称为warp。构成warp的各个线程从相同的程序地址开始执行,但它们拥有独立的指令地址计数器和寄存器状态,因此可以自由分支并独立执行。术语warp源自纺织技术,这是最早的并行线程技术。half-warp指的是warp的前半部分或后半部分。quarter-warp则指warp的第一、第二、第三或第四部分。
当一个多处理器被分配一个或多个线程块执行时,它会将这些线程块划分为多个warp,每个warp由warp调度器安排执行。线程块被划分为warp的方式始终相同:每个warp包含连续递增的线程ID,第一个warp包含线程0。Thread Hierarchy描述了线程ID如何与块中的线程索引相关联。
一个warp每次执行一条公共指令,因此当warp中的所有32个线程的执行路径一致时,才能实现完全效率。如果warp中的线程通过数据相关的条件分支发生分歧,则该warp会执行每个被采用的分支路径,同时禁用不在该路径上的线程。分支分歧仅发生在warp内部;不同的warp相互独立执行,无论它们执行的是相同还是不同的代码路径。
SIMT架构类似于SIMD(单指令多数据)向量组织,因为单个指令控制多个处理单元。一个关键区别在于,SIMD向量组织向软件暴露了SIMD宽度,而SIMT指令规定了单个线程的执行和分支行为。与SIMD向量机相比,SIMT使程序员能够为独立的标量线程编写线程级并行代码,也能为协作线程编写数据并行代码。就正确性而言,程序员基本上可以忽略SIMT行为;但通过确保代码很少需要让线程束中的线程分叉,可以实现显著的性能提升。实际上,这类似于传统代码中缓存行的作用:在设计正确性时可以安全地忽略缓存行大小,但在设计峰值性能时必须考虑代码结构。另一方面,向量架构要求软件将数据负载整合为向量,并手动管理数据发散。
在NVIDIA Volta架构之前,warp使用一个由warp内所有32个线程共享的程序计数器,以及一个指定warp中活动线程的活动掩码。因此,来自同一warp但处于不同分支区域或不同执行状态的线程无法相互发送信号或交换数据,而需要基于锁或互斥量进行细粒度数据共享的算法很容易导致死锁,具体取决于竞争线程来自哪个warp。
从NVIDIA Volta架构开始,独立线程调度允许线程之间实现完全并发,不受束(warp)的限制。通过独立线程调度,GPU能够维护每个线程的执行状态,包括程序计数器和调用栈,并且可以以线程级粒度暂停执行,这既有助于更好地利用执行资源,也能让一个线程等待另一个线程生成数据。调度优化器会决定如何将同一束中的活动线程分组到SIMT单元中。这保留了与之前NVIDIA GPU相同的SIMT执行高吞吐量特性,但提供了更大的灵活性:现在线程可以在子束粒度上分叉并重新汇合。
如果开发者基于之前硬件架构的warp同步性做出假设,独立线程调度可能导致实际参与执行的线程组与预期存在较大差异。特别是,所有warp同步代码(例如无需同步的warp内部规约操作)都应重新检查,以确保与NVIDIA Volta及后续架构的兼容性。
术语“warp同步”指的是隐式假设同一warp中的线程在每条指令中都保持同步的代码。
【注意】
参与当前指令的warp线程被称为active线程,而未执行当前指令的线程则被称为非活跃线程(已禁用)inactive (disabled)。线程可能因多种原因处于非活跃状态,包括比其他同warp线程提前退出、选择了与warp当前执行路径不同的分支路径,或是属于线程数不是warp大小整数倍的块中的末尾线程。
如果一个线程束执行的非原子指令向全局内存或共享内存中的同一位置写入,且该线程束中有多个线程执行此操作,那么该位置发生的序列化写入次数会根据设备的计算能力而变化,并且最终由哪个线程执行写入操作是未定义的。
如果一个由线程束执行的原子指令对全局内存中同一位置进行读取、修改和写入,且涉及该线程束中多个线程,那么对该位置的每次读取/修改/写入都会发生,并且它们都是串行化的,但发生的顺序是未定义的。
4.2. 硬件多线程
多处理器处理的每个warp的执行上下文(程序计数器、寄存器等)在warp的整个生命周期内都保持在芯片上。因此,从一个执行上下文切换到另一个执行上下文没有开销,并且在每条指令发出时,warp调度器会选择一个准备好执行下一条指令的warp(该warp的active threads),并向这些线程发出指令。
具体来说,每个多处理器都拥有一组32位寄存器,这些寄存器在warp之间进行分配,同时还有一个并行数据缓存或共享内存,在线程块之间进行分配。
对于给定的内核,可以在多处理器上同时驻留和处理的计算块和线程束数量取决于该内核使用的寄存器数量、共享内存大小,以及多处理器上可用的寄存器数量和共享内存容量。此外,每个多处理器还存在最大驻留块数量和最大驻留线程束数量的限制。这些限制以及多处理器上可用的寄存器数量和共享内存容量取决于设备的计算能力,具体数值详见计算能力章节。如果多处理器上没有足够的寄存器或共享内存来至少处理一个计算块,则该内核将无法启动。
一个块中的warp总数量如下:

-
T 是每个线程块中的线程数量,
-
Wsize 是线程束大小,等于32,
-
ceil(x, y) 等于将x向上取整到最接近y的倍数。
每个块分配的寄存器总数和共享内存总量记录在CUDA工具包提供的CUDA占用率计算器中。
