评价此页

详细 C++ Runtime API 教程#

作者: Jacob Szwejbka

在本教程中,我们将介绍如何使用更详细、更底层的 API 在 C++ 中运行 ExecuTorch 模型:准备 MemoryManager、设置输入、执行模型以及检索输出。但是,如果您正在寻找一个即插即用的更简单的接口,请考虑尝试 Module Extension TutorialUsing ExecuTorch with C++

有关 ExecuTorch Runtime 的高层概述,请参阅 Runtime Overview,有关每个 API 的更深入文档,请参阅 Runtime API Reference此处 提供了一个功能齐全的 C++ 模型运行器,而 Setting up ExecuTorch 文档显示了如何构建和运行它。

先决条件#

您将需要一个 ExecuTorch 模型才能继续学习。我们将使用从 Exporting to ExecuTorch tutorial 生成的 SimpleConv 模型。.

模型加载#

运行模型的第一步是加载它。ExecuTorch 使用一个称为 DataLoader 的抽象来处理检索 .pte 文件数据的具体细节,然后 Program 表示加载的状态。

用户可以定义自己的 DataLoader 以适应其特定系统的需求。在本教程中,我们将使用 FileDataLoader,但您可以查看 ExecuTorch 项目提供的其他选项,位于 Example Data Loader Implementations 下。

对于 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 的原则之一是让用户控制运行时使用的内存来自何处。今天(2023 年末),用户需要提供 2 种不同的分配器。

  • Method Allocator:一个 MemoryAllocator,用于在 Method 加载时分配运行时结构。例如 Tensor 元数据、指令的内部链以及其他运行时状态都来自这里。

  • Planned Memory:一个 HierarchicalAllocator,包含一个或多个内存区域,用于存放内部可变 Tensor 的数据缓冲区。在 Method 加载时,内部 Tensor 的数据指针会被分配到各种偏移量。这些偏移量位置和区域的大小是提前进行内存规划确定的。

在本例中,我们将动态地从 Program 获取计划内存区域的大小,但对于无堆环境,用户可以提前从 Program 中检索此信息并静态分配区域。我们还将使用一个基于 malloc 的分配器作为 Method 分配器。

// 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);

加载 Method#

在 ExecuTorch 中,我们以 Method 为粒度从 Program 加载和初始化。许多程序只有一个 Method,即 'forward'。 load_method 是进行初始化的地方,从设置 Tensor 元数据到初始化委托等。

Result<Method> method = program->load_method(method_name);
assert(method.ok());

设置输入#

现在我们有了 Method,在进行推理之前需要设置其输入。在本例中,我们知道我们的模型接受一个 (1, 3, 256, 256) 大小的 float 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);

进行推理#

现在我们的 Method 已加载且输入已设置,我们可以进行推理了。通过调用 execute 来完成。

Error execute_error = method->execute();
assert(execute_error == Error::Ok);

检索输出#

推理完成后,我们可以检索输出。我们知道我们的模型只返回一个输出 Tensor。这里的一个潜在陷阱是,我们获得的输出由 Method 拥有。用户应小心在对输出进行任何修改之前克隆它,或者如果需要它具有独立于 Method 的生命周期,则进行克隆。

EValue output = method->get_output(0);
assert(output.isTensor());

结论#

本教程演示了如何使用低级运行时 API 来运行 ExecuTorch 模型,这些 API 提供了对内存管理和执行的精细控制。但是,对于大多数用例,我们建议使用 Module API,它们提供更简化的体验,而不会牺牲灵活性。有关更多详细信息,请查看 Module Extension Tutorial