CUDA编程结构
CUDA提出了一个线程层次结构抽象的概念,以允许控制线程行为。在硬件层,理解线程是如何映射到核心可以帮助提高其性能。
内核(kernel)是CUDA编程模型的一个重要组成部分,其代码在GPU上运行。在GPU上编写核函数,在主机端,基于应用程序数据及GPU性能定义如何让设备实现算法功能。
多数情况下,主机可以独立地对设备进行操作。内核一旦被启动,管理权立刻返回给主机,释放CPU来执行由设备上运行的并行代码实现的额外任务。一个典型的CUDA程序实现流程遵循以下模式:
- 数据从CPU内存拷贝到GPU内存
- 调用核函数对存储在GPU内存的数据进行操作
- 将数据从GPU内存传送回CPU内存
内存管理
CUDA运行时负责分配与释放内存,并且在主机内存和设备内存之间传输数据。
用于执行GPU内存分配的是cudaMalloc函数,函数原型为
cudaError_t cudaMalloc (void** devPtr, size_t size)
该函数负责向设备分配一定字节的线性内存,并以devPtr的形式返回指向所分配内存的指针。
个人理解:我们用一张图理解一下为什么cudaMalloc中需要传入一个双重指针void** devPtr
因为C++函数不支持返回多个值,而cudaMalloc需要返回cudaError_t的状态来判断是否成功开辟了空间,所以cudaMalloc函数只能使用修改传入的指针的方式来返回分配的地址,也就是图中的&cuda;换句话说,为了在CPU中得到cudaMalloc开辟的地址,我们在CPU中开辟一块空间来保存这个地址的值,而这块空间的地址需要另外的一个指针来保存,所以需要一个双重指针传到cudaMalloc中来实现。
cudaMemcpy函数负责主机与设备间的数据传输,函数原型为
cudaError_t cudaMemcpy (void* dst, const void* src, size_t count, cudaMemcpyKind kind)
此函数从src指向的源存储区复制一定数量的字节到dst指定的目标存储区。复制方向由kind指定,其中的kind有以下几种。
- cudaMemcpyHostToHost
- cudaMemcpyHostToDevice
- cudaMemcpyDeviceToHost
- cudaMemcpyDeviceToDevice
cudaMemcpy函数返回以及传输操作完成以前,主机应用程序是阻塞的。
为什么需要指定传输方式kind?
个人理解:因为在cpu和gpu中存储的数据方式和体系结构都有所不同,而指针只是提供了数据的地址,而不能提供其他信息。所以需要指定传输的方式。
另外,chatgpt给出了其他的原因。内存拷贝操作的性能差异: 设备内存和主机内存之间的数据传输通常比主机内存之间的数据传输慢得多。因此,明确指定传输方向可以帮助CUDA运行时系统选择合适的数据传输路径以最大程度地提高性能。错误检测: 指定传输方向允许函数在发生错误时返回适当的错误代码。如果尝试在不同方向传输数据,可能会导致意外的行为或内存访问错误,因此CUDA运行时系统需要知道数据应该如何传输以进行错误检测。灵活性: CUDA支持在设备内存和主机内存之间进行多种不同类型的数据传输,包括主机到设备、设备到主机、设备到设备等。通过
kind
参数,函数可以根据需要执行不同方向的数据传输,提供了更大的灵活性。
为什么cudaMalloc和cudaMemcpy都需要指向void的指针作为参数?
void指针是一种可以指向任何数据的指针,这使得cudaMalloc和cudaMemcpy可以使用各种类型的数据,而非只有int, float等类型。
内存层次结构
在GPU内存层次结构中,最主要的两种内存是全局内存和共享内存。全局类似于CPU的系统内存,而共享内存类似于CPU的缓存。然而,GPU的共享内存可以由CUDA的内核直接控制。
后面会更详细介绍GPU内存层次结构。