【CUDA编程】【3】【3.Programming Interface】【3.1.Compilation with NVCC】

Compilation Workflow,Binary Compatibility,PTX Compatibility,Application Compatibility,C++ Compatibility,64-Bit Compatibility

Posted by x-jeff on October 24, 2024

【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_90acompute_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\"
  1. -gencode的第一行表示向应用程序中嵌入计算能力5.0的二进制代码。
  2. -gencode的第二行表示向应用程序中嵌入计算能力6.0的二进制代码。
  3. -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_90acompute_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代码一起使用。