在 C++ 中管理 Tensor 記憶體¶
Tensor 是 ExecuTorch 中的基本資料結構,表示用於神經網路和其他數值計算的多維陣列。在 ExecuTorch 中,Tensor 類不擁有其元資料(大小、步長、維度順序)或資料,從而保持執行時輕量級。使用者負責提供所有這些記憶體緩衝區,並確保元資料和資料的生命週期長於 Tensor 例項。雖然這種設計輕量且靈活,特別是對於微型嵌入式系統,但它給使用者帶來了沉重負擔。如果您的環境需要最少的動態分配、小的二進位制佔用空間或有限的 C++ 標準庫支援,您需要接受這種權衡並堅持使用常規的 Tensor 型別。
想象一下您正在使用 Module 介面,並且需要將 Tensor 傳遞給 forward() 方法。您將需要單獨宣告和維護至少 sizes 陣列和資料,有時還有 strides,這通常會導致以下模式
#include <executorch/extension/module/module.h>
using namespace executorch::aten;
using namespace executorch::extension;
SizesType sizes[] = {2, 3};
DimOrderType dim_order[] = {0, 1};
StridesType strides[] = {3, 1};
float data[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
TensorImpl tensor_impl(
ScalarType::Float,
std::size(sizes),
sizes,
data,
dim_order,
strides);
// ...
module.forward(Tensor(&tensor_impl));
您必須確保 sizes、dim_order、strides 和 data 保持有效。這使得程式碼維護變得困難且容易出錯。使用者一直在努力管理生命週期,許多人建立了自己的臨時託管 tensor 抽象來將所有部分整合在一起,這導致了生態系統的碎片化和不一致。
引入 TensorPtr¶
為了緩解這些問題,ExecuTorch 提供了 TensorPtr,一個管理 tensor 資料及其動態元資料生命週期的智慧指標。
有了 TensorPtr,使用者不再需要單獨擔心元資料的生命週期。資料所有權取決於它是透過指標傳遞還是作為 std::vector 移動到 TensorPtr 中。所有內容都捆綁在一起並自動管理,使您能夠專注於實際計算。
以下是使用方法
#include <executorch/extension/module/module.h>
#include <executorch/extension/tensor/tensor.h>
using namespace executorch::extension;
auto tensor = make_tensor_ptr(
{2, 3}, // sizes
{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}); // data
// ...
module.forward(tensor);
資料現在由 tensor 例項擁有,因為它是作為 vector 提供的。要建立一個非擁有 TensorPtr,只需透過指標傳遞資料即可。type 會根據資料 vector(float)自動推斷。strides 和 dim_order 會根據 sizes 自動計算為預設值,如果未顯式指定為額外引數的話。
Module::forward() 中的 EValue 直接接受 TensorPtr,確保無縫整合。EValue 現在可以透過智慧指標隱式構造,指向它可以容納的任何型別。這允許 TensorPtr 在傳遞給 forward() 時被隱式解引用,EValue 將持有 TensorPtr 指向的 Tensor。
API 概述¶
TensorPtr 實際上是 std::shared_ptr<Tensor> 的別名,因此您可以輕鬆使用它而無需複製資料和元資料。每個 Tensor 例項可以擁有自己的資料或引用外部資料。
建立 Tensor¶
有幾種方法可以建立 TensorPtr。
建立標量 Tensor¶
您可以建立一個標量 tensor,即零維度的 tensor 或其中一個大小為零的 tensor。
提供單個數據值
auto tensor = make_tensor_ptr(3.14);
生成的 tensor 將包含一個型別為 double 的單個值 3.14,型別會自動推斷。
提供帶型別的單個數據值
auto tensor = make_tensor_ptr(42, ScalarType::Float);
現在整數 42 將被轉換為 float,tensor 將包含一個型別為 float 的單個值 42。
擁有來自 Vector 的資料¶
當您提供 sizes 和資料 vector 時,TensorPtr 會擁有資料和 sizes 的所有權。
提供資料 Vector
auto tensor = make_tensor_ptr(
{2, 3}, // sizes
{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}); // data (float)
型別會從資料 vector 自動推斷為 ScalarType::Float。
提供帶型別的資料 Vector
如果您提供一種型別的資料但指定了另一種標量型別,資料將被轉換為給定的型別。
auto tensor = make_tensor_ptr(
{1, 2, 3, 4, 5, 6}, // data (int)
ScalarType::Double); // double scalar type
在此示例中,即使資料 vector 包含整數,我們也指定標量型別為 Double。整數被轉換為 double,並且新的資料 vector 由 TensorPtr 擁有。由於此示例中跳過了 sizes 引數,因此 tensor 是一個維度,其大小等於資料 vector 的長度。請注意,不允許反向轉換,即從浮點型別轉換為整型,因為這會丟失精度。類似地,不允許將其他型別轉換為 Bool。
將資料 Vector 作為 std::vector<uint8_t> 提供
您還可以提供 std::vector<uint8_t> 形式的原始資料,並指定 sizes 和標量型別。資料將根據提供的型別重新解釋。
std::vector<uint8_t> data = /* raw data */;
auto tensor = make_tensor_ptr(
{2, 3}, // sizes
std::move(data), // data as uint8_t vector
ScalarType::Int); // int scalar type
資料 vector 必須足夠大,以根據提供的 sizes 和標量型別容納所有元素。
來自原始指標的非擁有資料¶
您可以建立一個引用現有資料但不擁有所有權的 TensorPtr。
提供原始資料
float data[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
auto tensor = make_tensor_ptr(
{2, 3}, // sizes
data, // raw data pointer
ScalarType::Float); // float scalar type
TensorPtr 不擁有資料,因此您必須確保 data 保持有效。
提供帶自定義刪除器的原始資料
如果您希望 TensorPtr 管理資料的生命週期,您可以提供一個自定義刪除器。
auto* data = new double[6]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
auto tensor = make_tensor_ptr(
{2, 3}, // sizes
data, // data pointer
ScalarType::Double, // double scalar type
TensorShapeDynamism::DYNAMIC_BOUND, // default dynamism
[](void* ptr) { delete[] static_cast<double*>(ptr); });
當 TensorPtr 被銷燬時(即智慧指標被重置且不再有對底層 Tensor 的引用時),它將呼叫自定義刪除器。
共享現有 Tensor¶
由於 TensorPtr 是 std::shared_ptr<Tensor>,您可以輕鬆建立一個共享現有 Tensor 的 TensorPtr。對共享資料所做的任何更改都將反映在所有共享同一資料的例項中。
共享現有 TensorPtr
auto tensor = make_tensor_ptr({2, 3}, {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f});
auto tensor_copy = tensor;
現在 tensor 和 tensor_copy 指向相同的資料和元資料。
檢視現有 Tensor¶
您可以從現有 Tensor 建立一個 TensorPtr,複製其屬性並引用相同的資料。
檢視現有 Tensor
Tensor original_tensor = /* some existing tensor */;
auto tensor = make_tensor_ptr(original_tensor);
現在新建立的 TensorPtr 引用與原始 tensor 相同的資料,但擁有自己的元資料副本,因此它可以以不同方式解釋或“檢視”資料,但對資料的任何修改也將反映在原始 Tensor 中。
克隆 Tensor¶
要建立一個新的 TensorPtr,它擁有現有 tensor 資料的副本
Tensor original_tensor = /* some existing tensor */;
auto tensor = clone_tensor_ptr(original_tensor);
新建立的 TensorPtr 擁有自己的資料副本,因此可以獨立修改和管理資料。同樣,您可以建立現有 TensorPtr 的克隆。
auto original_tensor = make_tensor_ptr(/* ... */);
auto tensor = clone_tensor_ptr(original_tensor);
請注意,無論原始 TensorPtr 是否擁有資料,新建立的 TensorPtr 都將擁有資料的副本。
調整 Tensor 大小¶
TensorShapeDynamism 列舉指定了 tensor 形狀的可變性
STATIC:tensor 的形狀不能更改。DYNAMIC_BOUND:tensor 的形狀可以更改,但包含的元素數量不能超過其根據初始 sizes 建立時原本擁有的元素數量。DYNAMIC:tensor 的形狀可以任意更改。目前,DYNAMIC是DYNAMIC_BOUND的別名。
調整 tensor 大小時,必須遵守其 dynamism 設定。只有形狀設定為 DYNAMIC 或 DYNAMIC_BOUND 的 tensor 才允許調整大小,並且不能將 DYNAMIC_BOUND 的 tensor 調整為包含比最初更多元素的數量。
auto tensor = make_tensor_ptr(
{2, 3}, // sizes
{1, 2, 3, 4, 5, 6}, // data
ScalarType::Int,
TensorShapeDynamism::DYNAMIC_BOUND);
// Initial sizes: {2, 3}
// Number of elements: 6
resize_tensor_ptr(tensor, {2, 2});
// The tensor sizes are now {2, 2}
// Number of elements is 4 < initial 6
resize_tensor_ptr(tensor, {1, 3});
// The tensor sizes are now {1, 3}
// Number of elements is 3 < initial 6
resize_tensor_ptr(tensor, {3, 2});
// The tensor sizes are now {3, 2}
// Number of elements is 6 == initial 6
resize_tensor_ptr(tensor, {6, 1});
// The tensor sizes are now {6, 1}
// Number of elements is 6 == initial 6
便捷輔助函式¶
ExecuTorch 提供了幾個便捷輔助函式來建立 tensor。
使用 for_blob 和 from_blob 建立非擁有 Tensor¶
這些輔助函式允許您建立不擁有資料的 tensor。
使用 from_blob()
float data[] = {1.0f, 2.0f, 3.0f};
auto tensor = from_blob(
data, // data pointer
{3}, // sizes
ScalarType::Float); // float scalar type
使用流暢語法和 for_blob()
double data[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
auto tensor = for_blob(data, {2, 3}, ScalarType::Double)
.strides({3, 1})
.dynamism(TensorShapeDynamism::STATIC)
.make_tensor_ptr();
使用自定義刪除器和 from_blob()
int* data = new int[3]{1, 2, 3};
auto tensor = from_blob(
data, // data pointer
{3}, // sizes
ScalarType::Int, // int scalar type
[](void* ptr) { delete[] static_cast<int*>(ptr); });
當 TensorPtr 被銷燬時,它將呼叫自定義刪除器。
建立空 Tensor¶
empty() 建立一個指定 sizes 的未初始化 tensor。
auto tensor = empty({2, 3});
empty_like() 建立一個與現有 TensorPtr 具有相同 sizes 的未初始化 tensor。
TensorPtr original_tensor = /* some existing tensor */;
auto tensor = empty_like(original_tensor);
而 empty_strided() 建立一個指定 sizes 和 strides 的未初始化 tensor。
auto tensor = empty_strided({2, 3}, {3, 1});
建立填充特定值的 Tensor¶
full()、zeros() 和 ones() 分別建立一個填充有提供值、零或一的 tensor。
auto tensor_full = full({2, 3}, 42.0f);
auto tensor_zeros = zeros({2, 3});
auto tensor_ones = ones({3, 4});
與 empty() 類似,還有額外的輔助函式 full_like()、full_strided()、zeros_like()、zeros_strided()、ones_like() 和 ones_strided(),用於建立與現有 TensorPtr 具有相同屬性或具有自定義 strides 的填充 tensor。
建立隨機 Tensor¶
rand() 建立一個填充有 0 到 1 之間隨機值的 tensor。
auto tensor_rand = rand({2, 3});
randn() 建立一個填充有來自正態分佈的隨機值的 tensor。
auto tensor_randn = randn({2, 3});
randint() 建立一個填充有指定 min(包含)和 max(不包含)整數之間的隨機整數的 tensor。
auto tensor_randint = randint(0, 10, {2, 3});
建立標量 Tensor¶
除了使用單個數據值的 make_tensor_ptr() 外,您還可以使用 scalar_tensor() 建立一個標量 tensor。
auto tensor = scalar_tensor(3.14f);
請注意,scalar_tensor() 函式期望一個 Scalar 型別的值。在 ExecuTorch 中,Scalar 可以表示 bool、int 或浮點型別,但不能表示像 Half 或 BFloat16 等型別,對於這些型別您需要使用 make_tensor_ptr() 來跳過 Scalar 型別。
關於 EValue 和生命週期管理的注意事項¶
Module 介面期望資料以 EValue 的形式出現,EValue 是一個變體型別,可以容納 Tensor 或其他標量型別。當您將 TensorPtr 傳遞給期望 EValue 的函式時,您可以解引用 TensorPtr 以獲取底層 Tensor。
TensorPtr tensor = /* create a TensorPtr */
//...
module.forward(tensor);
或者甚至是一個 EValue vector 用於多個引數。
TensorPtr tensor = /* create a TensorPtr */
TensorPtr tensor2 = /* create another TensorPtr */
//...
module.forward({tensor, tensor2});
但是,請注意:EValue 不會持有 TensorPtr 中的動態資料和元資料。它僅持有一個常規的 Tensor,該 Tensor 不擁有資料或元資料,而是使用原始指標引用它們。您需要確保 TensorPtr 在 EValue 使用期間保持有效。
這同樣適用於使用諸如 set_input() 或 set_output() 等期望 EValue 的函式時。
與 ATen 的互操作性¶
如果您的程式碼在編譯時啟用了預處理器標誌 USE_ATEN_LIB,所有 TensorPtr API 將在底層使用 at:: API。例如,TensorPtr 變為 std::shared_ptr<at::Tensor>。這使得與 PyTorch ATen 庫的整合無縫進行。
API 等效表¶
下表列出了 TensorPtr 建立函式與其對應的 ATen API 的對應關係
ATen |
ExecuTorch |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最佳實踐¶
謹慎管理生命週期:儘管
TensorPtr處理記憶體管理,但要確保任何非擁有的資料(例如,使用from_blob()時)在 tensor 使用期間保持有效。使用便捷函式:利用輔助函式來實現常見的 tensor 建立模式,以編寫更簡潔、更易讀的程式碼。
注意資料所有權:瞭解您的 tensor 是擁有自己的資料還是引用外部資料,以避免意外的副作用或記憶體洩漏。
確保 TensorPtr 的生命週期長於 EValue:將 tensor 傳遞給期望
EValue的模組時,確保TensorPtr在EValue使用期間保持有效。
結論¶
ExecuTorch 中的 TensorPtr 透過將資料和動態元資料捆綁到一個智慧指標中,簡化了 tensor 記憶體管理。這種設計消除了使用者管理多個數據部分的需要,並確保了更安全、更易於維護的程式碼。
透過提供與 PyTorch 的 ATen 庫類似的介面,ExecuTorch 簡化了新 API 的採用,使開發人員能夠輕鬆過渡,沒有陡峭的學習曲線。