在C++中管理张量内存¶
张量是ExecuTorch中的基本数据结构,表示用于神经网络和其他数值算法计算的多维数组。在ExecuTorch中,Tensor类不拥有其元数据(尺寸、步长、维度顺序)或数据,保持运行时的轻量。用户需要负责提供所有这些内存缓冲区,并确保元数据和数据在Tensor实例之后仍然有效。虽然这种设计对于小型嵌入式系统来说轻量且灵活,但它会给用户带来显著的负担。如果你的环境要求最小的动态分配、较小的二进制体积或有限的C++标准库支持,你需要接受这种权衡并继续使用常规的Tensor类型。
想象你正在使用一个 Module 接口,你需要将一个 Tensor 传递给 forward() 方法。你将需要单独声明和维护至少 sizes 数组和 data,有时还需要 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 保持有效。这使得代码维护困难且容易出错。用户一直难以管理生命周期,许多人创建了自己定制的托管张量抽象来整合所有组件,导致生态系统碎片化且不一致。
介绍TensorPtr¶
为了解决这些问题,ExecuTorch 提供 TensorPtr,一个智能指针,用于管理张量数据及其动态元数据的生命周期。
使用 TensorPtr,用户不再需要单独担心元数据的生命周期。数据的所有权根据其是通过指针传递还是移动到 TensorPtr 中作为 std::vector 来确定。所有内容都捆绑在一个地方并自动管理,使您能够专注于实际计算。
以下是使用方法:
#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);
数据现在属于张量实例,因为它作为向量提供。要创建一个非拥有的TensorPtr,只需通过指针传递数据。type是根据数据向量(float)自动推导的。strides和dim_order会在未显式指定为额外参数的情况下,根据sizes自动计算为默认值。
EValue 在 Module::forward() 中直接接受 TensorPtr,确保无缝集成。EValue 现在可以隐式构造,智能指针指向它可以持有的任何类型。这允许 TensorPtr 在传递给 forward() 时隐式解引用,而 EValue 将保存 Tensor 所指向的 TensorPtr。
API 概述¶
TensorPtr 实际上是 std::shared_ptr<Tensor> 的别名,因此您可以轻松地使用它而无需复制数据和元数据。每个 Tensor 实例可能拥有其自己的数据或引用外部数据。
创建张量¶
有几种方法可以创建一个 TensorPtr。
创建标量张量¶
你可以创建一个标量张量,即一个零维张量或其中一个尺寸为零的张量。
提供一个单一的数据值
auto tensor = make_tensor_ptr(3.14);
生成的张量将包含一个类型为double的单个值 3.14,该类型会自动推断得出。
提供一个带有类型的单个数据值
auto tensor = make_tensor_ptr(42, ScalarType::Float);
现在整数 42 将被转换为浮点数,张量将包含一个类型为浮点数的单个值 42。
从向量拥有数据¶
当您提供尺寸和数据向量时,TensorPtr 将同时拥有数据和尺寸。
提供数据向量
auto tensor = make_tensor_ptr(
{2, 3}, // sizes
{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}); // data (float)
类型会自动推断为 ScalarType::Float,根据数据向量。
提供带有类型的数据显示向量
如果你提供一种类型的数据但指定了不同的标量类型,数据将被转换为指定的类型。
auto tensor = make_tensor_ptr(
{1, 2, 3, 4, 5, 6}, // data (int)
ScalarType::Double); // double scalar type
在这个例子中,尽管数据向量包含整数,但我们指定标量类型为 Double。整数会被转换为双精度浮点数,新的数据向量由 TensorPtr 所拥有。由于此例中跳过了 sizes 参数,张量是一维的,其大小等于数据向量的长度。请注意,从浮点类型转换到整数类型是不允许的,因为这会丢失精度。同样,将其他类型转换为 Bool 也是不允许的。
提供数据向量作为 std::vector<uint8_t>
您也可以以std::vector<uint8_t>的形式提供原始数据,指定尺寸和标量类型。数据将根据提供的类型进行重新解释。
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
The data vector must be large enough to accommodate all the elements according to the provided sizes and scalar type.
非拥有数据来自原始指针¶
您可以创建一个 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
The 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); });
The TensorPtr will call the custom deleter when it is destroyed, i.e., when the smart pointer is reset and no more references to the underlying Tensor exist.
共享现有张量¶
由于 TensorPtr 是一个 std::shared_ptr<Tensor>,您可以轻松创建一个 TensorPtr,它共享现有的 Tensor。对共享数据所做的任何更改都会在所有共享相同数据的实例中反映出来。
共享现有 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 指向相同的数据和元数据。
查看现有张量¶
你可以从现有的 TensorPtr 创建一个 Tensor,复制其属性并引用相同的数据。
查看现有张量
Tensor original_tensor = /* some existing tensor */;
auto tensor = make_tensor_ptr(original_tensor);
现在新创建的 TensorPtr 引用与原始张量相同的数据,但拥有自己的元数据副本,因此它可以以不同的方式解释或“查看”数据,但对数据的任何修改也会反映在原始 Tensor 中。
克隆张量¶
创建一个新 TensorPtr,该新对象拥有现有张量数据的副本:
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 都将拥有数据的副本。
张量缩放¶
The TensorShapeDynamism 枚举指定张量形状的可变性:
STATIC: 张量的形状不能更改。DYNAMIC_BOUND: 张量的形状可以更改,但不能包含比创建时初始大小更多的元素。DYNAMIC: 张量的形状可以任意更改。目前,DYNAMIC是DYNAMIC_BOUND的别名。
调整张量大小时,必须尊重其动态设置。仅允许对具有 DYNAMIC 或 DYNAMIC_BOUND 形状的张量进行调整大小,并且不能将 DYNAMIC_BOUND 张量调整为包含比初始更多元素的内容。
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 提供了多个辅助函数,方便创建张量。
使用 for_blob 和 from_blob 创建非拥有张量¶
这些助手允许你创建不拥有数据的张量。
使用 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() 与 Fluent 语法
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); });
The TensorPtr will call the custom deleter when it is destroyed.
创建空张量¶
empty() 创建一个未初始化的张量,大小由指定参数决定。
auto tensor = empty({2, 3});
empty_like() 创建一个未初始化的张量,其大小与现有 TensorPtr 相同。
TensorPtr original_tensor = /* some existing tensor */;
auto tensor = empty_like(original_tensor);
并且 empty_strided() 创建一个未初始化的张量,指定其大小和步长。
auto tensor = empty_strided({2, 3}, {3, 1});
创建填充特定值的张量¶
full(), zeros() and ones() 分别创建一个用提供的值、零或一填充的张量。
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具有相同属性的填充张量,或使用自定义步长。
创建随机张量¶
rand() 创建一个填充了0到1之间随机值的张量。
auto tensor_rand = rand({2, 3});
randn() 创建一个从正态分布中填充随机值的张量。
auto tensor_randn = randn({2, 3});
randint() 创建一个填充了在指定的最小值(包含)和最大值(不包含)之间的随机整数的张量。
auto tensor_randint = randint(0, 10, {2, 3});
创建标量张量¶
除了 make_tensor_ptr() 之外,您还可以使用 scalar_tensor() 创建一个标量张量。
auto tensor = scalar_tensor(3.14f);
注意 scalar_tensor() 函数期望一个类型为 Scalar 的值。在 ExecuTorch 中,Scalar 可以表示 bool、int 或浮点类型,但不包括像 Half 或 BFloat16 等类型,对于这些类型你需要使用 make_tensor_ptr() 来跳过 Scalar 类型。
关于EValue和生命周期管理¶
The Module 接口期望数据以 EValue 的形式提供,这是一种可以保存 Tensor 或其他标量类型的变体类型。当你将一个 TensorPtr 传递给期望 EValue 的函数时,你可以解引用 TensorPtr 来获取底层的 Tensor。
TensorPtr tensor = /* create a TensorPtr */
//...
module.forward(tensor);
甚至可以是一个 EValues 向量来表示多个参数。
TensorPtr tensor = /* create a TensorPtr */
TensorPtr tensor2 = /* create another TensorPtr */
//...
module.forward({tensor, tensor2});
然而,请注意:EValue 不会保留来自 TensorPtr 的动态数据和元数据。它仅仅持有普通的 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()时)保持有效。使用便捷函数: 使用辅助函数来创建常见张量模式,以编写更清晰、更易读的代码。
注意数据所有权: 知道你的张量是否拥有其数据或引用外部数据,以避免意外的副作用或内存泄漏。
确保
TensorPtr的生命周期长于EValue: 在将张量传递给期望EValue的模块时,确保TensorPtr在EValue使用期间保持有效。
结论¶
The TensorPtr in ExecuTorch 简化了张量内存管理,通过将数据和动态元数据捆绑到一个智能指针中。这种设计消除了用户管理多个数据片段的需要,并确保了更安全、更易维护的代码。
通过提供与 PyTorch 的 ATen 库类似的接口,ExecuTorch 简化了新 API 的采用,使开发者能够在不经历陡峭学习曲线的情况下进行迁移。