大数跨境

深度学习计算图内存分配:从精确求解到启发式算法的系统化工程实践

深度学习计算图内存分配:从精确求解到启发式算法的系统化工程实践 ai算法芯片与系统
2026-08-04
4
导读:深度学习计算图内存分配旨在复用生命周期不重叠的张量,最小化峰值显存。本文对比三种策略:整数规划精确解、离线降序最左适配、在线最佳适配合并。离线贪心近优且快,在线鲁棒但碎片多。实验表明,静态全局规划可节

 

关键词:内存复用 · 静态图 · 区间图着色 · 整数规划 · 贪心策略 · 碎片控制


📖 目录

  1. 1. 静态计算图与内存复用问题
  2. 2. 张量生命周期、峰值内存与冲突模型
  3. 3. 精确求解:混合整数线性规划模型
  4. 4. 从精确求解到启发式算法的必然过渡
  5. 5. 离线贪心:尺寸降序与最左适配
  6. 6. 在线贪心:时间推进、最佳适配与即时合并
  7. 7. 各类算法达到最优的充分条件
  8. 8. 对比实验与性能量化分析
  9. 9. 总结与未来方向

1. 🌐 静态计算图与内存复用问题

深度学习模型通常表示为 有向无环图(DAG),节点代表算子(卷积、矩阵乘法等),边代表张量数据流。编译器在运行前已知每个算子的执行顺序,因此能推导出每个中间张量的 产生时刻(算子输出时)和 最后使用时刻(被所有消费者使用完毕时)。这两个时刻构成张量的 生命周期

🎯 核心优化目标:通过将生命周期互不重叠的张量分配到同一块内存地址,最小化 峰值内存占用,从而允许更大模型或更大批量尺寸,避免硬件显存溢出(OOM)。

以下是一个典型计算图结构示意(算子与张量数据流):

  • • 该图展示了一个简单的前向传播路径,从输入到 Softmax 输出。
  • • 虚线箭头表示算子在执行过程中产生的中间张量,其形状标注在边上。
  • • 每个中间张量仅在其产生和最终使用之间存活,这为内存复用提供了机会。

2. ⏳ 张量生命周期、峰值内存与冲突模型

设张量集合为  。每个张量   具有:

  • • 尺寸 (以元素个数计)
  • • 生命周期区间 ,左闭右开

💡 解释:这里   是张量占用的内存单元总数,等于各维度长度之积,例如形状 (3, 224, 224) 的张量尺寸为  。生命周期左闭右开表示张量在时刻   被分配,在时刻   被释放,因此区间内任意时刻它都存活。

下图展示了三个张量在时间轴上的活跃区间(横轴为时间步):

  • • 区间长度表示张量存活的时间跨度。
  • • 重叠部分表示在同一时刻两个张量同时存在,它们因此冲突,不能共享内存。
  • • 例如 T0 与 T1 在 [1,2) 重叠,T1 与 T2 在 [2,3) 重叠,而 T0 与 T2 在边界处不重叠(因为左闭右开)。

📈 峰值内存   是整个计算图运行过程中任意时刻已分配内存总量的最大值。它直接决定了模型能否在给定的硬件(如 GPU)上运行,因为硬件显存容量是硬约束。优化内存复用的核心就是 最小化 

⚠️ 一个常见的误解是:峰值内存仅由同时存活张量的总大小决定。然而,如果没有合理的内存布局(即产生大量内存碎片),即使总存活量不大,也可能因为无法利用空闲空隙而导致被迫扩展池尾,最终使   显著增大。例如,若两个冲突张量之间产生了一个无法被其他张量利用的小空隙,系统可能不得不将新张量放在更靠后的位置,从而抬高了  。因此,碎片控制 与 压实(compaction) 是内存分配算法的关键课题。

🔗 冲突定义:两个张量   若满足  ,则它们同时存活,不能共用内存。

💡 解释:该条件等价于区间   与   有非空交集。由于区间是左闭右开的,边界相等(如  )不算冲突,允许张量在释放时刻立即复用该内存,这是安全且高效的。

🧩 冲突图(Conflict Graph) 是一种将冲突关系可视化的有效工具。节点表示张量,若两个张量冲突,则在它们之间添加一条无向边。以下是对应上述生命周期示例的冲突图:

  • • 图中 T0 与 T1 有边(冲突),T1 与 T2 有边(冲突),而 T0 与 T2 之间用虚线断开表示 不冲突(因为它们在边界 2 处不重叠)。
  • • 该图清晰地展现了张量之间的冲突关系,帮助我们快速识别 最大团(maximum clique),即两两冲突的张量集合。最大团的总尺寸是峰值内存的一个下界(任何算法都无法低于该下界)。
  • • 在本例中,最大团为 {T0, T1} 和 {T1, T2},其尺寸分别为 10+20=30 和 20+15=35,因此理论下界为 35。

📝 内存分配问题:为每个张量   寻找非负整数起始偏移  ,使得任意冲突对满足内存区间互斥:

目标是最小化峰值 

💡 解释:该式是内存分配的核心约束。对于任意两个冲突的张量,它们的占用区间必须完全分离——要么   在   的左边( ),要么   在   的左边( )。偏移量   是整数,表示从内存池起始地址算起的字节偏移(若元素大小为一)。目标函数   取所有张量右端最大者,也就是整个内存池所需的最小容量。


3. 🧮 精确求解:混合整数线性规划模型

我们采用 混合整数线性规划(MILP) 获得全局最优解,作为启发式算法的评价基准。

📌 决策变量

  • • 连续整数  :张量   的偏移
  • • 整数  :峰值内存
  • • 二元变量  ,对每个冲突对  ),表示   是否排在   之前

💡 解释  是辅助变量,用来在整数规划中表达“或”关系。若  ,意味着   在   左边;若为 0,则   在   左边。

🔒 约束

  1. 1. 容量上界:
  2. 2. 互斥约束(大M法):令  ,对每个冲突对:

💡 解释:第一个约束保证所有张量都落在内存池内。第二个约束利用大常数   将逻辑关系转化为线性不等式:当   时,第一式变为  ,第二式变为  (宽松,因   足够大,基本无效),于是强制   在   左边;当   时效果相反。  取所有张量尺寸之和,确保它大于任何可能的偏移差,不会错误地截断可行解。

🎯 目标函数

⚙️ 求解流程示意如下:

  • • 该流程将问题编码为标准的整数线性规划形式。
  • • 冲突矩阵从张量的生命周期区间预先计算。
  • • 求解器(如 CBC)通过分支定界法搜索最优整数解。
  • • 对于   的小规模问题,通常在几秒内可得最优解。

💻 核心实现


   
   
   
   
    
   
   
   
   def milp_allocate(tensors):
    n = len(tensors)
    sizes = [t.size for t in tensors]
    M_big = sum(sizes)
    conflicts = [(i,j) for i in range(n) for j in range(i+1,n) 
                 if
 tensors[i].overlap(tensors[j])]
    solver = pywraplp.Solver.CreateSolver('CBC')
    x = [solver.IntVar(0, M_big, f'x_{i}') for i in range(n)]
    peak = solver.IntVar(0, M_big, 'peak')
    y = {}
    for
 i,j in conflicts:
        y[(i,j)] = solver.BoolVar(f'y_{i}_{j}')
    for
 i in range(n):
        solver.Add(x[i] + sizes[i] <= peak)
    for
 i,j in conflicts:
        solver.Add(x[i] + sizes[i] <= x[j] + M_big*(1-y[(i,j)]))
        solver.Add(x[j] + sizes[j] <= x[i] + M_big*y[(i,j)])
    solver.Minimize(peak)
    status = solver.Solve()
    if
 status == pywraplp.Solver.OPTIMAL:
        return
 int(peak.solution_value()), [int(x[i].solution_value()) for i in range(n)]
    raise
 RuntimeError("求解失败")
  • • 代码中定义了大常数   作为安全上界。
  • • 对每个冲突对引入顺序变量,通过大M法实现互斥。
  • • 求解器若返回最优,则直接得到最优峰值和偏移列表。

针对第2章示例的三个张量,该求解器返回峰值 35,分配方案为:T1 偏移 0,T2 偏移 20,T0 偏移 20(复用 T2 的空隙)。这一结果验证了 MILP 的全局最优性。


4. 🔄 从精确求解到启发式算法的必然过渡

上一节我们看到,MILP 能够为小规模问题提供 全局最优解,这让它成为评估其他算法性能的 黄金标准。然而,当张量数量超过 100 时,MILP 的求解时间从秒级骤增至数十分钟甚至数小时,这在实际编译场景中是不可接受的——模型加载和编译必须控制在 秒级以内

与此同时,深度学习模型规模持续增长,GPT 类模型包含数千个中间张量,精确求解完全不可行。因此,工业界普遍采用 启发式算法:它们牺牲严格的最优性保证,换回 毫秒级的求解速度,且在大多数实际场景中能达到最优值的 95%~98%

接下来的两章将介绍两种代表性的贪心策略:

  • • 离线贪心(第 5 章)利用全局生命周期信息,通过 尺寸降序 + 最左适配 实现高效压实。
  • • 在线贪心(第 6 章)模拟运行时分配器,仅依赖当前时刻信息,通过 Best‑Fit + 即时合并 控制碎片。

两者共同揭示了 信息完备性 与 优化质量 之间的权衡关系。下表从三个维度对它们进行初步对比:

对比维度
离线贪心
在线贪心
是否依赖未来信息
✅ 是(全知)
❌ 否(因果)
时间复杂度
峰值质量
接近最优
可能偏离较大
适用场景
静态图编译优化
动态图运行时分配

5. 🚀 离线贪心:尺寸降序与最左适配

5.1 📋 算法步骤

  1. 1. 将所有张量按尺寸   从大到小排序。
  2. 2. 依次处理每个张量:
    • • 收集所有已分配且与其冲突的张量的占用区间 
    • • 合并这些区间为不相交的“已占用”集合。
    • • 从左向右扫描,找到第一个能容纳   的空隙,放置于此;若不存在,则放置在当前池尾(即峰值位置)。
  3. 3. 记录偏移并更新峰值。

5.2 📐 形式化表达

处理序列   满足  ,对张量   定义可行偏移集合

其中   为已分配集合。选择 

💡 解释:该公式刻画出所有不会与已有冲突张量重叠的偏移位置。  中记录的是已经放置好的张量的偏移和大小。对于每个已分配且冲突的   必须满足要么完全在其左侧,要么完全在其右侧。在所有满足条件的非负整数中,贪心选择最小的  ,这就是“最左适配”的数学表达。

5.3 🧭 启发式逻辑

  • • 降序排序:大尺寸张量对地址连续性要求更高,其可行间隙随尺寸增大急剧减少。优先处理它们能避免后期“大块无处安放”的困境,从而将剩余碎片空间留给数量众多的小张量填充。
  • • 最左适配:强制压实到低地址能最大化内存池右端的连续空闲长度。右端区域越大,后续突发的大块分配请求就越不需要触发池尾扩展,直接抑制峰值增长斜率,并减少碎片产生的可能性。

📊 流程示意图

  • • 该流程每次迭代只处理当前最大的张量,决策基于当前已放置的状态。
  • • 合并区间操作将多个碎片合并为一个大的不可用区间,简化空隙扫描。
  • • 由于贪心策略不考虑后续张量,但排序保证了后续张量尺寸更小,容易适配剩余空隙。
  • • 该算法在区间图着色问题中表现优异,平均接近最优。

💻 核心实现


   
   
   
   
    
   
   
   
   def offline_greedy(tensors):
    sorted_tensors = sorted(tensors, key=lambda t: -t.size)
    placements = {}
    peak = 0
    for
 t in sorted_tensors:
        occupied = []
        for
 pid, (off, sz, st, ed) in placements.items():
            if
 not (ed <= t.start or t.end <= st):
                occupied.append((off, off + sz))
        # 合并区间

        occupied.sort()
        merged = []
        for
 l, r in occupied:
            if
 not merged or l > merged[-1][1]:
                merged.append([l, r])
            else
:
                merged[-1][1] = max(merged[-1][1], r)
        offset = 0
        for
 l, r in merged:
            if
 offset < l and l - offset >= t.size:
                break

            offset = max(offset, r)
        placements[t.id] = (offset, t.size, t.start, t.end)
        peak = max(peak, offset + t.size)
    return
 peak, [placements[t.id][0] for t in tensors]
  • • 代码首先排序,然后逐个张量处理。
  • • 对每个张量,收集冲突已分配区间,合并后扫描空隙。
  • • 放置后更新峰值,最后按原始顺序返回偏移。

对示例张量运行该算法,排序后顺序为 T1(20)、T2(15)、T0(10)。放置结果:T1 占 [0,20),T2 占 [20,35),T0 扫描空隙发现 [20,35) 能容纳 10,因此放置于 20,峰值 = 35。与 MILP 最优解完全一致。复杂度  ,处理数千张量仅需数毫秒。


6. 🛡️ 在线贪心:时间推进、最佳适配与即时合并

6.1 📂 数据结构

  • • 已分配表:记录当前存活张量的 (偏移, 尺寸, 释放时刻)
  • • 空闲块列表:按偏移排序,记录可用连续内存区间

6.2 ⚡ 分配与释放规则

分配过程(时刻  ,张量  ):

  • • 在空闲块中查找所有尺寸   的块,选择 尺寸最小 的块(Best‑Fit)。
  • • 若找到,从该块切出  ,剩余部分放回空闲列表。
  • • 若未找到,则在当前池尾分配新内存,峰值增加 

释放过程(时刻  ):

  • • 从已分配表中移除张量,将其占用的块插入空闲列表。
  • • 立即合并相邻空闲块,维持大块连续空间。

6.3 📐 形式化数学

空闲块集  ,分配时选择 
释放张量   后,合并相邻块:  为左右相邻空闲块。

💡 解释  的表达式表示在所有大小不小于   的空闲块中,选取大小最小的那个,这就是 Best‑Fit 策略。合并公式中,  是位于刚释放块左右两侧的空闲块,将它们与新释放的块连接起来,形成更大的连续空闲区域,从而减少碎片。

6.4 🧭 启发式逻辑

  • • 最佳适配(Best-Fit):最小化本次分配产生的内部碎片大小。选择最接近请求尺寸的空闲块,能保留最大块给未来的大请求,这是一种保守的风险规避策略——宁可产生大量极小的“边角料”,也不愿将大块切碎。
  • • 即时合并(Coalescing):内存碎片化的本质是空闲块被已分配块物理隔离。只有在释放时立即合并相邻块,才能对抗时间累积导致的熵增,确保空闲总容量与最大连续块大小之间的差距始终保持收敛,从而抑制峰值因碎片而增大。

📊 流程示意图

  • • 该流程按时间顺序推进,每个事件驱动状态更新。
  • • 分配时 Best-Fit 查找是在排序空闲块上的线性扫描。
  • • 释放后立即合并,确保空闲块始终保持最大连续长度。
  • • 这种设计不依赖未来信息,鲁棒性强。

💻 核心实现


   
   
   
   
    
   
   
   
   def online_greedy(tensors):
    events = []
    for
 t in tensors:
        events.append((t.start, 0, t.id))
        events.append((t.end, 1, t.id))
    events.sort(key=lambda x: (x[0], x[1]))
    allocated = {}      # id -> (off, size, end)
    free_blocks = []    # (off, size)
    peak = 0
    offsets = [0]*len(tensors)
    for
 time, typ, tid in events:
        t = tensors[tid]
        if
 typ == 0:  # alloc
            size = t.size
            best_idx, best_fit = -1, float('inf')
            for
 i, (off, sz) in enumerate(free_blocks):
                if
 sz >= size and sz < best_fit:
                    best_fit, best_idx = sz, i
            if
 best_idx != -1:
                off, sz = free_blocks.pop(best_idx)
                if
 sz > size:
                    free_blocks.append((off+size, sz-size))
                    free_blocks.sort(key=lambda x: x[0])
            else
:
                off = peak
                peak += size
            allocated[tid] = (off, size, t.end)
            offsets[tid] = off
        else
:  # free
            off, size, _ = allocated.pop(tid)
            free_blocks.append((off, size))
            free_blocks.sort(key=lambda x: x[0])
            merged = []
            for
 off2, sz2 in free_blocks:
                if
 not merged or off2 > merged[-1][0] + merged[-1][1]:
                    merged.append([off2, sz2])
                else
:
                    merged[-1][1] += sz2
            free_blocks = [(o, s) for o, s in merged]
    return
 peak, offsets
  • • 事件列表包含分配(type=0)和释放(type=1),同一时刻先释放后分配。
  • • 空闲块始终按偏移排序,以便合并。
  • • Best-Fit 扫描所有空闲块,选择最小可用块。

对示例张量按时间顺序模拟:时刻0分配T0(峰值10),时刻1分配T1(无空闲块,扩展至30),时刻2释放T0(空闲块[0,10)),同时分配T2(Best-Fit选中[0,10)但尺寸不足15,被迫扩展至45)。最终峰值 45,比最优值高28.6%。这说明在线算法因无法预见未来,导致碎片无法利用,峰值增大。复杂度  ,常用于PyTorch缓存分配器。


7. ✅ 各类算法达到最优的充分条件

7.1 🟢 离线贪心的最优条件

  • • 生命周期呈嵌套(Laminar)结构:任意两个区间要么不相交,要么一个完全包含另一个。此时冲突图为森林,降序最左贪心等价于树的最优着色。
  • • 所有张量尺寸相等:问题退化为区间图着色,区间图是完美图,贪心首次适配直接得到最小颜色数。
  • • 所有大张量形成一个最大团:即最大尺寸的张量彼此都重叠,其总尺寸构成不可超越的下界,贪心不会突破该下界。

7.2 🟡 在线贪心的最优条件

  • • 释放顺序严格为分配逆序(LIFO):类似栈式内存,空闲块总在池尾,合并后无碎片,峰值等于历史最大同时存活量。
  • • 所有请求尺寸为幂次且可完全匹配:Best‑Fit配合Buddy系统,不会产生不可用碎片。

7.3 📊 近似比保证

  • • 对于区间图,离线降序首次适配的渐近竞争比 ≤ 1.22。
  • • 在线算法因缺少未来信息,最坏情况可能偏离较大(如示例中的45 vs 35)。

8. 📊 对比实验与性能量化分析

为了直观展示不同分配策略下的内存布局,下图绘制了张量在内存池中的具体起始偏移及占用区间(对应本例的三个张量):

  • • 该布局展示每个张量在内存池中的具体起始偏移位置。
  • • 多个张量起始于同一偏移(如 T0 和 T2)表明它们实现了内存复用。
  • • 总跨度即为峰值内存,直观反映算法性能。
  • • 若布局中出现较多未被占用的间隙,则说明碎片严重,峰值被抬高。

采用标准测试用例(三个张量尺寸10、20、15,生命周期 [0,2)、[1,3)、[2,4)),三种策略的分配结果如下表:

分配器
峰值内存
偏移分配方案
MILP 精确解
35
T1:0, T2:20, T0:20 (复用)
离线贪心
35
同上
在线贪心
45
T0:0, T1:10, T2:30 (无复用)

🔍 关键洞察

  • • 离线贪心在此达到最优,因大张量 T1 和 T2 重叠形成瓶颈,小张量 T0 可复用空隙。
  • • 在线贪心因无法预见 T2,在 T0 释放前扩展池尾,导致峰值增加 28.6%。
  • • 这量化了静态编译期优化的价值——全知全局时间表可节省约 30% 内存。

大规模随机测试(100张量)显示离线贪心平均比在线优 15%~20%,运行时间均 <10ms,而 MILP 在   时求解时间指数增长。离线贪心的性能优势在张量尺寸分布偏斜(少数大张量、大量小张量)时尤为显著,因为降序排序能最大化大张量之间的空隙复用率,减少碎片。


9. 🏁 总结与未来方向

本文系统梳理了三种典型策略:

  • • MILP精确求解 🎯:最优基准,但规模受限。
  • • 离线贪心(尺寸降序+最左适配) 🚀:接近最优,速度极快,编译优化首选。其启发性在于 先难后易的空间压实
  • • 在线贪心(时间推进+Best‑Fit+合并) 🛡️:鲁棒性强,适合动态场景。其启发性在于 无未来信息下的最小承诺与碎片抑制

我们给出了严格数学模型、完整实现和可视化辅助,通过对比实验揭示信息完备性对内存利用率的巨大影响,并强调了峰值内存的重要性以及碎片对峰值的负面影响。

🌟 未来方向

  • • 联合优化算子调度与内存分配,改变执行顺序以降低峰值。
  • • 支持多级存储(HBM、DDR、共享内存)的异构分配。
  • • 将启发式嵌入JIT编译器,实现自适应规划。

全部代码开源,可直接用于学术验证或工业原型开发。

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 220
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读4.9k
粉丝0
内容220