2. 显存分配与释放机制
各位同学,今天我们来聊聊显存分配与释放这个话题。说实话,很多做GPU加速的同学,一开始都不太在意这个——不就是调个cudaMalloc吗?但我在实际项目中踩过不少坑,才意识到这里面的门道有多深。
cudaMalloc/cudaFree:看似简单,实则暗藏玄机
先说说最基础的cudaMalloc。它的工作流程是这样的:
// 典型的显存分配
float *d_data;
cudaMalloc(&d_data, size * sizeof(float));
// ... 使用显存 ...
cudaFree(d_data);
看起来很简单对吧?但你知道cudaMalloc背后发生了什么吗?
我个人习惯把cudaMalloc理解为「向GPU驱动申请一块连续地址空间」。驱动会维护一个空闲显存列表,每次分配时去查找合适的空闲块。如果找不到,就会触发一次「隐式同步」——嗯,这里要注意,这个同步操作可能会让你的kernel执行停顿。
我在项目中遇到过这样的情况:一个训练循环里频繁调用cudaMalloc/cudaFree,结果性能直接掉了30%。排查了半天,才发现是显存分配释放导致的隐式同步在作祟。
显存池(Memory Pool)概念:为什么需要它?
你想想看,如果每次分配都要跟驱动打交道,那开销得多大?显存池就是来解决这个问题的。
显存池的核心思想很简单:提前申请一大块显存,然后自己管理这块显存的分配和释放。说白了,就是「批发」显存,然后「零售」给程序里的各个模块。
CUDA从11.0开始提供了官方支持的显存池API:
// CUDA 11.0+ 的显存池用法
cudaMemPool_t pool;
cudaMemPoolCreate(&pool, &poolProps);
// 从池中分配
float *d_data;
cudaMallocFromPoolAsync(&d_data, size, pool, stream);
// 释放回池中
cudaFreeAsync(d_data, stream);
// 销毁池
cudaMemPoolDestroy(pool);
为什么推荐用显存池?三个理由:
- 减少驱动调用开销:分配释放都在用户态完成
- 避免隐式同步:异步操作不会打断kernel执行
- 降低碎片化:池内管理更灵活
我记得有一次做吞吐压测,用cudaMalloc分配显存,QPS死活上不去。换成cudaMallocFromPoolAsync后,QPS直接翻了一倍。原因就是去掉了那些烦人的隐式同步。
系统级分配器 vs 自定义分配器
这里有个选择问题:用CUDA自带的分配器,还是自己写一个?
| 对比维度 | 系统级分配器 | 自定义分配器 |
|---|---|---|
| 实现难度 | 零成本,开箱即用 | 需要自己实现内存管理逻辑 |
| 灵活性 | 固定策略,无法定制 | 完全可控,可针对场景优化 |
| 性能 | 通用场景表现良好 | 特定场景可大幅超越系统级 |
| 调试难度 | 简单,有官方工具支持 | 需要自己加日志和监控 |
我个人建议:先用系统级的,性能瓶颈明确后再考虑自定义。别一上来就造轮子,容易翻车。
举个例子,我做过一个自定义分配器,专门针对固定大小的tensor分配:
class FixedSizeAllocator {
private:
char *pool_base;
size_t block_size;
std::vector<bool> free_list;
public:
void* allocate() {
for (int i = 0; i < free_list.size(); i++) {
if (free_list[i]) {
free_list[i] = false;
return pool_base + i * block_size;
}
}
return nullptr; // 池满了
}
void deallocate(void* ptr) {
size_t offset = (char*)ptr - pool_base;
int idx = offset / block_size;
free_list[idx] = true;
}
};
这个分配器在固定batch size的场景下,分配开销几乎为零。但如果是变长分配,那就得用更复杂的伙伴系统或者slab分配器了。
显存碎片化问题:看不见的性能杀手
显存碎片化,说白了就是「显存明明还有空,但就是分配不出连续的大块」。这个问题在长时间运行的推理服务中特别常见。
为什么会碎片化?因为分配和释放的顺序是随机的。比如你分配了A(100MB)、B(200MB)、C(150MB),然后释放了B。这时候空闲显存是200MB,但被分成了两段——A前面和C后面各有一段。如果这时候要分配一个180MB的连续块,就分配不出来。
- cudaMalloc返回out of memory,但nvidia-smi显示显存还有剩余
- 程序运行时间越长,分配越慢
- 偶尔出现莫名其妙的cudaErrorMemoryAllocation
我曾经在一个持续运行了72小时的推理服务上,遇到了严重的碎片化问题。排查过程很痛苦——一开始以为是内存泄漏,后来用cuda-memcheck和自定义的显存快照工具一查,才发现是碎片化。
解决碎片化的几个常用手段:
- 预分配策略:启动时就把所有可能用到的显存都分配好
- 对象池:复用相同大小的显存块,避免大小混杂
- 定期整理:在业务低峰期做显存整理(但代价很高)
- 使用显存池:CUDA的pool内部有碎片管理机制
嗯,关于显存分配与释放,今天就先聊这么多。记住一句话:显存管理不是小事,它直接决定了你的吞吐压测能不能跑起来、跑多快。下次遇到性能问题,不妨先看看显存分配这块有没有优化空间。