2、CUDA内存池机制:CUDA Driver的内存分配策略、cudaMalloc的延迟分配、cudaFree的回收机制、内存池的默认行为

聊完显存碎片,咱们得直面一个核心问题:CUDA到底怎么管显存?

很多同学写CUDA代码,上来就是cudaMalloc一把梭。用完了再cudaFree。看起来挺干净,对吧?

但说实话,这种“裸分配”的方式,在长时间运行的服务里,简直就是灾难。我见过太多项目,跑着跑着突然out of memory,查半天发现是碎片太多,显存明明还有几百MB,就是分配不出一块连续空间。

为什么会这样?

嗯,这就要从CUDA Driver的内存分配策略说起了。

2.1 CUDA Driver的内存分配策略

CUDA Driver管理显存,本质上是一个虚拟内存系统。它把显存分成一个个页(page),默认大小是4KB或64KB(取决于架构和驱动版本)。

当你调用cudaMalloc时,Driver会做两件事:

  1. 分配虚拟地址空间:在进程的虚拟地址空间里,划出一段连续区域。
  2. 映射物理页:把物理显存页映射到这段虚拟地址上。

但这里有个关键点:这两步不是同时发生的。

核心概念:CUDA Driver采用延迟分配(Lazy Allocation)策略。也就是说,cudaMalloc返回时,只保证了虚拟地址空间可用,但物理显存页并没有真正分配。

我刚开始接触这个机制时,也觉得挺反直觉的。明明调了cudaMalloc,为什么cudaMemGetInfo看到的空闲显存没怎么变?

答案就是:物理页还没真正给你。

2.2 cudaMalloc的延迟分配

延迟分配,说白了就是“用到才给”。

当你执行cudaMalloc(&d_ptr, size)时,Driver只是:

  • 在虚拟地址空间里预留了size大小的区域
  • 标记这些页为“未提交”(uncommitted)
  • 返回一个虚拟地址指针

真正的物理显存分配,发生在第一次访问这些地址的时候。比如你第一次往这个指针里写数据,或者启动kernel去读它,Driver才会触发缺页中断(page fault),然后从物理显存里找空闲页,映射过来。

这样做的好处很明显:

  • 节省显存:如果你分配了1GB,但只用了一小部分,剩下的物理页就不会被浪费。
  • 启动更快:cudaMalloc本身几乎不耗时,因为它只是虚拟地址操作。

但坏处也很致命:

  • 碎片风险增加:因为物理页是“随用随分”,分配顺序和释放顺序可能完全错乱,导致大量外部碎片。
  • 性能抖动:第一次访问时触发缺页,会有微秒级的延迟。在实时性要求高的场景里,这很要命。

避坑指南:我曾经在一个推理服务里,发现每次请求的第一次kernel调用特别慢。排查了半天,原来是cudaMalloc的延迟分配在作祟。解决方案?在初始化阶段,对分配好的显存做一次cudaMemset,强制触发物理页分配。这样运行时就不会有缺页开销了。

2.3 cudaFree的回收机制

说完分配,咱们看看释放。

cudaFree的行为,比很多人想象的要“懒”得多。

当你调用cudaFree(ptr)时,Driver会:

  1. 解除虚拟地址映射:把ptr对应的虚拟地址空间标记为“空闲”。
  2. 回收物理页:把物理显存页归还给Driver的内部空闲池。
  3. 但不会归还给OS:这些物理页仍然被Driver持有,不会真正释放给操作系统。

换句话说,cudaFree之后,你用cudaMemGetInfo看到的空闲显存,可能并没有增加多少。因为Driver把那些页“藏”起来了,留着自己用。

为什么要这么做?

嗯,原因很简单:避免频繁的页分配开销。如果每次cudaFree都把物理页还给OS,下次cudaMalloc时又要重新申请,这中间的延迟和系统调用开销,谁也受不了。

注意:这种“懒回收”机制,会导致一个现象:你的进程占用的显存只增不减。即使你释放了所有显存,nvidia-smi看到的显存占用可能还是很高。这不是内存泄漏,而是Driver的缓存策略。但如果你在写长时间运行的服务,一定要小心——这种缓存可能会让其他进程申请不到显存。

2.4 内存池的默认行为

从CUDA 11.0开始,NVIDIA引入了显存池(Memory Pool)机制。说白了,就是Driver自己维护一个“显存仓库”,所有cudaMalloc和cudaFree都在这个仓库里操作。

默认情况下,每个CUDA设备都有一个默认内存池。它的行为是这样的:

操作 默认行为
cudaMalloc 从内存池中分配一块空闲区域。如果池中不够,Driver会向OS申请新的物理页,并加入池中。
cudaFree 将显存块归还给内存池。池会尝试合并相邻的空闲块,减少碎片。
池大小 默认无上限。Driver会一直持有释放的物理页,直到进程退出。
碎片整理 默认不主动整理。但Driver会在分配时尝试合并相邻空闲块。

这个默认行为,对于大多数应用来说,已经够用了。但如果你遇到以下情况,就得考虑手动调整:

  • 显存占用只增不减:默认池不会把物理页还给OS,导致nvidia-smi看到的占用持续上升。
  • 碎片严重:默认的合并策略很保守,无法处理复杂的碎片场景。
  • 多进程共享GPU:一个进程的池占用了大量显存,其他进程可能分配不到。

我的建议:如果你在写推理服务、数据库、或者任何长时间运行的CUDA程序,不要完全依赖默认内存池。至少要做到:

  • 监控显存碎片率(可以用cudaMemPoolGetAttribute查询)
  • 定期调用cudaMemPoolTrimTo,把多余物理页归还给OS
  • 考虑使用cudaMemPoolCreate创建自定义池,设置大小上限

好了,这一章的内容就到这里。CUDA内存池的默认行为,说白了就是“懒分配+懒回收+懒整理”。理解了这个“懒”字,你就能明白为什么显存碎片会越来越严重,也知道该从哪里下手去治理它。

CUDA内存池核心机制 CUDA Driver 延迟分配 虚拟地址先占,物理页后给 懒回收 物理页归还池,不还OS 默认内存池 无上限,不主动整理 延迟分配细节 • cudaMalloc只分配虚拟地址 • 首次访问触发缺页中断 • 物理页“用到才给” • 节省显存,但有性能抖动 懒回收细节 • cudaFree解除虚拟映射 • 物理页归还Driver内部池 • 不归还OS,避免开销 • nvidia-smi显存只增不减 默认池细节 • 每个设备一个默认池 • 池大小无上限 • 合并策略保守 • 不主动碎片整理 核心:懒分配 + 懒回收 + 懒整理

一句话总结:CUDA内存池的默认行为,就是“三懒”——懒分配、懒回收、懒整理。理解了这个,你就掌握了治理显存碎片的第一把钥匙。