cuda c++是对c++的扩充,具体参考C++ Language Extensions。
cuda c++编写的kernel需要使用nvcc进行编译。
运行时库需要使用cuda runtime,其提供了在host上执行的c/c++函数,可用于allocate/deallcote device memory,transfer data between host and device memory等。
cuda runtime是构建在cuda driver api的基础上的,两者的区别是,driver api可在runtime api的基础上提供更加细粒度的控制(如context/modules)。用户可以直接调用driver api,但是大部分情况下,只使用cuda runtime api已经足够了。
3.1 nvcc编译
虽然一般使用cuda c++编写kernel,但也可以直接使用cuda指令集(PTX)编写kernel。两种情况编写的kernel都需要使用nvcc编译为可在device上执行的二进制程序。
nvcc是一个编译器driver,用于简化c++/PTX代码的编译过程。其提供了简洁/熟悉的指令,可用于完成多个阶段的编译任务。本节给出nvcc的workflow和常用options。
3.1.1 编译workflow
3.1.1.1 离线编译
nvcc编译的源文件可以是主机代码和device 代码的混合体。
nvcc的基本workflow是:
- 从host代码中分离出device代码;
- 将device代码编译成汇编格式(PTX代码)或二进制形式(cubin对象);
- 将host代码中的<<<...>>>替换为必要的cuda runtime函数,然后去调用和加载对应的编译过的kernel(即上一步生成的PTX代码/cubin对象);
修改后的host代码:
- 可以作为c++代码输出,然后使用其他的编译器进行编译;
- 也可以让nvcc调用host编译器在最后一个编译阶段当做目标代码使用;
接下来应用程序:
- 可以link到编译过的host代码(最常见的情况);
- 忽略修改过的host代码,使用cuda driver api加载和执行PTX代码/cubin对象;
3.1.1.2 即时编译
应用程序运行时加载的PTX代码会被设备驱动进一步编译成二进制码,称之为即时编译。即时编译会增加应用程序的加载耗时,但也会从device driver的更新中获益。这也是唯一的一种可以让应用程序运行在编译时尚不存在的device上的方法。
当即时编译器为应用程序编译PTX代码时,它会自动缓存生成的二进制码以避免重复编译。但当device driver升级后,缓存会变得不可用,所以应用程序会随着device driver中内置的即时编译器的更新而重新编译,从而获得新的编译器带来的提升。
即时编译需要通过环境变量控制,具体见 CUDA Environment Variables
另一种编译方式是NVRTC(nvidia runtime compiler),它可以在运行时将cuda c++代码编译成PTX。
3.1.2 二进制的兼容性
二进制代码是特定于架构的。编译时的 -code 选项用于设定生成的cubin对象的目标架构。例如,设定-code=sm_80表示产生特定于计算能力为8.0的架构的cubin。
生成的cubin是向后兼容的,但不能够跨主版本。也就是说,为计算能力X.y gpu生成的cubin,只能在 X.z (z >= y)的GPU上运行。
3.1.3 PTX兼容性
将c++代码编译为PTX代码时使用 -arch 选项设定target的计算能力。之所以需要这样的设置,是因为只有超过某个特定的计算能力后才能支持某些PTX指令,例如warp shuffle函数所需要的最小计算能力为5.0,因此对包含warp shuffle的代码,必须设置-arch=compute_50或更高。
为特定计算能力的GPU产生的PTX代码可以编译为适配于同样的或更高计算能力GPU的binary code。但是从老版本PTX代码中编译成的二进制码不能使用硬件的新特性。比如,为pascal(计算能力6.0)编译的PTX代码进一步编译为volta(计算能力7.0)上的二进制码无法使用tensor core指令。因为tensor core在pascal上不存在。所以最终产生的binary会比直接使用在volta上编译的PTX码的性能要差。
3.1.4 应用程序兼容性
3.2 cuda runtime
应用程序会link cuda runtime(libcudart.a/cudart.lib/libcudart.so/cudart.dll)。
所有的cuda runtime api都以cuda开头。