1. GPU Resource Management
GPU channel是GPU与CPU之间的桥接接口,通过CPU向GPU发送GPU指令的唯一通道,GPU channel包含了两类用于存储GPU指令的buffer:
- GPU command buffer (也称之为FIFO push buffer)
- Ring buffer (也称之为indirect buffer),从上图中看出,这个buffer是环形结构的,即其容量是固定的,这也是为什么叫Ring buffer的原因吧
当GPU指令被写入到GPU command buffer时,系统还会向Ring buffer中写入与此指令所对应的packet,packet包含了此指令在GPU command buffer中的偏移位置与长度数据。
在执行指令的时候,GPU不是直接从GPU command buffer中读取数据,而是先经过Ring buffer读取出当前待处理指令的相关信息,再据此读取GPU command(这也是为什么Ring buffer被称之为indirect buffer的原因)。
2. Warp/Wavefront
现代GPU为了加强数据的并行化处理的强度,使用的是SIMT(Single Instruction Multi Thread,SIMD的更高级版本)体系结构,shader program运行的最小单位是thread,多个运行相同shader的threads会被打包到一个组(thread group),这个thread group,在NVIDIA被称之为warp,在AMD中被称之为wavefront。
3. NVidia Turing GPU架构
上面这张图是从标题链接给出的Turing白皮书中截取的GPU架构图,其中包含如下几个关键缩写:
- GPC(Graphics Processor Cluster):每个GPU会包含多个GPCs,每个GPC对应于一个独有的Raster Engine,多个TPC(Texture Processor Cluster)。
- TPC(Texture Processor Cluster):每个TPC对应于一个PolyMorph Engine(负责vertex fetching, viewport transform, 以及tessellation等工作),以及多个(图中是两个)SM(Streaming Multiprocessor)。
- SM(Streaming Multiprocessor):SM是GPU中负责完成实际的计算过程的功能单元,是一种通用的处理器组件(即可以用于VS/GS/PS等的计算,这是从Tesla架构就开启的特性,可以用于平衡多种Shader之间的计算消耗,避免了专属Processor的浪费),与CPU不同,SM支持指令级别的并行处理,但是却不支持各线程独立的branch/loop操作。如下图所示,每个都有自己独有的控制单元(Warp Scheduler,Dispatch等),寄存器组件,执行管线以及共享缓存等。每个SM被分成多个处理块(Processing Block),每个Block中包含一个独立的Warp Scheduler,一个独立的Dispatch Unit,一个一定大小的Register File,一个独立的L0 Instruction Cache(图中未展示),多个INT32处理单元,多个FP32处理单元,以及多个TENSOR CORES单元。
- Warp Scheduler:实现Warp的快速切换,包括上下文切换,运行指令的切换等。每个SM包含了多个Warp(线程组),每个线程处理一个Vertex/Pixel,每个Warp当前执行的指令通过Warp Scheduler来指定,Warp中的所有线程按照lock-step方式执行,即所有线程同一时刻执行的是完全相同的指令。当某个Warp处于阻塞状态(比如I/O或者Texture RW等)时,此时会通过Scheduler完成Warp之间的切换,避免GPU浪费。每个Warp都需要用到一定的寄存器来进行数据的存储,当Warp所需的寄存器数目较多,在SM中总共的寄存器数目固定的情况下,SM所能同时支持的Warp的数目就会受到限制。
- Dispatch Unit:Instruction派发单元
- Registers File(简称RF):每个SM包含了数以千计的寄存器单元
- INT32:整数处理单元(占比36%,根据NVidia统计,shader运算中大概三成是整型计算,这个占比有助于提升GPU利用率),在此前的Pascal GPU架构中,整数计算与浮点数运算使用的是相同的数据处理路径,在进行整数处理的时候,会阻塞浮点数操作的执行,而在Turing架构中增加了整数处理路径,允许浮点数与整数同步进行处理,有助于提高GPU的利用率。
- FP32:浮点数(单精度)处理单元(其实现在已经有了双精度浮点数处理单元,只是图中未列出)
- TENSOR CORES:为tensor/matrix计算所专门设计的处理器核心,这类计算在deep learning中使用频率较高,可以用于支持Deep Learning Super Sampling(DLSS)抗锯齿技术(使用深度学习的算法从多帧中提取关键信息来补足锯齿处的采样精度,其采样点数目要少于传统的抗锯齿技术比如TAA,同时还可以避免透明物体处理时的复杂逻辑)。
- SFU:Special Function单元,用于实现一些特殊函数如三角函数,指数函数等的计算(单精度?)。此单元可以与Dispatch Unit解耦,即在此单元处于工作状态时,Dispatch Unit依然可以执行其他的运算指令的分派
- LD/ST:Load/Store单元,负责数据的读写逻辑
- TEX:用作贴图访问(SRV/UAV等)。
- L1 Data Cache / Shared Memory:所有Processor共享,降低Local/Global Memory读取的延迟
- RT Core:基于硬件的Ray Tracing加速组件。将此组件与一个降噪算法相结合,可以降低单个pixel所需要的射线数目。
4. GPU存储结构
GPU中用于存储数据的结构有多种[4],分别是:
- Local Memory(LMEM)
- Global Memory(GMEM)
- Texture Memory(TMEM)
- Constant Memory(CMEM)
- Register Memory(RMEM)
- Shared Memory(SMEM)
每种存储结构都有着各自的优缺点,因此适用于不同的应用场景,从访问速度来看,这些存储结构按照从高到低排序依次是:
RMEM > SMEM > CMEM > TMEM > LMEM > GMEM
RMEM与SMEM是直接集成在GPU芯片上的,而剩下的几种存储结构则是在GPU之外的芯片上的,此外,LMEM/CMEM/TMEM都有着各自的缓存机制,即在访问数据的时候都会首先从缓存中进行查找判断,再决定是否需要从更低一级速度的存储结构中进行读取。
4.1 Local Memory
存储在LMEM中的数据可见性与RMEM一样,都是只对负责对其进行读写的线程可见。LMEM实际上并不是一块物理存储空间,而是对GMEM的一个抽象,因此其访问速度与对GMEM的访问速度是相同的。LMEM中的数据对于一个线程而言是Local的(即只从属于当前thread的空间,对其他线程不可见),通常用于存储一些automatic变量(automatic变量指的是一些大尺寸的数据结构或者数组,因为寄存器不够,因此会塞入LMEM中),编译器在寄存器不足的时候,就会从GMEM中开辟一块空间用作LMEM。
虽然LMEM是从GMEM中分割出来的,但是其使用方式与GMEM还是有着一些区别:
- 对LMEM中数据的寻址是由编译器(compiler)完成的
- 对LEME中的数据的读写会有一个缓存机制
如上图所示(从图中可以看出,L1是位于GPU芯片上的,其中SMEM就存储在其中,RMEM也是在芯片上,而L2及以后的存储空间则都是芯片之外的存储空间了),在对LMEM进行数据读写的时候,会经历这样一个缓存层级流动:L1->L2->LMEM。因为LMEM实际上是临时开辟的一块空间,因此里面的数据实际上是GPU先写入的,在此之前发生的读取就相当于读到了一堆乱码。
那么什么情况下会使用到LMEM呢?一般来说有如下两种情形:
- 每个线程所需要的寄存器超出硬件支持的限度,出现spilling
- 每个线程需要分配一块连续空间进行数组数据的存储,在一些不支持索引操作(寄存器不可索引)
的编译器上,就需要采用LMEM实现数组数据存储
因为LMEM相对于寄存器访问速度的低效性,因此其对性能的影响主要有如下两个方面:
- 增加内存访问的冲突程度(增加了对GMEM的访问,对带宽消耗增加)
- 增加shader指令数
但是因为以下的两点原因,LMEM也不一定会造成性能下降:
- 由于LMEM支持缓存,因此通过这种方式可以减轻内存访问的冲突程度
- 如果shader并不受指令吞吐量的限制(即指令的吞吐量并非瓶颈)的话,LMEM的启用并不会对性能造成实质性影响
对于一些LMEM可能会存在瓶颈的情况,参考文献[3]中给出了一些分析的方法可供排查,同时还给出了对应的优化策略以及实战案例,有兴趣的同学可以前往参考。
4.2 Register Memory
存储在RMEM中的数据只对负责对此寄存器进行读写的线程可见,且其生命周期与此线程的生命周期一致。
通常情况下,对寄存器的访问不需要消耗时钟周期,但是在一些特殊情况(比如先进行了一个写操作,之后再进行读取,或者在bank访问冲突的情况下),会有例外。先写后读的延迟大概是24个时钟周期,对于更新的GPU(每个SM包含32个cores的情况),可能需要花费768个线程来隐藏这个延迟。
当需求的寄存器数目超出硬件所能支持的限额时,就会导致寄存器压力,在这种情况下,数据就会使用LMEM来进行存储(所谓的spilled over,即溢出),如下图所示[3]:
4.3 Shared Memory
存储在SMEM中的数据对处于同一个block所有的线程都是可见的(不负shared之名),因此通常用于多个线程之间的数据互换,为了避免多个线程同时访问相同的数据导致的阻塞,NVIDIA将SMEM划分成32个逻辑单元,每个单元叫做一个bank,在内存中连续的数据,在banks的分布也是连续的:
SMEM是位于L1 Cache中的,其尺寸通常为16/32/48KB,剩余部分用作L1 Cache,对于开普勒架构而言,每个bank每个时钟的带宽是64bits/clock,较早的Fermi架构时钟不太一样,但是带宽差不多是这个数值的一半。
由于一个warp中有32个线程,因此总共需要32个SMEM banks。由于每个bank在每个时钟周期中只支持一次访问请求,因此多个同时访问的请求就会导致bank conflict,这个的处理过程后面会讲。
默认每个bank占用32bits(4bytes),开普勒架构之后,可以通过指令(cudaDeviceSetSharedMemConfig(cudaSharedMemBankSizeEightByte))将每个bank扩充到64bits,以应对双精度数据的访问冲突。
4.4 Global Memory
存储在Global Memory中的数据对于当前进程中的所有线程都是可见的,其生命周期与进程一致。
4.5 Constant Memory
CMEM通常用于存储一些常量数据,当同一个warp中的所有线程都需要使用同一个参数时,可以将数据放在CMEM中,这种做法比将数据放在GMEM中更节省带宽。
4.6 Texture Memory
TMEM也是一种常量存储结构,当一个warp中的线程所需要读取的数据都是存储位置上相邻的时候,使用这种结构比GMEM具有更优的性能表现(也是出于带宽的原因)
参考文献
[1]. A HISTORY OF NVIDIA STREAM MULTIPROCESSOR
[2]. Life of a triangle - NVIDIA's logical pipeline
[3]. Local Memory and Register Spilling
[4]. GPU Memory Types – Performance Comparison