内核注册¶
概述¶
在 ExecuTorch 模型导出 的最后阶段,我们将方言中的运算符降低到 核心 ATen 运算符 的out 变体。然后我们将这些运算符名称序列化到模型工件中。在运行时执行期间,对于每个运算符名称,我们将需要找到实际的内核,即执行繁重计算并返回结果的 C++ 函数。
内核库¶
第一方内核库:¶
可移植内核库 是内部默认内核库,涵盖了大多数核心 ATen 运算符。它易于使用/阅读,并且是用可移植的 C++17 编写的。但是,它没有针对性能进行优化,因为它没有针对任何特定目标进行专门化。因此,我们为 ExecuTorch 用户提供内核注册 API,以便轻松注册他们自己优化的内核。
优化内核库 专门针对某些运算符的性能进行优化,利用现有的第三方库,例如 EigenBLAS。这与可移植内核库配合使用效果最佳,在可移植性和性能之间取得了良好的平衡。可以在 此处 找到结合这两个库的示例。
量化内核库 实现了用于量化和反量化的运算符。这些运算符不属于核心 ATen 运算符,但对于大多数生产用例至关重要。
自定义内核库:¶
实现核心 ATen 运算的自定义内核。即使我们没有用于核心 ATen 运算的自定义内核的内部示例,也可以将优化内核库视为一个很好的示例。我们优化了 add.out
和一个可移植的 add.out
。当用户结合这两个库时,我们提供 API 来选择要用于 add.out
的内核。为了编写和使用实现核心 ATen 运算的自定义内核,建议使用 基于 YAML 的方法,因为它提供了对以下方面的全面支持:
组合内核库并定义回退内核;
使用选择性构建来最大程度地减小内核大小。
自定义运算符 是 ExecuTorch 用户在 PyTorch 的 native_functions.yaml
之外定义的任何运算符。
运算符和内核约定¶
上述所有内核,无论是内部内核还是自定义内核,都应符合以下要求
匹配从运算符模式派生的调用约定。内核注册 API 将为自定义内核生成标头作为参考。
满足边缘方言中定义的数据类型约束。对于具有某些数据类型作为参数的张量,自定义内核的结果需要与预期的数据类型匹配。约束在边缘方言运算符中可用。
给出正确的结果。我们将提供一个测试框架来自动测试自定义内核。
API¶
以下是可用于将内核/自定义内核/自定义运算注册到 ExecuTorch 中的 API
如果不清楚使用哪个 API,请参阅 最佳实践。
YAML 入口 API 高层架构¶
ExecuTorch 用户需要提供
具有 C++ 实现的自定义内核库
与该库关联的 YAML 文件,用于描述该库正在实现的运算符。对于部分内核,yaml 文件还包含内核支持的数据类型和维度顺序信息。更多详细信息请参见 API 部分。
YAML 入口 API 工作流程¶
在构建时,与内核库关联的 yaml 文件将与模型运算信息(请参阅选择性构建文档)一起传递给内核解析器,结果是运算符名称和张量元数据的组合与内核符号之间的映射。然后,代码生成工具将使用此映射生成 C++ 绑定,将内核连接到 ExecuTorch 运行时。ExecuTorch 用户需要将生成的库链接到他们的应用程序中才能使用这些内核。
在静态对象初始化时,内核将被注册到 ExecuTorch 内核注册表中。
在运行时初始化阶段,ExecuTorch 将使用运算符名称和参数元数据作为键来查找内核。例如,对于 “aten::add.out” 和维度顺序为 (0, 1, 2, 3) 的浮点张量输入,ExecuTorch 将进入内核注册表并查找与名称和输入元数据匹配的内核。
核心 ATen 运算 Out 变体的 YAML 入口 API¶
顶层属性
op
(如果运算符出现在native_functions.yaml
中)或func
用于自定义运算符。此键的值需要是op
键的完整运算符名称(包括重载名称),或者如果我们描述的是自定义运算符,则为完整的运算符模式(命名空间、运算符名称、运算符重载名称和模式字符串)。有关模式语法,请参阅此 说明。kernels
:定义内核信息。它由arg_meta
和kernel_name
组成,它们绑定在一起以描述“对于具有这些元数据的输入张量,使用此内核”。type_alias
(可选):我们为可能的数据类型选项提供别名。T0: [Double, Float]
表示T0
可以是Double
或Float
之一。dim_order_alias
(可选):类似于type_alias
,我们为可能的维度顺序选项提供名称。
kernels
下的属性
arg_meta
:一个“张量参数名称”条目的列表。这些键的值是由相应的kernel_name
实现的数据类型和维度顺序别名。null
表示内核将用于所有输入类型。kernel_name
:将实现此运算符的 C++ 函数的预期名称。您可以在此处放置任何您想要的内容,但您应该遵循将重载名称中的.
替换为下划线并将所有字符小写的约定。在此示例中,add.out
使用名为add_out
的 C++ 函数。add.Scalar_out
将变为add_scalar_out
,并使用小写S
。我们支持内核的命名空间,但请注意,我们将在命名空间的最后一级插入native::
。因此,kernel_name
中的custom::add_out
将指向custom::native::add_out
。
运算符条目的一些示例
- op: add.out
kernels:
- arg_meta: null
kernel_name: torch::executor::add_out
具有默认内核的核心 ATen 运算符的 out 变体
具有数据类型/维度顺序专用内核的 ATen 运算符(适用于 Double
数据类型,并且维度顺序需要为 (0, 1, 2, 3))
- op: add.out
type_alias:
T0: [Double]
dim_order_alias:
D0: [[0, 1, 2, 3]]
kernels:
- arg_meta:
self: [T0, D0]
other: [T0 , D0]
out: [T0, D0]
kernel_name: torch::executor::add_out
自定义运算的 YAML 入口 API¶
如上所述,此选项在选择性构建和诸如合并运算符库之类的功能方面提供了更多支持。
首先,我们需要指定运算符模式以及 kernel
部分。因此,我们使用 func
和运算符模式而不是 op
。例如,以下是自定义运算的 yaml 条目
- func: allclose.out(Tensor self, Tensor other, float rtol=1e-05, float atol=1e-08, bool equal_nan=False, bool dummy_param=False, *, Tensor(a!) out) -> Tensor(a!)
kernels:
- arg_meta: null
kernel_name: torch::executor::allclose_out
kernel
部分与核心 ATen 运算中定义的部分相同。对于运算符模式,我们重用了此 README.md 中定义的 DSL,但有一些差异
仅限 Out 变体¶
ExecuTorch 仅支持 out 样式运算符,其中
调用者在最后一个位置提供输出张量或张量列表,名称为
out
。C++ 函数修改并返回相同的
out
参数。如果 YAML 文件中的返回类型为
()
(映射到 void),则 C++ 函数仍应修改out
,但不需要返回任何内容。
out
参数必须是仅关键字参数,这意味着它需要跟随一个名为*
的参数,例如下面的add.out
示例。按照惯例,这些 out 运算符使用模式
<name>.out
或<name>.<overload>_out
命名。
由于所有输出值都通过 out
参数返回,因此 ExecuTorch 忽略实际的 C++ 函数返回值。但是,为了保持一致性,当返回类型为非 void
时,函数应始终返回 out
。
只能返回 Tensor
或 ()
¶
ExecuTorch 仅支持返回单个 Tensor
或单元类型 ()
(映射到 void
)的运算符。它不支持返回任何其他类型,包括列表、可选类型、元组或标量,如 bool
。
支持的参数类型¶
ExecuTorch 不支持核心 PyTorch 支持的所有参数类型。以下是我们目前支持的参数类型列表
Tensor
int
bool
float
str
Scalar
ScalarType
MemoryFormat
Device
Optional
List
List<Optional
> Optional<List
>
CMake 宏¶
我们提供构建时宏来帮助用户构建他们的内核注册库。该宏采用描述内核库的 yaml 文件以及模型运算符元数据,并将生成的 C++ 绑定打包到 C++ 库中。该宏在 CMake 上可用。
generate_bindings_for_kernels(FUNCTIONS_YAML functions_yaml CUSTOM_OPS_YAML custom_ops_yaml)
接受核心 ATen 运算 out 变体的 yaml 文件以及自定义运算的 yaml 文件,为内核注册生成 C++ 绑定。它还依赖于 gen_selected_ops()
生成的选择性构建工件,有关更多信息,请参阅选择性构建文档。然后 gen_operators_lib
将这些绑定打包为 C++ 库。例如
# SELECT_OPS_LIST: aten::add.out,aten::mm.out
gen_selected_ops("" "${SELECT_OPS_LIST}" "")
# Look for functions.yaml associated with portable libs and generate C++ bindings
generate_bindings_for_kernels(FUNCTIONS_YAML ${EXECUTORCH_ROOT}/kernels/portable/functions.yaml)
# Prepare a C++ library called "generated_lib" with _kernel_lib being the portable library, executorch is a dependency of it.
gen_operators_lib("generated_lib" KERNEL_LIBS ${_kernel_lib} DEPS executorch)
# Link "generated_lib" into the application:
target_link_libraries(executorch_binary generated_lib)
我们还提供了合并两个 yaml 文件的能力,并给定优先级。merge_yaml(FUNCTIONS_YAML functions_yaml FALLBACK_YAML fallback_yaml OUTPUT_DIR out_dir)
将 functions_yaml 和 fallback_yaml 合并为一个 yaml,如果 functions_yaml 和 fallback_yaml 中存在重复条目,则此宏将始终采用 functions_yaml 中的条目。
示例
# functions.yaml
- op: add.out
kernels:
- arg_meta: null
kernel_name: torch::executor::opt_add_out
和 out 回退
# fallback.yaml
- op: add.out
kernels:
- arg_meta: null
kernel_name: torch::executor::add_out
合并后的 yaml 将具有 functions.yaml 中的条目。
自定义运算的 C++ API¶
与 YAML 入口 API 不同,C++ API 仅使用 C++ 宏 EXECUTORCH_LIBRARY
和 WRAP_TO_ATEN
进行内核注册,并且没有选择性构建支持。这使得此 API 在开发速度方面更快,因为用户不必进行 YAML 编写和构建系统调整。
请参阅 自定义运算最佳实践,了解使用哪个 API。
类似于 PyTorch 中的 TORCH_LIBRARY
,EXECUTORCH_LIBRARY
接受运算符名称和 C++ 函数名称,并将它们注册到 ExecuTorch 运行时。
准备自定义内核实现¶
为功能变体(在 AOT 编译中使用)和 out 变体(在 ExecuTorch 运行时中使用)定义您的自定义运算符模式。该模式需要遵循 PyTorch ATen 约定(请参阅 native_functions.yaml
)。例如
custom_linear(Tensor weight, Tensor input, Tensor(?) bias) -> Tensor
custom_linear.out(Tensor weight, Tensor input, Tensor(?) bias, *, Tensor(a!) out) -> Tensor(a!)
然后使用 ExecuTorch 类型,以及注册到 ExecuTorch 运行时的 API,根据模式编写您的自定义内核
// custom_linear.h/custom_linear.cpp
#include <executorch/runtime/kernel/kernel_includes.h>
Tensor& custom_linear_out(const Tensor& weight, const Tensor& input, optional<Tensor> bias, Tensor& out) {
// calculation
return out;
}
使用 C++ 宏将其注册到 ExecuTorch¶
在上面的示例中附加以下行
// custom_linear.h/custom_linear.cpp
// opset namespace myop
EXECUTORCH_LIBRARY(myop, "custom_linear.out", custom_linear_out);
现在我们需要为此运算编写一些包装器以使其在 PyTorch 中显示,但请不要担心,我们不需要重写内核。为此目的创建一个单独的 .cpp 文件
// custom_linear_pytorch.cpp
#include "custom_linear.h"
#include <torch/library.h>
at::Tensor custom_linear(const at::Tensor& weight, const at::Tensor& input, std::optional<at::Tensor> bias) {
// initialize out
at::Tensor out = at::empty({weight.size(1), input.size(1)});
// wrap kernel in custom_linear.cpp into ATen kernel
WRAP_TO_ATEN(custom_linear_out, 3)(weight, input, bias, out);
return out;
}
// standard API to register ops into PyTorch
TORCH_LIBRARY(myop, m) {
m.def("custom_linear(Tensor weight, Tensor input, Tensor(?) bias) -> Tensor", custom_linear);
m.def("custom_linear.out(Tensor weight, Tensor input, Tensor(?) bias, *, Tensor(a!) out) -> Tensor(a!)", WRAP_TO_ATEN(custom_linear_out, 3));
}
编译和链接自定义内核¶
将其链接到 ExecuTorch 运行时:在构建二进制文件/应用程序的 CMakeLists.txt
中,我们需要将 custom_linear.h/cpp 添加到二进制目标中。我们可以构建一个动态加载库(.so 或 .dylib)并将其链接起来。
以下是一个执行此操作的示例
# For target_link_options_shared_lib
include(${EXECUTORCH_ROOT}/build/Utils.cmake)
# Add a custom op library
add_library(custom_op_lib SHARED ${CMAKE_CURRENT_SOURCE_DIR}/custom_op.cpp)
# Include the header
target_include_directory(custom_op_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# Link ExecuTorch library
target_link_libraries(custom_op_lib PUBLIC executorch)
# Define a binary target
add_executable(custom_op_runner PUBLIC main.cpp)
# Link this library with --whole-archive !! IMPORTANT !! this is to avoid the operators being stripped by linker
target_link_options_shared_lib(custom_op_lib)
# Link custom op lib
target_link_libraries(custom_op_runner PUBLIC custom_op_lib)
将其链接到 PyTorch 运行时:我们需要将 custom_linear.h、custom_linear.cpp 和 custom_linear_pytorch.cpp 打包到一个动态加载库(.so 或 .dylib)中,并将其加载到我们的 python 环境中。一种方法是
import torch
torch.ops.load_library("libcustom_linear.so/dylib")
# Now we have access to the custom op, backed by kernel implemented in custom_linear.cpp.
op = torch.ops.myop.custom_linear.default
自定义运算 API 最佳实践¶
鉴于我们有 2 个用于自定义运算的内核注册 API,我们应该使用哪个 API?以下是每个 API 的一些优点和缺点
C++ API
优点
只需要更改 C++ 代码
类似于 PyTorch 自定义运算 C++ API
维护成本低
缺点
没有选择性构建支持
没有集中式簿记
Yaml 入口 API
优点
具有选择性构建支持
为自定义运算提供集中位置
它显示了正在注册哪些运算以及哪些内核绑定到应用程序的这些运算
缺点
用户需要创建和维护 yaml 文件
更改运算定义相对不灵活
总的来说,如果我们正在构建一个应用程序并且它使用自定义运算,在开发阶段,建议使用 C++ API,因为它使用成本低且更改灵活。一旦应用程序进入生产阶段,自定义运算定义和构建系统非常稳定,并且需要考虑二进制文件大小,则建议使用 Yaml 入口 API。