CUDA GPU编程中使用结构体传递函数参数
CUDA GPU编程中,虽然统一寻址(Unified Memory)技术能够大大简化编程难度和代码复杂度,但是速度略有牺牲,同时对运行环境提出更多的要求。而在不使用这项技术时,编程时需要同时创建CPU(host)和GPU(device)端的变量指针,然后为其分别分配内存。操作完成后,再分别释放内存。CUDA工程的范例程序中,单独编写了__global__函数,并将其和设备内存的创建,使用和释放包装在了一个函数里面,最终将这个包装后的函数提供给用户。用户只需管理自己的CPU内存,以及简单地调用该封装函数即可实现GPU加速。
但实际处理过程中,由于数据量大或者希望使用CUDA流来实现CPU和GPU并行,从而最大限度提高运行效率,由于内存的创建和释放效率极低,上诉方式并不可取。而是在程序开始创建变量指针并分配内存,随后对该内存进行多次使用,在程序运行结束时再释放资源。这样之前的一个封装函数显然不够用,现在需要用户自己创建各项主机和设备端内存指针,然后分别提供相应封装函数以实现内存的创建、核心的GPU并行计算代码、内存释放若干操作。应用程序往往需要使用若干数据,而分别要在主机和设备端建立一个指针,因此造成函数参数数量过多的问题,我的一些程序中经常超过20个参数,这对程序的调用和管理造成极大的不便。
为了解决这个问题,考虑把相关必要的参数封装在一个结构体里面,通过该结构体的指针进行传输传递,而封装函数通过该结构体获取必要的数据,从而函数的参数大大降低。而进一步,把结构体内部指针变量对应的内存创建、释放封装起来,用户只需要创建相应的结构体再调用这些封装函数即可,对代码的易用性和易维护性均有很大的提升。
下面首先提供一个范例,这是根据cuda工程创建的范例进行的修改。
- #include “cuda_runtime.h”
- #include “device_launch_parameters.h”
- #include <stdio.h>
- #include <string.h>
- struct ProcPara
- {
- int h_a; // host CPU
- int *h_b; // host CPU
- int *h_c; // host CPU
- int *d_a; // device GPU
- int *d_b; // device GPU
- int *d_c; // device GPU
- };
- global void addKernel(ProcPara para)
- {
- int i = threadIdx.x;
- para->d_c[i] = para->d_a[i] + para->d_b[i];
- }
- void InitProcPara(ProcPara* para, int arraySize)
- {
- cudaMallocHost((void)¶->h_a, arraySize sizeof(int));
- cudaMallocHost((void)¶->h_b, arraySize sizeof(int));
- cudaMallocHost((void)¶->h_c, arraySize sizeof(int));
- cudaMalloc((void)¶->d_a, arraySize sizeof(int));
- cudaMalloc((void)¶->d_b, arraySize sizeof(int));
- cudaMalloc((void)¶->d_c, arraySize sizeof(int));
- }
- void DeinitProcPara(ProcPara* para)
- {
- // free host memory
- cudaFreeHost(para->h_a);
- cudaFreeHost(para->h_b);
- cudaFreeHost(para->h_c);
- // free device memory
- cudaFree(para->d_a);
- cudaFree(para->d_b);
- cudaFree(para->d_c);
- }
- void addWithCuda(ProcPara* para, unsigned int arraySize)
- {
- cudaSetDevice(0);
- cudaMemcpy(para->d_a, para->h_a, arraySize sizeof(int), cudaMemcpyHostToDevice);
- cudaMemcpy(para->d_b, para->h_b, arraySize sizeof(int), cudaMemcpyHostToDevice);
- // Launch a kernel on the GPU with one thread for each element.
- addKernel << <1, arraySize >> >(para);
- cudaMemcpy(para->h_c, para->d_c, arraySize sizeof(int), cudaMemcpyDeviceToHost);
- cudaDeviceSynchronize();
- }
- int main()
- {
- const int arraySize = 5;
- const int a[arraySize] = { 1, 2, 3, 4, 5 };
- const int b[arraySize] = { 10, 20, 30, 40, 50 };
- int c[arraySize] = { 0 };
- ProcPara para = new ProcPara;
- InitProcPara(para, arraySize);
- memcpy(para->h_a, a, arraySize sizeof(int));
- memcpy(para->h_b, b, arraySize sizeof(int));
- addWithCuda(para, arraySize);
- memcpy(c, para->h_c, arraySize * sizeof(int));
- printf(”{1,2,3,4,5} + {10,20,30,40,50} = {%d,%d,%d,%d,%d}\n”,
- c[0], c[1], c[2], c[3], c[4]);
- DeinitProcPara(para);
- delete para;
- return 0;
- }
#include “cuda_runtime.h”
但是,这个程序并不能得到正确的结果。
这是因为,在传递给__global__函数的结构体指针的内存实体在主机内存中。而在非Unified Memory的条件下,GPU是不能访问到CPU端的内存数据的。由此,要继续使用结构体传递参数,需要进行如下的操作:
分别创建主机端和设备端结构体指针,并分配相应的内存。为主机端结构体内部成员变量分别分配主机端和设备端内存。然后将主机端结构体数据复制给设备端结构体。在核心计算代码中,主机端程序使用主机端结构体指针来获取所有数据的指针(包括数据拷贝),设备端程序使用设备端结构体指针获取设备端数据的指针。修改后的代码如下,这里需要注意的是,分配内存用到了指向指针的指针。因为指针本身是个变量,其保存的是个地址,分配内存要对该变量的内容进行更新,必须传递该变量的指针才有效,否则只是改变了参数变量的内容。这也是为什么cudaMalloc使用的是指针的指针的原因。
- #include “cuda_runtime.h”
- #include “device_launch_parameters.h”
- #include <stdio.h>
- #include <string.h>
- struct ProcPara
- {
- int h_a; // host CPU
- int *h_b; // host CPU
- int *h_c; // host CPU
- int *d_a; // device GPU
- int *d_b; // device GPU
- int *d_c; // device GPU
- };
- global void addKernel(ProcPara d_para)
- {
- int i = threadIdx.x;
- d_para->d_c[i] = d_para->d_a[i] + d_para->d_b[i];
- }
- void InitProcPara(ProcPara**ha_para, ProcPara**da_para, int arraySize)
- {
- //allocate stucture memory
- cudaMallocHost((void)ha_para, sizeof(ProcPara));
- cudaMalloc((void)da_para, sizeof(ProcPara));
- ProcPara*h_para = ha_para;
- cudaMallocHost((void)&h_para->h_a, arraySize sizeof(int));
- cudaMallocHost((void)&h_para->h_b, arraySize sizeof(int));
- cudaMallocHost((void)&h_para->h_c, arraySize sizeof(int));
- cudaMalloc((void)&h_para->d_a, arraySize sizeof(int));
- cudaMalloc((void)&h_para->d_b, arraySize sizeof(int));
- cudaMalloc((void)&h_para->d_c, arraySize sizeof(int));
- // exchange data
- cudaMemcpy(*da_para, *ha_para, sizeof(ProcPara), cudaMemcpyHostToDevice);
- }
- void DeinitProcPara(ProcPara*h_para, ProcPara*d_para)
- {
- // free host memory
- cudaFreeHost(h_para->h_a);
- cudaFreeHost(h_para->h_b);
- cudaFreeHost(h_para->h_c);
- // free device memory
- cudaFree(h_para->d_a);
- cudaFree(h_para->d_b);
- cudaFree(h_para->d_c);
- //release stucture memory
- cudaFreeHost(h_para);
- cudaFree(d_para);
- }
- void addWithCuda(ProcPara h_para, ProcPara* d_para, unsigned int arraySize)
- {
- cudaSetDevice(0);
- cudaMemcpy(h_para->d_a, h_para->h_a, arraySize sizeof(int), cudaMemcpyHostToDevice);
- cudaMemcpy(h_para->d_b, h_para->h_b, arraySize sizeof(int), cudaMemcpyHostToDevice);
- cudaDeviceSynchronize();
- // Launch a kernel on the GPU with one thread for each element.
- addKernel << <1, 5 >> >(d_para);
- cudaMemcpy(h_para->h_c, h_para->d_c, arraySize sizeof(int), cudaMemcpyDeviceToHost);
- cudaDeviceSynchronize();
- }
- int main()
- {
- const int arraySize = 5;
- const int a[arraySize] = { 1, 2, 3, 4, 5 };
- const int b[arraySize] = { 10, 20, 30, 40, 50 };
- int c[arraySize] = { 0 };
- ProcPara *h_para;
- ProcPara *d_para;
- InitProcPara(&h_para, &d_para, arraySize);
- memcpy(h_para->h_a, a, arraySize sizeof(int));
- memcpy(h_para->h_b, b, arraySize sizeof(int));
- addWithCuda(h_para, d_para, arraySize);
- memcpy(c, h_para->h_c, arraySize sizeof(int));
- printf(”{1,2,3,4,5} + {10,20,30,40,50} = {%d,%d,%d,%d,%d}\n”,c[0], c[1], c[2], c[3], c[4]);
- DeinitProcPara(h_para, d_para);
- return 0;
- }