CUDA的软件堆栈由以下三层构成:CUDA Library、CUDA runtime API、CUDA driver API,如图所示,CUDA的核心是CUDA C语言,它包含对C语言的最小扩展集和一个运行时库,使用这些扩展和运行时库的源文件必须通过nvcc编译器进行编译。
CUDA C语言编译得到的只是GPU端代码,而要管理分配GPU资源,在GPU上分配显存并启动内核函数,就必须借助CUDA运行时的API(runtime API)或者CUDA驱动API(driver API)来实现。在一个程序中只能使用CUDA运行时API与CUDA驱动API中的一种,不能混和使用。
一、CUDA C语言
CUDA C语言为程序提供了一种用C语言编写设备端代码的编程方式,包括对C的一些必要扩展和一个运行时库。CUDA对C的扩展主要包括以下几个方面
1.引入了函数类型限符。用来规定函数是在host还是在device上执行,以及这个函数是从host调用还是从device调用。这些限定符有:__device__,__host__,__global__.
2.引入了变量类型限定符。用来规定变量被存储在哪一类存储器上。传统的在CPU上运行的程序,编译器能自动决定将变量存储在CPU的寄存器还是内存中,在CUDA编程模型中,一共抽象出来多棕8种不同的存储器。为了区分各种存储器,必须引入一些限定符,包括:__device__,__shared__和__constant__。注意,此处的__device__与上节中的__device__限定符的含义不同。
3.引入了内置变量类型。如char4,ushort3,double2,dim3等,它们是由基本的整型可浮点型构成的矢量类型,通过x,y,z,w访问每一个分量,在设备端代码中各矢量类型有不同的对齐要求。
4.引入了4个内建变量。blockIdx和threadIdx用于索引线程块和线程,gridDim和blockDim用于描述线程网格和线程块的维度。warpZize用于查询warp中的线程数量。
5.引入了<<< >>>运算符。用于指定线程网格和线程维度,传递执行参数。
6.引入了一些函灵敏:memory fence函数,同步函数,数学函数,纹理函数,测时函数,原子函数,warp vote函数。
以上扩展均有一些限制,如果违背了这些限制,nvcc将给出错误或警告信息,但有时也不会报错,程序无法运行。
二、nvcc编译器
nvcc编译器根据配置编译CUDA C代码,可以生成三种不同的输出:PTX,CUDA二进制序列和标准C。nvcc是一种编译驱动。通过命令行选项,nvcc可以在编译的不同阶段启动不同的工具完成编译工作。
nvcc工作的基本流程是:首先通过CUDAfe分离源文件中的主机端和设备端代码,然后再调用不同的编译器分别编译。设备端代码由nvcc编译成ptx代码或者二进制代码;主机端代码则将以C文件形式输出,由其他主性能编译器,旧ICC,GCC或者其他合适的高性能编译器等进行编译。不过,也可以直接在编译的最后阶段,将主机端代码交给其他编译器生成.obj或者.o文件。在编译时,可以将设备端代码链接到所生成的主机端代码,将其中的cubin对象作为全局初始化数据数组包含进来。此时,内核执行配置也要被转换为CUDA运行启动代码,以加载和启动编译后的内核函数。使用CUDA驱动API时,可以单独执行ptx代码或者cubin对象,而忽略nvcc编译得到的主机端代码。
编译器前端按照C++语法规则对CUDA源文件进行处理。CUDA主机端代码可以支持完整的C++语法,而设备端代码则不能完全支持。
内核函数可能通过PTX编写,但通常还是通过CUDA C一类的高级语言进行编写。PTX或CUDA C语言编写的内核函数都必须通过nvcc编译器编译成二进制代码。一部分PTX指令只能在拥有较高计算能力的硬件上执行,比如对全局存储器的32bit原子操作指令就只有计算能力1.1以上的硬件才能支持,双精度计算只有计算能力1.3以上的硬件才能支持。nvcc通过编译选项来指定要输出的PTX代码的计算能力。因此,在需要双精度计算时,就必须加上-arch sm_13(或者更高计算能力)编译选项才能正常运行,否则双精度计算将被编译为间精度的计算。
三、运行时API与驱动API
CUDA runtime API和CUDA driver API提供了实现设备管理(Device management),上下文管理(Context management),存储器管理费用(Memory Control),代码块管理 (Code Module management),执行控制(Excution Control),纹理索引管理(Texture Reference management)与OpenGL和Direct3D的互操作性(Interoperity with OpenGL and Direct3D)的应用程序接口。
CUDA runtime API在CUDA driver API 的基础上进行了封装,隐藏了一些实现细节,编程更加方便,代码更加简洁。CUDA runtime API被打包放在CUDAArt包里,其中的函数都有CUDA 前缀。CUDA运行时没有专门的初始化函数,它将在第一次调用函数时自动完成初始化。对使用运行时函数的CUDA程序测试时要避免将这段初始化的时间计入。CUDA runtime API的编程较为简洁,通常都会用这种API进行开发。
CUDA driver API是一种基于句柄的底层接口(式多对象通过句柄被引用),可以加载二进制或汇编形式的内核函数模块,指定参数,并启动计算。CUDA driver API的编程复杂,但有时能通过直接操作硬件的执行实行一些更加复杂的功能键,或者获得更高的性能。由于它使用的设备端代码是二进制或者汇编代码,因此可以在各种语言中调用。CUDA driver API被放在nvCUDA包里,所有函数前缀为cu。
四、CUDA函数库
目前CUDA中有CUFFT,CUBLAS和CUDPP三个函数库,提供了简单高效的常用函数。未来,CUDA中还会提供视频编解码与图像处理库等,如CUVID,进一步扩充功能。CUFFT是利用GPU进行傅立叶变换的函数库,提供了与广泛使用的FFTW库相似的接口。不同的是FFTW操作的数据存储在内在中,而CUFFT操作的数据存储在显存,不能直接相互取代,必须加入显存与内存之间的数据交换,进行封装后才能替代FFTW库。CUBLAS库是一个基本的矩阵与向量运算库,提供了与BLAS相似的接口,可以用于简单的矩阵计算,也可以作为基础构建更加复杂的函数包,如LAPACK等,CUBLAS操作的数据也存储在显存中,同样需要封装后才能替代BLAS中的函数。CUDPP为提供了很多基本的常州用的并行操作灵敏,如排序、搜索等,可以作为基本组件快速地搭建出并行计算程序。调用上述函数库使得程序员无须按照硬件特性设计复杂的算法就能获得很高的性能,大大缩短开发时间;缺点是上述函数库灵活性稍差,并且有可能造成多余的存储器访问。