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 知识体系一览

下面这张图,把全局内存分配与释放的核心逻辑串起来了。你可以对照着看,哪里容易出问题:

全局内存分配与释放核心流程 cudaMalloc 分配 检查返回值 使用显存 cudaFree 释放 指针置 NULL 常见陷阱 • 异常路径忘记释放 • 循环中反复分配不释放 • cudaHostAlloc 用 free 释放 • 重复释放同一指针 • 分配时忘记取地址 & • 释放后继续使用指针 最佳实践 分配后立即写释放代码 封装 CUDA_CHECK 宏检查错误

3.5 总结

全局内存的分配与释放,看似简单,但细节决定成败。记住三点:

  • 分配时:用二级指针,检查返回值
  • 释放时:配对释放,释放后置空
  • 整体上:保证每条路径都有释放,别在循环里反复分配

做到这三点,显存泄漏基本就远离你了。嗯,其实CUDA编程里很多坑都是这种小细节,但往往就是这些小细节,决定了你的程序是稳定运行还是动不动就崩。