CUDA学习笔记-1: CUDA编程概览
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 其他
不同的block_size计算耗时会不同,可以多尝试后选择计算的更快的参数(学DL的调参是吧,这也搞黑盒?);考虑GPU的计算时间时要考虑数据移入移出GPU的时间。
不同的GPU有不同的性质,设备中也可能存在多个GPU,在设计程序时需要考虑这些问题,cuda也提供了访问这些信息的接口
// 获取设备数量
int deviceCount = 0;
cudaGetDeviceCount(&deviceCount); //获取第一个设备的各项性质
int devNo = 0;
cudaDeviceProp iProp;
cudaGetDeviceProperties(&iprop, devNo);
CUDA学习笔记-1: CUDA编程概览的更多相关文章
- 孙鑫VC学习笔记:多线程编程
孙鑫VC学习笔记:多线程编程 SkySeraph Dec 11st 2010 HQU Email:zgzhaobo@gmail.com QQ:452728574 Latest Modified ...
- Hadoop学习笔记(7) ——高级编程
Hadoop学习笔记(7) ——高级编程 从前面的学习中,我们了解到了MapReduce整个过程需要经过以下几个步骤: 1.输入(input):将输入数据分成一个个split,并将split进一步拆成 ...
- WCF学习笔记之事务编程
WCF学习笔记之事务编程 一:WCF事务设置 事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元: WCF通过System.ServiceModel.TransactionFlowA ...
- java学习笔记15--多线程编程基础2
本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡 ...
- CUDA学习笔记(三)——CUDA内存
转自:http://blog.sina.com.cn/s/blog_48b9e1f90100fm5f.html 结合lec07_intro_cuda.pptx学习 内存类型 CGMA: Compute ...
- java学习笔记14--多线程编程基础1
本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为 ...
- Python学习笔记6 函数式编程_20170619
廖雪峰python3学习笔记: # 高阶函数 将函数作为参数传入,这样的函数就是高阶函数(有点像C++的函数指针) def add(x, y): return x+y def mins(x, y): ...
- CUDA学习笔记(一)——CUDA编程模型
转自:http://blog.sina.com.cn/s/blog_48b9e1f90100fm56.html CUDA的代码分成两部分,一部分在host(CPU)上运行,是普通的C代码:另一部分在d ...
- CUDA学习笔记1
最近要做三维重建就学习一下cuda的一些使用. CUDA并行变成的基本四路是把一个很大的任务划分成N个简单重复的操作,创建N个线程分别执行. CPU和GPU,有各自的存储空间: Host, CPU a ...
随机推荐
- 如何使用 jest 和 lint-staged 只检测发生改动的文件
我们现在在推进 EPC 的过程中,单元测试是必备的技能,在本地的 Git commit 之前进行单测非常有必要,总不能把所有的单测的压力都放在流水线上. 毕竟在流水线运行单测的成本还是挺高的,从 pu ...
- Fiber 树的构建
我们先来看一个简单的 demo: import * as React from 'react'; import * as ReactDOM from 'react-dom'; class App ex ...
- 理解vertical-align
vertical-align 支持的属性值及组成 inherit 线类baseline, top, middle, bottom 文本类text-top, text-bottom 上标下标类sub, ...
- js笔记16
动画 (1)css样式提供了运动 过渡的属性transition 从一种情况到另一种情况叫过渡 transition:attr time linear delay attr 是变化的属性 ...
- ShardingSphere 看这一篇就够了
1.什么是shardingSphere Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC.Proxy 和 Sidecar(规划中)这 ...
- Linux中ls的用法
在linux系统中,可以说一切皆文件.文件类型包含:普通文件,目录,字符设备文件,块设备文件,符号链接文件等 我们可以用file这个命令来查看文件的属性: 这里可以看到1.sh是个脚本文件 下面开始介 ...
- Linux:监测收集linux服务器性能数据工具Sysstat的使用与安装
Sysstat是一个工具集,包括sar.pidstat.iostat.mpstat.sadf.sadc.其中sar是其中最强大,也是最能符合我们测试要求的工具,同时pidstat也是非常有用的东东,因 ...
- shell 中的for循环
第一类:数字性循环 #!/bin/bash for((i=1;i<=10;i++)); do echo $(expr $i \* 3 + 1); done #!/bin/bash for i i ...
- 在jsp中显示List中的数据
<% ArrayList list = (ArrayList)request.getAttribute("class"); for(int i = 0; i < lis ...
- Rust 与 Golang - 何时使用它们?
[转自 Fizer Khan的<Rust Vs Golang - When to use them?>(翻译)] 在过去的十年中,Rust 和 Go 两种新的编程语言主要为企业开发而开发和 ...