在 C++ 中執行 ExecuTorch 模型教學¶
作者: Jacob Szwejbka
在本教學中,我們將介紹如何使用更詳細、更低階的 API 在 C++ 中執行 ExecuTorch 模型:準備 MemoryManager
、設定輸入、執行模型並擷取輸出。但是,如果您正在尋找一種開箱即用、更簡單的介面,請考慮嘗試 模組擴充功能教學。
如需 ExecuTorch 執行階段的高階概述,請參閱 執行階段概述,如需每個 API 的更深入文件,請參閱 執行階段 API 參考。 此處是一個功能完整的 C++ 模型執行器版本,並且 設定 ExecuTorch 文件顯示如何建立和執行它。
先決條件¶
您需要一個 ExecuTorch 模型才能繼續。我們將使用從匯出至 ExecuTorch 教學產生的 SimpleConv
模型。
模型載入¶
執行模型的第一步是載入它。 ExecuTorch 使用一個稱為 DataLoader
的抽象概念來處理檢索 .pte
檔案資料的細節,然後 Program
代表已載入的狀態。
使用者可以定義自己的 DataLoader
以符合其特定系統的需求。在本教學中,我們將使用 FileDataLoader
,但您可以查看 範例 Data Loader 實作 以查看 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 的原則之一是讓使用者控制執行階段使用的記憶體來源。 目前 (2023 年底) 使用者需要提供 2 個不同的分配器
方法分配器:一個
MemoryAllocator
,用於在Method
載入時分配執行階段結構。 諸如 Tensor metadata、指令的內部鏈以及其他執行階段狀態之類的東西都來自這裡。規劃記憶體:一個
HierarchicalAllocator
,包含 1 個或多個記憶體區,其中放置了內部可變 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".
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);
載入方法¶
在 ExecuTorch 中,我們以方法粒度從 Program
載入和初始化。 許多程式將只有一個方法 'forward'。 load_method
是完成初始化的位置,從設定 tensor metadata 到初始化委派等。
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());
結論¶
本教學示範瞭如何使用低階執行階段 API 執行 ExecuTorch 模型,這些 API 提供對記憶體管理和執行的細微控制。 但是,對於大多數用例,我們建議使用 Module API,它提供更精簡的體驗,而不會犧牲彈性。 有關更多詳細資訊,請查看Module 擴充功能教學。