【IT168 技术】由nvcc生成的通用计算程序分为主机端程序和设备端程序两部分。那么,一个完整的CUDA程序是如何在CPU和GPU上执行的呢?在这一节,我们不仅将介绍CUDA的编程模型如何映射到硬件上,还会介绍GPU的硬件设计如何对CUDA程序效率产生影响。
通常,在计算开始前,需要将要计算的数据通过API从内存拷贝到显存中,再在计算结束后将数据从显存拷贝回内存。通过CUDA API的存储器管理功能进行数据传输,不需要SPA中的运算参与。CPU先通过存储器管理API在显存上开辟空间,将内存中的数据由北桥经过PCI-E总线传到显存中。在bandwidthTest例子中我们介绍过,主机端内存可以分为两种:pinned(page-locked)或者pageable memory。一般的操作系统使用了虚拟内存和内存分页管理,这种设计在带来种种好处的同时,也使得新开辟的空间可能会被分配在低速的磁盘上,或者频繁改变地址,对提高单个程序的速度不利。使用cudaMallocHost开辟的pinned memory的必定存在于物理内存中,而且地址固定,可以有效的提高主机端与设备端的通信效率。此外,只有pinned memory才能使用CUDA API提供的异步传输功能,允许在GPU进行计算时进行主机和设备间的通信,实现流式处理。虽然使用Pinned memory有许多好处,但是实际使用中不能分配太大的pinned memory,否则操作系统和其他应用程序就会因为没有足够的物理内存而使用虚拟内存,降低系统整体性能。现在,数据已经准备好了,指令又是如何传给GPU的呢?
在对CUDA的介绍中我们已经知道,前缀为__host__的程序运行在主机端,前缀为__global__或者__device__的函数运行在设备端。我们知道,计算机是执行二进制的机器代码的,CPU如是,GPU也如是。CUDA程序的二进制代码由两部分构成:主机端代码和设备端代码。在调用nvcc编译器编译cuda程序时,nvcc本身只负责编译运行在设备端的函数,主机端的函数是由其他编译器编译,而链接也是由开发环境中的链接器进行,最后生成的可执行程序或者库从外观上看和一般的程序或者库没有什么不同。在执行CUDA程序时,主机端执行的二进制代码和一般程序并没有什么不同,只是在调用核函数时可以将设备端代码通过CUDA API传给显卡。注意,GPU传给CUDA API的设备端代码不一定是二进制代码cubin,也可能是运行于JIT动态编译器上的PTX代码。最后传到显卡上的是适合具体GPU的二进制代码,其中的信息稍多于ptx或者cubin,这是因为cubin或者ptx只包含了block一级的信息,而不包括整个grid的信息。目前在GPU上可以运行的指令长度仍然有限制,不能超过两百万条ptx指令。
GPU端二进制代码主要包括线程网格的维度,线程块的维度,每个线程块使用的资源数量,要运行的指令,以及常数存储器中的数据。之前我们介绍过,SM是GPU中的完整的核心,而TPC则更多的是平衡单元硬件比例形成的单位。现在,可以向SPA中的各个SM分发任务了。任务分发是由计算分发( compute scheduler)单元完成的,分发的单位是协作线程阵列(CTA,Collaborative Tread Arrays)。如果觉得CTA的名字太绕口,它的另一个名字你一定已经很熟悉了:block。block是CTA在编程模型中的表述,由于一个block中的线程使用同一块shared memory,因此一个CTA里的所有线程也就必须被分配同一个SM中。计算分发单元采用了轮询算法,它的任务是尽可能平均的将CTA分发到各个SM上,同时在每个SM上分配尽可能多的CTA。SM以warp为单位执行CTA,在同一个SM上可以同时存在多个CTA的上下文,但一个时刻只有一个warp正在被执行。在第一章我们已经介绍过,GPU是通过在不同warp间进行切换来隐藏长延时操作,实现高吞吐量的。如果SM只分配了一个CTA,由于属于同一个CTA的若干个warp会有比较大的几率会同时进入长延时操作,就会使SM中的执行单元处于长时间等待中。而属于不同CTA的warp在同一个SM上执行时,当一个CTA的所有warp都进入长延时操作时,在其他CTA中会有较大几率存在处于就绪态,可以被马上执行的warp,可以更好的隐藏延时。这就是active block问题,即在一个SM上最多能够同时分配多少个CTA。那么,如何计算active block呢?首先,我们要知道限制active block数量的因素,它们是:
每个SM中最多能够同时存在的block数量:在所有计算能力的硬件上都是8个。
每个SM中最多能够同时存在的warp数量:在计算能力1.0和1.1的硬件上是24,在1.2和1.3硬件上是32。这也意味着在1.0和1.1硬件上最多能够同时存在768个线程,在1.2和1.3硬件上最多能同时存在1024线程。注意:同一warp中的线程必须全部来自同一block,并且同一block中的所有warp必须全部同时存在。每个SM上最多能够存在的线程数量和每个block中最多的线程数量是两个概念,一个是硬件限制,一个是编程模型的限制,要区分清楚。
每个SM中拥有的register数量,register的最小单位是大小为32bit(4Byte)的寄存器文件。计算能力1.0和1.1的硬件中的每个SM拥有8192个寄存器文件,而计算能力1.2和1.3硬件中的SM拥有16384个寄存器文件。翻倍的寄存器文件大小是GT200的重大改进之一,它使得GT200的可编程性和计算性能都上了一个台阶。
每个SM中的共享存储器大小,目前所有计算能力硬件中的每个SM都拥有16KB的共享存储器。
知道了每个SM的资源上限,计算分发单元就可以知道最多能在一个SM上分配多少个CTA。比如,假设我们有一个由256 thread(8 warp)构成的block,每个thread占用16个寄存器文件,同时整个block使用了3K的shared memory,那么我们可以这样计算只考虑单个因素影响时的active block数量:
warp限制的active blcok数量:
在计算能力1.0/1.1硬件上 24/8=3
在计算能力1.1/1.2硬件上 32/8=4
Register限制的active blcok数量:
在计算能力1.0/1.1硬件上8192/(16×256)=2
在计算能力1.1/1.2硬件上16384/(16×256)=4
Shared memory限制的 active block数量:
在所有计算能力硬件上16384/3072=5
单个SM上的最大block数量:
在所有计算能力硬件上都是8
最终的active block数量就是考率以上所有单个因素影响后计算出来的最小值。在刚才讨论的问题中,计算能力1.0/1.1硬件收到寄存器大小限制,使得active block数量只有2,而计算能力1.2/1.3硬件的active block数量达到了4,更有利于隐藏延时。
在实际的程序编写过程中,active block数量会对程序的性能造成相当大的影响,因此必须考虑。但是一定要记住最终的目的是要使程序的运行时间最短,而不是增加active block数量,有时使用大量的高速存储器反而会达到更高的效率。在算法确定的情况下,可以考虑平衡register和shared memory的使用,以及调整block中的线程数量来增加active block数量。Active block只是程序性能的间接衡量标准之一,较小的block可以比较容易的实现较多的active blcok,却可能因为其他因素造成性能上的损失。我们将在第四章优化的第 节介绍如何对active block进行优化,在第 节介绍如何减少register的使用。
在任务分发后,现在SM终于可以开始进行计算了。在前面已经介绍过,一个SM相当于一个完整的处理器,每个SM中含有8个SP。那么,为什么指令要以32线程组成的warp为单位发射执行呢?为什么又存在由16个线程构成的half-warp?要回答这些问题,我们必须了解一下SM的详细组成,请参考对GT200架构的介绍
GPU中的器件总的来说分别工作在三个不同的时钟域内,分别是计算单元工作的处理器频率,存储器控制单元工作在GDDR DRAM IO频率,而纹理流水线,ROP单元以及SM内除了计算单元外的其他单元都工作在GPU核心频率。实际上由于GDDR DRAM的核心频率和IO频率也不相同,我们经常会在Nvidia GPU规格说明中看到三到四个不同的频率。计算单元工作在处理器频率;而取指,发射,各种缓存,寄存器,共享存储器等单元都工作在GPU核心频率。