1.GPU编程模型及基本步骤

cuda程序的基本步骤如下:

  • 在cpu中初始化数据
  • 将输入transfer到GPU中
  • 利用分配好的grid和block启动kernel函数
  • 将计算结果transfer到CPU中
  • 释放申请的内存空间

从上面的步骤可以看出,一个CUDA程序主要包含两部分,第一部分运行在CPU上,称作Host code,主要负责完成复杂的指令;第二部分运行在GPU上,称作Device code,主要负责并行地完成大量的简单指令(如数值计算);

2.基本设施

运行在GPU中地函数称作kernel,该函数有这么几个要求:

  • 声明时在返回类型前需要添加"__globol__"的标识
  • 返回值只能是void
__global__ void addKernel(int *c, const int *a, const int *b)
{
int i = threadIdx.x;
c[i] = a[i] + b[i];
}

这就是一个合规的核函数。

除了声明时的不同,和函数的调用也是不一样的,需要以 “kernel_name <<< >>>();”的形式调用。而在尖括号中间,则是定义了启用了多少个GPU核,学习这一参数的使用,我们还需要知道下面几个概念:

  • dim3:一种数据类型,包含x,y,z三个int 类型的成员,在初始化时一个dim3类型的变量时,成员值默认为1
  • grid : 一个grid中包含多个block
  • block: 一个block包含多个thread

我们以一种更抽象的方式来理解GPU中程序的运行方式的话,可以这么看:

GPU中的每个核可以独立的运行一个线程,那我们就使用thread来代表GPU中的核,但一个GPU中的核数量很多,就需要有更高级的结构对全部用到的核进行约束、管理,这就是block(块),一个块中可以包含多个核,并且这些核在逻辑上的排布可以是三维的,在一个块中我们可以使用一个dim3类型的量threadIdx来表示每个核所处的位置,threadIdx.x、threadIdx.y、threadIdx.z分别表示在三个维度上的坐标;此外,每个块还带有一个dim3类型的属性blockDim,blockDim.x、blockDim.y、blockDim.z分别表示该block三个维度上各有多少个核,这个block中的总核数为blockDim.x * blockDim.y * blockDim.z;

我们一次使用的多个block,最好能使用一个容器把他们都包起来,这就是grid,类比于上文中thread和block的关系,block和grid也有相似的关系。我们使用blockIdx.x、blockIdx.y、blockIdx.z表示每个block在grid中的位置;同样,grid也具有gridDim.x、gridDim.y和gridDim.z三个属性以及三者相乘的总block数。

知道了上面这些知识后,我们可以对“kernel_name <<< >>>();”中尖括号中的参数做一个更具体的解释,它应该被定义为在GPU中执行这一核函数的所有核的组织形式,以"kernel_name <<< number_of_blocks, thread_per_block>>> (arguments)"的形式使用,一个典型的示例如下:

int nx = 16;
int ny = 4;
dim3 block(8, 2); // z默认为1
dim3 grid(nx/8, ny/2);
addKernel << <grid, block >> >(c, a, b);

这一示例中创建了一个有(2*2)个block的grid,每个block中有(8*2)个thread,下图给出了更直观的表述:

需要注意的是,对block、grid的尺寸定义并不是没有限制的,一个GPU中的核的数量同样是有限制的。对于一个block来说,总的核数不得超过1024,x、y维度都不得超过1024,z维度不得超过64,如下图

对于整个grid而言,x维度上不得有超过\(2^{32}-1\)个thread,注意这里是thread而不是block,在其y维度和z维度上thread数量不得超过65536.

在cuda编程中我们经常会把数组的每一个元素分别放到单独的一个核中处理,我们可以利用核的索引读取数组中的数据进行操作,但由于block、grid的存在,索引的获取需要一定的计算,在exercise2中给出了一个3D模型中取值的训练,实现如下

__global__ void print_array(int *input)
{
int tid = (blockDim.x*blockDim.y)*threadIdx.z + blockDim.x*threadIdx.y + threadIdx.x;
int xoffset = blockDim.x * blockDim.y * blockDim.z;
int yoffset = blockDim.x * blockDim.y * blockDim.z * gridDim.x;
int zoffset = blockDim.x * blockDim.y * blockDim.z * gridDim.x * gridDim.y;
int gid = zoffset * blockIdx.z + yoffset * blockIdx.y + xoffset * blockIdx.x + tid;
printf("blockIdx.x : %d, blockIdx.y : %d, blockIdx.z : %d,gid : %d, value: %d\n", blockIdx.x, blockIdx.y, blockIdx.z, gid, input[gid]);
}

3.数据在host和device之间的迁移

我们前边提到,cuda的编程步骤是将数据移入GPU,待计算完成后将其取出,官方对可能涉及到的内存操作类的操作都给出了接口。

首先是cudaMemCpy函数,其定义为

cudaError_t cudaMemcpy ( void* dst, const void* src, size_t count, cudaMemcpyKind kind )

该函数是将数据从CPU移入到GPU或者从GPU移出到CPU中,参数0指向目标区域的地址,参数1指向数据的源地址,参数2表示要移动的数据的字节数,最后一个参数表示数据的移动方向(cudaMemcpyHostToDevice、cudaMemcpyDeviceToHost或cudaMemcpyDeviceToDevice)

此外,对应C语言的内存空间操作,cuda也推出了CudaMalloc, CudaMemset, CudaFree三个接口

cudaError_t  cudaMalloc ( void** devPtr, size_t size );
cudaError_t cudaMemset ( void* devPtr, int value, size_t count );
cudaError_t cudaFree ( void* devPtr );

这里需要注意的一个点是cudaMalloc的第一参数的数据类型为void**,这一点怎么理解呢?

这里我们结合一个示例进行解释:

int *d_input;
cudaMalloc((void **) &d_input, bytesize);

之所以使用void,是因为这一步只管分配内存,不考虑如何解释指针,所以只需要传入待分配内存的地址,不需要传入具体的类型,其他API中的 void* 也是同理。为什么是两个*呢,这是因为我们在定义d_input时是定义了主存中的一个指针,它指向主存中的一个地址;而&d_input则是取得了存储该指针值的地址,cudaMalloc利用这一地址将在GPU中分配给该缓冲区的首地址赋值给d_input。

利用上述的几个接口函数,我们就可以实现一个基本的cuda程序的主函数:

int main()
{
const int arraySize = 64;
const int byteSize = arraySize * sizeof(int); int *h_input,*d_input;
h_input = (int*)malloc(byteSize);
cudaMalloc((void **)&d_input,byteSize); srand((unsigned)time(NULL));
for (int i = 0; i < 64; ++i)
{
if(h_input[i] != NULL)h_input[i] = (int)rand()& 0xff;
} cudaMemcpy(d_input, h_input, byteSize, cudaMemcpyHostToDevice); int nx = 4, ny = 4, nz = 4;
dim3 block(2, 2, 2);
dim3 grid(nx/2, ny/2, nz/2);
print_array << < grid, block >> > (d_input);
cudaDeviceSynchronize(); cudaFree(d_input);
free(h_input); return 0;
}

其中 cudaDeviceSynchronize(); 的作用是在此处等待GPU中计算完成后再继续执行后续的代码。

4 错误处理

在C++中,可以使用异常机制处理运行时错误,而cuda编程中由于Host和Device共同使用,难以利用异常机制,因此,cuda提供了检测运行时错误的机制。

看上面的API时会发现,每个函数的返回值类型都是 cudaError_t ,这正是cuda提供的错误检测机制,如果返回值是cudaSuccess则说明执行正确,否则就是出现了错误。可以使用 cudaGetErrorString( error )获取返回值的代表的错误的文本。前面的代码中没有使用这一机制主要是为了便于阅读,但实际的使用中这一机制是必不可少的,也会看到VS生成的demo代码中就包含着大量的错误检测代码

	cudaStatus = cudaSetDevice(0);
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaSetDevice failed! Do you have a CUDA-capable GPU installed?");
goto Error;
} cudaStatus = cudaMalloc((void**)&dev_c, size * sizeof(int));
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaMalloc failed!");
goto Error;
}
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaMalloc failed!");
goto Error;
} cudaStatus = cudaMalloc((void**)&dev_a, size * sizeof(int));
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaMalloc failed!");
goto Error;
}
...
...

5 其他

  1. 不同的block_size计算耗时会不同,可以多尝试后选择计算的更快的参数(学DL的调参是吧,这也搞黑盒?);考虑GPU的计算时间时要考虑数据移入移出GPU的时间。

  2. 不同的GPU有不同的性质,设备中也可能存在多个GPU,在设计程序时需要考虑这些问题,cuda也提供了访问这些信息的接口

    // 获取设备数量
    int deviceCount = 0;
    cudaGetDeviceCount(&deviceCount); //获取第一个设备的各项性质
    int devNo = 0;
    cudaDeviceProp iProp;
    cudaGetDeviceProperties(&iprop, devNo);

CUDA学习笔记-1: CUDA编程概览的更多相关文章

  1. 孙鑫VC学习笔记:多线程编程

    孙鑫VC学习笔记:多线程编程 SkySeraph Dec 11st 2010  HQU Email:zgzhaobo@gmail.com    QQ:452728574 Latest Modified ...

  2. Hadoop学习笔记(7) ——高级编程

    Hadoop学习笔记(7) ——高级编程 从前面的学习中,我们了解到了MapReduce整个过程需要经过以下几个步骤: 1.输入(input):将输入数据分成一个个split,并将split进一步拆成 ...

  3. WCF学习笔记之事务编程

    WCF学习笔记之事务编程 一:WCF事务设置 事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元: WCF通过System.ServiceModel.TransactionFlowA ...

  4. java学习笔记15--多线程编程基础2

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡 ...

  5. CUDA学习笔记(三)——CUDA内存

    转自:http://blog.sina.com.cn/s/blog_48b9e1f90100fm5f.html 结合lec07_intro_cuda.pptx学习 内存类型 CGMA: Compute ...

  6. java学习笔记14--多线程编程基础1

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为 ...

  7. Python学习笔记6 函数式编程_20170619

    廖雪峰python3学习笔记: # 高阶函数 将函数作为参数传入,这样的函数就是高阶函数(有点像C++的函数指针) def add(x, y): return x+y def mins(x, y): ...

  8. CUDA学习笔记(一)——CUDA编程模型

    转自:http://blog.sina.com.cn/s/blog_48b9e1f90100fm56.html CUDA的代码分成两部分,一部分在host(CPU)上运行,是普通的C代码:另一部分在d ...

  9. CUDA学习笔记1

    最近要做三维重建就学习一下cuda的一些使用. CUDA并行变成的基本四路是把一个很大的任务划分成N个简单重复的操作,创建N个线程分别执行. CPU和GPU,有各自的存储空间: Host, CPU a ...

随机推荐

  1. MaterialDesignInXamlToolkit“无法绑定到目标方法,因其签名或安全透明度与委托类型的签名或安全透明度不兼容”异常的解决思路

    前言: 最初是想解答园友"小代码大世界 "的问题,后来想举一反三,将这个问题简单剖析下,做到知其所以然. MaterialDesignInXAML 控件库高度封装,有一些控件在使用 ...

  2. 低代码开发LCDP,Power Apps系列 - 搭建入职选购电脑设备案例

    低代码简介 上世纪八十年代,美国就有一些公司和实验室开始了可视化编程的研究,做出了4GL"第四代编程语言",到后来衍生成VPL"Visual Programming La ...

  3. C语言:C99 中的37个关键字

    一.数据类型关键字(12个): 1.char [tʃɑ:]:声明字符型变量或函数 2.double [ˈdʌbəl] :声明双精度变量或函数 3.enum :声明枚举类型 4.float [fləut ...

  4. Linux-Jumpserver服务

    1.介绍 Jumpserver是一款由python编写开源的跳板机(堡垒机)系统,实现了跳板机应有的功能.基于ssh协议来管理,客户端无需安装agent. 特点: 完全开源,GPL授权 Python编 ...

  5. 17、mysql主从同步Last_IO_Errno错误代码说明

    登录mysql从库:mysql> show slave status\G; Last_IO_Errno:1005:创建表失败 1006:创建数据库失败 1007:数据库已存在,创建数据库失败 1 ...

  6. ORA-12560: 解决TNS:协议适配器错误

    1)安装成功,但无法连接数据库 2)网上查找原因:32位的不能运行64位的oracle,而且不会有64位的版本 3)解决办法:大致是修改客户端数据库为32位的(此方法OK) (1)解压instantc ...

  7. React中使用react-file-viewer,实现预览office文件(pdf,word,xlsx等文件)前端实现

    最近做一个项目要求在前端浏览器可以直接打开office文件(pdf,doc,xlsx等文件).pdf浏览器可以直接打开(可以直接用a标签href="文件地址"或者iframe标签s ...

  8. 使用Retrofit上传图片

    Retrofit使用协程发送请求参考文章 :https://www.cnblogs.com/sw-code/p/14451921.html 导入依赖 app的build文件中加入: implement ...

  9. Linux守护进程列表/守护进程

      在linux或者unix操作系统中在系统引导的时候会开启很多服务,这些服务就叫做守护进程.为了增加灵活性,root可以选择系统开启的模式,这些模式叫做运行级别,每一种运行级别以一定的方式配置系统. ...

  10. 利用bmob平台,使用云端逻辑在Xcode上实现用户注册、登录

    思路:bmob上构建云端逻辑,xcode通过http请求来在不引入bmob SDK的情况下,远程操作bmob上构建的数据库,实现注册.登录. xcode导入 AFNetWorking--------- ...