【问题标题】:Copying a struct containing pointers to CUDA device复制包含指向 CUDA 设备的指针的结构
【发布时间】:2012-03-07 17:28:22
【问题描述】:

我正在做一个项目,我需要我的 CUDA 设备对包含指针的结构进行计算。

typedef struct StructA {
    int* arr;
} StructA;

当我为结构分配内存然后将其复制到设备时,它只会复制结构而不是指针的内容。现在我正在通过首先分配指针来解决这个问题,然后将主机结构设置为使用该新指针(位于 GPU 上)。以下代码示例使用上面的结构描述了这种方法:

#define N 10

int main() {

    int h_arr[N] = {1,2,3,4,5,6,7,8,9,10};
    StructA *h_a = (StructA*)malloc(sizeof(StructA));
    StructA *d_a;
    int *d_arr;

    // 1. Allocate device struct.
    cudaMalloc((void**) &d_a, sizeof(StructA));

    // 2. Allocate device pointer.
    cudaMalloc((void**) &(d_arr), sizeof(int)*N);

    // 3. Copy pointer content from host to device.
    cudaMemcpy(d_arr, h_arr, sizeof(int)*N, cudaMemcpyHostToDevice);

    // 4. Point to device pointer in host struct.
    h_a->arr = d_arr;

    // 5. Copy struct from host to device.
    cudaMemcpy(d_a, h_a, sizeof(StructA), cudaMemcpyHostToDevice);

    // 6. Call kernel.
    kernel<<<N,1>>>(d_a);

    // 7. Copy struct from device to host.
    cudaMemcpy(h_a, d_a, sizeof(StructA), cudaMemcpyDeviceToHost);

    // 8. Copy pointer from device to host.
    cudaMemcpy(h_arr, d_arr, sizeof(int)*N, cudaMemcpyDeviceToHost);

    // 9. Point to host pointer in host struct.
    h_a->arr = h_arr;
}

我的问题是:这样可以吗?

这似乎是一项非常艰巨的工作,我提醒您这是一个非常简单的结构。如果我的结构体包含大量指针或本身带有指针的结构体,分配和复制的代码将相当冗长和混乱。

【问题讨论】:

  • 第 7 步和第 9 步是多余的,否则就是这样。正如下面的答案所说,最好避免在 GPU 上使用基于指针的复杂数据结构。 GPU 上的性能更差,而且 API 确实不是为它设计的。
  • 我可以看到第7步是多余的,但是为什么第9步呢?
  • h_a 是(或应该是)主机内存中保存的设备结构的“图像”。分配它以在主机内存中保存指针可能是不良做法/错误/设备内存泄漏的某种组合,具体取决于您的真实意图。将d_a 的内容复制回h_a 后,您“绕了一圈”,又回到了起点。
  • 但为了将结构正确复制到设备,我必须将h_a 的指针设置为d_arr(步骤4)。因此,当我将数据复制回来时,我还必须将h_a 中的指针设置为我刚刚复制到的数组。我同意在上面的示例中第 7 步是多余的,因为结构中没有其他信息,但如果有该步骤就不会是多余的。或者我完全错了吗?
  • 感谢 tahatmat,为我们提供了这种跨主机和设备内存来回复制结构的模式。但是我认为值得一提的是第二种方式,这似乎更一致,有助于避免实施第 9 步。函数 cudaMemcpy() 的具体特性实际上允许以这种方式取消引用主机代码中的设备指针:您跳过第 4 步并在复制之后在第 5 步将 h_a 复制到 d_a,您手动将每个设备指针地址复制到 d_a,如下所示:cudaMemcpy(&(d_a->arr), &(d_arr), sizeof(int*), cudaMemcpyHostToDevice)。同样,“d_a->arr”是合法的

标签: pointers struct cuda device host


【解决方案1】:

编辑: CUDA 6 引入了统一内存,这使得这个“深拷贝”问题变得更加容易。详情请见this post


不要忘记您可以将结构按值传递给内核。此代码有效:

// pass struct by value (may not be efficient for complex structures)
__global__ void kernel2(StructA in)
{
    in.arr[threadIdx.x] *= 2;
}

这样做意味着您只需将数组复制到设备,而不是结构:

int h_arr[N] = {1,2,3,4,5,6,7,8,9,10};
StructA h_a;
int *d_arr;

// 1. Allocate device array.
cudaMalloc((void**) &(d_arr), sizeof(int)*N);

// 2. Copy array contents from host to device.
cudaMemcpy(d_arr, h_arr, sizeof(int)*N, cudaMemcpyHostToDevice);

// 3. Point to device pointer in host struct.
h_a.arr = d_arr;

// 4. Call kernel with host struct as argument
kernel2<<<N,1>>>(h_a);

// 5. Copy pointer from device to host.
cudaMemcpy(h_arr, d_arr, sizeof(int)*N, cudaMemcpyDeviceToHost);

// 6. Point to host pointer in host struct 
//    (or do something else with it if this is not needed)
h_a.arr = h_arr;

【讨论】:

    【解决方案2】:

    正如 Mark Harris 所指出的,结构可以通过值传递给 CUDA 内核。但是,应该注意设置适当的析构函数,因为析构函数是在退出内核时调用的。

    考虑以下示例

    #include <stdio.h>
    
    #include "Utilities.cuh"
    
    #define NUMBLOCKS  512
    #define NUMTHREADS 512 * 2
    
    /***************/
    /* TEST STRUCT */
    /***************/
    struct Lock {
    
        int *d_state;
    
        // --- Constructor
        Lock(void) {
            int h_state = 0;                                        // --- Host side lock state initializer
            gpuErrchk(cudaMalloc((void **)&d_state, sizeof(int)));  // --- Allocate device side lock state
            gpuErrchk(cudaMemcpy(d_state, &h_state, sizeof(int), cudaMemcpyHostToDevice)); // --- Initialize device side lock state
        }
    
        // --- Destructor (wrong version)
        //~Lock(void) { 
        //  printf("Calling destructor\n");
        //  gpuErrchk(cudaFree(d_state)); 
        //}
    
        // --- Destructor (correct version)
    //  __host__ __device__ ~Lock(void) {
    //#if !defined(__CUDACC__)
    //      gpuErrchk(cudaFree(d_state));
    //#else
    //
    //#endif
    //  }
    
        // --- Lock function
        __device__ void lock(void) { while (atomicCAS(d_state, 0, 1) != 0); }
    
        // --- Unlock function
        __device__ void unlock(void) { atomicExch(d_state, 0); }
    };
    
    /**********************************/
    /* BLOCK COUNTER KERNEL WITH LOCK */
    /**********************************/
    __global__ void blockCounterLocked(Lock lock, int *nblocks) {
    
        if (threadIdx.x == 0) {
            lock.lock();
            *nblocks = *nblocks + 1;
            lock.unlock();
        }
    }
    
    /********/
    /* MAIN */
    /********/
    int main(){
    
        int h_counting, *d_counting;
        Lock lock;
    
        gpuErrchk(cudaMalloc(&d_counting, sizeof(int)));
    
        // --- Locked case
        h_counting = 0;
        gpuErrchk(cudaMemcpy(d_counting, &h_counting, sizeof(int), cudaMemcpyHostToDevice));
    
        blockCounterLocked << <NUMBLOCKS, NUMTHREADS >> >(lock, d_counting);
        gpuErrchk(cudaPeekAtLastError());
        gpuErrchk(cudaDeviceSynchronize());
    
        gpuErrchk(cudaMemcpy(&h_counting, d_counting, sizeof(int), cudaMemcpyDeviceToHost));
        printf("Counting in the locked case: %i\n", h_counting);
    
        gpuErrchk(cudaFree(d_counting));
    }
    

    使用未注释的析构函数(不要过多关注代码的实际作用)。如果您运行该代码,您将收到以下输出

    Calling destructor
    Counting in the locked case: 512
    Calling destructor
    GPUassert: invalid device pointer D:/Project/passStructToKernel/passClassToKernel/Utilities.cu 37
    

    然后对析构函数进行两次调用,一次在内核出口,一次在主出口。该错误消息与以下事实有关:如果d_state 指向的内存位置在内核出口处被释放,则它们不能在主出口处被释放。因此,对于主机和设备执行,析构函数必须不同。这是由上面代码中注释的析构函数完成的。

    【讨论】:

      【解决方案3】:

      数组结构是 cuda 的噩梦。您必须将每个指针复制到设备可以使用的新结构。也许您可以改为使用结构数组?如果不是我发现的唯一方法就是像你那样攻击它,这绝不是漂亮的。

      编辑: 因为我不能在顶帖上给 cmets:第 9 步是多余的,因为您可以将第 8 步和第 9 步更改为

      // 8. Copy pointer from device to host.
      cudaMemcpy(h->arr, d_arr, sizeof(int)*N, cudaMemcpyDeviceToHost);
      

      【讨论】:

      • 首先,这个答案很危险,因为它违背了并行计算中关于 AOS/SOA 的标准智慧。在所有并行计算中,包括具有 SSE/AVX 指令集的多核 CPU,阵列结构 (SOA) 优于结构阵列 (AOS)。原因是 SOA 维护跨线程的引用局部性(例如,d_a.arr 的相邻元素由同时运行的相邻线程访问)。带有指针的结构与数组结构不同。其次,您可以通过按值传递结构来简化此代码。
      • @harrism 为什么结构数组在 cuda 中不受欢迎?没看懂,能给个例子或者链接吗?谢谢
      • @GeoPapas here 是一个通过示例讨论 SOA 与 AOS 的问题/答案。
      • @RobertCrovella 感谢罗伯特的回复,但我已经提出了一个问题Here,答案很清楚。 :)
      猜你喜欢
      • 2013-11-26
      • 1970-01-01
      • 1970-01-01
      • 2018-08-28
      • 2011-07-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多