内存规划¶
受众:对自定义 ExecuTorch 程序运行内存区域感兴趣的后端集成商和嵌入式开发人员。
概述¶
内存规划是在执行ExportedProgram并生成 ExecuTorch 程序之前采取的最后一个操作。在此过程中,ExecuTorch 会获取每个可变张量的大小和生命周期,并规划它们在固定大小内存区域中的位置。
具体而言,内存规划涉及三个遍历阶段:
SpecPropPass为图中的每个张量(输入、中间或输出)计算一个 TensorSpec。张量规范中最重要的字段是张量形状的符号表达式,其中初始符号集来自输入张量的维度,中间张量形状的符号表达式通过张量操作进行传播。用户可以将维度标记为动态或静态,当维度为动态时,用户需要使用 ValueRange 对该维度进行注解。SymShapeEvalPass将符号表达式评估为带有其上界的实际整数。有两种方法进行上界特化: HintBasedSymShapeEval(即将弃用)是评估上界的旧方法。它不查看符号的 ValueRange,而是使用示例输入的形状来替换所有符号。我们称之为“基于提示”,因为示例输入的形状仅是对运行时可能出现的输入形状的提示,且仅用于追踪。ValueRangeBasedSymShapeEval 是进行 UpperBoundMemory 规划推荐的方法。它将实际查看符号的 ValueRange,并对这些范围进行推理以获得真实的上界。MemoryPlanningPass在给定所有张量并获得具有具体整数形状的 TensorSpec 后,执行实际的内存规划。
算法¶
ExecuTorch 开箱即用地提供两种内存规划算法选项,但如果提供的选项不适合或不足以满足您的使用场景,用户可以自行定义。
朴素算法只是将所有张量线性地拼接在一个内存块中,而不考虑内存复用。它代表了总内存消耗的上限,并作为基准线。
贪婪算法基于最佳适配准则尝试重用已分配的内存。具体而言: 当不存在生命周期与当前张量(我们正为其进行内存规划)不重叠的已分配内存时,我们将分配一个与当前张量具有相同大小和生命周期的新内存缓冲区。当存在一个或多个生命周期与当前张量重叠的已分配内存缓冲区时,我们选择大小最接近当前张量的缓冲区,以减少内存碎片。最后,我们在内存中线性地分配这些内存缓冲区。
方法输入和输出¶
MemoryPlanningPass 公开了不规划程序输入和输出内存的选项。如果未规划 IO,则用户需要在运行时提供数据缓冲区来支持这些值。示例:
program = edge_program.to_executorch(
exir.ExecutorchBackendConfig(
memory_planning_pass=MemoryPlanningPass(
alloc_graph_input=False, # Inputs will not be memory planned, the data_ptr for input tensors after model load will be nullptr
alloc_graph_output=True, # Outputs will be memory planned, the data_ptr for output tensors after model load will be in the `planned_memory`.
)
)
)
一种常见的设置是:模型的输出被作为后续推理的输入。在这种情况下,通常最好不要对输入/输出进行内存规划,而是在运行时为输入和输出提供相同的缓冲区,以避免数据拷贝。
自定义内存计划¶
用户可以编写自定义内存规划方案,以利用多个内存位置(如 SRAM 和 DRAM),将特定节点的输出放置到指定位置,甚至更改规划算法本身。以下示例展示了如何复用提供的规划算法,但采用多层级结构,并将特定算子的输出放置到特定的内存区域中。
class CustomPoolMemoryPlanningPass(MemoryPlanningPass):
def run(self, graph_module: GraphModule, graph_signature: Optional[ExportGraphSignature]) -> PassResult:
for subgm in graph_module.modules():
if not isinstance(subgm, GraphModule):
continue
for node in subgm.graph.nodes:
# mem_id = 1 placeholder and outputs of mul
# mem_id = 2 for outputs of add
# parent class will copy spec will to alloc nodes
if node.op == "placeholder":
node.meta["spec"].mem_id = 1
continue
if node.op != "call_function":
continue
if node.target == torch.ops.aten.add.out:
node.meta["spec"].mem_id = 2
elif node.target == torch.ops.aten.mul.out:
node.meta["spec"].mem_id = 1
return super().run(graph_module, graph_signature)
随后,在降低到 ExecuTorch 时,您可以按以下方式使用自定义计划:
program = edge_program.to_executorch(
exir.ExecutorchBackendConfig(
memory_planning_pass=CustomPoolMemoryPlanningPass(
memory_planning_algo=greedy,
)
)
)
用户如果想要编写自定义的内存规划算法,应该首先查看贪心算法的实现。