后端和委托¶
受众:供应商、后端代表开发人员,他们有兴趣将自己的编译器和硬件集成为 ExecuTorch 的一部分
后端委托是后端处理和执行 PyTorch 的入口点 利用 Specialized 的性能和效率优势的计划 后端和硬件,同时仍为 PyTorch 用户提供体验 接近 PyTorch 运行时的 PyTorch 运行时。
后端接口:概述¶
概括地说,后端的入口点由 2 个组件定义:
表示程序的 IR:Edge Dialect (通过 API 的 API
to_edge
后端要实现的几个接口:
提前 (AOT)
程序预处理(例如,提前编译、转换、优化......
运行
程序初始化(例如运行时编译)。
程序执行。
(可选)程序销毁(例如,释放后端拥有的资源)。
委托后端实现由以下部分组成:
预先预处理接口
运行时初始化和执行接口
该图如下所示
图 1.后端接口的高级别入口点,包括预先和运行时。
后端接口:预先预处理¶
后端实现主要有两个 Ahead-of-Time 入口点:和 。partition
preprocess
partitioner
是后端实现的一种算法,用于标记要降低到后端的节点。 API 将应用分区算法并将每个子图(由连接的标记节点组成)降低到目标后端。每个子图
将发送到后端提供的部分,以编译为二进制 blob。to_backend
preprocess
在分区期间,不允许改变程序,它应该为每个节点应用 tag。包括标记的导出程序和分区标记字典,用于查找标记和
链接到 和exported_program
PartitionResult
to_backend
backend_id
compile_spec
def partition(
exported_program: ExportedProgram,
) -> PartitionResult:
在预处理期间,后端被赋予一个 edge dialect 程序
指定编译所需值的编译规范列表,并且
预期返回一个已编译的 blob,或包含所需程序的二进制文件
运行。在序列化过程中,
编译后的 blob 将作为文件的一部分进行序列化,并直接加载到设备中。这
此过程的 API 为:.pte
def preprocess(
edge_program: ExportedProgram,
compile_specs: List[CompileSpec],
) -> PreprocessResult:
此处实现了 preprocess 函数的演示。
该演示遍历 和
将 、 和 指令序列化为字符串,稍后
在运行时解析和执行。edge_program
add
mul
sin
该图如下所示
图 2.图经过 partition,每个子图将被发送到 preprocess 部分。
后端接口:运行时初始化和执行¶
在运行时,来自该函数的已编译 blob 将为
loaded 并直接传递给后端的自定义函数。这
function 负责对编译后的单元进行进一步处理,以及
执行任何后端初始化。后端的自定义函数将
然后调用以执行 生成的句柄。最后,如果
销毁是某些后端所必需的,后端可以实现一个函数,当程序超出其生命周期时,将调用该函数。preprocess
init
execute
init
destroy
// Runtime check
ET_NODISCARD bool is_available();
// Runtime initialization
ET_NODISCARD virtual Result<DelegateHandle*> init(
BackendInitContext& context,
FreeableBuffer* processed,
ArrayRef<CompileSpec> compile_specs);
// Runtime execution
ET_NODISCARD virtual Error execute(
BackendExecutionContext& context,
DelegateHandle* handle,
EValue** args);
// [optional] Runtime destroy. Destroy the resource held by the backend
virtual void destroy(ET_UNUSED DelegateHandle* handle);
该图如下所示
图 3.标准 ExecuTorch 运行时与后端入口点之间的关系。
为了使后端可用于 ExecuTorch 运行时,必须通过 API 注册它:register_backend
ET_NODISCARD Error register_backend(const Backend& backend);
后端的静态注册(即在 libraray 初始化或加载时)可以按以下方式实现:
namespace {
auto cls = BackendWithCompiler();
Backend backend{"BackendWithCompilerDemo", &cls};
static auto success_with_compiler = register_backend(backend);
} // namespace
开发人员工具集成:可调试性¶
提供一致的调试体验(无论是运行时故障还是性能分析)都很重要。为此,ExecuTorch 使用本机开发人员工具,它支持通过调试句柄将程序指令与原始 PyTorch 代码相关联。您可以在此处阅读更多相关信息。
委托的程序或子图对 ExecuTorch 运行时是不透明的,并显示为特殊指令,它要求相应的后端处理子图或程序的执行。由于后端 delgate 的不透明性,本机 Developer Tools 无法查看委托程序。因此,与非委托执行相比,委托执行的调试、功能或性能体验会受到严重影响。call_delegate
为了向用户提供一致的调试体验,无论是否对模型使用委托,开发人员工具都提供了一个接口来将委托的(子)图与原始(子)图相关联。Developer Tools 通过 debug handles map 执行此操作,该 map 允许代理生成可与代理使用的原始(子)图关联的内部 handles。然后,在运行时,后端开发人员可以使用内部句柄报告错误或分析信息,这些句柄将使用调试句柄映射映射到原始(子)图。更多信息请参考 委托调试。
通过利用调试标识符,后端开发人员可以将调试嵌入为委托 blob 的一部分
这样,在执行阶段,后端开发人员可以通过 debug 标识符将失败的指令关联到委托内部 回到 PyThon 代码的确切行。
常见问题¶
1. 如何在 backend.preprocess 中获取数据?
正在预处理的 graph 模块是一个提升的图形,这意味着 static
权重和偏差等数据作为图形的输入提供。但是,我们
可以通过导出的程序提前访问权重和偏差。自
从给定的节点访问这些参数,我们可以使用get_params
torch/_export/utils.py
2. 我们如何将数据(如权重/偏差)嵌入到后端?
后端通常有一些方法来优化 const 数据。在这种情况下, 我们需要标记占位符节点,这些节点也是 partitioner,在 backend.preprocess 期间,我们可以按照 第一个问题来获取权重。
3. 我们如何使用特定的后端在 Python 中运行 reduceded 的模块?
我们还没有添加支持,但这就是计划!
4. 我们应该期望在 edge dialect 程序中看到节点吗?
get_attr
节点将仅显示用于 Control Flow 或
代表团。它不会保存任何数据。
5. 我们可以委托给多个后端吗?
是的!有两种方法可以执行此操作:
选项 1:针对不同的后端多次运行 to_backend
如果我们有两个后端,backend_1 和 backend_2,并且它们都有自己的 参与者:backend_1_parititioner 和 backend_2_partitioner,我们可以运行 喜欢:
# Will first lower nodes to backend_1 depending on the backend_1_parititioner depending on partitioner algorithm
exported_program_backend_1 = to_backend(exported_program, backend_1_parititioner())
# For the rest of nodes, they will be lowered to backend_2 depending on backend_2_parititioner
exported_program_backend_1_and_2 = to_backend(exported_program_backend_1, backend_2_parititioner())
更具体的例子可以在这里找到。 在此示例中, qnnpack 是一个后端,xnnpack 是另一个后端。我们还没有开源 这两个 backends delegate 尚未完成,此示例不会开箱即用。它可以 用作参考,看看如何做到这一点。
此选项很容易尝试,因为通常所有后端都会实现自己的 Parititioner.但是,如果我们将 to_backend 调用的顺序。如果我们想对节点进行更好的控制,比如 他们应该使用哪个后端,选项 2 更好。
选项 2:拥有一个分区程序,用于不同的后端进行分区
另一种选择是创建自定义的分区器,比如 partitioner ,在分区器逻辑中,backend_1_2_partitioner
class Backend_1_2_Partitioner(Partitioner):
"""
Partitions all add/mul nodes regardless of order for Backend2
"""
def __init__(self) -> None:
self.delegation_spec_1 = DelegationSpec("Backend1", [])
self.delegation_spec_2 = DelegationSpec("Backend2", [])
self.partition_tags = {}
def partition(
self, exported_program: ExportedProgram
) -> ExportedProgram:
# Tag all nodes in the first partiton to backend 1
node_to_backend_1 = ... # some logic to select the nodes from the graph
delegation_tag = f"backend2_tag{partitioner_1.id}"
node.meta["delegation_tag"] = delegation_tag
self.partition_tags[delegation_tag] = self.delegation_spec_1
# Tag all nodes in the first partiton to backend 2
node_to_backend_2 = ... # some logic to select the nodes from the graph
delegation_tag = f"backend2_tag{partitioner_2.id}"
node.meta["delegation_tag"] = delegation_tag
self.partition_tags[delegation_tag] = self.delegation_spec_2
return exported_program
6. 有没有一种简单的方法来编写分区程序?
我们在这里提供了一些帮助分区程序,以便于查找 来自分解运算符的节点。
7. 我们如何将 node 链接回源码?我们提供了一个辅助函数
from executorch.exir.print_program import inspect_node
print(inspect_node(graph, node))
它将突出显示图表中的节点并指向源代码,示例输出将如下所示:
_param_constant1 error_msg: Here is the node in the graph module:
graph():
%arg0_1 : [num_users=1] = placeholder[target=arg0_1]
%_param_constant0 : [num_users=1] = get_attr[target=_param_constant0]
--> %_param_constant1 : [num_users=1] = get_attr[target=_param_constant1]
%aten_convolution_default : [num_users=2] = call_function[target=executorch.exir.dialects.edge._ops.aten.convolution.default](args = (%arg0_1, %_param_constant0, %_param_constant1, [1, 1], [0, 0], [1, 1], False, [0, 0], 1), kwargs = {})
%_param_constant2 : [num_users=1] = get_attr[target=_param_constant2]
%_param_constant3 : [num_users=1] = get_attr[target=_param_constant3]
%aten_convolution_default_1 : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.convolution.default](args = (%aten_convolution_default, %_param_constant2, %_param_constant3, [1, 1], [0, 0], [1, 1], False, [0, 0], 1), kwargs = {})
%aten_add_tensor : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.add.Tensor](args = (%aten_convolution_default, %aten_convolution_default_1), kwargs = {})
%_param_constant4 : [num_users=1] = get_attr[target=_param_constant4]
%_param_constant5 : [num_users=1] = get_attr[target=_param_constant5]
%aten_convolution_default_2 : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.convolution.default](args = (%aten_add_tensor, %_param_constant4, %_param_constant5, [1, 1], [0, 0], [1, 1], False, [0, 0], 1), kwargs = {})
%aten_gelu_default : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.gelu.default](args = (%aten_convolution_default_2,), kwargs = {})
return [aten_gelu_default]
This node _param_constant1 has metadata of:
The node stacktrace:
Traceback (most recent call last):
File "/tmp/ipykernel_1204253/3382880687.py", line 7, in forward
return self.test_model(x)
File "/mnt/xarfuse/uid-25337/7b86ad0c-seed-nspid4026532987_cgpid2707357-ns-4026532984/torch/nn/modules/module.py", line 1528, in _call_impl
return forward_call(*args, **kwargs)
File "/tmp/ipykernel_1204253/712280972.py", line 10, in forward
a = self.conv1(x)