看了CUDA大概有10多天了,这里对学习内容做一个总结,主要参考的是《GPU高性能编程CUDA实战》这本书。
CUDA架构是有英伟达(NVIDIA)提出的,因此只能在配备英伟达显卡的电脑上使用,具体哪些型号的显卡支持CUDA,支持那一代的CUDA可以参考CUDA支持显卡列表。
我是在windows下使用CUDA,只要从官网下好toolkit安装好就可了,不需要过多的配置。如果是在linux下进行开发,可以参考Ubuntu12.04配置NVIDIA cuda5.5经验贴
CUDA C是C语言的一个扩展,是在C语言基础上添加了一些CUDA专用的语法,关键字。
为了方便描述,把CPU称为主机,把显卡称为设备,主机上的内存就称为内存,显卡上的缓存称为显存。
核函数以__global__修饰,告诉编译器这是需要设备来执行的代码,例如:
__global__ void kernel()
{
printf("Hello, world!");
}
核函数和主机执行的函数在传递参数方面没有什么不一样,看下面向量相加的代码:
__global__ void add(int *a, int *b, int *c)
{
int i = threadIdx.x+blockIdx.x*blockDim.x;
c[i] = a[i]+b[i];
}
a,b是两个输入向量,c是输出向量,大括号内的代码是执行部分。由于代码在GPU中是多线程同时执行,每个线程都有核函数的一份拷贝,因此每个线程要找到自己计算的数据,上边大括号内第一行就是计算当前线程需要计算的数据的下标,第二行就是把对应位置的两个数加起来存放在向量c中。这一段代码假设了线程数量和向量长度是相等的。
在内存里分配空间可以直接用malloc就可以了,后边介绍页锁定内存的时候在内存分配空间还可以用cudaHostAlloc(), 在显存里分配空间要用专门的函数cudaMalloc(),当然,销毁显存用的是cudaFree();
把内存分配的空间拷贝到显存需要用到cudaMemcpy函数,比如:
float *a;
a = (float *)malloc(sizeof(float)*10);
float d_a;
cudaMalloc((void **)&d_a, sizeof(float)*10);
cudaMemcpy(d_a, a, sizeof(float)*10, cudaMemcpyHostToDevice);
do something......
free(a);
cudaFree(d_a);
cudaMemcpyHostToDevice表示数据的流向,从内存到显存;当数据在GPU处理完毕需要拷回内存,cudaMemcpyDeviceToHost表示数据从显存到内存。
这一部分以向量求和为例介绍
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include
#define N 1024
__global__ void kernel(int *a, int *b, int *c)
{
int i = threadIdx.x;
c[i] = a[i] = b[i];
}
void genData(int *d)
{
for(int i = 0; i < N; i++)
{
d[i] = rand()%10;
}
}
int main()
{
//内存分配空间,并赋值
int *a, *b, *c;
int size = sizeof(int)*N;
a = (int *)malloc(size);
b = (int *)malloc(size);
c = (int *)malloc(size);
genData(a);
genData(b);
//显存分配空间,然后把内存空间中的数据拷贝到显存
int *d_a, *d_b, *d_c
cudaMalloc((void **)&d_a, size);
cudaMalloc((void **)&d_b, size);
cudaMalloc((void **)&d_c, size);
cudaMemcpy(d_a, a, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, b, size, cudaMemcpyHostToDevice);
//制定GPU中用到多少个block,每个block有多少个threads
int blocksPerGrid = 1;
int threadsPerBlock = N;
//执行核函数,每个threads都有一份核函数的拷贝
kernel<<>>(d_a, d_b, d_c);
//执行完成后把显存中的数据拷贝回内存
cudaMemcpy(c, d_c, size, cudaMemcpyDeviceToHost);
for(int i = 0; i < N; i++)
{
printf("%d "; c[i]);
}
//释放存储空间
free(a);
free(b);
free(c);
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
}
kernel<<>>(d_a, d_b, d_c)
<<<>>>内的数据表示在GPU中使用多少个block,每个block启动多少个threads
如果要在GPU中开辟一个二维的block,那么核函数中索引当前thread处理的数据时要用到
int x = threadIdx.x + blokcIdx.x*blockDim.x;
int y = threadIdx. + blockIdx.y*blockDim.y;
int offset = x + y*blockDim.x*gridDim.x;
上一章提到blocks,线程间协作就是在block内进行的
__shared__关键字表示该buffer是在block内共享的,GPU上的每个block将创建这样一个buffer然后再内部共享,看下边的例子
//求内积
__global__ void kernel(int *a, int *b, int *c)
{
__shared__ temp[BLOCK_SIZE];
int i = threadIdx.x + blockIdx.x*blockDim.x;
int val = 0;
while(i < N)
{
val += a[i] * b[i];
i+=blockDim.x*gridDim.x;
}
temp[threadIdx.x] = val;
__syncthreads();
//归约求和,适用于blockDim为2的指数的情况
int i = blockDim.x/2;
while(i != 0)
{
if(threadIdx.x < i)
threadIdx.x += temp[threadIdx.x+i];
__syncthreads();
i/=2;
}
}
GPU中的block和thread都不是无限多个;因此,如果需要处理的数据超过限制就需要一个thread处理多组数据,就像上边一样。
求内积应用中,需要对所有元素的乘积加起来,每个线程把相对应的两个元素加起来,然后再把所有的乘积加起来;可以在block内部建立一个共享缓存,把所有的乘积放在缓存里,然后再进行相加。
__syncthreads();起一个阻隔作用,就是block内的所有线程,执行到这个语句的时候都会停止,等待block内的所有线程都到达这一语句的时候大家都才开始执行后面的的语句。
顾名思义,常量内存是程序执行期间不会发生改变的内存
constant关键字修饰的变量在执行期间是不能变动的。
使用常量内存可以节约带宽,因为程序不需要频繁的去全局显存读数据,具体原因如下:
1. 对常量内存的单次读操作可以广播到其他“临近”线程(warp内的线程)
2. 常量内存的数据将缓存起来,因此对相同地址的连续操作将不会产生额外内存通信量。
缺点:
不能滥用常量,如果warp内的线程要访问常量内存中不同位置的数据,那么这些访问会串行化,从而降低效率
atomicAdd()函数
流的作用是任务之间的并行化
把要计算的数据分块,让不同的流执行不同的数据块,这样stream1执行数据块1的时候,stream2把数据块2从内存拷贝的显存,这样做不用等到所有数据拷贝完之后再计算,加大的并行的力度。
以上这些是我这些天看的内容,时间有限,仅仅是做一个粗糙的记录,索引一下自己的知识点。
所有的代码都托管在github上了。
深入浅出CUDA编程
CUDA C programing guide
CSDN-MarkDown编辑器使用手册
接下来要学习的CUDA资料
“programming_massively_parallel_processors”