在C++中操作GPU显存需要借助CUDA编程框架,通过CUDA提供的API可以完成显存的分配、主机与设备之间的数据传输以及显存的释放等核心操作,这是实现GPU加速计算的基础步骤。
C++操作GPU显存的基础流程
使用C++结合CUDA操作GPU显存的核心流程可以分为三步:分配GPU显存、在主机和设备之间传输数据、使用完成后释放显存。下面通过完整的代码示例展示这个流程。
基础显存操作示例
以下代码实现了一个简单的功能:在主机端定义数组,将数据拷贝到GPU显存,在GPU端对数组每个元素加1,再将结果拷贝回主机端。
#include <iostream>
#include <cuda_runtime.h>
// CUDA核函数,每个线程处理一个数组元素,将元素值加1
__global__ void add_one_kernel(int* arr, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
arr[idx] += 1;
}
}
int main() {
// 定义数组大小
int n = 10;
int byte_size = n * sizeof(int);
// 主机端数组
int h_arr[n];
for (int i = 0; i < n; i++) {
h_arr[i] = i;
}
// 1. 分配GPU显存
int* d_arr = nullptr;
cudaError_t err = cudaMalloc((void**)&d_arr, byte_size);
if (err != cudaSuccess) {
std::cout << "显存分配失败: " << cudaGetErrorString(err) << std::endl;
return -1;
}
// 2. 将主机数据拷贝到GPU显存
err = cudaMemcpy(d_arr, h_arr, byte_size, cudaMemcpyHostToDevice);
if (err != cudaSuccess) {
std::cout << "数据拷贝到设备失败: " << cudaGetErrorString(err) << std::endl;
cudaFree(d_arr);
return -1;
}
// 3. 启动CUDA核函数处理显存中的数据
int block_size = 256;
int grid_size = (n + block_size - 1) / block_size;
add_one_kernel<<<grid_size, block_size>>>(d_arr, n);
// 检查核函数执行是否有错误
err = cudaGetLastError();
if (err != cudaSuccess) {
std::cout << "核函数执行失败: " << cudaGetErrorString(err) << std::endl;
cudaFree(d_arr);
return -1;
}
// 4. 将GPU显存中的结果拷贝回主机
err = cudaMemcpy(h_arr, d_arr, byte_size, cudaMemcpyDeviceToHost);
if (err != cudaSuccess) {
std::cout << "数据拷贝到主机失败: " << cudaGetErrorString(err) << std::endl;
cudaFree(d_arr);
return -1;
}
// 打印结果
std::cout << "处理后的数组结果: ";
for (int i = 0; i < n; i++) {
std::cout << h_arr[i] << " ";
}
std::cout << std::endl;
// 5. 释放GPU显存
cudaFree(d_arr);
return 0;
}
常用CUDA内存类型及使用场景
除了基础的cudaMalloc分配的全局显存,CUDA还提供了多种不同的内存类型,适配不同的使用场景,开发者可以根据需求选择合适的内存类型。
| 内存类型 | 分配方式 | 特点 | 适用场景 |
|---|---|---|---|
| 全局显存 | cudaMalloc | 所有线程都可访问,读写速度较慢,生命周期和分配时一致 | 存储需要被多个核函数使用的大体量数据 |
| 共享显存 | 核函数内用__shared__声明 | 同一个线程块内的线程可访问,读写速度远快于全局显存,生命周期和线程块一致 | 线程块内线程需要共享数据的场景,比如矩阵分块计算 |
| 页锁定主机内存 | cudaMallocHost | 主机端内存,不会被操作系统换页,和GPU之间的传输速度更快 | 需要频繁在主机和GPU之间传输数据的场景 |
| 常量显存 | __constant__声明 | 只读,所有线程可访问,有缓存,速度快 | 存储核函数中不会修改的常量数据,比如系数、配置参数 |
显存操作注意事项
- 每次使用
cudaMalloc分配显存后,必须对应使用cudaFree释放,否则会造成显存泄漏,长期运行会导致程序崩溃。 - 主机到设备、设备到主机的数据传输是耗时操作,尽量减少不必要的传输次数,比如可以在GPU端完成多步计算后再将最终结果传回主机。
- 使用页锁定主机内存时,需要用
cudaFreeHost释放,不能用普通的free或者delete释放。 - 核函数操作显存时,要注意数组越界问题,越界访问可能导致程序崩溃或者得到错误结果。
常见错误排查
在C++操作GPU显存的过程中,经常会遇到一些典型错误,可以通过以下方式排查:
如果遇到显存分配失败的错误,首先检查是否显存已经被占满,可以通过nvidia-smi命令查看GPU显存使用情况;如果是数据传输错误,检查传输的字节大小是否正确,源地址和目标地址是否对应主机或设备端。
另外,每次调用CUDA API后,可以检查返回的cudaError_t值,确认操作是否成功,这是快速定位问题的有效方式。如果是核函数执行错误,可以通过cudaGetLastError获取错误信息,同时检查核函数的参数配置是否合理,比如线程块大小、网格大小是否和数据的维度匹配。