CUDA C++ Programming Guide chapter-four Hardware Implementation

1. Introduction — CUDA C Programming Guide (nvidia.com)

CUDA Runtime API :: CUDA Toolkit Documentation (nvidia.com)

以下的内容主要来自这个页面

1. Introduction — CUDA C Programming Guide (nvidia.com)

NVIDIA-GPU架构基于可缩放的多线程数组,当主机CPU上的CUDA程序调用核函数时,会涉及网格、网格内的线程块,线程块会被分散到还有执行容量的多处理器。在同一个multiprocessor多处理器上,线程块内的线程是同时并发执行的,并且多个线程块也是同时并发执行的。当线程块终止的时候,new线程块会在空出来的multiprocessors上被启动。

一个multiprocessor被设计用来同时执行成百上千个线程。要管理这么多的线程,它应用了SIMT的架构,也就是单指令多线程(single instruction,threads),见

SIMT Architecture 。指令是流程化的,在一个单个线程内是指令级别的并行,以及通过硬件上的多线程同时执行来实现线程级别的并行,见

Hardware Multithreading。不像CPU的多核处理器,每个处理器是按照顺序执行的,也没有分支预测 或者 预测执行。

SIMT Architecture

Hardware Multithreading 描述了streaming multiprocessor在所有设备上的共同架构特点。

Compute Capability 5.x,

Compute Capability 6.x, 和

Compute Capability 7.x 分别描述了计算能力是5.x, 6.x和7.x的设备的特点。

NVIDIA GPU架构使用了little-endian的表达方式。

four.one SIMT Architecture

multiprocessor产生、管理、调度和执行线程,是按照一组一组的方式每一组包括了32个线程,也被称作线程束。32个单独的线程组成了一个线程束,线程束都是从相同的程序地址开始的,也就是执行相同的指令。但是线程束内的线程分别有自己独立地指令地址计数器、寄存器状态,因此可以独立地分支或者执行。线程束也是来自weaving,第一个线程并行技术。一个half-warp是线程束的前一半或者后一半,一个是线程束的前1/(2x2), 2/(2x2), 3/(2x2)或者最后的1/(2x2)。

当一个或者多个线程块在一个multiprocessor上执行的时候,multiprocessor会将这些线程拆分成线程束,线程束在执行的时候被线程束调度器来调度。一个线程块被拆分到线程束的方式总是相同的:每个线程束都包含了continuous的线程,线程的id总是上升的,而且第一个线程束包含了id=0的线程。

Thread Hierarchy描述了线程块内的线程ID映射到线程索引的方式。

一个线程束内的线程会在同一时间执行相同的指令,所以当一个线程束内的所有32个线程都满足execution path时,也就实现了完全的执行效率也就是32个线程同时执行。若一个线程束内的线程被数据依赖的条件分支if分开了,线程束会执行在分支路径上的线程,禁用不在分支路径上的线程,也就是满足这个if条件的线程同时执行,其他不满足条件的线程闲置。分支发散只会发生在线程束内,不同的线程束是独立执行的,而不用管它们是否在执行相同的code路径或者不相交的不同的code路径。

SIMT架构和SIMD的向量形式是类似的,SIMD的向量形式是指一个指令控制多个正在被处理的数据。一个重要的不同点是SIMD的向量形式会告诉软件这个SIMD的width,但是SIMT指定了一个线程的执行和code的分支行为。和SIMD的向量机制比较,SIMT允许编程人员编写线程级别的并行code,这些线程是独立的、标量化的,也能编写数据并行的code,这些线程是按照坐标映射的。考虑到准确性,编程人员可以从根本上忽略SIMT的行为。然而, 减少codes内在一个线程束内的分叉或者路径也就是if判断可以极大地提升性能。 从实践上来看,和传统code上的缓存行角色类似:当为了正确性而设计时可以忽略缓存行的大小,当为了峰值表现而设计时在代码结构内必须考虑缓存行的大小。向量形式的结构,在另一个方面,要求软件合并地导入数据到向量内,并且需要手动地管理分支if。

NVIDIA Volta架构以前的话,一个线程束内的所有32个线程共享地使用同一个单个的程序计数器,并且使用一个mask来指定线程束内活跃的线程,以及忽略或者被禁用的线程。结果是:在同一个线程束内的线程,在分叉的区域,或者处在执行的不同状态,不能给对方发送信号,或者相互之间传输数据,并且要求用锁或者mutex来保护共享数据,很容易导致deadlock。取决于竞争的线程来自哪个线程束。

从NVIDIA Volta架构开始的,独立线程调度允许线程之间的并发,而不用管线程束。在有了独立线程调度以后,GPU维护每个线程的执行状态,包括一个程序计数器和调用栈,并且能在线程尺度上执行,可以更好的利用执行resource,或者允许一个线程等待另一个线程产生的数据。一个调度优化器决定怎么组合同一个线程束内的活跃线程到SIMT unit内。和之前的NVIDIA GPU比较起来的话,也保证了SIMT执行时候的高的吞吐量,但是更加的灵活,现在的线程能在子线程束的粒度上分叉或者聚合。

和之前的硬件架构有关线程束同步的内容比较,独立的线程调度会导致很不同的线程集合来参加执行code。特别的,任何线程束同步的codes(像synchronization-free, intra-warp reductions线程束内部规约)应该确保兼容NVIDIA Volta架构或者之后的架构。更多的细节可见:

Compute Capability 7.x

!Note:

线程束内参与执行当前指令的线程被称作活跃线程,然而不参与执行当前指令的线程被称作不活跃的线程。线程不活跃有着多种多样的原因,包括线程束内提前退出的某些线程,比起线程束内当前正在执行的路径,这些线程采取了另外一个不同的分支路径,或者是最后一个线程块,这个线程块的线程数量不是线程束大小32的整数倍。

若线程束执行了一个不是atomic类型的指令,这个线程束内很多的线程写入到全局内存/共享内存的同个地址,发生在这个地址的序列写入操作数量,取决于设备的计算能力(见:

Compute Capability 5.x,

Compute Capability 6.x, 和

Compute Capability 7.x),没有定义哪个线程是最后写入的,也就是不知道哪个线程是最后一个写入的。

若线程束执行了一个

atomic类型的指令-读取、修改和写入到全局内存中相同的地址,可能会是线程束内的很多个线程。每个读/修改/写入到当前地址都会执行,并且这些操作都是序列化的,但是没有定义执行操作具体执行的顺序。

four.two Hardware Multithreading

通过一个multiprocessor处理的每个线程束执行的上下文(程序计数器、寄存器等等),在线程束的整个生命周期内是在板端维护的。因此,从一个执行上下文切换到另外一个执行上下文是没有消耗的,并且在每个指令执行时,一个线程束调度器会选择已经准备好执行下一个指令的线程束(

active threads),然后将指令分发给线程束内的线程。

特别的,每个multiprocessor有一个32-bit寄存器的集合,这些寄存器会被分配给这些线程束,并且每个multiprocessor的并行数据缓存或者共享内存,会被分配给每个线程块。

一个指定的核函数,在multiprocessor上能分配的和同时处理的线程块和线程束的的数量,取决于核函数需要用到的寄存器和共享内存的数量,以及multiprocessor上寄存器和共享内存的数量,也就是需要的,和能够供应的供需要求。因此每个multiprocessor都有一个最大数量可分配的线程块,和最大数量可分配的线程束。

在multiprocessor可用的寄存器和共享内存数量存在限制,可以通过设备的计算能力函数得知,可见

Compute Capabilities。若任何一个multiprocessor内可用的的寄存器数量和共享内存数量不足以执行一个线程块,核函数启动会fail。

一个线程块内线程束的总量可以这么计算: ceil ( T w size , 1 ) \text{ceil}(\frac{T}{w_{\text{size}}},1) ceil(wsizeT,1) ,T是每个线程块内线程的数量;,默认是32;ceil(x, y)是x乘以y以后做截断。

一个线程块分配到的寄存器的总的数量、分配到的共享内存的总的数量,文档在CUDA Toolkit的CUDA Occupancy Calculator内。

The term warp-synchronous 表示隐式的假设了同一个线程束内的线程在每个指令以后都会同步的codes。

》》也就是要避免线程束内的分支,少用if条件判断语句,减少分支可以提升性能

》》核函数要考虑到multiprocessor的自带resource,寄存器数量和共享内存数量,保证核函数可以正常启动,一个multiprocessor至少要能保证一个线程块可以正常执行,否则启动就会fail掉。

》》线程块内的线程束数量是可以计算的,线程束内线程的个数是固定的32个,线程束内的线程在同一时刻会执行同一个指令,存在分支会出现某些线程执行,某些不执行,线程束内的线程执行顺序不固定,线程束内的线程物理地址都是Continuous的。


https://zhuanlan.zhihu.com/p/664647204

Logo

欢迎来到由智源人工智能研究院发起的Triton中文社区,这里是一个汇聚了AI开发者、数据科学家、机器学习爱好者以及业界专家的活力平台。我们致力于成为业内领先的Triton技术交流与应用分享的殿堂,为推动人工智能技术的普及与深化应用贡献力量。

更多推荐