3. 显存管理基础:cudaMalloc、cudaFree 与碎片化问题
显存管理,说白了就是给 GPU 分配和回收「工作台」。你想想看,一张显卡就那么多显存,模型要放、中间结果要存、多路并发还要抢。搞不好,显存就炸了。
我个人习惯,在写任何推理服务之前,先把显存管理这块理清楚。不然上线之后,OOM(Out of Memory)会像定时炸弹一样,随时爆给你看。
3.1 cudaMalloc:分配显存
cudaMalloc 是 CUDA 最基础的显存分配函数。它的用法跟 C 语言的 malloc 很像,只不过分配的是 GPU 上的内存。
float *d_ptr;
size_t size = 1024 * sizeof(float);
cudaError_t err = cudaMalloc((void**)&d_ptr, size);
if (err != cudaSuccess) {
printf("分配失败: %s\n", cudaGetErrorString(err));
}
这里有个坑,我刚开始做 CUDA 开发时踩过——cudaMalloc 返回的是设备指针,你不能在 CPU 代码里直接读写它。必须用 cudaMemcpy 来搬运数据。
3.2 cudaFree:释放显存
有分配就得有释放。cudaFree 就是干这个的。
cudaFree(d_ptr);
d_ptr = nullptr; // 我习惯释放后立即置空,防止野指针
嗯,这里要特别提醒一句:忘记释放显存,是显存泄漏的根源。我在项目中遇到过好几次,服务跑了一整天,显存占用从 4GB 慢慢涨到 24GB,最后直接 OOM 挂掉。查了半天,发现是某个循环里分配了显存但没释放。
3.3 显存碎片化问题
碎片化,说白了就是显存被切得七零八落,虽然总空闲空间够用,但找不到一块连续的大块来分配。
为什么会这样?
你想想看,如果频繁地分配和释放不同大小的显存块,就像在内存条上反复插拔不同尺寸的积木。时间一长,空闲的小块散落在各处,但最大的连续块可能只有 100MB,而你想分配 200MB——对不起,分配失败。
- cudaMalloc 返回 out of memory,但 nvidia-smi 显示显存还有大量空闲
- 服务刚启动时正常,运行一段时间后频繁分配失败
- 重启服务后问题消失,过一阵又出现
我曾经在一个并发推理项目中,遇到的就是这种情况。8 张 A100,每张 80GB 显存,跑着跑着就报 OOM。用 nvidia-smi 一看,每张卡还剩 30GB,但就是分配不出一个 2GB 的 tensor。这就是典型的显存碎片化。
3.4 碎片化的成因与影响
| 因素 | 说明 | 影响程度 |
|---|---|---|
| 频繁分配/释放 | 每次 cudaMalloc/cudaFree 都会在堆上留下痕迹 | 高 |
| 分配大小不均 | 大块和小块交替分配,容易产生碎片 | 高 |
| 长时间运行 | 碎片随时间累积,最终导致分配失败 | 中 |
| 多流并发 | 不同流分配释放相互交织,加剧碎片化 | 中 |
3.5 知识体系:显存管理核心逻辑
下面这张图,是我自己总结的显存管理核心逻辑。你看一眼,就能明白整个流程和风险点在哪。
3.6 如何应对碎片化
碎片化这个问题,说白了就是「分配太随意」造成的。解决方案也很直接:减少分配次数,尽量复用。
我常用的几个方法:
- 显存池(Memory Pool):提前申请一大块显存,自己管理内部的分配和回收。这样 cudaMalloc 只调用一次,后续都在池子里操作。
- 固定大小分配:尽量让每次分配的 tensor 大小一致,减少碎片产生。
- 延迟释放:不立即 cudaFree,而是把释放的块缓存起来,下次分配时优先复用。
3.7 避坑指南
我曾经犯过一个低级错误:在推理循环里每次请求都 cudaMalloc 一个临时 buffer,用完就 cudaFree。结果并发一高,显存碎片化直接导致服务崩溃。
后来我改成预分配一个足够大的 buffer,整个生命周期只分配一次。碎片化问题再也没出现过。
嗯,这里总结几条经验:
- 不要在热路径(hot path)上频繁分配释放显存
- 能用显存池就别裸调 cudaMalloc
- 监控显存碎片率,可以用
cudaMemGetInfo看空闲总量和最大连续块 - 如果碎片化严重,重启服务是最快的解决办法(但治标不治本)
显存管理这块,说白了就是「预则立,不预则废」。提前规划好分配策略,能省掉后面一大堆排查的麻烦。