0%

CUDA学习笔记

CUDA 学习笔记

主要参考《GPU高性能编程CUDA实战》

一般将CPU及其系统的内存称为主机,将GPU及其内存称为设备。
在GPU设备上执行的函数通常称为核函数(Kernal);

一个简单的程序:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

__global__ void kernel(void){
}

int main(void){
kernel<<<1,1>>>();
printf("Hello world\n");
return 0;
}

global修饰符告诉编译器,函数应该被编译为在设备上而不是在主机上运行。于是,kernel()函数将被交给编译设备代码的编译器,而main()函数将被交给主机编译器。
尖括号表示要将一些参数传递给运行时系统,用以告诉运行时如何启动设备代码。

再来看一个更复杂的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <cuda_runtime.h>
__global__ void add( int a, int b, int *c ) {
*c = a + b;
}
int main( void ) {
int c;
int *dev_c;
//cudaMalloc()
cudaMalloc( (void**)&dev_c, sizeof(int) );
//核函数执行
add<<<1,1>>>( 2, 7, dev_c );
//cudaMemcpy()
cudaMemcpy( &c, dev_c, sizeof(int),cudaMemcpyDeviceToHost ) ;
printf( "2 + 7 = %d\n", c );
//cudaFree()
cudaFree( dev_c );

return 0;
}

cudaMalloc()用于分配内存,但与malloc()不同的是,它是在GPU上分配内存。由于分配的地址时在GPU上,所以程序员一定不能在主机代码中对这个指针进行解引用。同样的,也不能用free()函数来释放这些内存,只能用cudaFree()。
为了让主机代码能得到GPU计算的结果,可以用cudaMemcpy()函数,它的第四个参数数据移动的方向,是从主机到设备、设备到主机还是设备到设备。

上面说的这些内容好像对加快程序的执行速度没什么帮助,我们用下面这个向量求和的例子来了解CUDA如何让程序变得更快:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<book.h>
#define N 10
__global__ void add(int* a,int* b,int* c){
int tid=blockIdx.x;
if(tid<N)c[tid]=a[tid]+b[tid];
}

int main(void){
int a[N],b[N],c[N];
int* dev_a,*dev_b,*dev_c;
HANDLE_ERROR(cudaMalloc((void**)&dev_a,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_b,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_c,N*sizeof(int)));
for(int i=0;i<N;i++)a[i]=-i,b[i]=i*i;
HANDLE_ERROR(cudaMemcpy(dev_a,a,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_b,b,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_c,c,N*sizeof(int),cudaMemcpyHostToDevice));
add<<<N,1>>>(dev_a,dev_b,dev_c);
HANDLE_ERROR(cudaMemcpy(c,dev_c,N*sizeof(int),cudaMemcpyDeviceToHost));
for(int i=0;i<N;i++)printf("%d %d\n",i,c[i]);
cudaFree(dev_a),cudaFree(dev_b),cudaFree(dev_c);
return 0;
}

注意到,我们用了add<<<N,1>>>。N代表了设备在执行核函数时使用的并行线程块(Block)的数量。当启动核函数时,我们告诉运行时,我们想要一个一维线程格(Grid),其中包含N个线程块。
如何在代码中知道当前正在运行的是哪个线程块?可以通过blockIdx.x得到,它是一个内置变量。这样,我们就把向量求和并行化,达到了加速的效果。

在上面的例子中,我们启动了多个线程块,但每个线程块只有1个线程。要想在每个线程块里启动多个线程,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<book.h>
#define N 100
__global__ void add(int* a,int* b,int* c){
int tid=threadIdx.x+blockIdx.x*blockDim.x;
if(tid<N)c[tid]=a[tid]+b[tid];
}

int main(void){
int a[N],b[N],c[N];
int* dev_a,*dev_b,*dev_c;
HANDLE_ERROR(cudaMalloc((void**)&dev_a,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_b,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_c,N*sizeof(int)));
for(int i=0;i<N;i++)a[i]=-i,b[i]=i*i;
HANDLE_ERROR(cudaMemcpy(dev_a,a,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_b,b,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_c,c,N*sizeof(int),cudaMemcpyHostToDevice));
add<<<(N+127)/128,128>>>(dev_a,dev_b,dev_c);
HANDLE_ERROR(cudaMemcpy(c,dev_c,N*sizeof(int),cudaMemcpyDeviceToHost));
for(int i=0;i<N;i++)printf("%d %d\n",i,c[i]);
cudaFree(dev_a),cudaFree(dev_b),cudaFree(dev_c);
return 0;
}

注意到启动核函数时用的是<<<(N+127)/128,128>>>,(N+127)/128是对N/128向上取整,得到线程块的数量。第二个128代表一个线程块有128个线程。这样子可能会导致线程数量大于N,但没有关系,我们会在核函数里判断线程索引是否越界。
在核函数中,线程索引通过$tid=threadIdx.x+blockIdx.x*blockDim.x$得到。
为了避免使用过多的线程块,我们直接指定线程块的数量,然后在核函数中用while循环对下表递增,如下所示:

1
2
3
4
__global__ void add(int* a,int* b,int* c){
int tid=threadIdx.x+blockIdx.x*blockDim.x;
while(tid<N)c[tid]=a[tid]+b[tid],tid+=blockDim.x*gridDim.x;
}