CUDA之向量加法

版权声明:林微原创,未经允许不得转载。 https://blog.csdn.net/Canhui_WANG/article/details/82913660

本文介绍CUDA环境下两个向量的加法运算。代码运行的系统环境为操作系统Ubuntu 16.04 LTS 64位,CUDA版本7.5,GCC版本5.4.0。项目Github下载地址为:CUDA向量加法Github项目


1. CUDA代码分析和实现

Step 1: 关于Host/Device 数组指针

CUDA编程而言,我们习惯把CPU端称为Host,GPU端称为Device。基于Host端和Device端拥有各自不同的两份内存地址空间的事实,代码层面上要求编程人员一方面维护两份指针地址,即一份指向Host端内存的指针地址,一份指向Device端显存空间的指针地址;另一方面必要时需要对Host端和Device端的内存数据进行相互访存,拷贝数据。

Host端内存指针,

int *a_host;
int *b_host;
int *c_host;
int *devresult_host;

Device端内存指针,

a_host = (int *)malloc(arraysize*sizeof(int));
b_host = (int *)malloc(arraysize*sizeof(int));
c_host = (int *)malloc(arraysize*sizeof(int));

Host/Device端内存数据拷贝操作,

cudaMemcpy(a_dev, a_host, arraysize*sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(b_dev, b_host, arraysize*sizeof(int), cudaMemcpyHostToDevice);
Step 2: 关于Device 并行线程配置

dim3 dimBlock()dim3 dimGrid()是Device端关于线程配置的两种数据结构,该数据结构具有两个层面的含义:其一,初始化并行线程的数量;其二,初始化这些并行线程的空间组织方式,即并行线程的维度。blocks/threads的组织方式通常出于编程方便上的考虑。

例1:给定总共1024个threads和4个blocks,问:如果blocks以一维空间形式组织,threads以一维空间形式组织,列举一个可行的并行线程调度模型?

int blocksize = 256;
int blocknum = 4;

dim3 dimBlock(blocksize, 1, 1);
dim3 dimGrid(blocknum, 1, 1);

例2:给定总共2048个threads和4个blocks,问:如果blocks以二维空间形式组织,threads以一维空间形式组织,列举一个可行的并行线程调度模型?

int blocksize = 256;
int blocknum = 2;

dim3 dimBlock(blocksize, 1, 1);
dim3 dimGrid(blocknum, blocknum, 1);

例3:给定总共2048个threads和4个blocks,问:如果blocks以二维空间形式组织,threads以二维空间形式组织,列举一个可行的并行线程调度模型?

int blocksize = 16;
int blocknum = 2;

dim3 dimBlock(blocksize, blocksize, 1);
dim3 dimGrid(blocknum, blocknum, 1);

本文中,blocks数量为blocknum,threads的数量 (每个block中包含的线程数量) 为512。blocks和threads均采用一维空间形式组织,即blocks的编号为0, 1, 2, 3, …, blocknum -1,每个block内部threads的编号为0, 1, 2, 3, …, 511。因此,我们可以用tid来表示这blocknum × \times 512个并行线程的唯一编号,如下,

int tid = blockIdx.x * blockDim.x + threadIdx.x

注:一维空间形式组织的blockDim.y = 1, blockDim.z=1, threadIdx.y=1 且 threadIdx.z = 1.

Step 3: 关于kernel的实现和调用

在数据从Host端拷贝到Device端之后,我们需要在Device端构造Kernel函数来计算这些数据。Kernel是CUDA并行结构的核心部分,一个kernel函数可以一次诱发多个线程并发运行,比方说,给定一个warp大小32的情况下,一个kernel函数可以诱发tid=0, 1, 2, …, 21的线程同时并行计算数据。如下,

__global__ void add_in_parallel(int *array_a, int *array_b, int *array_c)
{
   int tid = blockIdx.x * blockDim.x + threadIdx.x;
   array_c[tid] = array_a[tid] + array_b[tid];
}

__global__ 标识符表示add_in_parallel()函数是一个Device端运行的kernel函数。

CUDA编译器nvcc是一个修改版的C编译器,语法上两者的函数调用方式高度相似,如下,

add_in_parallel<<<dimGrid, dimBlock>>>(a_dev, b_dev, c_dev);

注:Kernel函数的调用需要事先定义blocks/threads的数量和空间组织形式,

Step 4: 关于Device 计算结果

将Device端的计算结果返回到Host端,如下,

cudaMemcpy(devresult_host, c_dev, arraysize*sizeof(int), cudaMemcpyDeviceToHost);

CPU端的计算结果,如下,

for (int i = 0; i < arraysize; i++)
{
   a_host[i] = i;
   b_host[i] = i;
   c_host[i] = a_host[i] + b_host[i];
}

校验CPU/GPU两者的计算结果的一致性,如下,

for (int i = 0; i < arraysize; i++)
{
   if (c_host[i]!=devresult_host[i])
   {
      status = 1;
   }
}

if (status)
{
   printf("Failed vervified.\n");
}
else
{
   printf("Sucessdully verified.\n");
}
Step 5: 关于释放内存

释放Host端的内存,如下,

free(a_host);
free(b_host);
free(c_host);

释放Device端的内存,如下,

cudaFree(a_dev);
cudaFree(b_dev);
cudaFree(c_dev);

2. 编译调试

源代码:vec_add.cu

编译,如下,

$ nvcc vec_add.cu -o vec_add

运行,如下,

$ ./vec_add

3. 更多改进版本

Version 1: 增加计时器,包括数据拷贝耗时,kernel耗时

源代码:vec_add_count.cu

Version 2: 增加GPU kernel warm up()函数

源代码:vec_add_warmup.cu

猜你喜欢

转载自blog.csdn.net/Canhui_WANG/article/details/82913660