第2章:CUDA内存管理基础:cudaMalloc与cudaFree的工作原理,虚拟内存与物理内存的映射
好,咱们正式开始啃显存管理这块硬骨头。这一章,我会带你搞清楚两个最基础的问题:cudaMalloc到底干了什么?cudaFree又干了什么? 以及背后那个绕不开的概念——虚拟内存与物理内存的映射。
说实话,我见过不少做AI训练的同学,写了半年CUDA代码,对cudaMalloc的理解还停留在「申请一块显存」这个层面。嗯,这其实挺危险的。你想想看,显存碎片问题,根源往往就出在这里。
2.1 cudaMalloc:不只是「分配一块内存」那么简单
先看一个最基础的调用:
float *d_ptr;
cudaMalloc((void**)&d_ptr, 1024 * sizeof(float));
这行代码,表面上是「给我1024个float的空间」。但背后,GPU驱动做了三件事:
- 在虚拟地址空间中预留一段地址 —— 注意,只是预留,还没真正分配物理显存。
- 建立虚拟地址到物理地址的映射关系 —— 这一步是懒加载的,也就是用到的时候才真正分配物理页。
- 返回用户态可用的虚拟地址 —— 也就是d_ptr的值。
我个人习惯把cudaMalloc比作「在虚拟地址簿上画了个圈」。圈是画好了,但物理显存还没真正占上。什么时候占?等你第一次往里面写数据的时候。
核心要点: cudaMalloc返回的是虚拟地址,不是物理地址。物理显存的分配是延迟发生的。
2.2 cudaFree:释放的到底是什么?
cudaFree的调用更简单:
cudaFree(d_ptr);
但这里有个坑,我踩过。cudaFree做了两件事:
- 解除虚拟地址到物理地址的映射
- 回收物理显存页 —— 注意,是回收给驱动管理,不是还给操作系统。
我曾经在项目里遇到一个诡异的问题:cudaFree之后,用nvidia-smi看显存占用,数字没降下来。当时我以为是内存泄漏,查了半天。后来才发现,cudaFree只是把物理页标记为「空闲」,但驱动为了性能,不会立刻把物理显存归还给全局堆。说白了,驱动手里攒着一批「热备用」的物理页,下次cudaMalloc可以直接复用。
避坑指南: 我曾经因为频繁cudaMalloc/cudaFree导致显存碎片率飙升到40%。后来改用cudaMallocManaged + 手动预分配池,才把碎片率压到5%以下。记住:频繁的分配释放是碎片的温床。
2.3 虚拟内存与物理内存的映射:一张「地址翻译表」
为什么GPU要用虚拟内存?说白了,就是为了让每个进程觉得自己独占整个显存空间。你想想看,如果没有虚拟内存,两个进程同时申请显存,物理地址冲突了怎么办?
GPU的虚拟内存映射机制,和CPU的MMU非常像。核心是一张页表:
| 虚拟地址范围 | 物理地址范围 | 页大小 | 状态 |
|---|---|---|---|
| 0x7f00_0000 - 0x7f00_1000 | 0x1000_0000 - 0x1000_1000 | 4KB | 已映射 |
| 0x7f00_1000 - 0x7f00_2000 | 未分配 | 4KB | 未映射 |
| 0x7f00_2000 - 0x7f00_3000 | 0x2000_0000 - 0x2000_1000 | 4KB | 已映射 |
这张表,驱动维护在显存里。每次GPU访问一个虚拟地址,硬件MMU就去查表,找到对应的物理地址。如果查不到,就触发缺页中断,驱动再分配物理页并更新页表。
小技巧: 你可以用 cudaPointerGetAttributes() 来查看一个指针的虚拟地址、物理地址(如果支持)、以及它属于哪个设备。这在调试显存泄漏时特别有用。
2.4 一张图看懂整个流程
下面这张SVG图,是我自己画的,把cudaMalloc到cudaFree的完整生命周期串起来了:
2.5 为什么理解这个对显存碎片分析很重要?
好,现在咱们把前面这些知识串起来。显存碎片是怎么产生的?
- 外部碎片: 频繁cudaMalloc/cudaFree,导致物理显存被切割成很多小块。新申请一个大块时,找不到连续空间。
- 内部碎片: 申请了4KB,实际只用1KB,剩下的3KB浪费了。这在页表层面表现为「映射了但没用」。
我记得有一次,一个训练任务跑着跑着突然OOM了。用nvidia-smi一看,显存还剩2GB,但cudaMalloc就是失败。为什么?因为那2GB被分割成了几百个4KB的小碎片,没有一个连续的大块能满足新申请的需求。
关键洞察: 虚拟地址空间是连续的,但物理显存不一定是连续的。cudaMalloc成功,只代表虚拟地址连续,不代表物理地址连续。但GPU硬件访问时,需要物理地址连续的场景(比如DMA传输),就会触发额外的内存整理操作,进一步加剧碎片。
2.6 实战建议:如何用好cudaMalloc/cudaFree
基于上面的理解,我给你几条实战建议:
- 尽量复用显存,不要频繁分配释放。 我习惯用一个显存池,提前申请一大块,然后自己管理内部的小块分配。
- 注意对齐。 cudaMalloc默认按256字节对齐,但如果你申请的大小不是页大小的整数倍,容易产生内部碎片。
- 用cudaMalloc3D替代cudaMalloc。 对于多维数组,cudaMalloc3D能更好地保证内存布局的连续性,减少碎片。
- 监控碎片率。 可以用
cudaMemGetInfo获取空闲显存和总显存,但更精确的碎片分析需要自己写工具遍历页表。
个人习惯: 我在项目里会写一个简单的显存池类,预分配256MB,内部用buddy system管理。这样cudaMalloc/cudaFree的次数从每秒几千次降到几次,碎片率从30%降到5%以下。效果立竿见影。
好了,这一章的内容就到这里。cudaMalloc和cudaFree看似简单,但背后涉及的虚拟内存映射机制,是理解显存碎片的基石。下一章,我们会深入显存分配器的内部实现,看看驱动到底是怎么管理这些物理页的。