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. Springboot集成Spring Security实现JWT认证

    我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 Spring Security作为成熟且强大的安全框架,得到许多大厂的青睐.而作为前后端分离的SSO方案,JWT ...

  2. 玩转STM32MP157-开发环境搭建

    (一)STM32MP 1.什么是 STM32MPU STM32MPU是 ST 推出的 Cortex-A7 + Cortex-M4 多核异构处理器 STM32MPU151 是单核 A7+M4,.STM3 ...

  3. Qt:报文接收不完成,产生分帧的处理方法

    最近在设备的测试工装时,通过串口,向设备发送自定义规约,其报文的枕结构已经编写,使用串口助手调试,设备可正常回复,但是通过工装,接收报文会不完整,导致解析失败.使用qDebug打印出来却发现数据被分成 ...

  4. excel VBA中正则模块vbscript.regexp的用法

    一.是一个对象,用于执行 正则表达式! 二.有三个属性:      1. Global属性: True or False, 指明模式是匹配整个字符串中所有与之相符的地方还是只匹配第一次出现的地方.默认 ...

  5. 『无为则无心』Python序列 — 19、Python列表的其他操作(切片和遍历)

    目录 1.通过切片对列表的操作 (1)通过切片对列表进行修改 (2)通过切片对列表进行删除 (3)注意 2.列表的循环遍历 (1)while循环遍历 (2)for循环遍历 3.列表嵌套 4.综合示例 ...

  6. 41、mysql数据库(存储过程)

    0.创建表: (1)建表: CREATE TABLE blog ( id INT PRIMARY KEY auto_increment, name CHAR(64), sub_time datetim ...

  7. 基于uniapp自定义Navbar+Tabbar组件「兼容H5+小程序+App端Nvue」

    uni-app跨端自定义navbar+tabbar组件|沉浸式导航条|仿咸鱼凸起标签栏 在跨端项目开发中,uniapp是个不错的框架.采用vue.js和小程序语法结构,使得入门开发更容易.拥有非常丰富 ...

  8. JavaScript编写计算器的发展史

    JavaScript编写计算器的发展史: 编写一个普通的四则运算: <!DOCTYPE html> <html lang="en"> <head> ...

  9. .NET Core 对象池的使用

    昨天在『.NET 大牛之路』技术群和大家聊到了对象池的话题,今天展开详细讲讲这个知识点. 池这个概念大家都很熟悉,比如我们经常听到数据库连接池和线程池.它是一种基于使用预先分配资源集合的性能优化思想. ...

  10. C++智能指针之shared_ptr与右值引用(详细)

    1. 介绍 在 C++ 中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露.解决这个问题最有效的方法是使用智能指针(smart pointer).智能指针是存储指向动态分配(堆)对象指针 ...