CUDA开发的配置方法与编译流程简介

一、前言

计算行业正在从只使用CPU的“中央处理”向CPU与GPU并用的“协同处理”发展。为打造这一全新的计算典范,NVIDIA®(英伟达™)发明了CUDA(Compute Unified Device Architecture,统一计算设备架构)这一编程模型,是想在应用程序中充分利用CPU和GPU各自的优点。现在,该架构现已应用于GeForce®(精视™)、ION™(翼扬™)、Quadro以及Tesla GPU(图形处理器)上,对应用程序开发人员来说,这是一个巨大的市场。

  在消费级市场上,几乎每一款重要的消费级视频应用程序都已经使用CUDA加速或很快将会利用CUDA来加速,其中不乏Elemental Technologies公司、MotionDSP公司以及LoiLo公司的产品。

  在科研界,CUDA一直受到热捧。例如,CUDA现已能够对AMBER进行加速。AMBER是一款分子动力学模拟程序,全世界在学术界与制药企业中有超过60,000名研究人员使用该程序来加速新药的探索工作。

  在金融市场,Numerix以及CompatibL针对一款全新的对手风险应用程序发布了CUDA支持并取得了18倍速度提升。Numerix为近400家金融机构所广泛使用。

  CUDA的广泛应用造就了GPU计算专用Tesla GPU的崛起。全球财富五百强企业现在已经安装了700多个GPU集群,这些企业涉及各个领域,例如能源领域的斯伦贝谢与雪佛龙以及银行业的法国巴黎银行

  随着微软Windows 7与苹果Snow Leopard操作系统的问世,GPU计算必将成为主流。在这些全新的操作系统中,GPU将不仅仅是图形处理器,它还将成为所有应用程序均可使用的通用并行处理器。

Nvcc是一种编译器驱动,通过命令行选项可以在不同阶段启动不同的工具完成编译工作,其目的在于隐藏了复杂的CUDA编译细节,并且它不是一个特殊的CUDA编译驱动而是在模仿一般的通用编译驱动如gcc,它接受一定的传统编译选项如宏定义,库函数路径以及编译过程控制等。

不知不觉做CUDA程序已经有大半年的时间了,参加CUDA校园竞赛也得到一个优秀作品奖,也算是对自己的一个肯定,但是从来没有好好整理和记录自己的技术应用心得。由于最近实验室要求统一自己的CUDA软件开发环境,当然一个大问题就是编译选项如何配置。原先一直使用网上勇哥提供的向导自动配置好的环境,以及参照模仿SDK下的一些模板进行配置,这样就做了这么久,但是boss的意思是要我们自己更多的掌握,也就是要知其所以然。故我在自己的技术博客里将分几次把我从CUDA提供的关于nvcc里整理出来的一些东西贴出来!欢迎有兴趣的朋友交流讨论。那么就从CUDA编译流程的介绍开始吧!

不畏浮云遮望眼,只缘身在最高层!要真正了解CUDA程序的编译和像高手一样自如的进行nvcc配置,第一步在我看来,就是整体上把握CUDA程序的编译流程。所以开篇还是要看起来很务虚的介绍下基本的程序编译框架。

二、CUDA编译流程简介

Nvcc是一种编译器驱动,通过命令行选项可以在不同阶段启动不同的工具完成编译工作,其目的在于隐藏了复杂的CUDA编译细节,并且它不是一个特殊的CUDA编译驱动而是在模仿一般的通用编译驱动如gcc,它接受一定的传统编译选项如宏定义,库函数路径以及编译过程控制等。CUDA程序编译的路径会因在编译选项时设置的不同CUDA运行模式而不同,如模拟环境的设置等。nvcc封装了四种内部编译工具,即在C:\CUDA\bin目录下的nvopencc(C:\CUDA\open64\bin)ptxasfatbincudafeNvcc的工作流程说明如下:

首先是对输入的cu文件有一个预处理过程,这一过程包括的步骤有将该源文件里的宏以及相关引用文件扩展开,然后将预编译已经产生的与C有关的CUDA系统定义的宏扩展开,并合并分支编译的结果。

预处理后,首先将预处理的结果送给CUDA前端,即CUDAfe。通过CUDAfe分离源文件,然后调用不同的编译器分别编译。cudafe被称为CUDA frontend,会被调用两次,完成两个工作:一是将主机代码与设备代码分离,生成gpu文件,二是对gpu文件进行dead code analysis,传给nvopencc Nvopencc生成ptx文件传给ptxas,最后将cubinptx传给fatbin

同时,在编译阶段CUDA源代码对C语言所扩展的部分将被转成regular ANSI C的源文件,也就可以由一般的C编译器进行更多的编译和连接。也即是设备代码被编译成ptxparallel thread execution)代码或二进制代码,host代码则以C文件形式输出,在编译时可将设备代码链接到所生成的host代码,将其中的cubin对象作为全局初始化数据数组包含进来,但此时,kernel执行配置也要被转换为CUDA运行启动代码,以加载和启动编译后的kernel。在使用CUDA驱动API时,可单独执行ptx代码或cubin对象,而忽略nvcc编译得到的host代码。

另外,Nvcc的各个编译阶段以及行为是可以通过组合输入文件名和选项命令进行选择的。它是不区分输入文件类型的,如object, library or resource files,仅仅把当前要执行编译阶段需要的文件传递给linker


以上是我个人对CUDA编译过程的一个阐述,我想再结合下图就应该可以对CUDA的编译驱动nvcc有个整体的把握了吧!

在我们的工程文件编译选项的命令行下加入"C:\CUDA\bin\nvcc.exe",表示启动nvcc,要成功启动nvcc首先必须保证所需绑定的一般的C/C++编译器如VS下的cl,在编译器搜索路径范围内,一般默认目录下,如:

"C:\CUDA\bin\nvcc.exe" -ccbin "C:\Program Files\Microsoft Visual Studio 8\VC\bin"

又或是,

"$(CUDA_BIN_PATH)\nvcc.exe" -ccbin "$(VCInstallDir)bin"

三、Nvcc启动与配置说明

1nvcc的启动条件

在我们的工程文件编译选项的命令行下加入"C:\CUDA\bin\nvcc.exe",表示启动nvcc,要成功启动nvcc首先必须保证所需绑定的一般的C/C++编译器如VS下的cl,在编译器搜索路径范围内,一般默认目录下,如:

"C:\CUDA\bin\nvcc.exe" -ccbin "C:\Program Files\Microsoft Visual Studio 8\VC\bin",又或是,"$(CUDA_BIN_PATH)\nvcc.exe" -ccbin "$(VCInstallDir)bin"Windows平台下编译选项--compiler-bindir是当编译器cl不在默认路径时用来重新设定路径,另外Windows平台下,nvcc需要能定位到脚本vsvars.bat(这个脚本必须先于cl编译器执行,因为它将指定cl编译器正确运行所需的环境参数),也是通过--compiler-bindir来重新指定。

CUDA提供了一个用于检查是否是启动nvcc编译的宏:__CUDACC__

2nvccprofile文件

nvcc所在的目录里有一个Nvcc.profile,它定义了nvccCUDA中的目录结构,它对于nvcc使用者来说不像是作为一个配置文件,而是提供了指定CUDA Toolkit中的binlib等目录的参数变量如PATH, LD_LIBRARY_PATH等,打开这个文件可以看到如下的内容:

PATH += $(TOP)/extools/bin;$(TOP)/open64/bin;$(TOP)/bin;$(TOP)/lib;

INCLUDES += "-I$(TOP)/include" "-I$(TOP)/include/cudart" $(_SPACE_)

LIBRARIES =+ $(_SPACE_) "/LIBPATH:$(TOP)/lib" cudart.lib

CUDAFE_FLAGS +=

OPENCC_FLAGS +=

PTXAS_FLAGS +=

来简单介绍下里面的语法:

#是注释符

name= :赋值

name?= :条件赋值

name+= :首部增加

name=+ :尾部增加

可通过以下任一种风格对所要赋值的环境变量字符串进行赋值:

%name% :DOS style

$(name) :‘make’ style

其中,nvcc定义了一个指定该profile文件所在目录的宏_HERE_它会根据nvcc的调用方式返回绝对或相对路径。

同样,nvcc使用宏 _SPACE_来代表空格字符,使用如下:

INCLUDES += -I../common $(_SPACE_)

去掉宏_SPACE_就会造成粘合效应,如 ‘–I../common-Iapps’。还可以看到profile文件中使用+=$(_SPACE_)放在后面,而使用=+时则放在了前面,就是因为这个原因。

3nvcc启动所需要使用的几个变量

在第一个部分,已经说明了Nvcc在进行编译时的主要工作流程,以下几个变量就是nvcc自身控制各个编译阶段所需要使用的相关变量。

1compiler-bindir

用于指定主机编译器所在的目录,可以使用编译选项--compiler-bindir进行修改。

2INCLUDES

这个字符串扩展nvcc的命令选项–Xcompiler,它也定义一些附加的include目录,在实际的编译选项语法范围内,如Linux下的gcc语法或Windows下的cl语法。

3LIBRARIES

这个字符串扩展nvcc的命令选项–Xlinker,它也定义一些附加的库文件和库文件目录,在实际的编译选项语法范围内,如Linux下的gcc语法或Windows下的cl语法。

4PTXAS_FLAGS

这个字符串扩展nvcc的命令选项–Xptxas,它主要是给CUDA内部工具ptxas传递一些命令。

5CUDAFE_FLAGS

这个字符串扩展nvcc的命令选项-Xcudafe,它主要是给CUDA内部工具cudafe传递一些命令。

6OPENCC_FLAGS

这个字符串扩展nvcc的命令选项–Xopencc,它主要是给CUDA内部工具nvopencc传递一些命令。

注意:CUDA提供的几个内部工具在C:\CUDA\bin目录下分别是ptxasfatbincudafenvopencc(C:\CUDA\open64\bin)。另外在使用CUDA编程环境向导后可在VS2005中的项目-->属性-->CUDA-->backends中设置相关命令。

Nvcc的选项命令形式大概有以下3类:boolean (flag-)选项,单值选项和列表(multivalued-)选项。下面是使用规则举例: -o file -o=file -Idir1,dir2 -I=dir3 -I dir4,dir5 每一个选项命令都有两个名字,全称和简写,例如–I就是--include-path的简称,注本文后面的选项命令说明均只列出简称,详细见参考资料1。一般来说,全称多用于述,简称多用于实际使用。

四、Nvcc的命令选项的分析说明

Nvcc的选项命令形式大概有以下3类:boolean (flag-)选项,单值选项和列表(multivalued-)选项。

下面是使用规则举例:

-o file

-o=file

-Idir1,dir2 -I=dir3 -I dir4,dir5

每一个选项命令都有两个名字,全称和简写,例如–I就是--include-path的简称,注本文后面的选项命令说明均只列出简称,详细见参考资料1。一般来说,全称多用于述,简称多用于实际使用。

编译选项可按用途分为以下7大类:

1、指定编译阶段的选项

这类选项主要用来控制编译的阶段,用以制定哪些阶段的输入文件要被编译,-ptx-cuda-gpu等等。最经常用到的是-c,用来生成object文件。

2File and path specifications指定相关文件的路径及名称的命令选项

下面这几个用于文件和路径说明命令是我们最常见:

-o:指定输出文件的位置和名称。-o $(ConfigurationName)\$(InputName).obj $(InputFileName)表示将$(InputFileName)作为输入文件,$(ConfigurationName)\$(InputName).obj为输出路径及文件名

-include:指定预处理和编译时预先需要包含的头文件。

-l:指定链接时需要的库文件,另外这些库文件的搜索路径必须已经被命令选项'-L'指定。

-D:指定预处理和编译时需要的宏,如-D_DEBUG -D_CONSOLE

-U:取消一个宏定义

-I:指定包含文件的搜索路径,如 -I"C:\CUDA\include" -I"C:\ProgramData\NVIDIA Corporation\NVIDIA GPU Computing SDK\C\common\inc" -I表示依赖的库的路径,所以如果有时候一些dlllab文件不在默认路径下,在这里可以添加路径!或者"$(CUDA_BIN_PATH)\nvcc.exe" -ccbin "$(VCInstallDir)bin"

-isystemSpecify system include search paths.

-L:指定库文件的搜索路径.

-odir:指定输出文件的目录,这选项也为代码生成步骤指定合适的输出目录做准备,与命令选项--generate-dependencies直接相关,如-odir "Debug"

-ccbin:指定host编译器的所在路径,如-ccbin "$(VCInstallDir)bin"或者"C:\Program Files\Microsoft Visual Studio 8\VC\bin"

3、调整编译器和链接器行为的选项

-g:产生可调试代码,这是调试模式下是必需的

-G:产生可调试的设备代码

-O:产生优化代码,包括O0O1O2O3,用于产生不同的指令集,现在一般采用O0(为优化,好处在于使hostdevice的生成代码相异度最小,不容易出差),这些优化的具体内容我还未找到详细说明,基本上也就是展开一些循环,函数,优化一些访存指令。但并不是自动优化最好,因为有时会得不到正确的结果,所以需要有深的认识才使用。

-m:指定平台结构32 vs 64.一般可不用,常见的如-m 32

4、内部编译工具的控制命令选项

这个主要是为nvcc封装的五种内部编译工具传递一些行为说明的命令,可以通过基本命令-h,即显示工具的帮助命令了解更多。我暂时还没整理出来。另一个基本命令-V可以显示工具的版本信息。

例如在命令行下键入nvcc – Xptxas –h就可以显示ptx工具的帮助信息。

这里特别说明一个很实用的命令选项,-Xptxas –v:显示代码生成的统计结果,也就是会显示经过编译分析得到的device函数对寄存器和存储器的使用情况。如下例:

nvcc -Xptxas –v acos.cu

ptxas info : Compiling entry function 'acos_main'

ptxas info : Used 4 registers, 60+56 bytes lmem, 44+40 bytes smem,

20 bytes cmem[1], 12 bytes cmem[14]

这里对上例进行一个简单的解释,smem表示共享存储器,这个地方它被分成了两个部分,第一个表示总共声明的共享存储器大小,后者表示系统在存储段中分配的数据总量:共享存储器中的device函数参数块和局部存储器中的线程索引信息。cmem表示常量存储器的使用情况,这里就是使用了一个20bytes的变量和一个长度为14的单位12byte的数组-Xopencc -LIST:source=on:包含在工具ptx中产生的源文件。这些选项的配置可自行配置,也可以在CUDA开发环境向导里的属性->CUDA->backends进行配置。

5、对编译驱动进行引导的命令选项

这些选项主要用于对nvcc的行为提供指引,现阶段对我们比较有用的是keep选项。

-dryrun:不执行nvcc产生的编译命令而只是列出它们。

-v:列出nvcc产生的编译命令,不影响其执行。

-keep:保留各步骤产生的中间文件.

-save-temps:同--keep.

-noprof:不使用nvcc.profile文件引导编译

-clean:可逆转nvcc的行为,当其被指定时,所有编译阶段都不执行,而且所有nvcc在其它时候产生的非临时文件都将被删除,keep用于调试,clean则用于去掉那些调试文件。

-run-args:与-R联用用于指定运行时的命令行参数。

-idp: Windows平台下,所有的命令行参数相关的文件名在执行前都需要转成本地格式,这一选项当前的开发环境表示的绝对路径,如使用'-idp /cygwin/'为 CygWin 构建环境。

-ddp: Windows平台下,生成依赖文件(如选项-M),所有的文件名都需要转成'make' 使用实例可识别的,在Windows格式里有一些'make'实例对绝对路径中的冒号存在识别问题,这些依赖于'make' 被编译的环境,为一个CygWin make使用'-ddp /cygwin/'予以说明,或者就采用本地Windows格式而不进行说明。

-dp:指定 作为input-drive-prefix和dependency-drive-prefix.

6、驾驭CUDA编译方式的命令选项

这些主要用以控制CUDA的编译方式,如模拟形式等。没有太多需要注意的选项选择。

-deviceemu:生成用于GPGPU库 模拟的代码.

-use_fast_math:使用快速数学计算库.

-e:在ptx或gpu文件送入cubin编译阶段使用:指定哪些global函数必须生成代码,默认情况是所有global函数。

-host-compilation:指定CUDA源文件中host的语言类型C vs. C++,允许的参数有C, c, C++ and c++. 如--host-compilation C++

7、驾驭GPU代码生成的命令选项

这个部分的选项命令主要控制代码生成,比较复杂,但现阶段对我的作用不大,它主要用在外部链接,GPU的虚拟和实际平台代码生成,以及多GPU代码生成等方面。有一个基本原则:前一个阶段的虚拟代码版本必须低于后面的实际代码生成,对于GPU代码编译我会在继续了解CUDA底层的运行机制后在下一节给出说明。到此配置基本完成了如CUDA编译命令选项的全面分析说明。希望大家都明白如何进行nvcc的编译环境的配置。

五、一个完整的CUDA源代码实例

/**
* @Copyright(C)2011-2012 by Alfred Z.Zheng / Cindogo(R). All rights reserved.
* 
* Cindigo, an innovative student team in Huazhong Univ. of Sci. & Tech., Wuhan, P.R.C
*/

#include "../common/example.h"
#include "../common/cpu_bitmap.h"

#define DIM 1000

struct cuComplex{
	float r;
	float i;
	
	cuComplex(float a, float b) : r(a), i(b){}
	__device__ float magnitude2(void){
		return cuComplex(r * a.r - i * a.i, i * a.r + r * a.i);
	}
	__device__ cuComplex operator * (comst cuComplex & a){
		return cuComplex(r + a.r, i + a.i);
	}
};

__device__ int example(int x, int y){
	const float scale = 1.5;
	
	float ex = scale * (float)(DIM / 2 - x) / (DIM / 2);
	float ey = scale * (float)(DIM / 2 - y) / (DIM / 2);

	cuComplex c(-0.8, 0.156);
	cuComplex a(ex, ey);
	
	int i = 0;
	for(i = 0 ; i < 200 ; i++){
		a = a * a + c;
		if(a.magnitude2() > 1000){
			return 0;
		}
	}
	
	return 1;
}

__global__ void kernel(unsigned char *pstr){
	int x = blockIdx.x;
	int y = blockIdx.y;
	int offset = x + y * griDim.x;
	
	int exampleValue = example(x, y);
	
	pstr[offset * 4 + 0] = 255 * exampleValue;
	pstr[offset * 4 + 1] = 0;
	pstr[offset * 4 + 2] = 0;
	pstr[offset * 4 + 3] = 255;
}

int main(void){

	CPUBitmap bitmap(DIM, DIM);
	unsigned char *dev_bitmap;
	
	HANDLE_ERROR(cudaMalloc((void *)&dev_bitmap, bitmap.image_size()));
	
	dim3 grid(DIM, DIM);
	kernel<<>>(dev_bitmap);
	
	HANDLE_ERROR(cudaMemory(bitmap.get_pstr(), dev_bitmap, bitmap.image_size(), cudaMemoryDeviceToHost));
	
	bitmap.display_and_exit();
	
	HANDLE_ERROR(cudaFree(dev_bitmap));
}


推荐参考书籍:

《大规模并行处理器编程实战》(英伟达官方网站推荐书籍)


《CUDA范例精解》(英伟达官方网站推荐书籍)


《GPU高性能运算之CUDA》 (国内第一本全面介绍CUDA软硬件体系架构的书籍)

本篇博文的著作权信息如下:

你可能感兴趣的:(CUDA开发的配置方法与编译流程简介)