2. GPU显存管理基础:CUDA显存分配、释放与碎片问题

好,咱们今天聊聊GPU显存管理。说实话,这是很多初学者容易翻车的地方。我见过不少同学,模型跑着跑着突然报错“out of memory”,然后一脸懵。其实说白了,就是没搞懂显存是怎么分配和释放的。

2.1 cudaMalloc:显存分配的“malloc”

在CUDA里,分配显存最常用的函数就是cudaMalloc。它的用法和C语言的malloc非常像,只不过分配的是GPU上的显存,而不是CPU的内存。

float *d_data;
cudaMalloc((void**)&d_data, size * sizeof(float));

这里有个细节要注意:cudaMalloc的第一个参数是二级指针。为什么?因为我们要修改指针本身的值,让它指向GPU显存地址。嗯,这个设计其实挺合理的,你想想看,如果传一级指针,函数内部只能修改指针指向的内容,改不了指针本身。

核心要点:cudaMalloc分配的是设备端(GPU)的线性内存,返回的地址是设备端虚拟地址。这个地址在CPU端不能直接访问,必须通过cudaMemcpy来传输数据。

我个人习惯在分配显存后立即检查返回值。虽然cudaMalloc不会返回NULL(它返回cudaError_t),但你可以用cudaGetLastError()来捕获错误。我在项目中遇到过,分配超大显存时(比如超过显存总量),cudaMalloc会静默失败,后续操作直接崩掉。所以,养成检查的习惯很重要。

2.2 cudaFree:用完记得还

有借有还,再借不难。cudaFree就是用来释放显存的。

cudaFree(d_data);

看起来简单吧?但坑就在这里。我曾经犯过一个低级错误:在同一个指针上调了两次cudaFree。结果程序直接崩溃,查了半天才发现是double free。所以我的建议是:释放后立即将指针置为NULL。

cudaFree(d_data);
d_data = NULL;  // 防止野指针

避坑指南:千万不要在CUDA kernel还在执行时调用cudaFree。虽然cudaFree是同步的,它会等待所有之前的操作完成,但如果你在stream里乱搞,可能会引发未定义行为。我曾经在异步操作中释放了还在使用的显存,结果显存数据被破坏,模型推理结果全错。

2.3 显存碎片问题:看不见的杀手

好,接下来聊一个比较隐蔽的问题——显存碎片。你可能会想:“我每次都正确释放了显存,为什么还会OOM?”

原因很简单:显存碎片化。就像硬盘碎片一样,频繁地分配和释放不同大小的显存块,会导致显存空间被切割成许多小碎片。虽然总空闲显存足够,但找不到一块连续的大空间来满足你的分配请求。

我举个例子:假设显存总共10GB,你分配了3个4GB的块,然后释放了中间那个。现在空闲显存是4GB,但被分成了两个2GB的碎片。这时候你想分配一个3GB的块,对不起,做不到。

小技巧:如果你发现程序运行一段时间后频繁OOM,但显存总量明明够,那多半是碎片问题。可以试试在程序启动时一次性分配一个大池子,然后自己管理。这就是后面要讲的显存池思想。

2.4 显存分配策略对比

为了让你更直观地理解不同分配策略的优劣,我整理了一个表格:

策略 优点 缺点 适用场景
频繁cudaMalloc/Free 实现简单 碎片严重,性能差 小规模、一次性任务
预分配大块内存 无碎片,速度快 内存利用率低 显存充足,性能敏感
显存池(内存池) 平衡碎片和利用率 实现复杂 长期运行、动态分配

看到这里你可能会问:“那我到底该用哪种?”我的经验是:如果是写demo或者小工具,直接cudaMalloc没问题。但如果是生产环境、长期运行的服务,一定要考虑显存池。

2.5 显存分配与释放的流程图

下面这张图展示了显存分配和释放的完整流程,以及碎片是如何产生的:

显存分配与释放流程 初始显存:连续空闲空间(10GB) 分配块A(3GB) 空闲(7GB) 块A(3GB) 块B(3GB) 空闲(4GB) 空闲(3GB) 块B(3GB) 空闲(4GB) 碎片1(3GB) 块B(3GB) 碎片2(4GB) 分配块C(5GB)→ 失败!OOM

从这张图可以看得很清楚:虽然总空闲显存有7GB(3+4),但因为不连续,你无法分配一个5GB的连续块。这就是碎片问题的本质。

2.6 如何缓解碎片问题

既然知道了问题,那怎么解决?我分享几个实战经验:

  • 尽量分配大小相近的块:如果所有块大小差不多,碎片会少很多。我在做推理服务时,会把不同batch size的请求统一padding到相同大小。
  • 使用cudaMallocAsync(CUDA 11.2+):这个函数支持流式分配,底层有更好的碎片管理。不过要注意,它需要配合cudaMemPool使用。
  • 自己实现显存池:预分配一大块显存,然后自己管理分配和释放。虽然麻烦,但效果最好。后面章节我会详细讲。

一句话总结:显存碎片是长期运行程序的隐形杀手。理解cudaMalloc和cudaFree的行为,是掌握显存管理的第一步。下一节我们会深入显存池的设计原理,敬请期待。

好了,这一章就到这里。记住,显存管理没有银弹,理解底层机制才能写出健壮的代码。如果你在实际项目中遇到奇怪的OOM问题,不妨先检查一下是不是碎片在作怪。