GPU通用计算编程模型
这是我阅读General-Purpose Graphics Processor Architecture的一篇笔记,本文对应书中第二章的内容。
执行模型
现代GPU普遍采用SIMD来实现数据级(data-level)并行,程序员通过诸如CUDA等API以在GPU上启动一系列的线程(thread)执行工作。其中,每个线程可以有独立的执行路径(execution path),并且可以访问不同区域的内存。从硬件的角度看,GPU会将线程分成若干组,每个组被称为一个warp(在AMD的术语中称为wavefront)以利用线程的规律性和空间局部性。这种执行模型被称为SIMT(single-instruction, multi-threads)。
GPU计算程序都从CPU开始。对于独立显卡而言,CPU需要首先为计算任务在GPU上申请显存,然后将数据传输到GPU,最后在GPU上加载计算任务。对于集成显卡而言,则可以直接在GPU上加载计算任务,而无需传输数据(集成显卡使用内存作为显存)。
CUDA的编程模型与compute shader很像。如上所述,GPU中若干线程组成一个warp,每个warp中的所有线程会尽量同时执行相同的指令以提高性能。多个warp组合在一起又组成线程块(thread block),这种组合方式很像划分网格。假设一个线程是第i
个线程块的第j
个warp的第k
个线程,那么使用(i, j, k)
可以唯一地标记这个线程。而又如果每个线程块包含warpNum
个warp,每个warp包含threadNum
个线程,那么用i * warpNum + j * threadNum + k
就可以作为每个线程唯一的ID。
不过在实际的编程中,warp对程序员是不可见的。取而代之的是,j * threadNum + k
这个整体作为线程的局部ID。在CUDA中,它对应内置变量threadIdx
,而线程块的ID对应blockIdx
,每个线程块包含的线程数量对应blockDim
。而在HLSL的compute shader中,线程块的ID对应于SV_GroupID
,而线程的局部ID对应于SV_GroupThreadID
。
同一线程块中的线程能够共享一块高速内存,这块内存在NVIDIA的术语中称为共享内存(shared memory)。GPU的每个流处理器(streaming multi-processor, SM)都包含一块独立的共享内存。在同一个流处理器内,这块共享内存根据线程块的数量又划分成多块。在AMD的术语中,共享内存被称为本地数据存储(local data storage, LDS),每个流处理器包含16-64KB的共享内存。共享内存的访问是很快的,合理利用共享内存做缓存能够有效提高运算速度。除了共享内存,AMD的GPU还存在一个全局数据存储(global data store, GDS)。在图形管线中,GDS用于在不同的shader之间传递运算结果。
GPU指令集架构
和CPU一样,GPU并不能直接执行高级语言,比如HLSL、GLSL或者CUDA,而只能执行二进制代码。不过与amd64和x86不太一样的是,GPU的指令集往往并不通用,而且很可能不向前兼容。诸如DirectX、Vulkan等API通常只能将shader交给驱动程序编译成GPU可执行的程序。
NVIDIA的ISA
NVIDIA提供了一组比较高级的ISA,叫做Parallel Thread Execution ISA(PTX)。PTX非常类似于汇编语言,但仍然不能在GPU上直接执行,它更像是DXIL、LLVM IR这种中间语言,也因此PTX不被局限在NVIDIA的某一个架构上。PTX仍然需要经过驱动程序转译或者CUDA SDK提供的ptxas
来编译成GPU可执行的ISA,NVIDIA称之为SASS。不同GPU的SASS可能会有所差别甚至完全不同。
PTX与SASS都很像RISC ISA,它们的不同主要体现在两点上:
- PTX可以使用无限多的寄存器,但SASS只能使用有限的寄存器;
- SASS中可以使用非访存指令访问参数,但PTX的参数必须放在独立的参数地址空间。
AMD的ISA
AMD的GCN架构和NVIDIA的架构最主要的区别之一是AMD的标量与向量指令是分开的。在AMD的GCN架构中,每个计算单元包含1个标量单元和4个向量单元,向量单元的计算结果只对本线程可见,但标量单元的计算结果被一个warp内的所有线程共享。
热门相关:补天记 我是仙凡 邻家三个女人的味道 我成了暴戾帝君的小娇包 上将大叔,狼来了!