一、显存池概述
什么是显存池
显存池,说白了就是一块预先分配好的显存区域。
你想想看,平时我们写 CUDA 程序,是不是经常这样:
// 常规做法:随用随分配
float* d_data;
cudaMalloc(&d_data, size * sizeof(float));
// ... 用完了释放
cudaFree(d_data);
这种做法有个问题——每次调用 cudaMalloc 和 cudaFree,都要和驱动打交道。我做过测试,一次 cudaMalloc 的开销大概在 10-50 微秒。听起来不多?但如果你在训练大模型,每秒钟可能要分配释放上千次显存,这开销就相当可观了。
显存池的思路很简单:一次分配,重复使用。就像你开公司,与其每次出差都临时订酒店,不如直接包下一层楼,随用随取。
核心定义:显存池是一个预先分配好、可复用的显存管理单元。它负责显存的分配、回收和复用,避免频繁与驱动交互。
为什么需要显存池
我在做 AI 框架优化时,遇到过这样一个场景:
一个 Transformer 模型训练,前向传播需要分配 12 块不同大小的显存,反向传播又要释放。如果每次都用 cudaMalloc,你会发现:
- 性能瓶颈:频繁的系统调用,GPU 利用率上不去
- 显存碎片:分配释放不规律,显存越用越碎
- 调试困难:OOM 了都不知道是哪块显存没释放
我曾经接手过一个项目,训练 7B 模型时总是莫名其妙 OOM。排查了三天,最后发现是显存碎片导致的——明明总显存还有 4GB,但就是分配不出一个 2GB 的连续块。
这就是显存池要解决的问题:
| 问题 | 传统方式 | 显存池方式 |
|---|---|---|
| 分配开销 | 每次 10-50μs | 几乎为 0(从池中取) |
| 显存碎片 | 严重,随时间加剧 | 可控,可整理 |
| 内存泄漏 | 容易发生 | 池化管理,自动回收 |
| 调试难度 | 高,需要手动跟踪 | 低,池内统一管理 |
显存池的核心价值
嗯,这里我要说三个核心价值,都是我在实际项目中验证过的:
1. 性能提升
我做过一个 benchmark:在 ResNet-50 训练中,使用显存池后,显存分配耗时从占总时间的 3.2% 降到了 0.1% 以下。别小看这 3%,在大规模训练中,这就是几小时的差距。
我的经验:显存池最适合那些频繁分配释放相同大小显存的场景。比如 Transformer 的 attention 计算,每次都需要相同大小的中间结果。
2. 显存复用
显存池最妙的地方在于:不同时间片可以复用同一块显存。
举个例子:
- 前向传播时,layer1 的输出需要 1GB
- 反向传播时,layer1 的梯度也需要 1GB
- 这两者不会同时存在
显存池会自动识别这种情况,让它们共用同一块物理显存。我见过一个 13B 模型,通过显存池复用,显存占用从 28GB 降到了 18GB。
3. 碎片整理
这是很多人容易忽略的点。显存碎片就像硬盘碎片,时间长了会严重影响性能。
我曾经遇到过最夸张的情况:一块 80GB 的 A100,训练 3 天后,最大可用连续块只剩 12GB。用显存池后,通过定期整理,这个数字稳定在 60GB 以上。
注意:显存池不是银弹。如果你的程序只分配一次显存,用完就退出了,那用显存池反而多此一举。它最适合长期运行、频繁分配释放的服务端场景。
显存池的核心逻辑
下面这张图展示了显存池的工作流程:
这个流程其实很简单:
- 用户请求显存时,先查空闲列表
- 有合适的块就直接返回,没有就调用
cudaMalloc - 用户释放时,显存回收到空闲列表,而不是调用
cudaFree
我刚开始实现显存池时,犯过一个低级错误:没有对空闲块按大小排序。结果每次分配都要遍历整个列表,性能反而更差了。后来加了个红黑树索引,查找时间从 O(n) 降到了 O(log n)。
避坑指南:实现显存池时,一定要考虑多线程安全。我曾经在线上环境遇到过两个线程同时分配显存,结果拿到了同一块地址,数据互相覆盖,排查了整整两天。
好了,这就是显存池的基本概念。说白了,它就是一个显存版的「内存池」,核心思想就是预分配 + 复用。下一节我们会深入实现细节,看看一个工业级的显存池到底该怎么写。