分配策略:首次适应、最佳适应、最差适应、快速适应
显存池的核心,说白了就是怎么把一块大内存切给多个任务用。分配策略选得好不好,直接影响显存利用率,甚至影响整个模型的训练速度。我这些年调过不少显存池,踩过的坑也不少,今天就把四种主流策略掰开揉碎了讲清楚。
一、首次适应(First Fit)
这是最直观的策略。从显存池的起始地址开始扫描,找到第一个足够大的空闲块就分配出去。
核心逻辑: 谁先够大,谁先上。
我在项目中遇到过一个问题:某个模型训练时频繁分配小张量,首次适应策略导致显存池头部被切得稀碎,全是小碎片。后来我加了个简单的优化——从上次分配的位置继续扫描,而不是每次都从头开始。效果立竿见影。
优点:
- 分配速度快,找到第一个合适的就停
- 实现简单,代码量少
缺点:
- 容易产生外部碎片,尤其是小分配频繁的场景
- 大块内存被切碎后,后续大分配可能失败
我的建议: 如果显存池主要用于分配大小相近的块,首次适应完全够用。但要是大小差异很大,你得小心碎片问题。
二、最佳适应(Best Fit)
这个策略会遍历所有空闲块,找到那个「刚好够用」的块。说白了,就是找最小的那个能满足需求的块。
你想想看,这样做的好处是什么?减少浪费。但代价也很明显——每次分配都要遍历整个空闲链表。
优点:
- 碎片浪费最小,每个块剩余的空间都很少
- 大块内存被保留下来,适合后续大分配
缺点:
- 分配速度慢,需要遍历所有空闲块
- 会产生大量极小的碎片,这些碎片几乎没法再用
我曾经踩过的坑: 在某个推理服务中用了最佳适应,结果显存池里全是几字节的小碎片,分配器光遍历就花了上百微秒。后来我加了个阈值——小于64字节的碎片直接合并,才解决问题。
三、最差适应(Worst Fit)
这个策略和最佳适应正好相反。它找最大的那个空闲块,然后从里面切一块出来。为什么要这么做?
嗯,这里要注意:最差适应的初衷是让剩余的空闲块尽可能大,这样后续分配更容易找到合适的块。但实际效果嘛...我试过几次,效果一般。
优点:
- 剩余块较大,减少小碎片
- 适合分配大小差异很大的场景
缺点:
- 同样需要遍历所有空闲块
- 大块内存被快速消耗,后续大分配可能无内存可用
我的经验: 最差适应在理论上有其合理性,但实践中很少单独使用。我一般把它和首次适应结合——当空闲块数量较少时用最差适应,数量多时用首次适应。
四、快速适应(Quick Fit)
这个策略我特别喜欢。它把空闲块按大小分类,比如分成8字节、16字节、32字节...每个大小类别维护一个空闲链表。分配时直接去对应大小的链表里取,不需要遍历。
为什么会这么快?因为查找时间从O(n)降到了O(1)。
优点:
- 分配速度极快,常数时间
- 几乎没有外部碎片
缺点:
- 内部碎片可能较多,因为只能分配固定大小的块
- 实现复杂,需要维护多个链表
- 内存利用率可能不如其他策略
我建议: 如果你的显存池主要服务固定大小的分配(比如Transformer中的注意力矩阵),快速适应是最佳选择。我在某个大模型推理引擎中就用这个策略,分配延迟从微秒级降到了纳秒级。
五、策略对比与选择
| 策略 | 分配速度 | 碎片控制 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 首次适应 | 快 | 一般 | 低 | 大小相近的分配 |
| 最佳适应 | 慢 | 好 | 中 | 对碎片敏感的场景 |
| 最差适应 | 慢 | 差 | 中 | 很少单独使用 |
| 快速适应 | 极快 | 好(外部) | 高 | 固定大小分配 |
六、核心流程图
下面这张图展示了四种策略的核心决策路径。我画的时候特意把关键判断点标红了,你一看就明白。
七、实战建议
说了这么多理论,最后给点实在的。我个人习惯这样选:
- 训练场景:用首次适应 + 快速适应混合。小张量走快速适应,大张量走首次适应。
- 推理场景:用快速适应。因为推理时分配模式固定,提前建好大小类就行。
- 碎片严重时:加一个后台线程做碎片整理,或者定期切换策略。
注意: 没有银弹。每种策略都有适用边界。我建议你在项目初期先做一轮性能分析,看看分配模式再定策略。别一上来就选最复杂的,往往最简单的首次适应就够用。
好了,分配策略就讲到这里。记住一点:策略选得好,显存用得少。下次遇到显存不足的问题,先别急着加卡,看看是不是分配策略的问题。