• 文件 >
  • 在 C++ 中管理 Tensor 記憶體
快捷方式

在 C++ 中管理 Tensor 記憶體

作者: Anthony Shoumikhin

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

您必須確保 sizesdim_orderstridesdata 保持有效。這使得程式碼維護變得困難且容易出錯。使用者一直在努力管理生命週期,許多人建立了自己的臨時託管 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)自動推斷。stridesdim_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

由於 TensorPtrstd::shared_ptr<Tensor>,您可以輕鬆建立一個共享現有 TensorTensorPtr。對共享資料所做的任何更改都將反映在所有共享同一資料的例項中。

共享現有 TensorPtr

auto tensor = make_tensor_ptr({2, 3}, {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f});
auto tensor_copy = tensor;

現在 tensortensor_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 的形狀可以任意更改。目前,DYNAMICDYNAMIC_BOUND 的別名。

調整 tensor 大小時,必須遵守其 dynamism 設定。只有形狀設定為 DYNAMICDYNAMIC_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_blobfrom_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 可以表示 boolint 或浮點型別,但不能表示像 HalfBFloat16 等型別,對於這些型別您需要使用 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 不擁有資料或元資料,而是使用原始指標引用它們。您需要確保 TensorPtrEValue 使用期間保持有效。

這同樣適用於使用諸如 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

at::tensor(data, type)

make_tensor_ptr(data, type)

at::tensor(data, type).reshape(sizes)

make_tensor_ptr(sizes, data, type)

tensor.clone()

clone_tensor_ptr(tensor)

tensor.resize_(new_sizes)

resize_tensor_ptr(tensor, new_sizes)

at::scalar_tensor(value)

scalar_tensor(value)

at::from_blob(data, sizes, type)

from_blob(data, sizes, type)

at::empty(sizes)

empty(sizes)

at::empty_like(tensor)

empty_like(tensor)

at::empty_strided(sizes, strides)

empty_strided(sizes, strides)

at::full(sizes, value)

full(sizes, value)

at::full_like(tensor, value)

full_like(tensor, value)

at::full_strided(sizes, strides, value)

full_strided(sizes, strides, value)

at::zeros(sizes)

zeros(sizes)

at::zeros_like(tensor)

zeros_like(tensor)

at::zeros_strided(sizes, strides)

zeros_strided(sizes, strides)

at::ones(sizes)

ones(sizes)

at::ones_like(tensor)

ones_like(tensor)

at::ones_strided(sizes, strides)

ones_strided(sizes, strides)

at::rand(sizes)

rand(sizes)

at::rand_like(tensor)

rand_like(tensor)

at::randn(sizes)

randn(sizes)

at::randn_like(tensor)

randn_like(tensor)

at::randint(low, high, sizes)

randint(low, high, sizes)

at::randint_like(tensor, low, high)

randint_like(tensor, low, high)

最佳實踐

  • 謹慎管理生命週期:儘管 TensorPtr 處理記憶體管理,但要確保任何非擁有的資料(例如,使用 from_blob() 時)在 tensor 使用期間保持有效。

  • 使用便捷函式:利用輔助函式來實現常見的 tensor 建立模式,以編寫更簡潔、更易讀的程式碼。

  • 注意資料所有權:瞭解您的 tensor 是擁有自己的資料還是引用外部資料,以避免意外的副作用或記憶體洩漏。

  • 確保 TensorPtr 的生命週期長於 EValue:將 tensor 傳遞給期望 EValue 的模組時,確保 TensorPtrEValue 使用期間保持有效。

結論

ExecuTorch 中的 TensorPtr 透過將資料和動態元資料捆綁到一個智慧指標中,簡化了 tensor 記憶體管理。這種設計消除了使用者管理多個數據部分的需要,並確保了更安全、更易於維護的程式碼。

透過提供與 PyTorch 的 ATen 庫類似的介面,ExecuTorch 簡化了新 API 的採用,使開發人員能夠輕鬆過渡,沒有陡峭的學習曲線。

文件

訪問 PyTorch 的全面開發者文件

檢視文件

教程

獲取針對初學者和高階開發者的深入教程

檢視教程

資源

查詢開發資源並獲得問題解答

檢視資源