在 C++ 中管理 Tensor 内存¶
张量是 ExecuTorch 中的基本数据结构,表示神经网络和其他数值算法计算中使用的多维数组。在 ExecuTorch 中,Tensor 类不拥有其元数据(大小、步幅dim_order)或数据,从而保持运行时的轻量级。用户负责提供所有这些内存缓冲区,并确保元数据和数据在实例之后的生存期。虽然这种设计轻量级且灵活,尤其是对于微型嵌入式系统,但它会给用户带来沉重的负担。但是,如果您的环境需要最少的动态分配、较小的二进制占用空间或有限的 C++ 标准库支持,则需要接受这种权衡并坚持使用常规类型。Tensor
Tensor
假设你正在使用一个接口,你需要将 a 传递给该方法。您至少需要分别声明和维护 sizes 数组和数据,有时还需要 strides,这通常会导致以下模式:
Tensor
forward()
#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 提供并通过新的 Tensor Extension 来管理 Tensor 及其实现的生命周期。这些本质上是智能指针(分别为 和 ,),用于处理张量数据及其动态元数据的内存管理。TensorPtr
TensorImplPtr
std::unique_ptr<Tensor>
std::shared_ptr<TensorImpl>
现在,用户不再需要单独担心元数据生命周期。数据所有权的确定取决于它是通过指针传递还是作为 .所有内容都捆绑在一个位置并自动管理,使您能够专注于实际计算。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);
数据现在归 Tensor 实例所有,因为它是作为向量提供的。要创建非拥有者,只需通过指针传递数据。这是从数据向量 () 自动推导的。 ,并根据 if not 显式指定为额外参数自动计算为默认值。TensorPtr
type
float
strides
dim_order
sizes
EValue
in 直接接受,确保无缝集成。 现在可以使用指向它可以容纳的任何类型的智能指针隐式构造,因此隐式取消引用并持有指向的 a 传递给 .Module::forward()
TensorPtr
EValue
TensorPtr
EValue
Tensor
TensorPtr
forward()
API 概述¶
新的 API 围绕两个主要的智能指针:
TensorPtr
:管理对象。由于每个实例都是唯一的,因此请确保独占所有权。std::unique_ptr
Tensor
Tensor
TensorPtr
TensorImplPtr
:管理对象。多个实例可以共享同一个 ,因此使用共享所有权。std::shared_ptr
TensorImpl
Tensor
TensorImpl
TensorImplPtr
创建 Tensor¶
有几种方法可以创建 .TensorPtr
创建标量张量¶
您可以创建标量张量,即维度为零的张量或其中一个大小为零的张量。
提供单个数据值
auto tensor = make_tensor_ptr(3.14);
生成的张量将包含一个 double 类型的单个值 3.14,该值是自动推断的。
提供具有类型的单个数据值
auto tensor = make_tensor_ptr(42, ScalarType::Float);
现在,整数 42 将转换为 float,张量将包含一个 float 类型的值 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
为 Data Vector 提供类型
如果您提供一种类型的数据,但指定了不同的标量类型,则数据将被强制转换为指定的类型。
auto tensor = make_tensor_ptr(
{1, 2, 3, 4, 5, 6}, // data (int)
ScalarType::Double); // double scalar type
在此示例中,即使数据向量包含整数,我们也将标量类型指定为 。整数被强制转换为双精度值,新的数据向量归 .在此示例中跳过了该参数,因此使用输入数据向量的大小。请注意,当浮点类型强制转换为整型时,我们禁止反向转换,因为这会丢失精度。同样,不允许将其他类型强制转换为 。Double
TensorPtr
sizes
Bool
提供 Data Vector 作为
您还可以将原始数据作为 提供,指定大小和标量类型。数据将根据提供的类型重新解释。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
向量必须足够大,以便根据提供的大小和标量类型容纳所有元素。data
不拥有原始数据指针¶
您可以创建引用现有数据的 ID,而无需获取所有权。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, // some default dynamism
[](void* ptr) { delete[] static_cast<double*>(ptr); });
当自定义删除器被销毁时,即当智能指针被重置并且不再存在对底层的引用时,它将调用自定义删除器。TensorPtr
TensorImplPtr
共享现有 Tensor¶
您可以通过包装现有 来创建 ,后者可以使用与 相同的 API 集合来创建。对 all 所做的任何更改或任何共享都会反映在 for all 中。TensorPtr
TensorImplPtr
TensorPtr
TensorImplPtr
TensorPtr
TensorImplPtr
共享现有 TensorImplPtr
auto tensor_impl = make_tensor_impl_ptr(
{2, 3},
{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f});
auto tensor = make_tensor_ptr(tensor_impl);
auto tensor_copy = make_tensor_ptr(tensor_impl);
两者 和 共享基础 ,反映数据中的更改,但不反映元数据中的更改。tensor
tensor_copy
TensorImplPtr
此外,您还可以创建与现有 .TensorPtr
TensorImplPtr
TensorPtr
共享现有 TensorPtr
auto tensor_copy = make_tensor_ptr(tensor);
查看现有 Tensor¶
您可以从现有 复制其属性并引用相同的数据。TensorPtr
Tensor
查看现有 Tensor
Tensor original_tensor = /* some existing tensor */;
auto tensor = make_tensor_ptr(original_tensor);
现在,新创建的引用与原始张量相同的数据,但有自己的元数据副本,因此可以以不同的方式解释或“查看”数据,但对数据的任何修改也会反映在原始数据中。TensorPtr
Tensor
克隆张量¶
要创建拥有现有张量数据副本的新 API,请执行以下操作: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
调整张量大小¶
枚举指定张量形状的可变性: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's sizes are now {2, 2}
// Number of elements is 4 < initial 6
resize_tensor_ptr(tensor, {1, 3});
// The tensor's sizes are now {1, 3}
// Number of elements is 3 < initial 6
resize_tensor_ptr(tensor, {3, 2});
// The tensor's sizes are now {3, 2}
// Number of elements is 6 == initial 6
resize_tensor_ptr(tensor, {6, 1});
// The tensor's sizes are now {6, 1}
// Number of elements is 6 == initial 6
便利助手¶
ExecuTorch 提供了多个辅助函数来方便地创建张量。
使用 和 创建非拥有的张量for_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
与 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();
将 Custom Deleter 与 结合使用
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
创建空张量¶
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()
,并创建一个分别填充了提供的值 0 或 1 的张量。zeros()
ones()
auto tensor_full = full({2, 3}, 42.0f);
auto tensor_zeros = zeros({2, 3});
auto tensor_ones = ones({3, 4});
与 类似,还有额外的辅助函数 , , , 和 创建具有与 existing 相同属性或具有自定义步幅的填充张量。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);
请注意,该函数需要 type 为 的值。在 ExecuTorch 中,可以表示 , , 或 浮点类型,但不能表示 or , 等类型,您需要使用这些类型来跳过该类型。scalar_tensor()
Scalar
Scalar
bool
int
Half
BFloat16
make_tensor_ptr()
Scalar
有关 EValue 和生存期管理的注意事项¶
该接口期望数据采用 的形式,这是一种可以保存 a 或其他标量类型的 variant 类型。当您将 a 传递给需要 的函数时,您可以取消引用 以获取底层 .
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});
但是,请谨慎:不会保留 .它只持有一个 regular ,它不拥有数据或元数据,而是使用原始指针引用它们。您需要确保 在使用 期间保持有效。EValue
TensorPtr
Tensor
TensorPtr
EValue
这也适用于使用像 OR EXPECT 这样的函数。set_input()
set_output()
EValue
与 ATen 的互操作性¶
如果您的代码是在打开预处理器标志的情况下编译的,则所有 API 都将在后台使用 API。例如 变为 A 并变为 。这允许与 PyTorch ATen 库无缝集成。USE_ATEN_LIB
TensorPtr
at::
TensorPtr
std::unique_ptr<at::Tensor>
TensorImplPtr
c10::intrusive_ptr<at::TensorImpl>
API 等效表¶
下面是一个将创建函数与其相应的 ATen API 匹配的表:TensorPtr
ATen |
ExecuTorch |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最佳实践¶
谨慎管理生命周期:即使处理内存管理,您仍然需要确保任何非拥有的数据(例如,使用 时)在使用张量时保持有效。
TensorPtr
TensorImplPtr
from_blob()
使用便捷函数:利用为常见张量创建模式提供的辅助函数来编写更简洁、更具可读性的代码。
注意数据所有权:了解您的张量是否拥有其数据或引用外部数据,以避免意外的副作用或内存泄漏。
确保 TensorPtr 比 EValue 长寿:将张量传递给 expect 的模块时,请确保只要 正在使用,它就保持有效。
EValue
TensorPtr
EValue
了解标量类型:在创建张量时要注意标量类型,尤其是在类型之间进行转换时。
结论¶
ExecuTorch 中的 and 通过将数据和动态元数据捆绑到智能指针中来简化张量内存管理。这种设计消除了用户管理多个数据片段的需要,并确保了更安全、更易维护的代码。TensorPtr
TensorImplPtr
通过提供类似于 PyTorch 的 ATen 库的接口,ExecuTorch 使开发人员更容易采用新的 API,而无需陡峭的学习曲线。