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(二进制形式)。

SASScubin 文件的直接前身,cubin 文件是 SASS 代码的二进制形式。 可以理解为 SASS 是可读的汇编代码,而 cubin 是不可读的二进制代码,两者都针对特定的 GPU 架构。最终生成的执行文件会包含 cubin 文件(cubin 是 GPU 执行的最终指令集合),以便在运行时加载到 GPU 上执行。

“执行文件”通常指完整的程序(主机 + GPU 代码),而 cubin 仅是 GPU 部分的二进制代码。cubin 仅包含 GPU kernel 的 SASS 指令,无法独立运行。它需要主机程序通过 CUDA 运行时(或驱动 API)加载和执行。

JIT 编译

GPU 驱动程序可以在运行时(JIT)将 PTX 编译成特定 GPU 架构的机器码。这种 JIT 编译的结果是二进制机器码, 不是 SASScubin

JIT 编译生成的二进制机器码更接近于 GPU 的硬件指令,更底层,更贴近硬件,因此在某些情况下可以获得更高的性能。 但它也牺牲了部分可移植性,因为编译过程依赖于运行时的 GPU 环境。 而传统的 SASScubin 文件则提供了更好的可移植性(在同一架构的 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 变量,