【CUDA编程】系列博客参考NVIDIA官方文档“CUDA C++ Programming Guide(v12.6)”。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。
1.Compilation with NVCC
所有包含CUDA代码的源文件都需要使用nvcc
编译器进行编译。nvcc
是NVIDIA提供的专门用于编译CUDA程序的编译器。
2.Compilation Workflow
2.1.Offline Compilation
用nvcc
编译的源文件可以是host代码(即在CPU上执行的代码)和device代码(即在GPU上执行的代码)的混合。nvcc
的基本工作流程是先将host代码和device代码分离,然后:
- 将device代码编译成汇编形式(PTX代码)或二进制形式(cubin对象)。
- 将host代码中的
<<<...>>>
替换为对CUDA runtime函数的调用,以在PTX代码或cubin对象中加载和启动编译好的kernel。
nvcc
可以将修改后的host代码以C++代码的形式输出,然后留给其他编译工具编译。此外,nvcc
也可以在最后的编译阶段调用host编译器将修改后的host代码输出为目标文件。
应用程序可以:
- 将编译后的host代码和device代码链接在一起,生成最终的可执行文件。这是最常见的情况。
- 忽略修改后的host代码,直接使用CUDA驱动API来加载和执行device代码,这种方式提供了更大的灵活性。
2.2.Just-in-Time Compilation
即时编译(just-in-time compilation)是指在应用程序运行时,由device驱动程序将PTX代码进一步编译为可直接执行的二进制代码。即时编译会增加应用程序的加载时间,但这也使得应用程序可以从新的device驱动程序所引入的改进中受益。这也是应用程序能够在其编译时尚不存在的device上运行的唯一方法。
当device驱动程序为某个应用程序即时编译一些PTX代码时,它会自动缓存生成二进制代码的副本,以避免在后续调用该应用程序时重复编译。这个缓存被称为计算缓存(compute cache),在device驱动程序升级时会自动失效,以便应用程序可以从新的即时编译器的改进中受益。
可以通过设置环境变量来控制即时编译。
在第2.1部分,我们提到了,在编译阶段,可以使用nvcc
将CUDA C++代码编译为PTX代码。此外,在应用程序运行时,我们可以使用NVRTC将CUDA C++代码编译为PTX代码。
3.Binary Compatibility
二进制代码是与GPU架构相关的。通过-code
来生成指定架构下的cubin对象。比如,-code=sm_80
可以生成计算能力为8.0的device的二进制代码。二进制代码可以兼容往后的小版本,但不能兼容往前的小版本或不同的大版本。比如,为计算能力为$X.y$生成的cubin对象,只能在计算能力为$X.z$($z \geqslant y$)的device上运行。
仅desktop具有二进制兼容性,而Tegra不具有二进制兼容性(Tegra是NVIDIA的嵌入式设备平台)。并且,desktop和Tegra之间的二进制也互不兼容。
4.PTX Compatibility
某些PTX指令仅在较高计算能力的device上支持。比如,Warp Shuffle Functions仅支持计算能力大于等于5.0的device。编译器选项-arch
可以指定编译C++到PTX时所假定的计算能力。因此,若想支持Warp Shuffle,必须使用-arch=compute_50
(或更高)进行编译。
为某个特定计算能力生成的PTX代码总是可以编译为具有更高或相同计算能力的二进制代码。需要注意的是,从早期PTX版本编译得到的二进制文件可能无法利用某些硬件功能。例如,从为计算能力6.0(Pascal架构)生成的PTX编译出的针对计算能力7.0(Volta架构)的二进制文件不会利用Tensor Core指令,因为这些指令在Pascal架构上是不可用的。这可能会导致生成的二进制文件的性能比预期要差。
编译时指定特定架构的PTX代码不具有兼容性(既不向前兼容,也不向后兼容)。例如,使用sm_90a
或compute_90a
编译的PTX代码只能在计算能力为9.0的device上运行。
5.Application Compatibility
要在特定计算能力的device上执行代码,应用程序必须加载与该计算能力兼容的二进制或PTX代码,如第3部分和第4部分所描述的。特别是,为了能够在未来架构(尚无二进制代码可生成)上执行代码,应用程序必须加载PTX代码,这些PTX代码将在运行时由device驱动程序即时编译为适合这些device的二进制代码。
哪些PTX或二进制代码嵌入到CUDA C++应用程序中由-arch
和-code
或-gencode
控制。
1
2
3
4
nvcc x.cu
-gencode arch=compute_50,code=sm_50
-gencode arch=compute_60,code=sm_60
-gencode arch=compute_70,code=\"compute_70,sm_70\"
-gencode
的第一行表示向应用程序中嵌入计算能力5.0的二进制代码。-gencode
的第二行表示向应用程序中嵌入计算能力6.0的二进制代码。-gencode
的第三行表示向应用程序中嵌入计算能力7.0的PTX代码(支持即时编译)和二进制代码。
生成的host代码在运行时会自动选择加载和执行最合适的代码,按照上述例子:
- 计算能力为5.0和5.2的device会选择5.0的二进制代码。
- 计算能力为6.0和6.1的device会选择6.0的二进制代码。
- 计算能力为7.0和7.5的device会选择7.0的二进制代码。
- 计算能力为8.0和8.6的device会在运行时即时编译7.0的PTX代码。
在device代码中,可以使用宏__CUDA_ARCH__
来指定device的计算能力。比如,__CUDA_ARCH__ = 800
表示计算能力为8.0。
如果x.cu
在编译时指定了类似sm_90a
或compute_90a
的编译选项,则代码只能在计算能力为9.0的device上运行。
在第2.1部分中,我们提到应用程序可以直接使用CUDA驱动API来加载和执行device代码,对于这种情况,我们必须将代码编译为独立的文件,并在运行时显式加载和执行最合适的文件。
Volta架构引入了独立线程调度(Independent Thread Scheduling),改变了GPU上线程调度的方式。对于依赖以前架构中SIMT调度的代码,独立线程调度可能会改变参与线程的集合,导致结果不正确。为了避免这种情况,Volta架构允许开发者在编译选项中选择-arch=compute_60 -code=sm_70
,以采用Pascal架构的线程调度方式。
nvcc
用户手册还列出了-arch
、-code
、-gencode
的各种简写形式。比如,-arch=sm_70
表示-arch=compute_70 -code=compute_70,sm_70
(等价于-gencode arch=compute_70,code=\"compute_70,sm_70\"
)。
6.C++ Compatibility
host代码支持完整的C++,而device代码只支持C++的一个子集。
7.64-Bit Compatibility
64位版本的nvcc
编译64位的device代码(即指针是64位)。64位的device代码仅支持和64位的host代码一起使用。