nvcc 和 ptxas
nvcc(NVIDIA CUDA Compiler Driver)
nvcc 是 NVIDIA CUDA 编译器驱动程序,它负责管理整个 CUDA 编译过程。它的主要职责包括:
- 预处理:处理宏定义、头文件包含等预处理步骤。
- 分离 Host 代码和 Device 代码:将 CUDA 源代码分离成 Host 代码(Host Code)和 Device 代码(Device Code)。
- 编译 Host 代码:调用标准的 C++ 编译器(如 g++ 或 clang)来编译 Host 代码部分。
- 编译 Device 代码:nvcc 将 device 代码编译成 PTX 代码,并传递给 ptxas 进行进一步编译。
- 链接:将 Host 代码和 Device 代码链接在一起,生成最终的可执行文件或库文件。
nvcc -ptx my_kernel.cu -o my_kernel.ptx 查看对应的 PTX 代码。
ptxas(Parallel Thread Execution Assembler)
ptxas 是 CUDA 工具链中的汇编器,它专门负责将 PTX(Parallel Thread Execution)代码编译成 GPU 可以执行的机器代码(SASS)。它的主要职责包括:
- 接收 PTX 代码:从 nvcc 接收 PTX 中间表示代码。
- 优化 PTX 优化:对 PTX 代码进行优化,以提高 GPU 执行效率。
- 生成 SASS 代码:将优化后的 PTX 代码编译成 SASS 机器代码,这些代码可以直接在 NVIDIA GPU 上执行。
两者的关系
- 用 nvcc 编译 CUDA 程序时,nvcc 会首先处理预处理、代码分离和主机代码编译等步骤。
- 对于 Host 代码部分调用标准的 C++ 编译器(如 g++ 或 clang)来编译。
- 对于 Device 代码部分,nvcc 会生成 PTX 代码,并将其传递给 ptxas。
- ptxas 接收到 PTX 代码后,会对其进行优化,并将其编译成 SASS 机器代码(SASS是一种低级汇编语言,非常接近实际的GPU机器码,但仍然是人类可读的),SASS 不等同于最终的机器码。它是机器码的一种表示形式,SASS 会被进一步转换成实际的二进制机器码(cubin)。
- 最终,nvcc 会将 Host 代码和 Device 代码链接在一起,生成最终的可执行文件或库文件。
PTX 代码 SASS 代码 和 cubin 文件
CUDA 代码 –[nvcc]-> PTX 代码(与硬件无关,低级别的汇编风格的IR) –[ptxas]-> SASS 代码(特定架构的) 进一步生成 cubin 文件(特定架构的):
- CUDA 代码 -> PTX(架构无关)。
- PTX -> SASS(架构特定的汇编)。
- SASS -> cubin(二进制形式)。
SASS 是 cubin 文件的直接前身,cubin 文件是 SASS 代码的二进制形式。 可以理解为 SASS 是可读的汇编代码,而 cubin 是不可读的二进制代码,两者都针对特定的 GPU 架构。最终生成的执行文件会包含 cubin 文件(cubin 是 GPU 执行的最终指令集合),以便在运行时加载到 GPU 上执行。
“执行文件”通常指完整的程序(主机 + GPU 代码),而 cubin 仅是 GPU 部分的二进制代码。cubin 仅包含 GPU kernel 的 SASS 指令,无法独立运行。它需要主机程序通过 CUDA 运行时(或驱动 API)加载和执行。
JIT 编译
GPU 驱动程序可以在运行时(JIT)将 PTX 编译成特定 GPU 架构的机器码。这种 JIT 编译的结果是二进制机器码, 不是 SASS 或 cubin 。
JIT 编译生成的二进制机器码更接近于 GPU 的硬件指令,更底层,更贴近硬件,因此在某些情况下可以获得更高的性能。 但它也牺牲了部分可移植性,因为编译过程依赖于运行时的 GPU 环境。 而传统的 SASS 和 cubin 文件则提供了更好的可移植性(在同一架构的 GPU 之间),但优化程度可能不如 JIT 编译。 两者各有优劣,选择哪种方式取决于具体的应用场景和性能需求。
JIT 编译和传统编译的区别:
- 编译时机: 传统的 CUDA 编译流程在程序运行之前 完成编译,将 PTX 代码编译成 SASS,然后打包成
cubin文件。而 JIT 编译则在程序运行 期间,根据运行时确定的 GPU 架构和特性,动态地将 PTX 代码编译成机器码。 - 目标代码表示: 传统的流程生成 SASS 代码,然后将其转换为
cubin的二进制格式。cubin文件虽然是二进制的,但它仍然是一种相对高级的表示,包含了指令、数据等信息,需要 GPU 的驱动程序进一步解释执行。 而 JIT 编译直接生成 GPU 的 原生机器码,这是 GPU 最底层的指令集,可以直接由 GPU 的硬件执行单元处理,无需额外的解释。 - 优化机会: JIT 编译器可以在运行时获得更多关于 GPU 的信息,例如可用资源、内存带宽等,从而进行更精细的优化。 这使得 JIT 编译生成的机器码可能比预先编译的
cubin文件具有更高的性能,尤其是在处理复杂或特定于硬件的计算时。 - 可移植性:
cubin文件是特定于 GPU 架构的,不能在不同架构的 GPU 上直接运行。 而 JIT 编译生成的机器码也是特定于 GPU 架构的,但其优势在于,驱动程序负责处理架构差异,应用程序代码本身无需针对不同的 GPU 架构进行修改。
业界使用 JIT 的场景是怎样的?
@ CUDA 文件如何 JIT 编译
hold
libcudart.so
CUDA 运行时库。对于 cuda,依然可以使用 ldd 等工具查看 cuda 程序的依赖关系。CUDA 10.0及以后版本支持** Device 代码的动态链接**,使得 Device 代码也可以被编译成动态库并在运行时加载。
libcudart.so 是 nvidia 提供的,不是用户编译出来的,因为 cuda 不是开源的。
nvcc 编译时指明头文件的搜索路径
- 使用
-I选项: 这是最常用的方法。-I选项后跟头文件的目录路径。 您可以多次使用-I选项来指定多个目录。 nvcc 会按照您指定的顺序搜索这些目录。 - 使用环境变量
INCLUDE: 设置环境变量INCLUDE来指定额外的头文件搜索路径。 nvcc 会自动搜索INCLUDE环境变量中指定的目录。 这在需要在多个编译命令中使用相同的头文件路径时非常方便。 - 使用 Makefile: 如果您使用 Makefile 来管理编译过程,则可以在 Makefile 中指定
INCLUDE变量,