CUDA:NVCC编译过程和兼容性详解
https://codeyarns.com/2014/03/03/how-to-specify-architecture-to-compile-cuda-code/
https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#supported-phases
前言
本篇主要讲述CUDA程序是怎么编译的,已经其兼容性是如何保证和控制的,主要是原理,具体实现见CUDA:nvcc编译参数示例。
编译流程
CUDA编译阶段将源文件中包含C++ externed(cuda 相关代码)的内容变换到标准的C++代码,然后交给c++编译器进行进一步的编译和链接。如图:
CUDA编译的工作原理如下:输入程序首先被设备编译器(nvcc)编译即在设备编译器(nvcc)预处理过程,并将CUDA相关代码(主要是核函数)编译为放置在fatbinary中的CUDA二进制(Cubin)和/或PTX中间代码,并将CUDA特定的C ++扩展转换为标准C ++构造合成嵌入fatbinary。输入程序再被主机端编译器编译即主机端的预处理,当两个预处理过程完成后,C ++主机编译器将fatbinary的嵌入合成到主机对象(库文件或可执行文件)。
可以看到,编译过程其实分两部分,一部分是主机端和普通c++一样的编译,另一部分是针对CUDA中扩展的C++程序的编译,设备端的编译最终的结果文件为fatbinary文件,GPU(的驱动)通过fatbinary文件来执行GPU功能。
Ps:为什么是fatbinary,fat是肥的意思,(这里简单讲解,下文会有深入的理解),为了让应用程序适应不同的GPU,fatbinary里可能会有多种GPU的实现,程序在运行的时候会根据自己的特点选择合适的最高效的GPU实现进行运行:
CUDA 运行时系统(GPU驱动程序)会监视fatbinary文件中的内容,每次程序运行时,CUDA 运行时系统(GPU驱动程序)都会找到fatbinary中最合适部分并映射到当前GPU。(fatbinary会有适合不同的GPU的实现)
下面重点讲解,在设备编译器NVCC编译的具体过程和兼容性
一些基础知识
GPU的“代”
GPU有个重要参数,计算能力,计算能力的值对应GPU的“代”值,如计算能力3.0,对应的“代”为sm_30,也对应kepler架构。
为了实现GPU架构的演变,NVIDIA GPU以不同代次发布。新一代产品在功能和/或芯片架构方面会引入重大改进,而同一代产品中的GPU模型显示出较小的配置差异,对功能和/或性能产生中等的影响。
NVCC编译器编译出的应用程序的二进制程序兼容性无法保证。例如,已经为Fermi GPU编译的CUDA应用程序很可能不会在Kepler GPU上运行(反之亦然)。 这是因为不同代的指令集不同指令编码的不同。
(注:实际中的结论,compute_30以上的程序,计算能力高的GPU可以运行编译成低代的程序,反之则不行,如计算能力6.1的GPU可以运行编译成compute_30,sm_30的程序)
一个GPU代中的二进制兼容性可以在某些条件下得到保证,因为它们共享基本的指令集。 两个GPU版本之间的情况就是这样,它没有功能上的差异(例如,当一个版本是另一个版本的缩小版本时),或者一个版本在功能上被包含在另一个版本中。 后者的一个例子是基础的Kepler版本sm_30,其功能是所有其他Kepler版本的子集:针对sm_30编译的任何代码将在所有其他Kepler GPU上运行。
GPU的小“代”
除了sm_20,sm_30,sm_50,sm_60这些大的代号,还有sm_21, sm_35, sm_53 ,sm_61这些小代,这些小代不会做大的改变,会有一些小的调整,如调整寄存器和处理器集群的数量,这只影响执行性能,不会改变功能。程序更精确的对应GPU代号可能可以达到最佳性能。
应用程序的兼容性
CPU中不同代CPU应用程序的兼容性很好,已发布的指令集体系结构是确保当这些分布式应用程序成为主流时能够继续在新版CPU上运行的常用机制。
这种情况对于GPU而言是不同的,因为NVIDIA不能保证二进制兼容性,同时不会牺牲GPU的改进。 相反,正如在图形编程领域中已经习惯的那样,nvcc依靠两阶段编译模型来确保应用程序与未来GPU世代的兼容性。
即虚拟架构和真实架构:虚拟架构确定编译成的代号的功能,真实架构确定编译成的真实代号的功能和性能。
如图,这种两个阶段编译的方式将生成的二进制文件写死了,即只有计算能力超过所编译时确定的代号时,程序才会正常运算,即向上兼容。
后面会介绍即时编译(JIT)能够好的体现兼容性。
虚拟架构
GPU编译是通过中间表示PTX来执行的,PTX可以视为虚拟GPU架构的程序集。与实际的图形处理器相反,这种虚拟GPU完全由它提供给应用程序的一组功能或特征来定义。特别是虚拟GPU架构提供了一个(很大程度上)通用的指令集,二进制指令编码是一个无关紧要的问题,因为PTX程序总是以文本格式表示。
因此,nvcc编译命令总是使用两种体系结构:虚拟中间体系结构,以及真实的GPU体系结构来指定要执行的目标处理器。要使这样的nvcc命令有效,真正的体系结构必须是虚拟体系结构的实现。这在下面进一步解释。
所选择的虚拟架构更多地体现了应用程序所需的GPU功能:使用最小的虚拟架构仍然可以为第二个nvcc阶段提供最广泛的实际架构。相反,指定提供应用程序未使用的功能的虚拟体系结构不必要地限制了可以在第二个nvcc阶段中指定可能的GPU的集合。
由此可见,应始终尽可能小的选择虚拟架构,从而最大限度地提高运行的实际GPU。应该选择尽可能高的真实体系结构(假设这会始终生成更好的效果),但只有了解应用程序预计运行的实际GPU的情况才可能产生更好的效果。正如我们稍后将看到的那样,在即时编译(JIT)的帮助下,GPU设备有能力知道要怎样运行。
虚拟框架由compute_开头。
虚拟架构功能列表
虚拟框架 | 功能更新 |
---|---|
compute_30 and compute_32 | Basic features + Kepler support |
compute_35 |
|
compute_50, compute_52, and compute_53 |
|
compute_60, compute_61, and compute_62 |
|
compute_70 |
|
真实架构
虚拟架构通常是从大的GPU代上控制的,真实框架必须大于等于虚拟框架,真实框架对应真正运行的GPU,即编译阶段就确定要运行的GPU是什么。
真实框架由sm_开头。
真实架构的功能列表
真实框架 | 功能更新 |
---|---|
sm_30 and sm_32 | Basic features + Kepler support |
sm_35 |
|
sm_50, sm_52, and sm_53 |
|
sm_60, sm_61, and sm_62 |
|
sm_70 |
|
提高兼容性的方式
编译阶段本身无助于实现与未来GPU应用兼容的目标。 为此,我们需要其他两种机制:即时编译(JIT)和fatbinaries。
即时编译(Just-In-Time)
到实际GPU的编译步骤将代码绑定到一代GPU。 在那一代中,它涉及GPU覆盖和可能的性能之间的选择。 例如,编译为sm_30可让代码在所有Kepler GPU上运行,但如果开普勒GK110(计算能力3.5)及更高版本是唯一的目标,则编译为sm_35可能会产生更好的代码。
通过指定虚拟代码架构而不是真实的GPU,nvcc推迟PTX代码的组装,直到应用程序运行时(目标GPU完全已知)。 例如,当应用程序在sm_50或更高版本的架构上启动时,下面的命令允许生成完全匹配的GPU二进制代码。
nvcc x.cu –gpu-architecture=compute_50 –gpu-code=compute_50
即使编译,即程序运行时,再根据当前的GPU编译成自己计算能力动态编译成应用程序。这就可以让GPU选择想要的版本进行编译。即双compute_的组合。但这种只能保证同一代的兼容性。
即时编译的缺点是增加了应用程序的启动延迟,但这可以通过让CUDA驱动程序使用编译缓存(参见CUDA C编程指南的“第3.1.1.2节”即时编译“)来缓解 在应用程序的多次运行中保持不变。
当GPU计算能力低于编译的虚拟框架时,JIT将失败。
Fatbinaries
在JIT中克服启动延迟,同时仍允许在新GPU上执行的另一种解决方案是指定多个代码实例,如
nvcc x.cu –gpu-architecture = compute_50 –gpu-code = compute_50,sm_50,sm_52
该命令为两个Kepler变体生成精确代码,以及在遇到下一代GPU时由JIT使用的PTX代码。 nvcc将其设备代码组织在fatbinaries中,这些代码能够保存相同GPU源代码的多个翻译。 在运行时,CUDA驱动程序将在设备功能启动时选择最合适的翻译。
即一次保存多个精确的真实框架的二进制结果,当程序被传给GPU时,GPU选择最好的结果,因为一次性加入了多个真实框架,所以被称为‘fat‘。但这也仅仅是保证了大代之间的兼容性。
–generate-code
对于不同代的GPU兼容性,需要使用–generate-code。它会在编译的时候生成所有要求的不同代的PTX,再配合JIT或者fatbinary就可以实现所有GPU的兼容了。但缺点是,由于要编译所有要求的代的GPU,编译过程耗时长。
选项–gpu-architecture和–gpu-code可用于使用通用虚拟架构为一个或多个GPU生成代码的所有情况。这将导致nvcc阶段1的一次调用(即,预处理和生成虚拟PTX汇编代码),随后为每个指定的GPU重复编译阶段2(二进制代码生成)。
使用通用的虚拟体系结构意味着所有假定的GPU功能对于整个nvcc编译都是固定的。例如,以下nvcc命令假定sm_50代码和sm_53代码都不支持半精度浮点运算:
nvcc x.cu –gpu-architecture = compute_50 –gpu-code = compute_50,sm_50,sm_53
有时需要执行不同的GPU代码生成步骤,并根据不同的体系结构进行分区。这可以使用nvcc选项–generate-code,然后必须使用它,而不是使用–gpu-architecture和–gpu-code组合。
与选项–gpu-architecture选项–generate-code不同,它可以在nvcc命令行上重复。它需要子选项arch和code。
CUDA程序兼容性
CUDA程序的兼容性,是要在编译时就要决定和设计好的,为了让程序在编译时保证让所有GPU都可以更好的运行,这里给出方案:
JIT和fatbinaryies只能解决每一代的问题,使用-generate arch,可解决所有代的问题。其具体实现命令将在CUDA:nvcc编译参数示例详细描述。
总结
- CUDA程序的编译必须经历两个过程,即虚拟框架和真实框架,虚拟框架决定了程序最小的可运行GPU框架,而真实框架决定了程序可运行的最小的实际GPU。
例如-arch=compute_30;-code=sm_30
表示计算能力3.0及以上的GPU都可以运行编译的程序。但计算能力2.0的GPU就不能运行了。 - 即时编译(Just-In-Time)机制让程序可以在大的GPU框架内动态选择与电脑GPU最合适的小代。
- –generate-code保证用户GPU可以动态选择最适合的GPU框架(最适合GPU的大代和小代)。