何时应创建编译器插件?
当您需要将具有编译器依赖项的特定硬件加速器集成到 LiteRT 框架中时,需要使用 LiteRT 编译器插件。
在以下情况下,您应创建编译器插件:
- 您定位的新硬件后端不受支持。
- 您希望将特定模型操作分流到该硬件加速器,以提高性能或能效。
- 您需要支持 AOT 编译(在工作站上)或设备端编译。
该插件充当桥梁,通过调用后端编译器,获取部分机器学习模型并将其转换为目标硬件可以执行的格式。LiteRT 会将插件生成的自定义字节码捆绑到 .tflite 模型中,从而可以使用 LiteRT 运行时执行该模型。
编译器插件的工作原理是什么?
LiteRT 框架在模型加载或离线预处理阶段使用编译器插件来识别和准备模型子图,以便在目标硬件上执行。
此流程涉及两个主要阶段,由框架使用插件的导出函数进行编排:
- 分区:插件会检查整个模型图,并识别其支持且可在目标硬件上高效加速的运算子集。这些受支持的子图会进行“分区”(标记)以进行编译并概述。
- 编译:LiteRT 框架将分区子图传递回插件。然后,插件会使用其内部逻辑和可能的外部工具链(编译器)来生成一个或多个实现分区的特定于硬件的字节码模块。此字节码是目标硬件的运行时(HAL/驱动程序)最终将加载并执行的字节码。
该框架会使用调用硬件驱动程序的自定义操作替换原始子图,并传递由插件创建的已编译字节码。
LiteRT Dispatch 是编译器插件的运行时类似物。它们提供了一种根据编译器输出调用 HAL 的方法。如需了解详情,请参阅调度文档。
AOT 与设备端
LiteRT 可以使用编译器插件通过我们的工具支持 AOT 编译,以及支持设备端编译。设备端编译更加灵活,完全在 LiteRT 运行时 API 中实现,并且只需要管理单个模型。当在设备上运行过于消耗资源时(许多当代大型模型都是如此),AOT 流程可以解除编译阻塞。
后备
LiteRT 在构建时考虑到了对异构图的支持。插件未选择的任何操作都将留给 CPU 或在其他后端上进行加速。
实现编译器插件
LiteRT 编译器插件以共享库的形式实现,该共享库会导出 LiteRT C API 中定义的一组特定的 C 函数。
基本接口功能
核心功能围绕两个关键编译步骤展开:LiteRtCompilerPluginPartition 和 LiteRtCompilerPluginCompile。
| 函数 | 用途 |
|---|---|
| LiteRtCompilerPluginPartition | 选择并标记给定模型子图中的所有支持的操作(分区步骤)。 |
| LiteRtCompilerPluginCompile$ | 为预选的分区生成特定于硬件的字节码(编译步骤)。 |
C API 代码段
// Name associated with the manufacturer this plugin relates to.
LITERT_CAPI_EXPORT const char* LiteRtGetCompilerPluginSocManufacturer();
// Create and initialize the plugin instance.
LITERT_CAPI_EXPORT LiteRtStatus
LiteRtCreateCompilerPlugin(LiteRtCompilerPlugin* compiler_plugin,
LiteRtEnvironmentOptions env, LiteRtOptions options);
// Choose ops for compilation.
// This is the PARTITION step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtSubgraph subgraph, LiteRtOpList selected_ops);
// Prepare result to pass to the runtime for given model containing partitioned
// subgraphs. This is the COMPILE step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result);
1. 分区函数
函数签名如下:
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtSubgraph subgraph, LiteRtOpList selected_ops);
partition 函数的作用:这是选择阶段。插件会遍历输入 LiteRtSubgraph 中的操作。对于目标硬件支持并可加速的每个操作,插件都会将该操作添加到 selected_ops 参数中提供的 LiteRtOpList$中。LiteRt 框架使用此列表来定义将发送到最终编译步骤的分区的边界。
默认情况下,LiteRT 会将所有选定的操作分组到尽可能大的子 DAG 中。为了实现更精细的分区,在选择操作时可以关联一个索引,以进一步细分这些子图。
2. 编译函数
函数签名如下:
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result);
compile 函数的作用:这是生成阶段。输入 partitions 表示已隔离所有所选子图的模型。插件会处理这些分区,调用其特定的工具链来为目标硬件生成 bytecode。预计插件的输出会为传递以进行编译的每个子图提供一个入口点。在大多数情况下,这要么是每个输入子图的各个字节码模块,要么是具有多个入口点的单个字节码模块。
compile 返回的数据类型:LiteRtCompilerPluginCompile 函数使用 out 参数 LiteRtCompiledResult 返回其输出。
LiteRtCompiledResult 是插件管理的结构的不透明(相对于 LiteRT)句柄。它表示编译的输出,包含两个主要信息:
- 字节码模块:一个或多个包含特定于硬件的可执行字节码(即编译后的指令)的原始内存缓冲区。
- 通话信息:每个分区的元数据。这提供了从第
i个输入子图到结果字节码模块以及该模块中的入口点标识符的映射。
实施示例
以下代码段展示了基本插件如何实现核心功能。此示例取自 litert/vendors/examples/ 中的一个功能齐全的示例
插件识别和设置
这些函数可为框架提供有关插件和硬件的基本信息。
// Define the plugin's internal state structure
struct LiteRtCompilerPluginT {};
// Identify the manufacturer
const char* LiteRtGetCompilerPluginSocManufacturer() {
return "AcmeCorp"; // Example manufacturer name
}
// Specify the supported hardware (in this example, it supports kLiteRtHwAcceleratorNpu)
LiteRtStatus LiteRtGetCompilerPluginSupportedHardware(
LiteRtCompilerPlugin compiler_plugin,
LiteRtHwAccelerators* supported_hardware) {
// ... argument checking ...
*supported_hardware = kLiteRtHwAcceleratorNpu;
return kLiteRtStatusOk;
}
分区逻辑 (LiteRtCompilerPluginPartition)
此示例展示了插件仅在所有输入和输出均为 32 位浮点数时选择有限的一组运算(mul、sub 和特定的复合运算)。通常,确定是否应选择某项操作将包括对后端编译器工具链中的验证钩子的调用。
LiteRtStatus LiteRtCompilerPluginPartition(LiteRtCompilerPlugin compiler_plugin,
const char* soc_model,
LiteRtSubgraph subgraph,
LiteRtOpList selected_ops) {
// Iterate over ops and check criteria for selection
// (using a C++ wrapper namespace '::litert' for convenience).
// `subgraph` is a single subgraph from the original model, as such
// this function will be called for each subgraph in the original model.
::litert::Subgraph main_subgraph(subgraph);
for (const auto& op : main_subgraph.Ops()) {
// 1. Check a constraint: require all tensors to be Float32
bool only_f32 = true;
// ... logic to check input/output types ...
if (!only_f32) {
continue;
}
// 2. Check op codes and push to selected_ops list
if (op.Code() == kLiteRtOpCodeTflMul) {
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
} else if (op.Code() == kLiteRtOpCodeTflSub) {
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
} else if (op.Code() == kLiteRtOpCodeShloComposite) {
// Example of checking composite op options
// ... logic to check for "odml.rms_norm" name ...
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
}
}
return kLiteRtStatusOk;
}
在调用编译之前,LiteRT 会验证所选的所有操作,并将其“概括”为新的中间模型中的新子图。此中间模型会传递给编译。
编译逻辑 (LiteRtCompilerPluginCompile)
此函数接受分区子图,并生成自定义 LiteRtCompiledResult。此示例会为要编译的每个分区生成一个独立的字节码模块。在实际应用中,这通常涉及将 LiteRT 操作转换为后端编译器库的类型。功能性示例插件的“编译”会创建一个对图进行编码的人类可读字符串。
// Internal structure defining the compiled output
struct LiteRtCompiledResultT {
std::vector<std::string> byte_code; // The hardware bytecode buffers
std::vector<std::string> per_op_data; // Per-call metadata (CallInfo)
};
LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result) {
// 1. Create the internal result structure
auto model = litert::Model::CreateFromNonOwnedHandle(partitions);
const auto num_partitions = model.NumSubgraphs();
auto result = std::make_unique<LiteRtCompiledResultT>();
result->byte_code.resize(num_partitions);
result->per_op_data.resize(num_partitions);
// 2. Iterate and compile each partition
for (auto i = 0; i < num_partitions; ++i) {
// CompileSinglePartition is an internal helper that converts the subgraph
// into the target hardware's format and stores it in result->byte_code.
// In the case of the example this is just a stringification of the graph.
// ... internal call to CompileSinglePartition ...
// Example: result.byte_code[i] = generated_hw_code;
// Example: result.per_op_data[i] = absl::StrFormat("Partition_%d", i);
// The "per_op_data" is a unique identifier associated to the `ith` partition.
// This is analogous to the name of a function in a library.
// This is only meaningful when the plugin is preparing single modules with multiple entry points.
}
// 3. Pass ownership of the result back to the framework
*compiled_result = result.release();
return kLiteRtStatusOk;
}
// Functions to expose the compiled result data to the framework
LiteRtStatus LiteRtGetCompiledResultByteCode(
LiteRtCompiledResult compiled_result, LiteRtParamIndex byte_code_idx,
const void** byte_code, size_t* byte_code_size) {
// ... implementation reads from compiled_result->byte_code ...
}
// ... other LiteRtGetCompiledResult* functions ...
使用情况和验证
LiteRT 提供了各种工具,用于将编译器插件应用于模型文件、执行结果以及进行验证/基准比较。请参阅加速器测试套件文档以及基准比较和分析文档。