在 C++ 中运行 ExecuTorch 模型教程
作者: Jacob Szwejbka
在本教程中,我们将介绍如何使用更详细、更低层级的 API 在 C++ 中运行 ExecuTorch 模型:准备 MemoryManager
、设置输入、执行模型并获取输出。不过,如果您在寻找一个开箱即用的更简单接口,可以尝试 模块扩展教程。
有关 ExecuTorch 运行时的高层次概述,请参阅 运行时概述,有关每个 API 的更深入文档,请参阅 运行时 API 参考。这里 是一个功能完整的 C++ 模型运行器,而 设置 ExecuTorch 文档展示了如何构建和运行它。
前提条件
您需要一个 ExecuTorch 模型来继续操作。我们将使用从 导出到 ExecuTorch 教程 生成的 SimpleConv
模型。
模型加载
运行模型的第一步是加载它。ExecuTorch 使用一个名为 DataLoader
的抽象来处理检索 .pte
文件数据的具体细节,然后 Program
表示加载后的状态。
用户可以根据自己系统的需求定义自己的 DataLoader
。在本教程中,我们将使用 FileDataLoader
,但您可以在 示例数据加载器实现 下查看 ExecuTorch 项目提供的其他选项。
对于 FileDataLoader
,我们只需要向构造函数提供文件路径即可。
usingexecutorch::aten::Tensor;
usingexecutorch::aten::TensorImpl;
usingexecutorch::extension::FileDataLoader;
usingexecutorch::extension::MallocMemoryAllocator;
usingexecutorch::runtime::Error;
usingexecutorch::runtime::EValue;
usingexecutorch::runtime::HierarchicalAllocator;
usingexecutorch::runtime::MemoryManager;
usingexecutorch::runtime::Method;
usingexecutorch::runtime::MethodMeta;
usingexecutorch::runtime::Program;
usingexecutorch::runtime::Result;
usingexecutorch::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 种不同的分配器:
-
方法分配器:一个
MemoryAllocator
,用于在Method
加载时分配运行时结构。像 Tensor 元数据、内部指令链以及其他运行时状态都来源于此。 -
计划内存:一个
HierarchicalAllocator
,包含一个或多个内存区域,用于放置内部可变的 Tensor 数据缓冲区。在Method
加载时,内部 Tensors 的数据指针会被分配到这些区域中的不同偏移位置。这些偏移的位置和区域的大小由预先进行的内存规划决定。
在这个示例中,我们将从 Program
中动态获取计划内存区域的大小,但在无堆环境中,用户可以提前从 Program
中获取此信息并静态分配内存区域。此外,我们将为方法分配器使用基于 malloc 的分配器。
// Method names map back to Python nn.Module method names. Most users will only
// have the singular method "forward".
constchar*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_tnum_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_tid=0;id<num_memory_planned_buffers;++id){
// .get() will always succeed because id < num_memory_planned_buffers.
size_tbuffer_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});
}
HierarchicalAllocatorplanned_memory(
{planned_arenas.data(),planned_arenas.size()});
// Version of MemoryAllocator that uses malloc to handle allocations rather then
// a fixed buffer.
MallocMemoryAllocatormethod_allocator;
// Assemble all of the allocators into the MemoryManager that the Executor will
// use.
MemoryManagermemory_manager(&method_allocator,&planned_memory);
加载方法
在 ExecuTorch 中,我们以方法粒度从 Program
加载和初始化。许多程序只有一个方法 ‘forward’。load_method
是完成初始化的地方,从设置张量元数据到初始化委托等操作都在此处进行。
Result<Method>method=program->load_method(method_name);
assert(method.ok());
设置输入
现在我们有了方法,在执行推理之前需要设置其输入。在本例中,我们知道我们的模型接受一个大小为 (1, 3, 256, 256) 的浮点张量。
根据模型的存储规划方式,规划的存储空间可能包含也可能不包含输入和输出的缓冲区空间。
如果输出未被存储规划,用户将需要使用 set_output_data_ptr
来设置输出数据指针。在本例中,我们假设我们的模型在导出时已经处理了输入和输出的存储规划。
// Create our input tensor.
floatdata[1*3*256*256];
Tensor::SizesTypesizes[]={1,3,256,256};
Tensor::DimOrderTypedim_order={0,1,2,3};
TensorImplimpl(
ScalarType::Float,// dtype
4,// number of dimensions
sizes,
data,
dim_order);
Tensort(&impl);
// Implicitly casts t to EValue
Errorset_input_error=method->set_input(t,0);
assert(set_input_error==Error::Ok);
执行推理
现在我们的方法已经加载并且输入也已设置,我们可以执行推理了。这可以通过调用 execute
来完成。
Errorexecute_error=method->execute();
assert(execute_error==Error::Ok);
获取输出
当我们的推理完成后,我们可以获取输出结果。我们知道我们的模型只返回一个输出张量。这里一个潜在的陷阱是,我们得到的输出是由 Method
所拥有的。用户应注意在执行任何修改操作之前克隆他们的输出,或者如果他们需要该输出的生命周期与 Method
分离时,也应进行克隆操作。
EValueoutput=method->get_output(0);
assert(output.isTensor());
结论
本教程演示了如何使用低级运行时 API 运行 ExecuTorch 模型,这些 API 提供了对内存管理和执行的细粒度控制。然而,对于大多数用例,我们建议使用 Module API,它在不牺牲灵活性的情况下提供了更加简化的体验。更多详细信息,请参阅 Module 扩展教程。