CUDA扩展std::vector来管理主机和设备数据

CUDA extending std::vector to manage host and device data

本文关键字:数据 主机 扩展 std vector CUDA 管理      更新时间:2023-10-16

我了解到std::vector是C++中原始数组的一个很好的包装器,所以我开始在我的CUDA应用程序[1]中使用它来管理主机数据。由于必须手动分配和复制内容会使代码更加复杂,可读性较差,因此我考虑扩展std::vector。由于我不是很有经验,我想知道你对它的看法。特别是如果它做得正确(例如std::vector的析构函数是隐式调用的,对吧?),如果你认为这是个好主意。

我写了一个小例子来说明这个

#include <vector>
#include <cuda.h>
#include <cstdio>
void checkCUDAError(const char *msg)
{
    cudaError_t err = cudaGetLastError();
    if( cudaSuccess != err) {
        fprintf(stderr, "Cuda error: %s: %s.n", msg, cudaGetErrorString(err));
        exit(EXIT_FAILURE);
    }
}
// Wrapper around CUDA memory
template<class T>
class UniversalVector: public std::vector<T>
{
    T* devicePtr_;
    bool allocated;
public:
    // Constructor
    UniversalVector(unsigned int length)
        :std::vector<T>(length), 
         allocated(false)
    {}
    // Destructor
    ~UniversalVector()
     {
        if(allocated)
            cudaFree(devicePtr_);
     }
    cudaError_t allocateDevice()
    {
        if(allocated) free(devicePtr_);
        cudaError_t err = 
            cudaMalloc((void**)&devicePtr_, sizeof(T) * this->size());
        allocated = true;
        return err;
    }
    cudaError_t loadToDevice()
    {
        return cudaMemcpy(devicePtr_, &(*this)[0], sizeof(T) * this->size(),
            cudaMemcpyHostToDevice);
    }
    cudaError_t loadFromDevice()
    {
        return cudaMemcpy(&(*this)[0], devicePtr_, sizeof(T) * this->size(),
            cudaMemcpyDeviceToHost);
    }
    // Accessors
    inline T* devicePtr() {
        return devicePtr_;
    }
};
__global__ void kernel(int* a)
{
    int i = threadIdx.x;
    printf("%in", a[i]);
}
int main()
{
    UniversalVector<int> vec(3);
    vec.at(0) = 1;
    vec.at(1) = 2;
    vec.at(2) = 3;
    vec.allocateDevice();
    vec.loadToDevice();
    kernel<<<1, 3>>>(vec.devicePtr());
    checkCUDAError("Error when doing something");
    return 0;
}

[1] 在CUDA中,它区分了主机和设备内存,其中主机内存是GPU可访问的内存,设备内存是GPU上的内存。程序员必须将内存从主机移动到GPU,然后再移动回来。

您可能需要了解Thrust。它为CUDA代码提供了一些STL容器。

我看到的最大问题是,它并不能真正帮助管理GPU方面的事情,并且在这个过程中混淆了许多非常重要的信息。

虽然容器类包含关于设备指针是否已被分配的信息,但无法知道主机容器的内容是否已被复制到其所持有的GPU存储器,或者GPU存储器是否已被拷贝回设备。因此,每次希望在主机或设备代码中使用容器时,都必须调用loadToDevice()loadFromDevice()方法。这可能意味着至少在某些时候不必要的PCI-e内存传输。因为您选择只包装同步CUDA内存复制例程,所以每次执行此操作时都会出现主机阻塞。

最终,与一组设计良好的辅助例程相比,我看不出这个想法有多大的净收益,这些例程抽象掉了CUDA API中最丑陋的部分,并在标准STL类型上操作。

我会扩展一下David Rodríguez-dribeas的评论:

为什么你应该更喜欢组合而不是继承(尽管它需要额外的外观工作)这个问题已经被问了很多次,也被回答了很多次。一个好的答案是:更喜欢组合而不是继承?

决定因素是接口:您想要底层类的全部或部分方法吗

在您的情况下,修改向量大小的std::vector方法,如resizepush_backpop_backeraseinsert等,如果在loadToDeviceloadFromDevice的调用之间调用,可能会造成混乱。

在您的问题中,您指出您需要一个原始数组的包装器。那些是固定尺寸的!因此,您可以在包装器类内部使用std::vector(composition!),但您需要隐藏它的所有动态大小

您最好将allocateDeviceloadToDevice等函数作为自由函数,而不是从std::vector继承的类成员。它可以节省大量将其他库/类与您的东西集成在一起的时间。整体看起来不错。