3. 全局内存的分配与释放:cudaMalloc、cudaFree的正确用法
全局内存,说白了就是GPU上那块最大的存储空间。你在主机端(CPU)写代码,想要让GPU干活,就得先把数据搬过去。怎么搬?靠的就是cudaMalloc和cudaFree这对搭档。
我刚开始接触CUDA时,觉得这不就是malloc和free的翻版嘛,有啥难的?结果呢?嗯,第一个项目就踩了坑——内存泄漏,显存越用越少,最后程序直接崩了。从那以后,我再也不敢小看这两个API了。
3.1 cudaMalloc:给GPU分配显存
cudaMalloc的作用,就是在GPU的全局内存里划出一块区域。它的函数签名长这样:
cudaError_t cudaMalloc(void **devPtr, size_t size);
注意第一个参数是二级指针。为什么?因为你要修改指针本身的值,让它指向GPU上的地址。我见过不少新手写成这样:
float *d_data;
cudaMalloc(d_data, sizeof(float) * N); // 错误!
少了取地址符&,程序不会报错,但分配的内存地址根本没传回来。你后面再用d_data,访问的就是野指针。这种bug特别难查,我当年调试了整整一个下午才找到原因。
正确写法:
float *d_data;
cudaMalloc((void**)&d_data, sizeof(float) * N);
分配完一定要检查返回值。虽然cudaMalloc失败时会返回错误码,但很多人懒得检查。我个人习惯是封装一个宏:
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA error at %s:%d - %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while(0)
// 使用
CUDA_CHECK(cudaMalloc((void**)&d_data, sizeof(float) * N));
这样一旦分配失败,你能立刻知道是哪行代码出的问题。省心。
3.2 cudaFree:用完记得还回去
有借有还,再借不难。cudaFree就是用来释放显存的:
cudaError_t cudaFree(void *devPtr);
释放后,记得把指针置为NULL。为什么?防止悬空指针。你想想看,如果代码里不小心又用了一次这个指针,程序不会立刻崩溃,但行为完全不可预测。我遇到过最诡异的情况是:释放后没置空,后续代码偶然访问到了别的数据,结果算出来的结果时对时错,排查了整整两天。
警告:不要重复释放同一个指针。两次调用cudaFree同一个地址,会导致未定义行为。轻则程序崩溃,重则损坏CUDA驱动状态。
3.3 内存泄漏的常见陷阱
说白了,内存泄漏就是分配了没释放。但在CUDA里,有些情况特别容易中招。
陷阱一:异常路径忘记释放
看这段代码:
float *d_a, *d_b;
cudaMalloc(&d_a, size);
cudaMalloc(&d_b, size);
// 一些计算...
if (some_error_condition) {
return; // 糟糕!d_a和d_b没释放
}
cudaFree(d_a);
cudaFree(d_b);
函数提前返回时,显存就漏了。我建议用RAII思想,或者至少保证每个cudaMalloc都有对应的cudaFree,不管走哪条路径。
陷阱二:循环中反复分配不释放
for (int i = 0; i < 1000; i++) {
float *d_temp;
cudaMalloc(&d_temp, huge_size);
// 用d_temp做点事...
// 忘了释放!
}
循环1000次,显存就爆了。我曾经接手过一个项目,里面就有这样的代码。跑一次训练,显存从8GB慢慢涨到满,然后OOM。修复方法很简单:要么在循环末尾加cudaFree,要么在循环外分配、循环内复用。
陷阱三:cudaHostAlloc分配的主机内存
用cudaHostAlloc分配的内存,必须用cudaFreeHost释放,不能用普通的free。混用的话,轻则内存泄漏,重则程序崩溃。我见过有人把cudaMalloc和cudaFreeHost配对使用,结果CUDA驱动直接报错。
小技巧:写代码时,我习惯在cudaMalloc之后紧接着写cudaFree,就像写一对括号一样。先把释放的架子搭好,再往中间填逻辑。这样基本不会漏。
3.4 知识体系一览
下面这张图,把全局内存分配与释放的核心逻辑串起来了。你可以对照着看,哪里容易出问题:
3.5 总结
全局内存的分配与释放,看似简单,但细节决定成败。记住三点:
- 分配时:用二级指针,检查返回值
- 释放时:配对释放,释放后置空
- 整体上:保证每条路径都有释放,别在循环里反复分配
做到这三点,显存泄漏基本就远离你了。嗯,其实CUDA编程里很多坑都是这种小细节,但往往就是这些小细节,决定了你的程序是稳定运行还是动不动就崩。