CUDA C++编程指南(3.1)——使用NVCC编译
AI-安全-功耗 CUBE 博客目录导读
目录
3.1. 使用NVCC编译
3.1.1. 编译工作流程
3.1.1.1. 离线编译
3.1.1.2. 即时编译
3.1.2. 二进制兼容性
3.1.3. PTX兼容性
3.1.4. 应用兼容性
3.1.5. C++ 兼容性
3.1.6. 64位兼容性
CUDA C++为熟悉C++编程语言的用户提供了一条简单路径,可以轻松编写由设备执行的程序。
它包含了对C++语言的最小扩展集和一个运行时库。
核心语言扩展已在编程模型中介绍。它们允许程序员将内核定义为C++函数,并在每次调用函数时使用新语法指定网格和块维度。所有扩展的完整描述可在C++语言扩展中找到。包含这些扩展的任何源文件都必须按照“使用NVCC编译”中概述的方式用nvcc进行编译。
运行时在CUDA Runtime中介绍。它提供了在主机上执行的C和C++函数,用于分配和释放设备内存、在主机内存与设备内存之间传输数据、管理多设备系统等。运行时的完整描述可在CUDA参考手册中找到。
运行时建立在更低层次的C API——CUDA驱动API之上,应用程序也可以访问该驱动API。驱动API通过暴露更低层次的概念(例如CUDA上下文——设备端的主机进程对应物,以及CUDA模块——设备端的动态加载库对应物)提供了额外的控制层级。大多数应用程序不需要这种额外控制层级,因此不采用驱动API;而使用运行时API时,上下文和模块管理是隐式进行的,这使得代码更为简洁。由于运行时API可与驱动API互操作,大多数需要某些驱动API功能的应用程序可以默认使用运行时API,仅在必要时才调用驱动API。驱动API在Driver API中介绍,并在参考手册中完整描述。
3.1. 使用NVCC编译
内核可以使用CUDA指令集架构编写,称为PTX,这在PTX参考手册中有详细描述。然而,通常更有效的方法是使用高级编程语言如C++。无论哪种情况,内核都必须通过nvcc编译成二进制代码才能在设备上执行。
nvcc 是一个编译器驱动程序,它简化了编译 C++ 或 PTX 代码的过程:它提供了简单且熟悉的命令行选项,并通过调用实现不同编译阶段的工具集合来执行这些选项。本节概述了 nvcc 的工作流程和命令选项。完整描述可以在 nvcc 用户手册中找到。
3.1.1. 编译工作流程
3.1.1.1. 离线编译
使用nvcc编译的源文件可以包含主机代码(即在主机上执行的代码)和设备代码(即在设备上执行的代码)的混合。nvcc的基本工作流程包括将设备代码与主机代码分离,然后:
-
将设备代码编译为汇编形式(PTX代码)或二进制形式(cubin对象),
-
并通过将Kernels中引入的
<<<...>>>语法替换为必要的CUDA运行时函数调用,来修改主机代码,以便从PTX代码或cubin对象加载并启动每个已编译的内核。
修改后的主机代码可以输出为C++代码,留待使用其他工具进行编译;或者通过让nvcc在最后编译阶段调用主机编译器,直接输出为目标代码。
应用程序可以:
-
要么链接到已编译的主机代码(这是最常见的情况),
-
或者忽略修改后的主机代码(如果有的话),使用CUDA驱动API来加载和执行PTX代码或cubin对象。
3.1.1.2. 即时编译
应用程序在运行时加载的任何PTX代码都会被设备驱动程序进一步编译为二进制代码。这被称为即时编译。即时编译会增加应用程序的加载时间,但允许应用程序受益于每个新设备驱动程序带来的编译器改进。正如Application Compatibility中详细说明的那样,这也是应用程序能在编译时尚未存在的设备上运行的唯一方式。
当设备驱动程序为某个应用即时编译一些PTX代码时,它会自动缓存生成的二进制代码副本,以避免在后续调用该应用时重复编译。这个被称为计算缓存的cache会在设备驱动程序升级时自动失效,从而使应用程序能够受益于内置在设备驱动程序中的新即时编译器的改进。
环境变量可用于控制即时编译,具体说明请参阅CUDA Environment Variables
作为使用nvcc编译CUDA C++设备代码的替代方案,可以利用NVRTC在运行时将CUDA C++设备代码编译为PTX。NVRTC是一个用于CUDA C++的运行时编译库;更多信息请参阅NVRTC用户指南。
3.1.2. 二进制兼容性
二进制代码是与架构相关的。通过编译器选项-code生成cubin对象时,需要指定目标架构:例如,使用-code=sm_80编译会生成针对计算能力8.0设备的二进制代码。二进制兼容性保证在次版本号之间向前兼容,但不能向后兼容或跨主版本兼容。换句话说,为计算能力X.y生成的cubin对象只能在计算能力为X.z(其中z≥y)的设备上执行。
【注意】:
二进制兼容性仅支持桌面平台,不支持Tegra平台。此外,桌面平台与Tegra平台之间的二进制兼容性也不支持。
3.1.3. PTX兼容性
某些PTX指令仅支持在更高计算能力的设备上运行。例如,Warp Shuffle Functions仅支持计算能力5.0及以上的设备。-arch编译器选项指定了将C++编译为PTX代码时假设的计算能力。因此,例如包含warp shuffle的代码必须使用-arch=compute_50(或更高版本)进行编译。
PTX 代码针对特定计算能力生成后,始终可以编译为计算能力相当或更高的二进制代码。需要注意的是,从较早PTX版本编译的二进制文件可能无法利用某些硬件特性。例如,针对计算能力7.0(Volta)设备、从计算能力6.0(Pascal)生成的PTX编译的二进制文件将无法使用Tensor Core指令,因为这些指令在Pascal架构上不可用。因此,最终二进制文件的性能可能不如使用最新PTX版本生成的二进制文件。
PTX 代码编译为针对architecture conditional features时,仅能在完全相同的物理架构上运行,无法在其他架构上执行。具有架构条件的PTX代码不具备向前或向后兼容性。 例如,使用sm_90a或compute_90a编译的示例代码只能在计算能力为9.0的设备上运行,既不向后兼容也不向前兼容。
3.1.4. 应用兼容性
要在特定计算能力的设备上执行代码,应用程序必须加载与该计算能力兼容的二进制或PTX代码,如二进制兼容性和PTX兼容性中所述。特别是,为了能够在具有更高计算能力的未来架构上执行代码(目前还无法生成二进制代码),应用程序必须加载将被这些设备即时编译的PTX代码(参见即时编译)。
在CUDA C++应用程序中嵌入哪些PTX和二进制代码,是由-arch和-code编译器选项或-gencode编译器选项控制的,具体细节请参阅nvcc用户手册。例如,
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\"
嵌入与计算能力5.0和6.0兼容的二进制代码(第一和第二个-gencode选项),以及兼容计算能力7.0的PTX和二进制代码(第三个-gencode选项)。
主机代码会在运行时自动选择加载和执行最合适的代码,在上述示例中,生成的代码将是:
-
适用于计算能力5.0和5.2设备的5.0二进制代码
-
适用于计算能力6.0和6.1设备的6.0二进制代码
-
适用于计算能力7.0和7.5设备的7.0二进制代码
-
PTX代码会在运行时被编译为二进制代码,适用于计算能力8.0和8.6的设备。
x.cu 可以包含使用warp规约操作等优化代码路径,这些功能仅在计算能力8.0及以上的设备中支持。__CUDA_ARCH__宏可用于根据计算能力区分不同的代码路径。该宏仅在设备代码中定义。例如当使用-arch=compute_80编译时,__CUDA_ARCH__的值等于800。
如果x.cu是针对 architecture conditional features编译的,例如使用sm_90a或compute_90a,则该代码只能在具有计算能力9.0的设备上运行。
使用驱动API的应用程序必须将代码编译为单独的文件,并在运行时显式加载和执行最合适的文件。
Volta架构引入了独立线程调度技术,这改变了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\")。
3.1.5. C++ 兼容性
编译器前端根据C++语法规则处理CUDA源文件。主机代码完全支持标准C++,但设备代码仅支持部分C++功能,具体描述见C++ Language Support。
3.1.6. 64位兼容性
64位版本的nvcc会以64位模式编译设备代码(即指针为64位)。以64位模式编译的设备代码仅支持与以64位模式编译的主机代码配合使用。
