GPU&VS2012&CUDA&matlab&Arrayfire杂记(三)——cuda

突然想找到这个文章的链接,但是可惜找不到了忘记了版主是谁呢,但是这篇文章比较系统的介绍了CUDA方面的知识。(忘记说了我使用的电脑配置gtx970 和 980都用过,toolkit7.5)

1       CUDA C编程入门-介绍

1.1   从图形处理到通用并行计算

在实时、高清3D图形的巨大市场需求的驱动下,可编程的图形处理单元或者GPU发展成拥有巨大计算能力的和非常高的内存带宽的高度并行的、多线程的、多核处理器。如图1和图2所示。

  

图 1 CPU和GPU每秒的浮点计算次数

图 2 CPU和GPU的内存带宽

  在CPU和GPU之间在浮点计算能力上的差异的原因是GPU专做密集型计算和高度并行计算-恰好是图形渲染做的-因此设计成这样,更多的晶体管用于数据处理而不是数据缓存和流控制,如图3所示。

图 3 GPU将更多的晶体管用于数据处理

  更具体的说,GPU特别适合处理能表示为数据并行计算的问题-相同程序并行地在许多元素上执行-更高的计算强度-计算操作的比例相对于内存操作来说的。因为相同的程序在每个数据元素上执行,所以对于复杂的流控制只有比较低要求。因为在每个数据元素上执行和有高的计算强度,计算中的内存访问延迟被隐藏,从而替换大的数据缓存。

  数据并行处理映射数据元素到并行处理的线程(Thread)。许多处理大数据集的应用程序使用数据并行编程模型加速计算过程。在3D渲染,大型集合的像素和顶点映射到并行的线程中处理。同样地,图像和媒体处理应用程序,比如已渲染的图像的后处理、视频编码和解码、图像缩放、立体视觉和模式识别能映射图像的区域和像素点到并行的线程中计算。事实上,许多图像渲染和处理以外的领域能通过数据并行处理来加速,从一般的信号处理、物理仿真到计算金融学和计算生物学。

1.2.CUDA:一个通用的并行计算平台和编程模型

  在2006年11月,NVIDIA介绍CUDA,一个通用的并行计算平台和编程模型,利用在NVIDIA的GPUs的并行计算引擎,比CPU更有效的解决许多复杂的计算问题。CUDA自带的软件环境允许开发者使用C作为高级的编程语言。如图4所示,其它语言、应用程序接口、基于指令的方法的支持,比如FORTRAN、DirectCompute、OpenACC。

图 4GPU计算程序,CUDA设计成支持多种语言和应用程序接口

1.3.一个可伸缩的编程模型

  多核CPU和GPU的出现意味着主流的处理芯片现在是并行的系统。此外,其并行性继续按摩尔法则增加。开发应用软件的挑战是显式地缩放并行性去利用增加的处理器核心的数目。和3D图形应用显式地缩放其并行性到带有不同数目的多核心的GPUs一样。

  CUDA并行编程模型被设计去克服这样的挑战,并且对熟悉标准编程语言,比如C,保持较低的学习曲线。

  核心有3个关键的抽象:线程组的层次、共享内存和屏蔽同步-只是暴露给程序员一个最小的语言扩展集。

  这些抽象提供细粒度的数据并行和线程并行,嵌入进粗细度的数据并行和任务并行。指导程序员把问题划分成粗的能被独立地并行地在多块线程中处理的子问题,每个子问题被并行地在每一块的线程处理。(也就是说每个问题被划分成许多子问题,每个子问题在单独的一块线程中处理,子问题和子问题的处理是并行的,而每个子问题又是在一块线程中处理,这个处理也是并行的。)

  分解保持语言表达性,允许线程合作地解决每个子问题。同一时刻支持自动地可收缩。甚至,每块线程能被安排在GPU中的任何的空闲的多处理器,以任意的次序,同时地或者连续地,所以编译的CUDA程序能够执行在任何数量的多核心上,如图5。只有运行时系统需要知道物理的多处理器的个数。

  这可伸缩编程模型通过简单地伸缩多处理器的数目和内存划分而允许CPU结构跨越广阔的市场范围:从高性能爱好者-GeForceGPUs、专业的Quadro和Tesla计算产品到各种便宜的、主流的GeForce GPUs(查看支持CUDA的GPU章节)。

图 5 自动可扩展。

一个GPU由一个流处理器(SM)阵列组成(更多细节可查考硬件实现这一章节)。每个多线程程序被划分到各个线程块独立地执行,所以拥有更多处理核心的自动地比更少的处理核心执行所花费的时间更少。

 

2       Cuda编程模型:

CUDA编程模型将CPU作为主机,GPU作为协处理器(co-processor)或设备。在这个模型中,CPU负责逻辑性强的事务处理和串行计算,GPU则专注于高度线程化的并行处理任务。CPU、GPU各自拥有相互独立的存储器地址空间。

一旦确定了程序中的并行部分,就可以考虑把这部分计算工作交给GPU。

kernel:运行在GPU上的C函数称为kernel。一个kernel函数并不是一个完整的程序,而是整个CUDA程序中的一个可以被并行执行的步骤。当调用时,通过N个不同的CUDA线程执行N次。

一个完整的CUDA程序是由一系列的设备端kernel函数并行步骤和主机端的串行处理步骤共同组成的。

一个kernel函数中存在两个层次的并行,即Grid中的block间并行和block中的thread间并行。

2.1   硬件映射

计算单元

计算核心:GPU中有多个流多处理器(Stream Multiprocessor, SM),流多处理器即计算核心。每个流多处理器又包含8个标量流处理器(Stream Processor),以及少量的其他计算单元。SP 只是执行单元,并不是完整的处理核心。拥有完整前端的处理核心,必须包含取指、解码、分发逻辑和执行单元。隶属同一 SM 的8个 SP共用一套取指与射单元,也共用一块共享存储器。

CUDA 中的 kernel 函数是以 block 为单元执行的,同一 block 中的线程需要共享数据,因此必须在同一个 SM 中发射,而 block 中的每一个 thread 则被送到一个 SP 上执行。

一个 block 必须被分配到一个 SM 中,但一个 SM 中同一时刻可以有多个活动线程块(active block)在等待执行,即在一个 SM 中可同时存在多个 block 的上下文。在一个 SM 中放入多个线程块是为了隐藏延迟,更好地利用执行单元的资源。当一个 block 进行同步或访问显存等高延迟操作时,另一个 block 就可以“乘虚而入”,占用 GPU执行资源。

限制 SM 中活动线程块数量的因素包括:SM中的活动线程块数量不超过 8 个;所有活动线程块中的 warp 数之和在计算能力 1.0/1.1 设备中不超过 24,在计算能力 1.2/1.3 设备中不超过 32;所有活动线程块使用的寄存器和存储器之和不超过SM 中的资源限制。

线程结构(Thread Hierarchy)

CUDA中以线程网格(Grid)的形式组织,每个线程网格由若干个线程块(block)组成,而每个线程块又由若干个线程(thread)组成。

threadIdx:CUDA中使用了dim3类型的内建变量threadIdx和blockIdx。threadIdx是一个包含3个组件的向量,这样线程可以用一维、二维或三维线程索引进行识别,从而形成一个一维、二维或三维线程块。一个线程的索引和它的线程ID之间的关系非常直接:

对于一个一维的块,线程的threadIdx就是threadIdx.x;

对于一个二维的大小为(Dx,Dy)的块,线程的threadIdx就是(threadIdx.xthreadIdx.y * Dx);

对于一个三维的大小为(Dx,Dy,Dz)的块,线程的threadIdx是(threadIdx.xthreadIdx.y * Dx threadIdx.z * Dx * Dy)。

一个block中的线程数量不能超过512个。

在同一个block中的线程可以进行数据通信。CUDA中实现block内通信的方法是:在同一个block中的线程通过共享存储器(sharedmemory)交换数据,并通过栅栏同步保证线程间能够正确地共享数据。具体来说,可以在kernel函数中需要同步的位置调用__syncthreads()函数。

一个block中的所有thread在一个时刻执行指令并不一定相同。例如,在一个block中可能存在这样的情况:有些线程已经执行到第20条指令,而这时其他的线程只执行到第8条vkjsfdsvd第21条语句的位置通过共享存储器共享数据,那么只执行到第8条语句的线程中的数据可能还没来得及更新,就被交给其他线程去处理了,这会导致错误的计算结构。而调用__syncthreads()函数进行栅栏同步(barrier)以后,就可以确保只有当block中的每个线程都运行到第21条指令以后,程序才会继续向下进行。

每个线程块中的线程数量、共享存储器大小和寄存器数量都要受到处理核心硬件资源的限制,其原因是:

在GPU中,共享存储器与执行单元的物理距离必须很小,处于同一个处理核心中,以使得共享存储器的延迟尽可能小,从而保证线程块中的各个线程能够有效协作。

为了在硬件上用很小的代价就能实现__syncthreads()函数,一个block中所有线程的数据都必须交由同一处理核心进行处理。

3       Kernel函数的定义与调用

内核函数必须通过__global__函数类型限定符定义,并且只能在主机端代码中调用。在调用时,必须声明内核函数的执行参数。例如:

// Define kernel

__global__ void VecAdd(float * A, float *B, float * C)

{

int i = threadIdx.x;

C[i] = A[i] B[i];

}

int main

{

// Call kernel

VecAdd<<<1, N>>>(A, B,C);

}

必须先为Kernel中用到的数组或变量分配好足够的空间,再调用kernel函数。否则,在GPU计算时会发生错误。

在设备端运行的线程之间是并行执行的,其中的每个线程按指令的顺序串行执行一次kernel函数。每一个线程有自己的block ID和thread ID用于与其他线程相区分。blockID和thread ID只能在kernel中通过内建变量访问。内建变量是由设备中的专用寄存器提供的,是只读的,且只能在GPU端的kernel函数中调用。

 

Matlab 中调用C/C++程序或CUDA程序

3.1    kernels(核函数)

CUDA C扩展了C语言,允许程序员定义C函数,称为kernels(核函数)。并行地在N个CUDA线程中执行N次。  使用__global__说明符声明一个核函数,调用使用<<>>,并且指定执行的CUDA线程数目。执行的每个线程都有一个独一的ID,在核函数中可以通过变量threadIdx获取。  例子,两个向量的加,A加B,并把结果存入C,A、B和C的长度为N。

__global__ void addKernel(int *c, const int*a, const int *b) {

  int i = threadIdx.x;

  c[i] = a[i] + b[i];

}

 

int main() {

  ...

  // Launch a kernel onthe GPU with one thread for each element.

  addKernel<<<1,N>>>(c, a, b);

  ...

}

其中,每一个线程都会在数组中的每个元素上执行addKernel这个核函数。

3.2   线程层次

threadIdx是一个3元组,因此线程可以被一维、二维和三维的threadIdx标识,形成一维、二维和三维的线程块。

线程的索引和ID之间的关系:对于一维的线程块,索引和ID是相同的;对于大小为(Dx,Dy)的二维的线程块,索引为(x,y),而ID为x+y*Dx;对于大小为(Dx,Dy,Dz)的三维线程块,索引为(x,y,z),ID为(x+y*Dx+z*Dx*Dy)。

例子,   二维矩阵加,N*N大小的A和B相加,结果存入C。单个线程块

// Kernel definition

__global__ void MatAdd(float A[N][N], floatB[N][N], float C[N][N]) {

  int i = threadIdx.x;

  int j = threadIdx.y;

  C[i][j] = A[i][j] +B[i][j];

}

 

int main() {

  ...

  // Kernel invocationwith one block of N * N * 1 threads

  int numBlocks = 1;

  dim3threadsPerBlock(N, N);

  MatAdd<<<numBlocks,threadsPerBlock>>>(A, B, C);

  ...

}

其中,因为一个block的线程一般会在同一个处理核心中,并且共享有限的内存,所以一个块的线程的个数是有限制的。在当前的GPU,块中的线程数目最大为1024。

然而,一个核函数可以在多个相同大小的线程块中执行,所以总的线程数等于线程块的个数乘以每个线程块中线程的个数。

线程块被组织进一维、二维或者三维的grid中,如图6所示。

调用核函数的时候,可以通过<<>>指定每个线程块中线程的个数以及每个grid中线程块的个数,<<>>的类型可以为int和dim3。核函数中可以通过内建的变量blockIdx得到grid中的每个线程块的索引。同时可以通过blockDim获得每个线程块的维数。

例子,   扩展先前的矩阵加的例子为多个线程块的。

//Kernel definition

__global__void MatAdd(float A[N][N], float B[N][N], float C[N][N])

 {

  int i = blockIdx.x * blockDim.x + threadIdx.x;             // blockIdx.线程块索引,blockDim.x线程块中所启动的线程。

  int j = blockIdx.y * blockDim.y + threadIdx.y;

  if (i < N && j < N) C[i][j] = A[i][j] + B[i][j];

}

 

intmain() {

  ...

  // Kernel invocation

  dim3 threadsPerBlock(16, 16);//线程块的大小

  dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);

  MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);

  ...

}

其中,线程块的大小为16*16,总共有256个线程。在同一个线程块中线程可以通过shared memory(共享内存)共享数据。可以使用__syncthreads()函数同步线程对共享内存的数据访问。

3.3   内存层次

执行时,核函数可以获取多个内存空间中的数据,如图7所示。每个线程有自己的局部内存。每个线程块拥有共享内存空间,块中的每个线程都可以访问。还有执行核函数的每个线程都可以访问的全局内存空间。

 

另外,还有两个额外的只读的内存空间:常量和纹理内存空间。全局、常量和纹理内存空间为不同的内存用法做了最佳的优化。纹理模型提供多种的寻址方式。


图7 内存层次

3.4   异构编程

如图8所示,CUDA编程模型假设CUDA线程执行在一个与主机的C程序分离的装置上。核函数在GPU上执行,而其他的在CPU上执行。CUDA编程模型同时假设主机和设备独立操作它们在DRAM中的内存空间。因此,程序调用CUDA运行时(在编程接口这一章描述)管理全局、常量和纹理内存空间对核函数的访问性,运行时包括内存分配和回收、主机和设备之间的内存数据的拷贝等。

在主机上执行串行的代码,而GPU执行并行的核函数

图8 异构编程

3.5   计算能力

设备的计算能力定义为一个主版本号和一个次的修订号。

相同的主版本号的GPU拥有相同的核心架构。主版号为5的是Maxwell架构,3为Kepler架构,2为Fermi架构,1为Tesla架构。

次修订号相当于核的不断改进和新的特性。

支持CUDA的GPU这一章节列出支持CUDA的所有GPU的计算能力。计算能力这一章节给出每个计算能力的详细技术规格。

3.6   MEX文件

所有C/C++MEX 文件必须包含4项内容:

#include mex.h (for C and C++ MEX-fles)

每个MEX文件的入口程序称为mexFunction,这是MATLAB访问DLL等的入口点,在C/C++中,通常定义为:

mexFunction(int nlhs, mxArray *plhs[ ],intnrhs, const mxArray *prhs[ ]) { . }

这里,

nlhs = 预计的 mxArrays 数(左手边)

plhs = 预计的输出指针数组

nrhs = 输入数 (右手边)

prhs = 输入数据的指针数组,输入数据只读

mxArray: 这是一个包含MATLAB数据的特殊结构,它是MATLAB数组的C表示。所有类型的MATLAB数组(scalars, vectors, matrices, strings, cell arrays 等)都是mxArray。

API函数(如内存分配和释放)。

基本的CUDA MEX文件包括以下几部分内容

在GPU上分配内存。

将数据从主存移到GPU。注意MATLAB中的双精度浮点数会被转换成GPU中的单精度浮点数。

用CUDA代码处理数据。

将数据从GPU移回主机。

回收GPU中的内存。

编译MEX文件,调用C/C++程序

假设C代码的MEX文件名为cmex.c,则可从MATLAB提示行中执行mex命令编绎MEX文件:

>> mex cmex.c

该命令会生成一个编译了的MEX文件,其后缀取决于操作系统。

然后就可以在MATLAB中直接调用MEX文件中的函数了。注意,为了使MATLAB能运行C或C++函数,必须将编绎了的MES文件放在MATLAB路径的一个目录中,或是放在当前的工作目录。

编译基于CUDA的MEX文件

NVIDIA提供了nvmex工具和一个配置选项文件,可用于编绎基于CUDA的MEX文件(扩展名是.cu)。从MATLAB中编绎.cu文件的命令是:

nvmex -f nvmexopts.bat filename.cu-IC:\cuda\include -LC:\cuda\lib –lcudart

4       编程接口(CUDA C)

CUDA C给熟悉C编程语言的人提供一个简单的途径去编写在设备(GPU)上执行的代码。

  由一个最小的C语言的扩展集和运行时库组成。

核心的语言扩展在编程模型这一章节已经介绍过了。允许程序员定义核函数并且使用一些新的语法指定核函数每次运行时的grid和block的维数。可以在C语言扩展这个章节里找到扩展的完整描述。所有的含有这些扩展的源代码都需要使用nvcc编译,nvcc的概述可以查看使用nvcc编译这一小节。

  在CUDA C运行这一小节介绍运行时。运行时提供在主机执行的用于分配和回收设备内存、设备和主机内存之间传输数据、多个设备的管理等的C函数。可以在CUDA 查考手册中查看关于运行时的完整描述。

运行时由低级的C API-可被应用访问的CUDA驱动API,作为基础。驱动API通过低级的概念比如CUDA上下文-就像主机处理器的上下文一样、CUAD模块-就像设备动态加载库一样而提供额外级别的控制。大多数应用程序因为不需要额外级别的控制,所以不会使用驱动API,而是用运行时时,上下文和模块管理是隐式的,这样的结果是编写的代码就会简单明了。驱动API在驱动API这一章节介绍,完整的描述在参考手册。

4.1   使用NVCC编译(直接在dos界面转换成ptx文件)

可以使用一个叫PTX的CUDA指令集架构编写核函数,PTX的描述在PTX参考手册。然而一般使用更加有效的高级语言,比如C。这两种情况,核函数都必须通过nvcc编译成二进制代码,这样才能在设备上执行。

nvcc是一个编译器,简化编译C和PTX代码的流程:提供简单和熟悉的命令行选项,执行相关命令去调用实现不同编译阶段的工具集。这节给出nvcc工作流程和命令行选项的概述。可以在nvcc用户手册找到完整的描述。

4.1.1编译的工作流程
4.1.2离线编译

使用nvcc编译的源代码可以混有主机(在主机执行的)和设备(在设备执行的)的代码。ncvv的工作流程主要在于分离主机和设备的代码:  编译设备代码成装配形式(PTX代码)或者二进制形式(cubin对象)。修改主机代码:把调用核函数的<<<...>>>替换成必要的从PTX代码和cubin对象加载和运行的已经编译的核函数的CUDA C运行时函数。   修改的主机代码输出不是被另外的工具编译的C代码就是允许直接让nvcc调用主机编译器完成最后编译阶段的对象代码。    然后应用程序能:  链接已经编译的主机代码(大多数的情况下)或者忽略已经被修改的主机代码(如果有的话),使用CUDA驱动API加载和执行PTX或者cubin对象。

4.1.3 即时编译

   任何被应用程序在运行时加载的PTX代码都会被设备驱动编译成二进制代码。这就叫做即时编译。即时编译增加应用加载的时间,但是能使应用从新的设备驱动带的新的性能更好的编译器获得益处。只有这一条路能使那些在编译时没设备的应用在设备上运行,详细在应用兼容性这一小节描述。    当设备驱动程序为应用即时编译一些PTX代码时,为了避免应用再次调用时重复编译,会自动缓存生成的二进制代码的副本。缓存-指得是计算机缓存,当设备驱动更新时会自动无效,因此应用可以从新的的设备驱动的更加完善的新的即时编译器获得益处。    可用的控制即时编译的环境变量在CUDA环境变量这一章节里描述

4.1.3 二进制兼容性

   二进制代码是架构特有的,也就是说每种架构下运行的二进制是有差异有的,就像汇编一样。使用编译选项-code指定目标架构产生相应架构的cubin对象。例如,-code=sm_13编译选项生成计算能力为1.3的设备上运行的二进制代码。二进制兼容保证一个次版本号兼容下一个次版本号,不能兼容上一个次版本号或者跨主版本号。换句话说,计算能力X.y生成的cubin对象只能保证运行在计算能力为X.z(z>=y)的设备上。

4.1.4 PTX兼容性

   一些PTX指令只能支持高计算能力的设备。例如,全局内存的原子指令只能支持计算能力为1.1或者更高的设备;双精度指令只能支持计算能力为1.3或更高的设备。当将C编译成PTX是,可以使用-arch编译选项指定计算能力。所以比如含有双精度算法的代码编译时需要指定-arch=sm_13(或者更高的计算能力)选项,否则双精度的算法将会降级为单精度。    一些特殊计算能力的设备生成的PTX代码总是能够编译成更高或者相同计算能力的二进制代码。

 4.1.4 应用兼容

    在指定计算能力的设备上执行代码,应用需要加载在二进制兼容和PTX兼容这两小节描述的计算能力的二进制或者PTX代码。具体地说,为了使代码(那些还未生成二进制的代码)能执行在未来更高计算能力的架构上,应用需加载将为这些设备即时编译的PTX代码,因为二进制代码已经是设备架构相关的,所以只能是PTX代码。    CUDA C应用嵌入进去的PTX或者二进制代码的计算能力编译时可以是使用-arch和-code编译选项或者nvcc用户手册中描述的-gencode编译选项控制。例如,  nvcc x.cu     -gencode arch=compute_10,code=sm_10    -gencode arch=compute_11,code=\'compute_11,sm_11\'   第一个-gencode选项表示嵌入进的二进制代码兼容计算能力1.0,而第二个表示PTX和二进制代码兼容计算能力1.1。    生成的主机代码在运行时会自动地选择加载和执行最合适的代码,比如上面的例子,将会:  为1.0计算能力的设备生成1.0二进制代码为1.1、1.2、1.3计算能力的设备生成1.1二进制代码 编译1.1PTX代码时,为2.0或者更高计算能力的设备生成二进制代码  x.cu会有最优化的使用原子操作的代码路径,例如,只支持1.1或者更高计算能力的设备。__CUDA_ARCH__宏能被用于区分基于计算能力的不同的代码路径。只在设备代码中定义。比如当使用-arch=compute_11编译选项编译时,__CUDA_ARCH__等于110    使用驱动API的应用需编译代码去分开文件,在运行时明确地加载和执行最合适的文件(我猜,应该指的是编译器会分开主机和设备的代码,然后运行时CPU执行主机代码、GPU执行设备代码)。    nvcc用户手册列出-arch、-code和-gencode的各种的短命名。例如,-arch=sm_13是-arch=compute_13-code=compute_13,sm_13(-gencodearch=compute_13,code=\'compute_13,sm_13\'也可以)的短写。 

4.1.5 C/C++兼容性

   编译器前端基于C++语法规则处理CUDA源代码文件。主机代码支持所有的C++语法。然而设备代码只能支持C++语法的子集(C/C++这章节描述)。

 4.1.6 64位兼容性 

64位版本的nvcc使用64位模式(比如,指针是64位的)编译设备代码。64位模式编译的设备代码只支持64位模式编译的主机代码。    同样地,32位版本的nvcc使用32模式编译设备代码,32位模式编译的设备代码只能支持32模式编译的主机代码。    32版本的nvcc能使用-m64选项编译64位模式的设备代码。    64版本的nvcc能使用-m32选项编译32位模式的设备代码。

4.2   CUDA C编程入门-编程接口 CUDA C运行时

 在cudart库里实现了CUDA C运行时,应用可以链接静态库cudart.lib或者libcudart.a,动态库cudart.dll或者libcudart.so。动态链接cudart.dll或者libcudart.so的应用需要把CUDA的动态链接库(cudart.dll或者libcudart.so)包含到应用的安装包里。

  CUDA所有的运行时函数都以cuda为前缀。

  在异构编程这一章节提到,CUDA编程模型假设系统是由一个自带各自的内存的主机和设备组成。设备内存这一小节概述用于管理设备内存的运行时函数。

  共享内存这一小节说明在线程层次中提到的共享内存的用法以达到最大化性能。

  Page-Locked主机内存这节介绍page-locked内存,要求与核函数执行在主机和设备之间数据交换时同时发生。

  异步并行执行这节描述系统中在不同级别使用的异步并行执行的概念和API。

  多设备系统这节展示编程模型怎样扩展同样一个主机连接多个设备的系统。

  错误检查这节描述怎么合适地检查运行时产生的错误。

  调用堆栈这节提到用于管理CUDA C调用堆栈的运行时函数。

  纹理和曲面内存这节显示提供另外的访问设备内存的方法的纹理和曲面内存,同时它们也显示GPU纹理硬件的子集。

  图形互操作性介绍各种提供与两种主要的图形API-OpenGL和Direct3D的交互的运行时函数。

4.2.1      初始化

没有明确的运行时的初始化函数。运行时函数(更具体地说,除了设备的和参考手册的版本控制章节的函数)初次调用时会初始化。在运行时,一点需要记住的是定时的运行时和解释错误代码的函数会被调用。

在初始化期间,运行时为系统中的每个设备建立一个CUDA上下文(上下文这节有关于CUDA上下文的描述)。这个是设备primary上下文,被应用的所有主机线程共享。作为建立上下文的一部分,设备代码需要时会即时编译和加载进设备内存。这个所有在高级选项下发生,且运行时没有暴露主要的上下文给应用。

当一个主机线程调用cudaDeviceReset()销毁主机当前操作的设备的主要上下文。任何当前拥有这个设备的主机线程调用运行时函数时将会为这个设备建立一个新的主要的上下文。

4.2.2      设备内存

在异构编程中提到的,CUDA编程模型假设系统是由有自己独立内存的主机和设备组成的。核函数操作设备内存,因此运行时提供分配、回收和在主机和设备内存直接拷贝数据的函数。

  设备内存能被分配成线性的内存或者CUDA数组。

  CUDA数组是为纹理fetching优化的不透明的内存布局。在纹理和曲面内存这小节描述。

  线性的内存存在于计算能力为1.x的、32位地址空间的设备,更高计算能力为40位地址空间,所以可以通过指针各自地引用分配的空间,例如在一棵二叉树中。一般使用cudaMalloc()分配线性的空间,cudaFree()回收内存空间,cudaMemcpy()在主机和设备内存中拷贝数据。在向量加的核函数的代码例子中,需要在主机和设备之间拷贝向量。

#include "cuda_runtime.h"

#include"device_launch_parameters.h"

 

#include <stdio.h>

#include <stdlib.h>

 

// Device Code

__global__ void VecAdd(float *A, float *B,float*C, int N)

{

   int i = blockDim.x * blockIdx.x + threadIdx.x;

   if(i < N)

    {

       C[i] = A[i] + B[i];

    }

}

 

// Host Code

int main()

{

   int N = 10;

   size_t size = N * sizeof(float);

 

   //allocate input vectors in host memory

   float *host_A = (float*)malloc(size);

   float *host_B = (float*)malloc(size);

   float *host_C = (float*)malloc(size);

 

   for(int i = 0; i < N; ++i)

    {

       host_A[i] = i;

       host_B[i] = i;

       host_C[i] = 0;

    }

 

   //printf A

   printf("A:");

   for(int i = 0; i < N; ++i)

    {

       printf(" %.2f", host_A[i]);

    }

   printf("\n");

 

   //printf B

   printf("B:");

   for(int i = 0; i < N; ++i)

    {

       printf(" %.2f", host_B[i]);

    }

   printf("\n");

 

   //printf C

   printf("C:");

   for(int i = 0; i < N; ++i)

    {

       printf(" %.2f", host_C[i]);

    }

   printf("\n");

   //allocate vectors in device memory

   float *dev_A;

   cudaMalloc(&dev_A, size);

   float *dev_B;

   cudaMalloc(&dev_B, size);

   float *dev_C;

   cudaMalloc(&dev_C, size);

 

    //copy vectors from host memory to devicememory

   cudaMemcpy(dev_A, host_A, size, cudaMemcpyHostToDevice);

   cudaMemcpy(dev_B, host_B, size, cudaMemcpyHostToDevice);

 

   //invoke hernel

   int threadsPerBlock = 256;

   //因为N可能不能被threadsPerBlock整除,

   //如果能被整除的话,blocksPerGrid 可以设为N/threadsPerBlock,

   //如果不能整除,则N除threadsPerBlock余数可以为1到threadsPerBlock-1之间的数(包括端点1和threadsPerBlock-1)

   //那么要使blocksPerGrid*threadsPerBlock>=N,则可以设blocksPerGrid为(N +threadsPerBlock - 1) / threadsPerBlock

   int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;

   VecAdd<<<blocksPerGrid, threadsPerBlock>>>(dev_A,dev_B, dev_C, N);

 

   //copy result from device memory to hosy memory

   cudaMemcpy(host_C, dev_C, size, cudaMemcpyDeviceToHost);

 

   //printf C

   printf("C:");

   for(int i = 0; i < N; ++i)

    {

       printf(" %.2f", host_C[i]);

    }

   printf("\n");

 

   //free device memory

   cudaFree(dev_A);

   cudaFree(dev_B);

   cudaFree(dev_C);

 

   //free host memory

   free(host_A);

   free(host_B);

   free(host_C);

 

   return 0;

}

线性内存也能通过cudaMallocPitch()和cudaMalloc3D()函数分配。这些函数一般用于分配2维和3维的数组,并且适当地填充以满足在设备内存访问这一小节描述的对齐要求,所以保证访问行地址或者在2维数组和其它区域的设备内存之间拷贝数据(使用cudaMemcpy2D()和cudaMemcpy3D()函数)达到最好的性能。返回的pitch(或者stride)必须用于访问数组元素。下面代码是分配一个width*height的2维的浮点数值数组,并演示如何在设备代码中遍历数组元素:

// Host code

int width = 64, height = 64;

float* devPtr;

size_t pitch;

cudaMallocPitch(&devPtr, &pitch,width * sizeof(float),height);

MyKernel<<<100, 512>>>(devPtr, pitch, width,height);

// Device code

__global__ void MyKernel(float* devPtr,

size_t pitch, int width, int height)

{

for (int r = 0; r < height; ++r) {

    float* row =(float*)((char*)devPtr + r * pitch);

        for (int c = 0; c <width; ++c) {

            float element =row[c];

        }

    }

}

下面的代码分配一个width*height*depth的3维的浮点数值数组,并演示如何在设备代码中遍历数组元素:

// Host code

int width = 64, height = 64, depth = 64;

cudaExtent extent = make_cudaExtent(width * sizeof(float),

height, depth);

cudaPitchedPtr devPitchedPtr;

cudaMalloc3D(&devPitchedPtr, extent);

MyKernel<<<100, 512>>>(devPitchedPtr, width,height, depth);

// Device code

__global__ void MyKernel(cudaPitchedPtr devPitchedPtr,

int width, int height, int depth)

{

    char* devPtr =devPitchedPtr.ptr;

    size_t pitch =devPitchedPtr.pitch;

    size_t slicePitch = pitch* height;

    for (int z = 0; z <depth; ++z) {

        char* slice = devPtr +z * slicePitch;

        for (int y = 0; y <height; ++y) {

            float* row =(float*)(slice + y * pitch);

            for (int x = 0; x< width; ++x) {

                float element = row[x];

            }

        }

    }

}

参考手册列出了所有各种用于使用cudaMalloc分配的线性内存、使用cudaMallocPitch或者cudaMalloc3D分配的线性内存、CUDA数组和在全局和常量内存空间分配的变量拷贝内存的函数。

  下面的代码展示各种通过运行时的API访问全局变量:

__constant__ float constData[256];

float data[256];

cudaMemcpyToSymbol(constData, data, sizeof(data));

cudaMemcpyFromSymbol(data, constData, sizeof(data));

__device__ float devData;

float value = 3.14f;

cudaMemcpyToSymbol(devData, &value, sizeof(float));

__device__ float* devPointer;

float* ptr;

cudaMalloc(&ptr, 256 * sizeof(float));

cudaMemcpyToSymbol(devPointer, &ptr, sizeof(ptr));

cudaGetSymbolAddress()用于获得在全局内存空间分配的变量的地址指针。分配的内存的大小可以通过cudaGetSymbolSize()函数获得。

CUDA中如何在分配全局变量

我需要一个数组,动态分配,那么直接调用cudaMalloc来为a分配内存的话,是不行的。

具体做法如下:

int *tmp;

cudaMalloc((void **)&tmp, sizeof(int) * num);

cudaMemcpyToSymbol(a,&tmp, sizeof(int *),size_t(0),cudaMemcpyHostToDevice);

使用cudaMemcpyToSymbol来把一个动态分配的设备指针写入一个静态的符号。所以是sizeof(int *),只是把指针写给a。

4.2.3      共享内存

在变量类型限定符这节中,使用__shared__限定符分配共享内存。线程层次这节提到共享内存访问速度比全局内存快,细节描述在共享内存这节。以下面的矩阵乘例子说明,任何有机会使用共享内存访问替代全局内存就应该利用。下面的代码例子只是简单地实现矩阵乘,并为利用共享内存的优势。如图9所示,每个线程读取一行A和一列B,计算相应的元素C。因此从全局内存读取B.width次的A和A.height次的B(应该指的是每一行的A重复读取B.width次和每一列B重复读取A.height次)。

// Matrices are stored in row-major order:

// M(row, col) = *(M.elements + row *M.width + col)

typedef struct {

   int width;

   int height;

   float* elements;

} Matrix;

// Thread block size

#define BLOCK_SIZE 16

// Forward declaration of the matrixmultiplication kernel

__global__ void MatMulKernel(const Matrix,const Matrix, Matrix);

// Matrix multiplication - Host code

// Matrix dimensions are assumed to bemultiples of BLOCK_SIZE

void MatMul(const Matrix A, const Matrix B,Matrix C)

{

   // Load A and B to device memory

   Matrix d_A;

   d_A.width = A.width; d_A.height = A.height;

   size_t size = A.width * A.height * sizeof(float);

   cudaMalloc(&d_A.elements, size);

   cudaMemcpy(d_A.elements, A.elements, size,cudaMemcpyHostToDevice);

   Matrix d_B;

   d_B.width = B.width; d_B.height = B.height;

   size = B.width * B.height * sizeof(float);

   cudaMalloc(&d_B.elements, size);

   cudaMemcpy(d_B.elements, B.elements, size,cudaMemcpyHostToDevice);

   // Allocate C in device memory

   Matrix d_C;

   d_C.width = C.width; d_C.height = C.height;

   size = C.width * C.height * sizeof(float);

   cudaMalloc(&d_C.elements, size);

   // Invoke kernel

   dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);

   dim3 dimGrid(B.width / dimBlock.x, A.height / dimBlock.y);

   MatMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C);

   // Read C from device memory

   cudaMemcpy(C.elements, Cd.elements, size,

   cudaMemcpyDeviceToHost);

   // Free device memory

   cudaFree(d_A.elements);

   cudaFree(d_B.elements);

   cudaFree(d_C.elements);

}

// Matrix multiplication kernel called byMatMul()

__global__ void MatMulKernel(Matrix A,Matrix B, Matrix C)

{

   // Each thread computes one element of C

   // by accumulating results into Cvalue

   float Cvalue = 0;

   int row = blockIdx.y * blockDim.y + threadIdx.y;

   int col = blockIdx.x * blockDim.x + threadIdx.x;

   for (int e = 0; e < A.width; ++e)

       Cvalue += A.elements[row * A.width + e] * B.elements[e * B.width + col];

   C.elements[row * C.width + col] = Cvalue;

}

下面的代码例子是利用共享内存的优势的矩阵乘实现。实现中,每个线程block只计算C的一个子矩阵Csub,block中的每个线程计算Csub的每个元素。如图10所示,Csub矩阵等于两个矩形矩阵的乘积:维数为(A.width,block_size)的A的子矩阵与Csub有一样的行下标,维数为(block_size,A.width)的B的子矩阵有于Csub一样的列下标。为了适配设备的资源,两个矩形矩阵被分为许多维数为block_size的方块矩阵,Csub计算为这些方块矩阵的乘积的和。每个积在第一次计算的时候会从全局内存加载两个相应的方块矩阵到共享内存中,每个线程加载每个矩阵的一个元素,并计算乘积。每个线程把每次的积累加进一个寄存器,最后把结果写入全局内存。  通过这样的方块计算,由于只需从全局内存中读取(B.width/block_size)次的A和(A.height/block_size)次的B,我们利用快速的共享内存访问的优势并节约许多的全局内存的带宽。  向前面的代码中的矩阵类型中增加一个stride字段,因此子矩阵可以有效的表示为同样的类型。__device__函数用于获取、设置和建立矩阵的子矩阵。

// Matrices are stored in row-majororder:

// M(row, col) = *(M.elements + row* M.stride + col)

typedef struct {

   int width;

   int height;

   int stride;

   float* elements;

} Matrix;

// Get a matrix element

__device__ float GetElement(constMatrix A, int row, int col) {    

   return A.elements[row * A.stride + col];

}

 

// Set a matrix element

__device__ void SetElement(MatrixA, int row, int col, float value) {

   A.elements[row * A.stride + col] = value;

}

// Get the BLOCK_SIZExBLOCK_SIZEsub-matrix Asub of A that is

// located col sub-matrices to theright and row sub-matrices down

 // from the upper-left corner of A

__device__ MatrixGetSubMatrix(Matrix A, int row, int col) {

   Matrix Asub;

   Asub.width = BLOCK_SIZE;

   Asub.height = BLOCK_SIZE;

   Asub.stride = A.stride;

   Asub.elements = &A.elements[A.stride * BLOCK_SIZE * row + BLOCK_SIZE* col];

   return Asub;

}

// Thread block size

#define BLOCK_SIZE 16

// Forward declaration of thematrix multiplication kernel

__global__ void MatMulKernel(constMatrix, const Matrix, Matrix);

// Matrix multiplication - Hostcode

// Matrix dimensions are assumed tobe multiples of BLOCK_SIZE

void MatMul(const Matrix A, constMatrix B, Matrix C) {

// Load A and B to device memory

   Matrix d_A;

   d_A.width = d_A.stride = A.width;

   d_A.height = A.height;

   size_t size = A.width * A.height * sizeof(float);

   cudaMalloc(&d_A.elements, size);

   cudaMemcpy(d_A.elements, A.elements, size,         

   cudaMemcpyHostToDevice);

   Matrix d_B;

   d_B.width = d_B.stride = B.width;

   d_B.height = B.height;

   size = B.width * B.height * sizeof(float);

   cudaMalloc(&d_B.elements, size);

   cudaMemcpy(d_B.elements, B.elements, size,    

   cudaMemcpyHostToDevice);

   // Allocate C in device memory

   Matrix d_C;

   d_C.width = d_C.stride = C.width;

   d_C.height = C.height;

   size = C.width * C.height * sizeof(float);

   cudaMalloc(&d_C.elements, size);

   // Invoke kernel

   dim3 dimBlock(BLOCK_SIZE,BLOCK_SIZE);

   dim3 dimGrid(B.width / dimBlock.x, A.height / dimBlock.y);

   MatMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C);

   // Read C from device memory

   cudaMemcpy(C.elements, d_C.elements, size,    

   cudaMemcpyDeviceToHost);

   // Free device memory

   cudaFree(d_A.elements);

   cudaFree(d_B.elements);

   cudaFree(d_C.elements);

}

// Matrix multiplication kernelcalled by MatMul()

__global__ void MatMulKernel(MatrixA, Matrix B, Matrix C) {

   // Block row and column

   int blockRow = blockIdx.y;

   int blockCol = blockIdx.x;

   // Each thread block computes one sub-matrix Csub of C

   Matrix Csub = GetSubMatrix(C, blockRow, blockCol);

   // Each thread computes one element of Csub

   // by accumulating results into Cvalue

   float Cvalue = 0;

   // Thread row and column within Csub

   int row = threadIdx.y;

   int col = threadIdx.x;

   // Loop over all the sub-matrices of A and B that are

   // required to compute Csub

   // Multiply each pair of sub-matrices together

   // and accumulate the results

   for (int m = 0; m < (A.width / BLOCK_SIZE); ++m) {

        // Get sub-matrix Asub of A

        Matrix Asub = GetSubMatrix(A, blockRow,m);

        // Get sub-matrix Bsub of B

        Matrix Bsub = GetSubMatrix(B, m,blockCol);

        // Shared memory used to store Asub andBsub respectively

        __shared__ floatAs[BLOCK_SIZE][BLOCK_SIZE];

        __shared__ floatBs[BLOCK_SIZE][BLOCK_SIZE];

        // Load Asub and Bsub from devicememory to shared memory

        // Each thread loads one element ofeach sub-matrix

        As[row][col] = GetElement(Asub, row,col);

        Bs[row][col] = GetElement(Bsub, row,col);

        // Synchronize to make sure thesub-matrices are loaded

        // before starting the computation

        __syncthreads();

        // Multiply Asub and Bsub together

        for (int e = 0; e < BLOCK_SIZE; ++e)

            Cvalue += As[row][e] * Bs[e][col];

        // Synchronize to make sure that thepreceding

        // computation is done before loadingtwo new

        __syncthreads();

   }

   // Write Csub to device memory

   // Each thread writes one element

   SetElement(Csub, row, col, Cvalue);

}

4.2.4      Page-Locked主机内存

运行时提供允许用户page-locked的主机内存的函数(与之相对的是传统的使用malloc()分配的可调页的主机内存)。

cudaHostAlloc()和cudaFreeHost()分配和回收page-locked主机内存

cudaHostRegister() page-locked一段由malloc()分配的内存(参考手册中查看局限性)。

  使用page-locked内存有几点好处:

在page-locked主机内存和设备内存直接拷贝数据能与在异步并行执行这节提到的某些设备的核执行体一同执行。

某些设备上,page-locked主机内存能够映射到设备的内存的地址空间。消除映射内存这节提到的需要拷贝诸主机数据到设备或者从设备拷贝数据到主机。

在系统的前端总线上,如果主机分配的是page-locked内存,主机和设备间的带宽将更高,如果额外分配成在写合并内存这节描述的写合并内存,带宽将达到更高。

  然而,page-locked是一个稀缺的资源,所以在分配可调页的内存之前,分配page-locked内存开始将失败。另外,会减少操作系统的物理内存的分页数量,消耗太多的page-locked内存会降低系统整体的性能。

在page-locked内存API详细的文档中,有一个简单的零拷贝的CUDA例子。

4.2.4.1 Portable内存

一块page-locked内存能被系统中的所有设备所共享,但是,默认地,上面描述的使用page-locked内存的好处只能被已经块分配的那些共享设备利用的(在统一的虚拟地址空间这节描述的,如果有的话,所有的设备共享相同的地址空间)。需要分配的块通过从cudaHostAllocPortable、cudaHostAlloc()或者page-locked的从cudaHostRegisterPortable传递标识到cudaHostRegister()函数才能利用这个对所有设备可用的优势。

4.2.4.2 Write-Combining内存

默认下,分配的page-locked主机内存是可缓存的。通过传递cudaHostAllocWriteCombined标识给cudaHostAlloc(),可以分配成写合并的内存。写合并腾出主机的L1和L2缓存资源,给剩下的应用留下更多可用的缓存。另外,传输通过PCIExpress总线时,写合并内存不会被检查,这样可以使传输性能提高40%。从主机的写合并内存读取数据的速度会很慢,所以写合并一般用于主机写。

4.2.4.3 映射内存

在计算能力大于1.0以上的设备上,通过传递cudaHostAllocMapped标识给cudaHostAlloc()或者cudaHostRegisterMapped给cudaHostRegister(),page-locked主机内存块可以映射到设备内存的地址空间。因此这一的块一般拥有两个地址:一个由cudaHostAlloc()或者malloc()返回的主机内存,另一个为使用cudaHostGetDevicePointer()获得的设备内存,然后被用于在核函数中访问内存块。当主机和设备使用统一地址空间(统一虚拟地址空间这节提到)时,cudaHostAlloc()分配的指针是一个例外。

  在核函数中直接访问主机内存的有几点优势:

不需要在设备中分配内存块、在主机和设备之间拷贝数据、数据传输会在核函数需要时隐式的执行。、

不需要使用流在核执行与数据传输同时发生;源自核函数的数据传输会自动地与核函数执行同时发生。

由于映射的page-locked内存被主机和设备共享,所以应用需要使用流或者事件(查看异步并行执行这节)同步数据访问去避免任何潜在的写之后读、读之后写或者写之后写的危害。

  为了获取指向映射的page-locked内存的指针,在任何其他的CUDA调用之前,page-locked内存映射需要通过传递cudaDeviceMapHost标识调用cudaSetDeviceFlags()启用。否则,cudaHostGetDevicePointer()会返回错误。

  cudaHostGetDevicePointer()也会在设备不支持映射page-locked主机内存时返回错误。应用应该需要通过检测设备canMapHostMemory属性查询是有这个能力,支持映射page-locked主机内存的设备,canMapHostMemory属性为1。

注意:从主机或者设备的角度看,在映射的page-locked内存上的原子函数操作不是原子的。

4.2.5      异步并行执行

4.2.5.1 在主机和设备之间并行执行

为了方便在主机和设备之间并行执行,某些函数的调用是异步的:在设备完成任务之前,控制返回至主机线程。它们是:

l  调用核函数

l  同一设备内存的两个地址之间的内存拷贝

l  主机和设备之间的小于或者等于64KB的内存的拷贝

l  以Async后缀的函数执行的内存拷贝

l  调用内存设置的函数

程序员可以通过设置CUDA_LAUNCH_BLOCKING环境变量为1使得在系统运行的所有的CUDA应用禁止异步地执行核函数。这个特性只是提供用于调试而不是用于作为一个方法使软件产品可靠地运行。

l  在下列情况下,运行核函数是同步的:

l  在计算能力为1.x的设备上,应用通过一个调试器(cuda-gdb)或者内存检测器(cuda-memcheck,Nsight)运行的。

l  通过分析工具(Nsight,Visual Profiler)收集硬件计数

4.2.5.2 数据传输与核函数执行同时发生

计算能力为1.x或者更高的某些设备能与核函数执行时同时在page-locked主机内存和设备内存之间执行拷贝数据。应用可以通过检测asyncEngineCount设备属性查询是否具备这个能力,大于0表示这个设备支持这种能力。计算能力为1.x的设备仅支持不是通过cudaMallocPitch()分配的CUDA数组和2维数组的内存拷贝。 

4.2.5.3 同时发生的核函数执行

   计算能力为2.x或者更高的设备可以同时执行多个核函数。应用可以通过检测concurrentKernels设备属性来查询是否有这个能力,等于1时设备支持这样的能力。    在计算能力为3.5的设备上,设备能同时执行的核函数最大个数为32,而低的的为16。    一个CUDA上下文的核函数不能与另外的CUDA上下文的核函数同时执行。    使用许多纹理的或者大量的局部内存的核函数较少可能同时与其它的核函数同时执行。 

4.2.5.4 同时发生的数据传输

   计算能力为2.x或者更高的设备能同时执行从page-locked主机内存到设备内存和从设备内存到page-locked主机内存数据拷贝。应用可以检测asyncEngineCount设备属性来查询是否有这个能力,等于2时设备支持这样的能力。 

4.2.5.5 流 

  应用可以通过流管理并行。流就是按次序执行的一序列命令(可能由不同主机的线程提交的)。另一方面,不同的流可以以相对次序或者并行地执行命令;这样的行为没有保障,也不能依赖于这样的行为来保持准确性(内部的核函数之间的通信是未定义的)。

4.2.5.5.1 创建和销毁 

  通过创建流对象来定义流,在调用核函数或者内存拷贝函数时,传递流作为参数。下面的代码创建两个流对象,分配float类型的page-locked内存hostPtr。

cudaStream_tstream[2];

for (int i = 0;i < 2; ++i)

    cudaStreamCreate(&stream[i]);

float* hostPtr;

cudaMallocHost(&hostPtr,2 * size);

每个流以参数的形式传递给内存拷贝和核函数调用:

for (int i = 0;i < 2; ++i) {

    cudaMemcpyAsync(inputDevPtr + i * size,hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]);

    MyKernel <<<100, 512, 0,stream[i]>>>(outputDevPtr + i * size, inputDevPtr + i * size, size);

    cudaMemcpyAsync(hostPtr + i * size,outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);

}

每个流拷贝输入数组hostPtr的一部分数据到设备内存的数组inputDevPtr中,调用MyKernel()核函数处理inputDevPtr数组里的数据,然后把核函数的结果outputDevPtr的数据拷贝到hostPtr中去。同时发生的行为这节描述流依靠设备的能力怎样在这样的例子中同时发生。注意,hostPtr必须指向page-locked主机内存,这样核函数调用和数据拷贝才能同时发生。

 

  调用cudaStreamDestroy()函数销毁流对象。

for (int i = 0;i < 2; ++i)

    cudaStreamDestroy(stream[i]);

cudaStreamDestroy()在销毁流对象之前会等待给定流完成先前的命令,并把控制返回给主机线程。

4.2.5.5.2 默认的流

核函数调用和主机-设备的内存拷贝不会指定流参数,或者相当于设置默认流参数为0。因此它们(核函数、内存拷贝)按次序执行。

4.2.5.5.3 显式同步

有很多的方法可以显式同步流和流。

  cudaDeviceSynchronize()等待主机所有的线程的所有流执行完命令。

  cudaStreamSynchronize()传递一个流作为参数,等待给定流的命令完成。用于同步主机和指定的流,允许其它的流继续在设备上执行。

  cudaStreamWaitEvent()传递一个流和事件作为参数,调用cudaStreamWaitEvent()延迟流所有的命令执行直到传递的事件发生。这个流可以为0,这样情况下,调用cudaStreamWaitEvent()后,添加到所有的流的命令会等待事件发生。

  cudaStreamQuery()提供应用查询一个流的所有的命令是否执行完成。

  为了避免不必要的运行的速度降低,所有同步函数一般最好用于定时,或者不能阻止核函数调用或者内存拷贝。

3.2.5.5.4 隐式同步

来自不同流的两个命令,如果在下面的两个命令之间的任何一个被主机线程调用的操作发生,不能同时运行:

l  分配page-locked主机内存

l  分配设备内存

l  设置设备内存

l  同一设备内存的不同地址的之间的内存拷贝

l  默认流的任何CUDA命令

l  L1和共享内存的配置的转换

计算能力为3.0或者以下的、支持并发的核函数执行的设备,任何操作需要检查一个流的核函数调用是否执行完成:

l  在CUDA上下文的任何流的先前调用的核函数的所有线程块开始执行时,任何操作才可以开始执行。

l  在核函数调用完成之后,CUDA上下文中的任何流的后面的核函数才能调用。

需要独立检测的操作包括检测在同一流的任何其它的命令是否执行和任何在流上调用cudaStreamQuery()。因此,应用需要遵循下面的指导改进潜在的核函数并行:

l  在不是独立的操作之前需要调用所有独立的操作。

l  任何类型的同步应该越晚调用越好。        

4.2.5.5.5 同时发生的行为

无论设备是否支持数据拷贝与核函数、核函数与核函数和数据拷贝与数据拷贝同时执行,两个之间的同时执行的核函数数量取决于提交给每个流的命令的次序。

比如,在不支持数据拷贝和数据拷贝同时发生,创建和销毁这节的代码中两个流根本不会同时执行,因为提交给stream[1]的主机到设备的内存拷贝,在之后,提交给stream[0]的设备到主机的内存拷贝,stream[1]这个流只会在stream[0]执行完毕之后再执行。如果代码写成下面的方式(假设设备支持数据拷贝和核函数同时执行):

for (int i = 0;i < 2; ++i)

    cudaMemcpyAsync(inputDevPtr + i * size,hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]);

for (int i = 0;i < 2; ++i)

    MyKernel<<<100, 512, 0,stream[i]>>>(outputDevPtr + i * size, inputDevPtr + i * size, size);

for (int i = 0;i < 2; ++i)

    cudaMemcpyAsync(hostPtr + i * size, outputDevPtr+ i * size, size, cudaMemcpyDeviceToHost, stream[i]);

这样,stream[0]和stream[1]会同时执行。

  在支持数据拷贝和数据拷贝同时执行的设备上,即使把核函数调用提交给stream[0](设备支持核函数和数据拷贝同时执行),创建和销毁这节的代码中两个流会也同时执行。然而,计算能力为3.0或者以下的设备,核函数不会同时执行。如果代码写成上面的样子,核函数会同时执行。

4.2.5.5.6 回调函数

运行时提供一个cudaStreamAddCallback()函数可以在流中任何地方插入一个回调函数。回调函数是在主机执行的。

下面的代码示例添加一个叫MyCallback的回调函数到每个流中:

void CUDART_CBMyCallback(cudaStream_t stream, cudaError_t status, void *data){

    printf("Inside callback %d\n",(size_t)data);

}

...

for (size_t i =0; i < 2; ++i) {

    cudaMemcpyAsync(devPtrIn[i], hostPtr[i],size, cudaMemcpyHostToDevice, stream[i]);

    MyKernel<<<100, 512, 0,stream[i]>>>(devPtrOut[i], devPtrIn[i], size);

    cudaMemcpyAsync(hostPtr[i], devPtrOut[i],size, cudaMemcpyDeviceToHost, stream[i]);

    cudaStreamAddCallback(stream[i],MyCallback, (void*)i, 0);

}

任何在回调函数之后提交给流命令只会在回调函数执行完成之后才能执行。cudaStreamAddCallback()的最后一个参数保留。

  回调函数不能调用CUDA API(间接地或者直接地),否则可能会陷入死锁。

4.2.5.5.7 流的优先级

使用cudaStreamCreateWithPriority()在创建流指定优先级。允许的优先级范围为[ highest priority, lowest priority ],可以通过cudaDeviceGetStreamPriorityRange()获得。运行时,低优先级的会等待高优先级的blocks。

下面的代码获得当前设备的允许的优先级范围,并创建可用的高优先级和低优先级的流:

// get the rangeof stream priorities for this device

intpriority_high, priority_low;

cudaDeviceGetStreamPriorityRange(&priority_low,&priority_high);

// createstreams with highest and lowest available priorities

cudaStream_tst_high, st_low;

cudaStreamCreateWithPriority(&st_high,cudaStreamNonBlocking, &priority_high);

cudaStreamCreateWithPriority(&st_low,cudaStreamNonBlocking, &priority_low);

3.2.5.6 事件

 运行时同时也通过监视设备运行的方法,比如执行精确的时间,让应用在程序的任何点异步地标记事件,查询事件什么时候完成。事件当所有的任务、指定的、给定流的所有命令完成后完成。在流0的事件会在先前的所有任务和所有流的命令执行完成之后完成。

3.2.5.6.1 创建和销毁

创建事件:

 

cudaEvent_t start, stop;

cudaEventCreate(&start);

cudaEventCreate(&stop);

  销毁事件:

cudaEventDestroy(start);

cudaEventDestroy(stop);

3.2.5.6.2 运行时间

用于计时的代码例子:

cudaEventRecord(start, 0);

for (int i = 0; i < 2; ++i) {

   cudaMemcpyAsync(inputDev + i * size, inputHost + i * size, size,cudaMemcpyHostToDevice, stream[i]);

   MyKernel<<<100, 512, 0, stream[i]>>>(outputDev + i *size, inputDev + i * size, size);

   cudaMemcpyAsync(outputHost + i * size, outputDev + i * size, size,cudaMemcpyDeviceToHost, stream[i]);

}

cudaEventRecord(stop, 0);

cudaEventSynchronize(stop);

float elapsedTime;

cudaEventElapsedTime(&elapsedTime,start, stop);

3.2.5.7 同步调用

 

  当一个同步的函数被调用时,在设备执行好任务之前控制不会返回到主机线程。在任何被主机线程调用的CUDA调用执行之前,无论主机线程将会yield(产生)、block(阻塞)或者spin(不知道怎么翻译),可以通过传递一些特定的标识调用cudaSetDeviceFlags()函数。

 

3.2.6 多设备系统

 

3.2.6.1 设备枚举

 

一个主机可以有多个设备,下面的代码显示怎样枚举这些设备,查询属性和确定支持CUDA的设备数目。

int deviceCount;

cudaGetDeviceCount(&deviceCount);

int device;

for (device = 0;device < deviceCount; ++device) {

    cudaDeviceProp deviceProp;

    cudaGetDeviceProperties(&deviceProp,device);

    printf("Device %d has computecapability %d.%d.\n", device, deviceProp.major, deviceProp.minor);

}

3.2.6.2 设备选择

 

  主机可以在任何时候通过调用cudaSetDevice()函数设置当前操作的设备。设备内存分配和核函数调用作用于当前设置的设备。在当前相关的设备中创建流和事件。如果没有调用cudaSetDevice()函数,当前的设备默认为设备0。

 

  下面的代码演示怎么样设置当前设备用于内存分配和执行核函数。

size_t size =1024 * sizeof(float);

cudaSetDevice(0);// Set device 0 as current

float* p0;

cudaMalloc(&p0,size); // Allocate memory on device 0

MyKernel<<<1000,128>>>(p0); // Launch kernel on device 0

cudaSetDevice(1);// Set device 1 as current

float* p1;

cudaMalloc(&p1,size); // Allocate memory on device 1

MyKernel<<<1000,128>>>(p1); // Launch kernel on device 1

3.2.6.3 流和事件的行为

 

  下面的代码说明提交核函数给流没有与之相关的设备,核函数执行会失败。

cudaSetDevice(0);// Set device 0 as current

cudaStream_t s0;

cudaStreamCreate(&s0);// Create stream s0 on device 0

MyKernel<<<100,64, 0, s0>>>(); // Launch kernel on device 0 in s0

cudaSetDevice(1);// Set device 1 as current

cudaStream_t s1;

cudaStreamCreate(&s1);// Create stream s1 on device 1

MyKernel<<<100,64, 0, s1>>>(); // Launch kernel on device 1 in s1

// This kernellaunch will fail:

MyKernel<<<100,64, 0, s0>>>(); // Launch kernel on device 1 in s0

提交没有与之相关的设备的流的内存拷贝,执行会成功。

  当事件和输入的流不相关,cudaEventRecord()执行会失败。

  当输入的两个事件关联不同的设备,cudaEventElapsedTime()会执行会失败。

  当前的设备与事件关联的设备不同,cudaEventSynchronize()和cudaEventQuery()也会执行成功。

  输入流和事件关联到不同的设备,cudaStreamWaitEvent()也会执行成功。cudaStreamWaitEvent()能用于同步多个设备。

  每个设备有自己默认的流,提交给设备的默认流的命令可能乱序或者同时地与提交给任何其它的设备的默认流的命令执行。

3.2.6.4 点对点内存访问

  当一个应用运行在64位处理器,计算能力为2.0或者更高的Tesla系列的设备可以访问其它设备的内存。只有cudaDeviceCanAccessPeer()返回true的两个设备上支持这样的特性。

  像下面代码说明一样,必须调用cudaDeviceEnablePeerAccess()函数启用点对点内存访问。

  统一的地址空间被用于两个设备,所以同个指针能引用两个设备上的内存。

3.2.6.5 点对点内存拷贝

  可以在两个设备之间执行内存拷贝。当统一地址空间被用于两个设备,可以定期地调用拷贝函数。

  另外,可以使用cudaMemcpyPeer()、cudaMemcpyPeerAsync()、cudaMemcpy3DPeer()或者cudaMemcpy3DPeerAsync()函数。

cudaSetDevice(0);// Set device 0 as current

float* p0;

size_t size =1024 * sizeof(float);

cudaMalloc(&p0,size); // Allocate memory on device 0

cudaSetDevice(1);// Set device 1 as current

float* p1;

cudaMalloc(&p1,size); // Allocate memory on device 1

cudaSetDevice(0);// Set device 0 as current

MyKernel<<<1000,128>>>(p0); // Launch kernel on device 0

cudaSetDevice(1);// Set device 1 as current

cudaMemcpyPeer(p1,1, p0, 0, size); // Copy p0 to p1

MyKernel<<<1000,128>>>(p1); // Launch kernel on device 1

不同设备之间的内存拷贝(在隐式的流0):

 

l  先前提交给的两个设备之一的所有命令完成之前,拷贝不会执行

l  之后的,拷贝先执行完成。

  与正常的流行为一致的,两个设备内存之间异步拷贝可能会与其它流拷贝或者核函数执行同时发生。

 

  注意,可以通过调用cudaDeviceEnablePeerAccess()函数启用点对点的内存访问,两设备之间的点对点内存拷贝不再需要通过主机,并且更快。

3.2.7 统一虚拟地址空间

 

  当一个应用运行在64位的处理器上时,只有单一的地址空间被用于主机和计算能力为2.0或者以上的设备。这个地址空间通过调用cudaHostAlloc()函数,用于所有的在主机内存的分配,调用cudaMalloc*()函数分配设备内存。可以调用你cudaPointerGetAttributes()获取一个指针是指向主机还是设备的内存。结论:

 

当拷贝从或者到一个使用统一地址空间的设备内存时,cudaMemcpy*()函数的cudaMemcpyKind参数是无效的,能被设置为cudaMemcpyDefault

通过cudaHostAlloc()分配的内存自动portable跨那些使用统一地址空间的所有设备,并且cudaHostAlloc()返回的指针能被用于其它设备运行的核函数(不在需要使用cudaHostGetDevicePointer()获得一个设备指针)。

  应用可以检测unifiedAddressing设备属性来查看一个具体的设备是否使用统一地址空间。

 

3.2.8 进程间通信

 

  主机线程创建的任何设备内存指针或者事件句柄能直接被任何其它在同一处理器的线程使用。因为在其它的核心无效,所以不能直接得被属于其它处理器的线程所引用。

 

  为了在不同处理器上共享设备内存指针和事件,应用必需使用进程间通信的API。IPC API只支持Linux上的64位的处理器,并且计算能力在2.0级以上的设备上。

 

  通过使用cudaIpcGetMemHandle()函数,应用能够得到设备内存指针的IPC句柄,使用标准的IPC机制传递句柄给另外的处理器(进程共享内存或者文件),并且使用cudaIpcOpenMemHandle()从IPC句柄中获得在其它进程合法的指向设备的指针。事件句柄也能使用同样的方式共享。

 

3.2.9 错误检查

 

  所有的运行时函数都回返回以恶搞错误代码,但对于异步函数,由于函数在设备完成任务之前返回,所以这个错误代码不能表示异步函数是否有错误发生。这个错误码只能表示执行任务之前在主机发生的错误,典型地比如与无效参数相关;如果异步函数发生错误,将在后来无关的运行时函数调用被表示。

  唯一检查异步错误的方法是在异步调用之后紧接着调用cudaDeviceSynchronize()同步,检查cudaDeviceSynchronize()函数的返回码。

  运行时会给每个主机线程维持一个错误码的变量,并被初始化为cudaSuccess,每次错误发生时都会被重写。cudaPeekAtLastError()返回这个错误码。cudaGetLastError()返回错误码并且设置成为码为cudaSuccess。

  核函数调用不返回任何错误码,cudaPeekAtLastError()和cudaGetLastError()之后返回核函数调用之前发生的错误。

  确保调用cudaPeekAtLastError()或者cudaGetLastError()返回的错误码不是来源于调用核函数先前的,只需要确保在调用核函数之前运行时错误码为cudaSuccess就行,比如在调用核函数之前调用cudaGetLastError()。核函数调用是异步的,所以为了检查异步的错误码,应用必需在核函数调用和cudaPeekAtLastError()或者cudaGetLastError()之间做同步。

  注意cudaStreamQuery()和cudaEventQuery()可能会返回cudaErrorNotReady,cudaErrorNotReady不会被认为是一个错误,因此不会被cudaPeekAtLastError()或者cudaGetLastError()表示。

 

3.2.10 调用堆栈

 

  计算能力大于等于2.x的设备,可以通过调用cudaDeviceGetLimit()获得调用栈大小,调用cudaDeviceSetLimit()设置调用栈大小。当调用栈溢出时,通过CUDA调试器(cuda-gdb、Nsight)的应用调用核函数将失败并产生一个栈溢出的错误或者未指定的加载错误或者其它错误。

 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值