一、显存池概述

什么是显存池

显存池,说白了就是一块预先分配好的显存区域。

你想想看,平时我们写 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 新块 返回 用户释放显存 回收到空闲列表 空闲显存块列表(按大小排序)

这个流程其实很简单:

  1. 用户请求显存时,先查空闲列表
  2. 有合适的块就直接返回,没有就调用 cudaMalloc
  3. 用户释放时,显存回收到空闲列表,而不是调用 cudaFree

我刚开始实现显存池时,犯过一个低级错误:没有对空闲块按大小排序。结果每次分配都要遍历整个列表,性能反而更差了。后来加了个红黑树索引,查找时间从 O(n) 降到了 O(log n)。

避坑指南:实现显存池时,一定要考虑多线程安全。我曾经在线上环境遇到过两个线程同时分配显存,结果拿到了同一块地址,数据互相覆盖,排查了整整两天。

好了,这就是显存池的基本概念。说白了,它就是一个显存版的「内存池」,核心思想就是预分配 + 复用。下一节我们会深入实现细节,看看一个工业级的显存池到底该怎么写。

可可精品资料库