天天看点

CUDA代码笔记(二) cudaOpenMPOpenMP 代码笔记

cudaOpenMP项目展示了如何在cuda项目中运用openmp技术。

该项目位于cuda samples文件夹下的0_Simple/cudaOpenMP文件夹下。

在正式开始剖析代码之前,让我们先来了解一下openmp的背景知识。

OpenMP

根据百度百科,OpenMP是用于共享内存并行系统的多线程程序设计的一套指导性的编译处理方案。 简单来讲,我们可以把OpenMP理解为一个并行线程库,它跟linux下的pthread库很类似,但是相比后者而言,OpenMP具有更加简单的编程逻辑。 OpenMP目前支持两类语言:C/C++和Fortran,对于C/C++程序员来说,使用OpenMP非常容易,目前市面上流行的各种编译器:gcc、visual c++ cl以及clang均不同程度地实现了对OpenMP的支持,更多的信息可以从下面几个链接中获得: OpenMP官网: http://openmp.org GCC OpenMP: http://gcc.gnu.org/wiki/openmp Visual C++ OpenMP:http://msdn.microsoft.com/zh-cn/library/vstudio/tt15eb9t.aspx Clang OpenMP: http://clang-omp.github.io/

代码笔记

注:代码笔记中的代码并非完整的项目代码,有一些博主认为不太重要的部分被省略了,想要查看完整代码的读者请直接查看CUDA Samples

首先我们来关注在代码中GPU做了哪些事情,为此我们查看相应的__global__函数,其代码如下:

__global__ void kernelAddConstant(int *g_a, const int b) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    g_a[idx] += b;
}
           

可以看到,每一个CUDA线程仅仅从全局内存空间g_a中读取其线程ID对应位置的数值,并将其与一个常量b相加。 这个函数非常简单,这样我们更容易将注意力集中在如何结合CUDA和OpenMP上。

接下来,我们将目光转向main函数,main函数主要包括3个部分: 1、初始化 2、执行计算 3、正确性检查

先来看"初始化"部分:

int num_gpus = 0;

printf("%s Starting...\n\n", argv[0]);

cudaGetDeviceCount(&num_gpus);

printf("number of host CPUs:\t%d\n", omp_get_num_procs());
printf("number of CUDA devices:\t%d\n", num_gpus);

unsigned int n = num_gpus * 8192;
unsigned int nbytes = n * sizeof(int);
int *a = 0;
int b = 3;
a = (int *)malloc(nbytes);

for (unsigned int i = 0; i < n; ++i)
    a[i] = i;
           

上面的代码中,函数cudaGetDeviceCount()用来获得系统中可用的GPU数目,它的值保存在变量num_gpus中。omp_get_num_procs()是OpenMP提供的库函数,它的作用是获取函数调用时系统中可用的CPU数目。变量n保存待处理的整数的数目,nbytes保存这些整数所需要的内存空间。指针a指示这部分空间的起始地址,代码最后对这部分内存的每一个元素赋初值。至此,初始化过程结束。

下面进入到"执行计算"过程:

omp_set_num_threads(2 * num_gpus);
#pragma omp parallel
{
    unsigned int cpu_thread_id = omp_get_thread_num();
    unsigned int num_cpu_threads = omp_get_num_threads();
    
    int gpu_id = -1;
    checkCudaErrors(cudaSetDevice(cpu_thread_id % num_gpus));
    checkCudaErrors(cudaGetDevice(&gpu_id));
    printf("CPU thread %d (of %d) uses CUDA device %d\n", cpu_thread_id, num_cpu_threads, gpu_id);

    int *d_a = 0;
    int *sub_a = a + cpu_thread_id * n / num_cpu_threads;
    unsigned int nbytes_per_kernel = nbytes / num_cpu_threads;
    dim3 gpu_threads(128);
    dim3 gpu_blocks(n / (gpu_threads.x * num_cpu_threads));

    checkCudaErrors(cudaMalloc((void **)&d_a, nbytes_per_kernel));
    checkCudaErrors(cudaMemset(d_a, 0, nbytes_per_kernel));
    checkCudaErrors(cudaMemcpy(d_a, sub_a, nbytes_per_kernel, cudaMemcpyHostToDevice));
    kernelAddConstant<<<gpu_blocks, gpu_threads>>>(d_a, b);

    checkCudaErrors(cudaMemcpy(sub_a, d_a, nbytes_per_kernel, cudaMemcpyDeviceToHost));
    checkCudaErrors(cudaFree(d_a));
}
           

计算过程一开始首先调用openmp函数omp_set_num_threads()设置执行并行代码片段的CPU线程数目。 然后,使用#pragma omp parallel指示下面的代码块将会使用openmp多线程执行。

omp_get_thread_num()函数返回执行当前代码的线程编号。 omp_get_num_threads()返回执行该代码块的线程总数。 也就是说,对于该代码片段,总共有2 * num_gpus个CPU线程将会执行它。 之后,代码对总的工作量进行了划分,将所有工作均匀分配给每一个GPU内核,并启动内核执行,然后将执行结果拷贝回来。 指针d_a指向GPU全局内存空间,sub_a指向执行该代码块的CPU线程所对应的主机内存空间的开始位置。代码中为每个CUDA block分配了128个CUDA线程,因此每个Grid需要的block数目为n / (num_cpu_threads * 128),至此完成划分开始拷贝数据和计算。 最后,使用cudaFree()函数释放GPU内存。

最后是"正确性检查"部分:

if (cudaSuccess != cudaGetLastError())
    printf("%s\n", cudaGetErrorString(cudaGetLastError()));

bool bResult = correctResult(a, n, b);

if (a) free(a);

cudaDeviceReset();

exit(bResult ? EXIT_SUCCESS : EXIT_FAILURE);
           

这部分代码首先检查GPU执行是否出错,然后调用函数correctResult()检查执行结果是否正确,最后释放内存并退出。 correctResult()函数功能很简单,就是对比GPU执行结果和CPU执行结果:

int correctResult(int *data, const int n, const int b) {
    for (int i = 0; i < n; ++i)
        if (data[i] !== i + b) return 0;

    return 1;
}
           

继续阅读