ONNX Runtime/推理内存布局

复用张量释放后的空间,生成推理内存布局

ONNX Runtime 记录一次执行中非字符串内部张量的分配与释放,用 best-fit 选择可用偏移,生成后续执行可以复用的内存布局。

e19a9ae83内存管理核验 2026-10-12
相关概念

工程问题#

中间张量往往只存活于部分执行阶段。每个张量独立分配会增加分配开销,而简单把所有张量放到不重叠的位置,又浪费已经结束生命周期的空间。

实际实现#

执行器先按设备对齐要求与 kAllocAlignment 中较大的值计算张量存储大小,非字符串内部张量才进入分配跟踪。非 program-counter 路径用按偏移排序的 blocks_ 链表表示当前仍存活的块。分配时扫描块间与缓冲区尾部的空隙,挑选足够大且剩余空间最小的一个;没有合适空隙才扩展高水位。释放只从存活链表移除,历史分配仍保存在 allocs_ 中,最终生成每个 OrtValue 的偏移和大小。

处理流程#

  1. 按设备和运行时的对齐要求计算大小,跟踪非字符串内部张量分配,按设备交给 MemPatternPlanner
  2. 扫描当前存活块之间的空隙,选择足够大的最小剩余空间
  3. 记录偏移;释放时移除存活记录,使相同空间可被后续张量复用
  4. 生成 MemoryPattern,后续命中且大小相符时从预分配缓冲区加偏移取内存

源码片段#

固定版本 · 原文摘录
onnxruntime/core/framework/execution_frame.cce19a9ae8318a5367792eff5697057364f3076c9a · L938–L952
查看原文件
938void ExecutionFrame::TraceAllocate(int ort_value_idx, size_t size) {939  if (planner_.has_value()) {940    // don't trace the output tensors or external outputs.941    auto& allocation_plan = GetAllocationPlan(ort_value_idx);942    if (allocation_plan.alloc_kind == AllocKind::kAllocateOutput ||943        allocation_plan.alloc_kind == AllocKind::kAllocatedExternally) {944      return;945    }946    auto status = planner_->TraceAllocation(ort_value_idx, size);947    if (!status.IsOK()) {948      LOGS(session_state_.Logger(), WARNING) << "TraceAllocation for ort_value_idx=" << ort_value_idx949                                             << " size=" << size << " failed: " << status.ErrorMessage();950    }951  }952}
ExecutionFrame::TraceAllocate

这里接收调用者已按对齐要求计算的大小;调用点排除了字符串张量,本函数再排除输出和外部输出。OrtValuePatternPlanner 随后按设备位置转发。

CPPMIT
onnxruntime/core/framework/mem_pattern_planner.he19a9ae8318a5367792eff5697057364f3076c9a · L148–L168
查看原文件
148    for (auto it = blocks_.begin(); it != blocks_.end(); it++) {149      if (allocs_[*it].block_.offset_ >= current) {150        auto gap = allocs_[*it].block_.offset_ - current;151        if (gap >= size && (gap - size) < waste_bytes) {152          waste_bytes = gap - size;153          best_offset = current;154          best_offset_found = true;155        }156      }157      current = std::max(current, allocs_[*it].block_.offset_ + allocs_[*it].block_.size_);158    }159160    ORT_ENFORCE(current <= buffer_size_);161162    if (current < buffer_size_) {163      auto gap = buffer_size_ - current;164      if ((gap >= size) && ((gap - size) < waste_bytes)) {165        best_offset = current;166        best_offset_found = true;167      }168    }
MemPatternPlanner::TraceAllocation:选空隙

按偏移扫描存活块,比较足够大的间隙剩余量,末尾已分配缓冲区内的空隙也参与选择。

onnxruntime/core/framework/mem_pattern_planner.he19a9ae8318a5367792eff5697057364f3076c9a · L170–L189
查看原文件
170    if (!best_offset_found) {171      best_offset = current;172    }173174    // we only need to bounds check the addition of size to best_offset as that is the only time we extend175    // the maximum size of the buffer.176    buffer_size_ = std::max(buffer_size_, SafeInt<size_t>(best_offset) + size);177    allocs_.emplace_back(ml_value_idx, MemoryBlock(best_offset, size));178    std::list<int>::iterator best_fit_it = blocks_.end();179    for (auto it = blocks_.begin(); it != blocks_.end(); it++) {180      if (allocs_[*it].block_.offset_ < best_offset)181        continue;182183      if ((allocs_[*it].block_.offset_ > best_offset) || (allocs_[*it].block_.size_ >= size)) {184        best_fit_it = it;185        break;186      }187    }188189    blocks_.insert(best_fit_it, (static_cast<int>(allocs_.size()) - 1));
MemPatternPlanner::TraceAllocation:记录位置

无合适空隙时从 current 开始放置,更新高水位并按偏移把新块插入存活链表。

onnxruntime/core/framework/mem_pattern_planner.he19a9ae8318a5367792eff5697057364f3076c9a · L192–L201
查看原文件
192  void TraceFree(int ml_value_index) {193    std::lock_guard<std::mutex> lock(lock_);194195    for (auto it = blocks_.begin(); it != blocks_.end(); it++) {196      if (allocs_[*it].index_ == ml_value_index) {197        blocks_.erase(it);198        break;199      }200    }201  }
MemPatternPlanner::TraceFree

释放从 blocks_ 删除存活项,不清除 allocs_ 的历史布局;后续分配因此能够看到新出现的空隙。

onnxruntime/core/framework/mem_pattern_planner.he19a9ae8318a5367792eff5697057364f3076c9a · L236–L244
查看原文件
236    MemoryPattern pattern;237    pattern.peak_size_ = buffer_size_;238    pattern.patterns_.reserve(allocs_.size());239    for (auto& alloc : allocs_) {240      pattern.patterns_.insert_or_assign(alloc.index_, alloc.block_);241    }242243    return pattern;244  }
MemPatternPlanner::GenerateMemPattern

把高水位与每个值的块位置转成布局,历史上不同值的块可以因生命周期不重叠而共享偏移。

onnxruntime/core/framework/execution_frame.cce19a9ae8318a5367792eff5697057364f3076c9a · L575–L591
查看原文件
575  if (mem_patterns_ && per_alloc_plan.alloc_kind != AllocKind::kAllocateOutput &&576      per_alloc_plan.alloc_kind != AllocKind::kAllocatedExternally) {577    auto pattern = mem_patterns_->GetPatterns(location);578    if (pattern) {579      auto block = pattern->GetBlock(ort_value_index);580      // if block not found, fall back to default behavior581      if (block) {582        auto it = buffers_.find(location);583        if (it != buffers_.end()) {584          // if the block is not correct, log message then fall back to default behavior585          if (block->size_ == size) {586            void* buffer = it->second.get();587            auto status = AllocateTensorWithPreAllocateBufferHelper(588                ort_value, static_cast<void*>(static_cast<char*>(buffer) + block->offset_), element_type, location,589                shape);590            return status;591          } else {
ExecutionFrame:使用缓存布局

查到布局且块大小与实际请求一致时,以缓冲区基址加 offset 构造张量;缺失或不匹配会走后面的回退路径。

CPPMIT

片段保持固定提交的连续原文;完整源码与许可证保存在本地。每段说明交代所在分支,省略部分可通过出处查看。

收益与取舍#

工程分析

带来的收益

  • 复用的是不同时刻的同一段空间,避免把所有历史张量大小直接相加
  • 一次记录可服务后续匹配的执行,减少重复分配操作

付出的代价

  • best-fit 是局部贪心,不保证全局最小峰值;每次分配扫描和插入按存活块数量线性增长,释放查找也是线性
  • 字符串张量、输出和外部分配的值不走此跟踪复用路径;块缺失、缓冲区分配失败或实际大小不符时回退普通分配
  • 布局依赖实际生命周期与输入形状等缓存条件,不能用于任意并行执行时序;训练的 program-counter 分支另有重叠判断

可比较的选择

每个张量独立分配生命周期简单,但分配器调用和峰值内存可能较高。

first-fit 空隙选择可更早停止扫描,但可能留下不同的碎片布局;两者都不保证全局最优。

版本与适用范围#

对应固定提交的非 program-counter MemPatternPlanner,为非字符串内部张量按实际执行生命周期规划布局。输入大小已按设备对齐要求与 kAllocAlignment 中较大的值计算,不是直接拼接原始元素字节数。它是执行器上的内存规划层,不等同底层 BFCArena 分配器;优化开关、执行计划和输入条件在 ExecutionFrame 中检查。

参考来源#

源码、官方文档与原始论文
01
ONNX Runtime · ExecutionFrame::TraceAllocate

onnxruntime/core/framework/execution_frame.cc · ExecutionFrame::TraceAllocate

源码e19a9ae8318a5367792eff5697057364f3076c9a
02
ONNX Runtime · MemPatternPlanner::TraceAllocation:选空隙

onnxruntime/core/framework/mem_pattern_planner.h · MemPatternPlanner::TraceAllocation:选空隙

源码e19a9ae8318a5367792eff5697057364f3076c9a
03
ONNX Runtime · 按设备分派 TraceAllocation / TraceFree

onnxruntime/core/framework/ort_value_pattern_planner.cc · 按设备分派 TraceAllocation / TraceFree

源码e19a9ae8318a5367792eff5697057364f3076c9a

相关实现与比较#

4 个相关案例
19 个匹配案例