
边缘推理引擎张量内存复用算法详解基于数据流图推导 Lifetime 与 Freelist 动态回收机制一、边缘端的内存账本为什么一张 224×224 的输入图会耗尽 64MB SRAM将 ResNet-50 部署到 Cortex-A53 单板时如果按照框架默认的逐层分配策略中间张量总内存往往超过可用物理内存的三到四倍。以 ImageNet 标准输入为例仅 conv2_x 到 conv5_x 的中间特征图就累积超过 120MB而典型边缘芯片的片上 SRAM 通常只有 32MB 到 64MB。问题的根源在于推理引擎采用了静态全量分配策略。每一层计算前分配输出张量计算后并不立即释放输入张量而是等到整个计算图执行完毕再统一回收。这种策略在训练场景中无可厚非——梯度依赖要求前向激活全部保留但推理场景下一旦某个张量的所有消费者节点都已完成计算该张量就不再被需要其占用的内存理应归还给分配器。内存复用本质上是一个经典的寄存器分配问题的泛化——我们有一组变长区间张量生命周期需要将其映射到最小数量的物理寄存器内存块上且任意两个区间重叠的虚拟寄存器不能共用同一个物理寄存器。区别在于编译器寄存器分配面对的是几十个虚拟寄存器映射到十几个物理寄存器而推理引擎面对的是数百个张量映射到几十兆内存块且张量大小差异可达两个数量级。二、数据流图中的 Lifetime 传播与区间图染色从计算图到内存分配方案的推导链整个分析链路分为三步构建计算图依赖关系、推导每个张量的活跃区间、在区间约束下求解最优复用方案。graph TB A[加载 ONNX/Protobuf 模型] -- B[构建有向无环计算图 DAG] B -- C[拓扑排序确定节点执行序] C -- D[反向传播活跃区间br/liveness analysis] D -- E{张量消费计数清零?} E --|否| F[保持活跃标记] E --|是| G[标记为可回收候选] G -- H[按 size 降序插入 Freelist] H -- I[下一节点分配时优先br/从 Freelist 检索复用] I -- C style D fill:#f9f,stroke:#333,stroke-width:2px style H fill:#bbf,stroke:#333,stroke-width:2px具体推导过程如下。首先匿名化所有张量节点建立生产者-消费者关系表。每个张量在创建时记录其first_used时间戳即生产者节点的拓扑序号每次被消费时更新last_used时间戳即最后一个消费者节点的拓扑序号。遍历完所有节点后每个张量获得一个[first_used, last_used]的闭区间。接下来是关键步骤判断两个张量是否可以复用同一块内存。张量 A 和张量 B 可以共存的充要条件是它们的活跃区间不相交即last_used(A) first_used(B)或last_used(B) first_used(A)。所有无法共存的张量构成一个区间图图中的边表示这两个张量不能共用内存。问题转化为图染色——为每个节点分配一种颜色内存块相邻节点颜色不同且每种颜色的节点所需的最大 size 为最终内存块大小。实际工程中并不真正构建图并染色——区间图的染色可在线性时间内通过贪心算法求解。将所有张量按first_used升序排列依次处理维护一个最小堆追踪当前可用的内存块。处理新张量时先弹出堆中所有last_used first_used(new_tensor)的块这些块已释放若堆为空则分配新块否则复用堆顶最大块。三、Freelist 复用器的生产级实现从 Bump Allocator 到多级尺寸分桶的演进以下实现展示了一个轻量级张量内存复用器的核心逻辑适用于裸机或 RTOS 环境下的推理运行时。/* tensor_memory_pool.h — 张量内存池支持 Freelist 复用与 Bump 兜底 */ #include stdint.h #include stdbool.h #include string.h #define MAX_TENSORS 256 #define POOL_SIZE (32 * 1024 * 1024) /* 32MB 片上池 */ typedef struct { uint32_t first_used; /* 生产者节点拓扑序号 */ uint32_t last_used; /* 最后消费者节点拓扑序号 */ uint32_t size; /* 张量字节数 */ uint32_t offset; /* 在内存池中的偏移 */ bool allocated; } TensorLifetime; typedef struct { uint32_t offset; uint32_t size; bool in_use; } MemoryBlock; typedef struct { uint8_t* pool_base; /* 内存池基地址 */ uint32_t pool_capacity; uint32_t bump_offset; /* Bump 分配游标 */ MemoryBlock freelist[MAX_TENSORS]; /* 空闲块链表 */ uint32_t freelist_count; TensorLifetime tensors[MAX_TENSORS]; uint32_t tensor_count; } TensorMemPool; /* * 初始化内存池所有内存从 pool_base 开始Freelist 为空 * 首次分配采用 Bump 策略后续释放的块进入 Freelist 等待复用 */ int tensor_pool_init(TensorMemPool* pool, uint8_t* base, uint32_t capacity) { if (!pool || !base || capacity 0) return -1; memset(pool, 0, sizeof(TensorMemPool)); pool-pool_base base; pool-pool_capacity capacity; pool-bump_offset 0; return 0; } /* * 在 Freelist 中按 best-fit 策略查找可复用块 * 返回命中的块索引未命中返回 -1 * 设计考量best-fit 比 first-fit 产生更少的外部碎片 * 但搜索开销 O(n)在张量数 256 的场景下可接受 */ static int freelist_find_best_fit(TensorMemPool* pool, uint32_t request_size) { int best_idx -1; uint32_t best_size UINT32_MAX; for (uint32_t i 0; i pool-freelist_count; i) { MemoryBlock* blk pool-freelist[i]; if (blk-in_use) continue; /* 已被逻辑复用 */ if (blk-size request_size blk-size best_size) { best_size blk-size; best_idx (int)i; } } return best_idx; } /* * 压缩 Freelist移除已标记为 in_use 的空洞条目 * 当释放-分配循环多次后 Freelist 会膨胀定期压缩降低扫描成本 */ static void freelist_compact(TensorMemPool* pool) { uint32_t write 0; for (uint32_t read 0; read pool-freelist_count; read) { if (!pool-freelist[read].in_use) { if (read ! write) { pool-freelist[write] pool-freelist[read]; } write; } } pool-freelist_count write; }核心分配函数如下优先从 Freelist 检索命中则直接返回偏移未命中则走 Bump 分配撞到容量上限时触发紧凑或直接报错。/* * 为张量分配内存优先从 Freelist 复用fallback 到 Bump 分配 * 返回内存池内偏移量失败返回 UINT32_MAX * * 线程安全说明在单核 MCU 裸机场景无需锁 * 若在 SMP Linux 环境调用需要外层加自旋锁保护 pool 结构体 */ uint32_t tensor_alloc(TensorMemPool* pool, uint32_t tensor_id, uint32_t size) { if (!pool || tensor_id MAX_TENSORS || size 0) return UINT32_MAX; /* 对齐到 16 字节满足 SIMD 加载指令的对齐要求 */ size (size 15) ~15U; /* 第一步尝试从 Freelist 中 best-fit 复用 */ int best_idx freelist_find_best_fit(pool, size); if (best_idx 0) { MemoryBlock* blk pool-freelist[best_idx]; blk-in_use true; pool-tensors[tensor_id].offset blk-offset; pool-tensors[tensor_id].size blk-size; pool-tensors[tensor_id].allocated true; /* Freelist 条目超过 200 条时触发压缩 */ if (pool-freelist_count 200) { freelist_compact(pool); } return blk-offset; } /* 第二步Freelist 未命中走 Bump 分配 */ if (pool-bump_offset size pool-pool_capacity) { /* 内存池耗尽上报错误触发上层重试或降级推理 */ return UINT32_MAX; } uint32_t alloc_offset pool-bump_offset; pool-bump_offset size; pool-tensors[tensor_id].offset alloc_offset; pool-tensors[tensor_id].size size; pool-tensors[tensor_id].allocated true; return alloc_offset; } /* * 释放张量将内存块回收到 Freelist 而非真正归还给 Bump 区域 * Bump 区一旦增长就无法收缩这是 Bump Allocator 的固有限制 */ int tensor_free(TensorMemPool* pool, uint32_t tensor_id) { if (!pool || tensor_id MAX_TENSORS) return -1; if (!pool-tensors[tensor_id].allocated) return 0; /* 幂等释放 */ /* 将块加入 Freelist 供后续复用 */ if (pool-freelist_count MAX_TENSORS) { /* Freelist 溢出不应发生做防御性紧凑 */ freelist_compact(pool); if (pool-freelist_count MAX_TENSORS) return -1; } uint32_t idx pool-freelist_count; pool-freelist[idx].offset pool-tensors[tensor_id].offset; pool-freelist[idx].size pool-tensors[tensor_id].size; pool-freelist[idx].in_use false; pool-tensors[tensor_id].allocated false; return 0; }四、Freelist 方案的能力边界Bump 不可收缩、碎片不可消除与混部场景的脆弱性Bump Allocator 的核心局限在于只进不退。一旦 Bump 游标越过某个偏移即便该位置的张量已被释放并进入 FreelistBump 区域的总消耗量也不会下降。如果模型的前半部分张量总量远超后半部分内存复用算法对前半部分完全无效——因为前半段的所有张量生命周期重叠无法复用。Freelist 的 best-fit 策略在长期运行后会加剧外部碎片。一个已释放的 4MB 块无法满足 3MB 的请求吗可以。但两个相邻的 2MB 块无法合并成一个 4MB 块——Freelist 不做合并。这是因为合并需要记录块之间的邻接关系相当于退化为通用 malloc 实现失去了 Bump Allocator 的简单性和确定性。在多模型混部场景如同时运行人脸检测和人脸识别两个模型下区间图染色方法需要全局考虑所有张量导致计算复杂度陡增。实际工程中通常采用两级策略模型内部做区间分析复用模型之间做独立的 Bump 区域隔离。这种隔离带来约 5%-15% 的内存浪费但避免了跨模型脆弱耦合。对于稀疏计算模式如动态批处理、可变长度序列张量的生命周期在执行前无法静态确定。此时需要降级为基于引用计数的运行时管理——每创建一次引用加一每消费一次引用减一计数归零时释放。这种方案在确定性上不如静态分析但覆盖了动态场景。五、总结张量内存复用算法的本质是将编译器中的寄存器分配技术降维应用到推理引擎内存管理。通过数据流图推导张量活跃区间构建区间图再通过贪心染色求解最小内存用量可以在不改变模型结构的前提下将峰值内存降低 40%-70%。Freelist Bump Allocator 的混合方案是目前边缘推理引擎的工业标准选择。Freelist 负责回收中间张量Bump 区作为保底分配路径。实现的关键细节包括16 字节对齐满足 SIMD 需求、best-fit 减少外部碎片、定期紧凑 Freelist 控制扫描开销。该方案的适用边界明确适用于静态图、固定输入尺寸的推理场景。对于动态形状、多模型混部、稀疏计算场景需要引入引用计数、分区隔离或降级为通用分配器。在选型时建议优先评估模型的张量生命周期分布——如果 80% 张量的生命周期集中在前 30% 的节点中复用收益上限仅 20%此时引入 Freelist 的工程投入需要重新评估。