4、工具核心概念:内存池(Memory Pool)、块(Block)、段(Segment)、分配器(Allocator)
好,咱们进入正题。这一章要聊的四个概念,是显存碎片治理工具的「四根柱子」。你搞懂了它们,再看任何工具的源码或配置,都不会觉得晕。
我个人习惯把内存池比作「仓库」,块是「货架」,段是「货架上的隔板」,分配器就是「仓库管理员」。嗯,这个比喻虽然糙,但理不糙。咱们一个一个拆开看。
4.1 内存池(Memory Pool)—— 你的专属显存仓库
内存池是什么?说白了,就是工具提前从 GPU 那里「批发」来的一大块显存。它不跟 CUDA 的 cudaMalloc 一个一个地要,而是一次性要一大片,然后自己管自己用。
为什么要这么干?
你想想看,每次调用 cudaMalloc 都是一次内核态的系统调用,开销不小。更关键的是,频繁地申请和释放不同大小的显存,会让 CUDA 驱动层的分配器产生大量外部碎片。内存池把这块地圈起来,内部怎么切分、怎么回收,都是工具说了算,碎片率能大幅降低。
我在项目中遇到过一种情况:有人把池子设得特别大,结果多个进程同时跑,显存直接 OOM。嗯,这里要注意,池子不是越大越好,得算总账。
4.2 块(Block)—— 池子里的基本存储单元
内存池拿到一大块连续显存后,会把它切成若干「块」。每个块是一段连续的显存地址空间,大小可以不同。块是分配器能直接操作的最小单位。
块的状态通常有三种:
- 空闲(Free): 还没被用,等着被分配。
- 已分配(Allocated): 已经被某个张量或算子占用了。
- 分裂(Split): 一个大块被切成了多个小块,其中一部分被用了,另一部分还闲着。
我记得有一次调试一个模型,发现显存占用不高但碎片率爆表。一查,原来是很多大块被切成了极小的碎片块,每个块只有几百字节,根本没法复用。这就是典型的「块分裂」问题。
4.3 段(Segment)—— 块的逻辑分组
段这个概念,不同工具的叫法可能不一样,有的叫 Region,有的叫 Chunk。但本质都一样:段是多个块的逻辑集合。
为什么要引入段?因为一个内存池可能服务于不同的用途。比如,有些块专门给计算密集型算子用(比如卷积),有些块专门给显存带宽敏感型算子用(比如数据传输)。把它们分到不同的段里,可以避免相互干扰。
段的典型划分方式:
| 段名称 | 用途 | 块大小策略 |
|---|---|---|
| 计算段 | 存放卷积、矩阵乘等算子的中间结果 | 大块为主(4MB+) |
| 通信段 | 存放 AllReduce、Broadcast 等通信缓冲区 | 中等块(1MB ~ 4MB) |
| 临时段 | 存放生命周期极短的临时张量 | 小块为主(< 1MB) |
我个人习惯把临时段的大小设得小一点,因为临时张量来得快去得也快,用大块反而容易造成浪费。
4.4 分配器(Allocator)—— 真正的「大脑」
分配器就是那个决定「谁来用哪块显存」的算法。它不直接跟 GPU 打交道,而是操作内存池里的块和段。
常见的分配策略:
- Best-Fit: 找一块大小最接近请求大小的空闲块。碎片少,但查找慢。
- First-Fit: 找到第一块够大的空闲块就分配。速度快,但容易产生碎片。
- Buddy System: 把内存按 2 的幂次切分,分配和释放都很快,但内部碎片率稍高。
我见过最常用的工具是采用 Best-Fit + 空闲块合并 的组合策略。分配时用 Best-Fit 找最合适的块,释放时检查相邻块是否空闲,如果是就合并成一个更大的块。这样既能减少碎片,又能保持较高的分配效率。
4.5 知识体系结构图
下面这张图,把四个概念的关系画清楚了。你可以把它当作本章的「思维导图」。
从这张图你能看到:分配器位于最底层,它操作的是段和块;段是块的逻辑分组;块是实际存储数据的单元;而内存池是这一切的「容器」。搞懂这个层级关系,你就能理解工具内部是怎么运作的了。
好了,这四个概念就讲到这里。它们就像工具箱里的扳手、螺丝刀、锤子和电钻——你不需要每次都记住所有细节,但用的时候得知道该拿哪个。