本文介绍CUDA环境下两个向量的加法运算。代码运行的系统环境为操作系统Ubuntu 16.04 LTS 64位,CUDA版本7.5,GCC版本5.4.0。项目Github下载地址为:CUDA向量加法Github项目
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);
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.
在数据从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的数量和空间组织形式,
将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");
}
释放Host端的内存,如下,
free(a_host);
free(b_host);
free(c_host);
释放Device端的内存,如下,
cudaFree(a_dev);
cudaFree(b_dev);
cudaFree(c_dev);
源代码:vec_add.cu
编译,如下,
$ nvcc vec_add.cu -o vec_add
运行,如下,
$ ./vec_add
源代码:vec_add_count.cu
源代码:vec_add_warmup.cu