nvcc编译器之设备和主机独立编译(chapter 6)
创始人
2024-03-06 21:14:29
0

目录

6. CUDA中的独立编译

6.1 单独编译时的代码改动

6.2 nvcc独立编译选项

 6.3 库

6.4 示例

6.5 分布编译优化

6.6 独立编译的潜在问题


6. CUDA中的独立编译

在5.0版本之前,CUDA不支持分开编译,因此CUDA代码不能访问跨文件(编译单元)的设备函数或变量, 这种编译称为全程序编译。cuda一直都支持单独编译主机侧代码,实际上,只有设备端CUDA代码在单文件中需要支持。从CUDA 5.0开始,支持了单独编译设备端代码,但旧的全程序编译模式仍然是默认的方式,所以有新的选项来触发单独编译

6.1 单独编译时的代码改动

单独编译设备端(GPU)代码所需对主机端代码的更改,与单主机端(CPU)代码分文件编译相同,即使用extern和static来控制符号的可见性。注意,之前(cuda 5.0前面)的extern,在CUDA代码中被忽略了,5.0之后则不会。通过使用static,可以在不同的文件中,使用多个具有相同名称的设备符号。由于这个原因,通过字符串名称引用符号的CUDA API调用被弃用了,取而代之,应该由使用API的地址。

6.2 nvcc独立编译选项

CUDA的工作原理是将设备端代码嵌入到主机目标对象中。 在全程序编译模式中,CUDA将可执行的设备代码嵌入到主机对象(object)。

在独立(分开)的编译中,则将可重定位设备代码嵌入到主机对象中,并通过运行nvlink(设备链接器),将所有设备代码链接到一起。 然后,nvlink的输出,再通过主机链接器与所有主机侧目标对象链接在一起,形成最终的可执行文件。

可重定位与可执行设备代码的生成是由--relocatable-device-code选项控制的.

--compile选项已经被用于控制主机端编译器对对象的编译,所以添加了一个新选项--device-c,等同于--relocable-device-code=true --compile。

需要只调用设备端链接器时,可以使用--device-link选项,它给出一个包含嵌入式可执行设备代码的主机侧目标对象。然后,它的输出必须传递给主机链接器。

或者,使用nvcc 隐式调用设备端和主机端链接器。如果设备链接器没有看到任何可重定位的设备端代码,它就不会做任何事情,因而这样也是可以工作的。

图4显示了流程(nvcc --device-c具有与#unique_50/unique_50_connect_42_cuda-compiler-from-cu-to-o相同的流程)

 6.3 库

设备链接器具有读取主机侧静态库的能力(Linux和Mac OS X上的.a,Windows上.lib),但它忽略任何动态(.so或.dll)库。可以使用--library和--library-path选项将库传递给设备端和主机端链接器。当使用--library选项时,指定的库名没有库文件扩展名:

nvcc --gpu-architecture=sm_50 a.o b.o --library-path= --library=foo 

或者,在windows上,不使用--library选项时可以使用带后缀名的库名。

nvcc --gpu-architecture=sm_50 a.obj b.obj foo.lib --library-path=  

注意,设备链接器忽略任何没有可重定位设备代码的主机目标对象。

6.4 示例

 //---------- b.h ---------- #define N 8 extern __device__ int g[N];extern __device__ void bar(void); 

//---------- b.cu ----------#include "b.h"__device__ int g[N];__device__ void bar (void){g[threadIdx.x]++;}
 //---------- a.cu ---------- #include #include "b.h" __global__ void foo (void) {__shared__ int a[N];a[threadIdx.x] = threadIdx.x;__syncthreads();g[threadIdx.x] = a[blockDim.x - threadIdx.x - 1]; bar();
}
int main (void) {unsigned int i;int *dg, hg[N];int sum = 0; foo<<<1, N>>>(); if(cudaGetSymbolAddress((void**)&dg, g)){printf("couldn't get the symbol addr\n");return 1;}if(cudaMemcpy(hg, dg, N * sizeof(int), cudaMemcpyDeviceToHost)){printf("couldn't memcpy\n"); return 1; } for (i = 0; i < N; i++) {sum += hg[i];}if (sum == 36) {printf("PASSED\n"); } else {printf("FAILED (%d)\n", sum); }return 0; } 

Linux 环境的编译命令如下:

nvcc --gpu-architecture=sm_50 --device-c a.cu b.cu
nvcc --gpu-architecture=sm_50 a.o b.o

如果想分步触发设备和主机侧的链接器,则如下操作:

nvcc --gpu-architecture=sm_50 --device-c a.cu b.cu
nvcc --gpu-architecture=sm_50 --device-link a.o b.o --output-file device_link.o
g++ a.o b.o device_link.o --library-path= --library=cudart

需要注意,所有运行的目标架构都必须传递给设备链接器(因此使用了sm_50,忽略--gpu-code使得其默认值也是cm_50),因为它指定了最终可执行文件中的生成内容(一些objects对象或库可能包含多个目标架构的设备代码,链接步骤因而会选择将哪些代码编译放入最终可执行文件中)。

如果想使用driver API来加载一个已经链接过的cubin二进制,可以直接使用--cubin生成设备端二进制代码(.cubin格式):

nvcc --gpu-architecture=sm_50 --device-link a.o b.o \--cubin --output-file link.cubin

使用--lib将上述目标生成库(设备链接器只支持静态库)

nvcc --gpu-architecture=sm_50 --device-c a.cu b.cu
nvcc --lib a.o b.o --output-file test.a
nvcc --gpu-architecture=sm_50 test.a

ptx码文件可以被编译到主机目标文件,并被链接使用:

nvcc --gpu-architecture=sm_50 --device.c a.ptx

使用库、主机链接器、动态机制,示例:

nvcc --gpu-architecture=sm_50 --device-c a.cu b.cu
nvcc --gpu-architecture=sm_50 --device-link a.o b.o --output-file link.o
nvcc --lib --output-file libgpu.a a.o b.o link.o
g++ host.o --library=gpu --library-path= \--library=cudadevrt --library cudart

可以在一个主机端的可执行文件中链接多个设备端目标,只要每个设备链接是相互独立的。这种独立性要求,不能在跨设备可执行文件间共享代码,也不能共享地址(例如,可以从主机侧传递一个主机侧的函数地址给设备,以便设备侧进行回调,但这只有在设备链接时可以同时看到caller和潜在的回调callee(调用者)时才行得通。无法将一个设备可执行文件中的地址传递到另一个设备可执行文件中,因为它们是独立的地址空间)。

6.5 分布编译优化

单独编译的代码可能没有全程序代码编译那么高的性能,因为不能跨文件内联代码(inline)。

获得最佳性能的一个方法是使用链接时优化(link-time optimization),它存储中间代码,然后将中间代码链接在一起以执行高级别的优化。这可以通过--dlink-time-opt或-dlto选项支持,此选项必须在编译和链接同时指定。如果只是部分文件使用-dlto编译,则这些文件将被链接和优化在一起,其余的使用普通的单独编译。副作用是,这会将一些编译时间转移到链接阶段,并且对于真正的大型代码可能存在一些可扩展性问题。如果你想使用-gencode编译到多个目标架构(arch),使用-dc -gencode arch=compute_NN,code=lto_NN来指定要存储的中间IR(其中NN是SM架构版本)。然后使用-dlto选项链接特定的架构。对于LTO代码没有运行时JIT支持,因此需要静态链接到最终的sm_NN架构。作为一个review功能,CUDA 11.4的驱动程序API支持LTO下的JIT,请参阅CUDA驱动程序API文档获取更多信息。

6.6 独立编译的潜在问题

6.6.1 目标文件的兼容性

只有具有相同ABI版本、链接兼容的SM目标架构以及相同指针大小(32或64)的可重定位设备代码才能链接在一起。 不兼容的对象将产生链接错误。链接兼容的SM目标架构,具有兼容的SASS二进制文件,这些二进制文件无需转换就可以组合,例如:sm_52和sm_50。

一个对象可能已经被编译为支持不同的架构,但同时也包含PTX码。在这种情况下,设备连接器将把PTX 码即时编译(JIT)到所需目标架构的cubin二进制,然后进行链接。可重定位设备代码,需要CUDA 5.0或更高版本的Toolkit。

如果使用launch_bounds属性或--maxrregcount选项,将内核函数使用的寄存器限制在一定数量,那么内核调用的所有函数,都不能使用超过这个数量的寄存器;如果它们超过了限制,那么将给出一个链接错误。

6.6.2 JIT链接支持

CUDA 5.0不支持JIT(即时)链接,而CUDA 5.5支持。这意味着要使用JIT链接,必须使用CUDA 5.5或更高版本重新编译代码。

JIT链接意味着在启动时对代码进行重新链接。 设备链接器(nvlink)在cubin层进行链接。如果cubin在加载时与目标架构不匹配,驱动程序将重新调用设备链接器来,为目标架构生成cubin。方法是:首先将每个对象的PTX码即时编译(JIT)到适当的cubin,然后将新的cubin链接在一起。

6.6.3 隐式CUDA主机代码

像上面的b.cu这样的文件只包含CUDA设备代码,所以可能会认为b.o对象不需要传递给主机链接器。但实际上,只要设备符号可以从主机端访问,就会生成隐式的主机代码,无论是通过launch还是像cudaGetSymbolAddress()这样的API调用。

这个隐式的主机代码放在b.o中,需要传递给主机链接器。另外,为了支持即时编译(JIT)的链接,所有设备代码都必须传递给主机链接器,否则主机可执行文件将不包含即时编译(jit)链接所需的设备代码。

所以,一般的规则是设备链接器和主机链接器必须看到相同的主机目标文件(如果该主机目标文件中有任何设备端的引用;如果一个目标文件是纯主机文件,那么设备链接器就不需要看到它)。如果一个包含设备代码的目标文件没有传递给主机链接器,那么你将看到一个关于函数__cudaRegisterLinkedBinary_name调用一个未定义或未解析的符号__fatbinwrap_name的错误消息。

6.6.4 使用__CUDA_ARCH__宏

在单独的编译中,不能在头文件中使用__CUDA_ARCH__宏,这样不同的目标对象可能包含不同的行为。或者,必须保证所有对象都为同一个compute_arch编译。如果在头文件中定义了一个弱函数或模板函数,并且它的行为依赖于__CUDA_ARCH__,那么如果目标对象为不同的compute arch编译,该函数在对象中的实例可能会发生冲突。例如,a.h包含:

 template__device__ T* getptr(void) {#if __CUDA_ARCH__ == 500return NULL; /* no address */ #else__shared__ T arr[256];return arr;#endif 
} 

如果a.cu和b.cu都include了a.h,并且实例化了相同类型的getptr,而b.cu需要一个非null地址,编译时使用:

nvcc --gpu-architecture=compute_50 --device-c a.cu
nvcc --gpu-architecture=compute_52 --device-c b.cu
nvcc --gpu-architecture=sm_52 a.o b.o

在链接时,只使用getptr的一个版本,因此行为将取决于选择的版本(为不同架构编译的不同版本)。为了避免这种情况,a.cu和b.cu必须编译为同一个计算架构,否则在共享头函数中不应该使用__CUDA_ARCH__。

6.6.5 库中的设备代码

如果一个使用非weak(弱符号)的外部链接设备函数,该函数符号同时定义在一个库和非库对象(或另一个库)中,设备链接器将产生多个定义(这有别于传统主机连接器,可以忽略库中的函数对象定义,如果它已经在前面的目标对象中找到该函数符号)的链接错误。

关于作者:

犇叔,浙江大学计算机科学与技术专业,研究生毕业,而立有余。先后在华为、阿里巴巴和字节跳动,从事技术研发工作,资深研发专家。主要研究领域包括虚拟化、分布式技术和存储系统(包括CPU与计算、GPU异构计算、分布式块存储、分布式数据库等领域)、高性能RDMA网络协议和数据中心应用、Linux内核等方向。

专业方向爱好:数学、科学技术应用

关注犇叔,期望为您带来更多科研领域的知识和产业应用。

内容坚持原创,坚持干货有料。坚持长期创作,关注犇叔不迷路

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...