详细 C++ Runtime API 教程#
作者: Jacob Szwejbka
在本教程中,我们将介绍如何使用更详细的底层 API 在 C++ 中运行 ExecuTorch 模型:准备 MemoryManager、设置输入、执行模型以及获取输出。但是,如果您在寻找一个开箱即用且更简单的接口,请尝试 Module Extension 教程 和 在 C++ 中使用 ExecuTorch。
有关 ExecuTorch Runtime 的高层概述,请参阅 Runtime 概述;有关每个 API 更深入的文档,请参阅 Runtime API 参考。这里有一个完整功能的 C++ 模型运行程序,而 设置 ExecuTorch 文档介绍了如何构建并运行它。
先决条件#
您需要一个 ExecuTorch 模型来跟随本教程。我们将使用由 导出到 ExecuTorch 教程 生成的 SimpleConv 模型。.
模型加载#
运行模型的首步是加载它。ExecuTorch 使用一种称为 DataLoader 的抽象来处理检索 .pte 文件数据的具体细节,然后由 Program 表示加载后的状态。
用户可以定义自己的 DataLoader 以适应特定系统的需求。在本教程中,我们将使用 FileDataLoader,但您可以查看 数据加载器示例实现 以了解 ExecuTorch 项目提供的其他选项。
对于 FileDataLoader,我们只需要向构造函数提供文件路径即可。
using executorch::aten::Tensor;
using executorch::aten::TensorImpl;
using executorch::extension::FileDataLoader;
using executorch::extension::MallocMemoryAllocator;
using executorch::runtime::Error;
using executorch::runtime::EValue;
using executorch::runtime::HierarchicalAllocator;
using executorch::runtime::MemoryManager;
using executorch::runtime::Method;
using executorch::runtime::MethodMeta;
using executorch::runtime::Program;
using executorch::runtime::Result;
using executorch::runtime::Span;
Result<FileDataLoader> loader =
FileDataLoader::from("/tmp/model.pte");
assert(loader.ok());
Result<Program> program = Program::load(&loader.get());
assert(program.ok());
设置 MemoryManager#
接下来我们将设置 MemoryManager。
ExecuTorch 的原则之一是让用户能够控制 Runtime 所使用内存的来源。目前(2023 年底),用户需要提供 2 种不同的分配器:
方法分配器 (Method Allocator):一个用于在
Method加载时分配 Runtime 结构的MemoryAllocator。Tensor 元数据、内部指令链以及其他 Runtime 状态均由此分配。规划内存 (Planned Memory):一个包含一个或多个内存池 (memory arenas) 的
HierarchicalAllocator,用于放置内部可变 Tensor 的数据缓冲区。在Method加载时,内部 Tensor 的数据指针将被分配到其中的各个偏移量。这些偏移量的位置和内存池的大小由预先进行的内存规划(memory planning)决定。
在本例中,我们将从 Program 中动态获取规划内存池的大小,但对于无堆(heapless)环境,用户可以提前从 Program 中获取此信息并静态分配内存池。我们还将使用基于 malloc 的分配器作为方法分配器。
// Method names map back to Python nn.Module method names. Most users will only
// have the singular method "forward".
const char* method_name = "forward";
// MethodMeta is a lightweight structure that lets us gather metadata
// information about a specific method. In this case we are looking to get the
// required size of the memory planned buffers for the method "forward".
Result<MethodMeta> method_meta = program->method_meta(method_name);
assert(method_meta.ok());
std::vector<std::unique_ptr<uint8_t[]>> planned_buffers; // Owns the Memory
std::vector<Span<uint8_t>> planned_arenas; // Passed to the allocator
size_t num_memory_planned_buffers = method_meta->num_memory_planned_buffers();
// It is possible to have multiple layers in our memory hierarchy; for example,
// SRAM and DRAM.
for (size_t id = 0; id < num_memory_planned_buffers; ++id) {
// .get() will always succeed because id < num_memory_planned_buffers.
size_t buffer_size =
static_cast<size_t>(method_meta->memory_planned_buffer_size(id).get());
planned_buffers.push_back(std::make_unique<uint8_t[]>(buffer_size));
planned_arenas.push_back({planned_buffers.back().get(), buffer_size});
}
HierarchicalAllocator planned_memory(
{planned_arenas.data(), planned_arenas.size()});
// Version of MemoryAllocator that uses malloc to handle allocations rather then
// a fixed buffer.
MallocMemoryAllocator method_allocator;
// Assemble all of the allocators into the MemoryManager that the Executor will
// use.
MemoryManager memory_manager(&method_allocator, &planned_memory);
加载方法 (Loading a Method)#
在 ExecuTorch 中,我们以“方法”为粒度从 Program 进行加载和初始化。许多程序只有一个名为 ‘forward’ 的方法。load_method 是执行初始化的地方,包括设置 Tensor 元数据、初始化委托(delegates)等。
Result<Method> method = program->load_method(method_name);
assert(method.ok());
设置输入#
现在我们有了方法,在执行推理之前需要设置其输入。在这种情况下,我们知道模型接收一个形状为 (1, 3, 256, 256) 的单精度浮点 Tensor。
根据模型的内存规划方式,规划内存中可能包含或不包含输入和输出的缓冲区空间。
如果输出没有经过内存规划,则用户需要使用 ‘set_output_data_ptr’ 来设置输出数据指针。在本例中,我们假设模型在导出时已将输入和输出纳入内存规划管理。
// Create our input tensor.
float data[1 * 3 * 256 * 256];
Tensor::SizesType sizes[] = {1, 3, 256, 256};
Tensor::DimOrderType dim_order = {0, 1, 2, 3};
TensorImpl impl(
ScalarType::Float, // dtype
4, // number of dimensions
sizes,
data,
dim_order);
Tensor t(&impl);
// Implicitly casts t to EValue
Error set_input_error = method->set_input(t, 0);
assert(set_input_error == Error::Ok);
执行推理#
现在方法已加载且输入已设置,我们可以执行推理。我们通过调用 execute 来实现。
Error execute_error = method->execute();
assert(execute_error == Error::Ok);
获取输出#
推理完成后,我们可以获取输出。我们知道模型仅返回单个输出 Tensor。这里一个潜在的陷阱是:获取的输出是由 Method 拥有的。用户在对其进行任何修改之前,或者需要其生命周期独立于 Method 时,应注意克隆输出结果。
EValue output = method->get_output(0);
assert(output.isTensor());
结论#
本教程演示了如何使用底层 Runtime API 运行 ExecuTorch 模型,这些 API 提供了对内存管理和执行的细粒度控制。然而,对于大多数用例,我们建议使用 Module API,它在不牺牲灵活性的情况下提供了更精简的体验。欲了解更多详情,请查看 Module Extension 教程。