4、动态内存分配策略:运行时按需分配、内存池管理、碎片化问题与对策
动态内存分配,说白了就是“用多少,要多少”。在NPU推理场景下,YOLO模型每次推理的中间张量大小可能不一样——尤其是当输入分辨率可变,或者开启了TTA(Test-Time Augmentation)时。这时候如果还搞静态预分配,要么浪费空间,要么直接OOM崩溃。
我刚开始做NPU部署时,踩过最大的坑就是动态分配导致的碎片化。模型跑着跑着,突然分配失败,但一看总内存还剩不少。嗯,这就是典型的“内存够用,但不够连续”的问题。这一章咱们就把动态分配、内存池、碎片化这三个话题彻底聊透。
4.1 运行时按需分配:灵活但危险
运行时按需分配,就是每次需要张量时,调用malloc(或NPU驱动提供的分配接口)去申请内存。用完就free掉。
优点很明显:
- 内存利用率高,不浪费
- 支持动态形状输入
- 实现简单,不用提前规划
缺点也很致命:
- 分配/释放开销大——每次调用都可能涉及内核态切换
- 碎片化严重——频繁分配释放会导致内存“千疮百孔”
- 实时性差——分配时间不可预测
所以,纯按需分配在NPU推理中基本不可行。我们需要更聪明的策略。
4.2 内存池管理:工业级解决方案
内存池的核心思想很简单:提前申请一大块内存,然后在池内自己管理分配和释放。这样避免了频繁的系统调用,也更容易控制碎片。
我习惯把内存池分为两种:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 固定大小池 | 每个slot大小相同,分配O(1),无碎片 | 固定尺寸的中间张量(如特征图) |
| 可变大小池 | 支持不同大小请求,需要伙伴算法或最佳适配 | 动态形状输入、后处理缓冲区 |
对于YOLO推理,我推荐混合池策略:
- 预分析模型计算图,统计所有中间张量的尺寸分布
- 对频繁出现的尺寸,建立固定大小池(比如3×640×640的特征图)
- 对偶发的大尺寸请求,使用可变大小池兜底
- 池内使用引用计数,避免重复分配
4.3 碎片化问题:看不见的杀手
碎片化分两种:
- 外部碎片:空闲内存总量够,但被分割成小块,无法满足大块请求
- 内部碎片:分配的内存块比实际需求大,浪费了空间
在NPU推理中,外部碎片是主要矛盾。为什么会这样?
你想想看,YOLO的中间张量尺寸差异很大——从几KB的卷积核权重,到几MB的特征图。频繁分配释放后,内存就像被狗啃过的蛋糕,到处都是小洞。
4.3.1 碎片化对NPU的特殊影响
NPU和CPU不同。CPU有MMU和虚拟内存,物理上不连续但虚拟地址可以连续。但很多NPU要求物理内存连续,或者有对齐约束(比如128字节对齐)。这就让碎片化问题雪上加霜。
我记得有一次调试,模型在CPU模拟器上跑得好好的,上NPU就报“分配失败”。查了半天,发现NPU驱动要求分配4MB连续物理内存,但内存池里最大的连续块只有3.8MB。这就是典型的碎片化问题。
4.4 碎片化对策:实战技巧
对付碎片化,我有几个经过实战检验的策略:
4.4.1 伙伴算法(Buddy System)
伙伴算法把内存按2的幂次分割。分配时找最小满足的块,释放时合并相邻的“伙伴”。
优点: 实现简单,合并效率高
缺点: 内部碎片最多50%(比如申请3KB,实际分配4KB)
// 伙伴算法核心逻辑(伪代码)
void* buddy_alloc(size_t size) {
// 向上取整到2的幂
int order = ceil_log2(size);
// 从空闲链表中找对应order的块
if (free_list[order] != NULL) {
return remove_from_list(free_list[order]);
}
// 如果没有,从更高order分裂
return split_buddy(order + 1);
}
4.4.2 内存规整(Memory Compaction)
当碎片化严重时,可以触发内存规整——把已分配的内存移动到一端,合并空闲空间。
但要注意:规整开销很大,而且需要暂停推理。我建议在推理空闲时(比如两帧之间)做规整,或者设置碎片率阈值,超过阈值才触发。
4.4.3 生命周期感知分配
这是我最喜欢的方法。分析张量的生命周期,把生命周期相近的张量放在同一区域。这样它们会一起释放,自然形成大块空闲。
比如YOLO的backbone特征图,生命周期都差不多——在neck阶段结束后全部释放。把它们放在一个子池里,释放后就能得到一大块连续内存。
4.5 动态分配策略对比
我把几种策略的优缺点整理成了一张表,方便你选型:
| 策略 | 分配速度 | 碎片化程度 | 实现复杂度 | 推荐场景 |
|---|---|---|---|---|
| 纯按需分配 | 慢(系统调用) | 高 | 低 | 原型验证 |
| 固定大小池 | 极快(O(1)) | 无 | 低 | 固定尺寸张量 |
| 伙伴算法池 | 快(O(logN)) | 中(内部碎片) | 中 | 通用场景 |
| 生命周期感知池 | 快(O(1)) | 极低 | 高(需静态分析) | 生产部署 |
| 混合池 | 快 | 低 | 高 | YOLO推理首选 |
4.6 本章知识体系
下面这张图展示了动态内存分配策略的核心逻辑和决策路径:
4.7 实战建议
最后,给你几条我亲身验证过的建议:
- 不要裸用malloc/free——在NPU推理中,这是性能杀手
- 优先用固定大小池——覆盖高频尺寸,简单高效
- 监控碎片率——设置告警阈值,及时触发规整
- 利用编译期信息——静态分析张量生命周期,指导内存池设计
- 测试长时间运行——跑10000次推理,看内存是否稳定