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. Fiber 树的构建

    我们先来看一个简单的 demo: import * as React from 'react'; import * as ReactDOM from 'react-dom'; class App ex ...

  2. ABP Framework V4.4 RC 新增功能介绍

    目录 新增功能概述 启动模板删除 EntityFrameworkCore.DbMigrations 项目 CMS-Kit 动态菜单管理 Razor引擎对文本模板的支持 DbContext/Entiti ...

  3. 《手把手教你》系列技巧篇(七)-java+ selenium自动化测试-宏哥带你全方位吊打Chrome启动过程(详细教程)

    1.简介 经过前边几篇文章和宏哥一起的学习,想必你已经知道了如何去查看Selenium相关接口或者方法.一般来说我们绝大多数看到的是已经封装好的接口,在查看接口源码的时候,你可以看到这个接口上边的注释 ...

  4. 暑假自学java第六天

    1,方法的覆盖:当子类继承父类,而子类中的方法与父类中方法的名称,返回类型及参数都完全一致时,就称子类中的方法覆盖了父类中的方法,有时也称方法的"重写" [不需要关键字] 2,th ...

  5. 资源:docker-compose下载路径

    docker-compose下载路径: compose所有版本:https://github.com/docker/compose/releases

  6. 在idea的控制台中中文显示为乱码

    显示乱码的原因不一定相同 我目前解决方法: -Dfile.encoding=UTF-8

  7. Redis 高级面试题

    Redis 持久化机制 Redis 是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来 保证数据持久化.当 Redis 重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目 ...

  8. 2013年第四届蓝桥杯C/C++程序设计本科B组省赛 马虎的算式

    题目描述 马虎的算式 小明是个急性子,上小学的时候经常把老师写在黑板上的题目抄错了. 有一次,老师出的题目是:36 x 495 = ? 他却给抄成了:396 x 45 = ? 但结果却很戏剧性,他的答 ...

  9. get和post两种表单提交方式的区别

    今天看到一篇博客谈论get和post区别,简单总结一下https://www.cnblogs.com/logsharing/p/8448446.html 要说两者的区别,接触过web开发的人基本上都能 ...

  10. ESP32智能配网笔记

    基于ESP-IDF4.1 #include <string.h> #include <stdlib.h> #include "freertos/FreeRTOS.h& ...