框架应用程序编程接口
Synap 框架的核心功能是执行预编译的神经网络。这是通过 Network 类实现的。Network 类的设计在最常见的情况下使用简单,同时对于大多数高级用例来说也足够灵活。实际的推理将根据模型的编译方式在不同的硬件单元(NPU、GPU、CPU 或它们的组合)上进行。
基本用法
Network 类
Network 类非常简单,如下图所示。
使用网络只能做两件事:
- 加载模型,提供
.synap格式的已编译模型。 - 执行推理。
网络还具有输入张量数组用于存放要处理的数据,以及输出张量数组用于在每次推理后包含结果。
图 5 Network 类
类 synaptics::synap::Network
在 NPU 加速器上加载和执行神经网络。
概要
| 函数 | 描述 |
|---|---|
bool load_model(const std::string &model_file, const std::string &meta_file = "") | 从文件加载模型。 |
bool load_model(const void *model_data, size_t model_size, const char *meta_data = nullptr) | 从内存加载模型。 |
bool predict() | 运行推理。 |
公共函数
bool load_model(const std::string &model_file, const std::string &meta_file = "")
- 加载模型。
- 如果之前已加载其他模型,在加载指定模型之前会先释放之前的模型。
- 参数:
model_file:.synap模型文件的路径。也可以是旧版.nb模型文件的路径。meta_file:对于旧版.nb模型,必须是模型元数据文件的路径(JSON 格式)。在所有其他情况下,必须是空字符串。
- 返回值:如果成功则返回
true。
bool load_model(const void *model_data, size_t model_size, const char *meta_data = nullptr)
- 加载模型。
- 如果之前已加载其他模型,在加载指定模型之前会先释放之前的模型。
- 参数:
model_data:模型数据,例如从model.synap通过fread()读取。调用者保留模型数据的所有权,可以在此方法结束时删除它们。model_size:模型大小(字节)。meta_data:对于旧版.nb模型,必须是模型的元数据(JSON 格式)。在所有其他情况下,必须是nullptr。
- 返回值:如果成功则返回
true。
bool predict()
- 运行推理。
- 从输入张量读取要处理的输入数据。推理结果生成在输出张量中。
- 返回值:如果成功则返回
true,如果推理失败或网络未正确初始化则返回false。
公共成员
Tensors *inputs*
- 输入张量的集合,可以通过索引访问和迭代。
Tensors *outputs*
- 输出张量的集合,可以通过索引访问和迭代。
使用网络
执行神经网络的先决条件是创建一个 Network 对象并加载其 .synap 格式的模型。此文件是使用 Synap 工具包转换网络时生成的。这只需要执行一次,加载网络后就可以用于推理:
- 将输入数据放入网络输入张量。
- 调用网络的
predict()方法。 - 从网络输出张量获取结果。

图 6 运行推理
示例
Network net;
net.load_model("model.synap");
vector[uint8_t](uint8_t) in_data = custom_read_input_data();
net.inputs[0].assign(in_data.data(), in_data.size());
net.predict();
custom_process_result(net.outputs[0].as_float(), net.outputs[0].item_count());
请注意:
- 权重和输入/输出数据的所有内存分配和对齐都由 Network 对象自动完成。
- 当 Network 对象被销毁时,所有内存都会自动释放。
- 为简单起见,省略了所有错误检查。如果出现问题,方法通常返回
false。不返回显式错误代码,因为错误通常太复杂,无法用简单的枚举代码解释。有关错误的详细信息可以在日志中找到。 - 示例中名为
custom_read_input_data的例程是用户代码的占位符。 - 在上面的代码中,将
in_data向量分配给张量时会进行数据复制。in_data向量中包含的数据不能直接用于推理,因为无法保证它们按照硬件要求正确对齐和填充。在大多数情况下,这种额外复制的成本可以忽略不计,当这不是问题时,有时可以通过直接写入张量数据缓冲区来避免复制,例如:
custom_generate_input_data(net.inputs[0].data(), net.inputs[0].size());
net.predict();
- 张量中的数据类型取决于网络的生成方式。常见的数据类型包括
float16、float32、量化的uint8和int16。assign()和as_float()负责处理所有必需的数据转换。
仅使用本节所示的简单方法,就可以使用 NPU 硬件加速器执行推理。这几乎是在大多数应用程序中使用 SyNAP 所需要知道的全部内容。以下各节将详细解释幕后发生的事情:这使得可以充分利用可用的硬件来处理更高要求的用例。
高级主题
张量
我们在上一节中看到,对网络输入和输出数据的所有访问都是通过张量对象完成的,因此值得详细了解 Tensor 对象可以做什么。基本上,张量允许:
- 获取有关所包含数据的信息和属性。
- 访问数据。
- 访问用于包含数据的底层
Buffer。更多内容将在下一节中介绍。
图 7 Tensor 类
类 synaptics::synap::Tensor
Synap 数据张量。
不可能在 Network 外部创建张量,用户只能访问由 Network 本身创建的张量。
概要
| 函数 | 描述 |
|---|---|
const std::string &name() const | 获取张量的名称。 |
const Shape &shape() const | 获取张量的形状。 |
const Dimensions dimensions() const | 获取张量的维度。 |
Layout layout() const | 获取张量的布局。 |
std::string format() const | 获取张量的格式。 |
DataType data_type() const | 获取张量数据类型。 |
Security security() const | 获取张量安全属性。 |
size_t size() const | 获取张量数据的字节大小。 |
size_t item_count() const | 获取张量中的项目数量。 |
bool is_scalar() const | 检查张量是否为标量。 |
bool assign(const uint8_t *data, size_t count) | 规范化并将数据复制到张量数据缓冲区。 |
bool assign(const int16_t *data, size_t count) | 与前一个 assign 函数类似,但用于 int16_t 数据。 |
bool assign(const float *data, size_t count) | 与前一个 assign 函数类似,但用于 float 数据。 |
bool assign(const void *data, size_t size) | 将原始数据复制到张量数据缓冲区。 |
bool assign(const Tensor &src) | 将张量的内容复制到张量数据缓冲区。 |
bool assign(int32_t value) | 将值写入张量数据缓冲区。 |
template[typename T](typename T) T *data() | 获取指向张量数据缓冲区内数据开始处的指针。 |
void *data() | 获取指向张量数据缓冲区内原始数据的指针。 |
const float *as_float() const | 获取指向转换为 float 的张量内容的指针。 |
Buffer *buffer() | 获取指向张量当前数据 Buffer 的指针。 |
bool set_buffer(Buffer *buffer) | 设置张量的当前数据缓冲区。 |
公共函数
const std::string &name() const
- 获取张量的名称。
- 在具有多个输入或输出的网络中,可以使用字符串而不是位置索引来标识张量时很有用。
- 返回值:张量名称。
const Shape &shape() const
- 获取张量的形状。
- 获取
Tensor的形状,即每个维度中的元素数量。 维度的顺序由张量布局指定。 - 返回值:张量形状。
const Dimensions dimensions() const
- 获取
Tensor的维度,即每个维度中的元素数量。 返回的值与张量布局无关。 - 返回值:张量维度(如果张量的秩不是 4,则全为 0)。
Layout layout() const
- 获取
Tensor的布局,即数据在内存中的组织方式。 SyNAP 支持两种布局:NCHW和NHWC。N 维度(样本数量)为了与标准约定兼容而存在,但必须始终为 1。 - 返回值:张量布局。
std::string format() const
- 获取
Tensor的格式,即数据表示的内容的描述。 这是一个自由格式的字符串,其含义取决于应用程序,例如,"rgb"、"bgr"。 - 返回值:张量格式。
DataType data_type() const
- 获取张量数据类型。整数类型用于表示量化数据。量化参数和量化方案的详细信息不直接可用,用户可以使用下面的
as_float()方法将量化数据转换为 32 位 float 来使用。 - 返回值:张量中每个项目的类型。
Security security() const
- 获取张量安全属性。
- 返回值:张量的安全属性(如果模型不安全则为 none)。
size_t size() const
- 返回值:张量数据的字节大小。
size_t item_count() const
- 获取张量中的项目数量。张量的
size()始终等于item_count()乘以张量数据类型的大小。 - 返回值:张量中的数据项目数量。
bool is_scalar() const
- 返回值:如果这是标量张量(即只包含一个元素)则返回
true。(标量张量的形状有一个维度,等于 1)。
bool assign(const uint8_t *data, size_t count)
- 规范化并将数据复制到张量数据缓冲区。
- 数据会被规范化并转换为张量的类型和量化方案。
数据数量必须等于张量的
item_count()。 - 参数:
data:指向要复制的数据的指针。count:要复制的数据项目数量。
bool assign(const int16_t *data, size_t count)
- 与前一个
assign函数类似,但用于int16_t数据。 - 返回值:如果成功则返回
true。
bool assign(const float *data, size_t count)
- 与前一个
assign函数类似,但用于float数据。 - 返回值:如果成功则返回
true。
bool assign(const void *data, size_t size)
- 将原始数据复制到张量数据缓冲区。数据被视为原始数据,因此不会进行规范化或转换。数据大小必须等于张量的
size()。 - 返回值:如果成功则返回
true。
bool assign(const Tensor &src)
- 将张量的内容复制到张量数据缓冲区。
- 不会进行规范化或转换;两个张量的数据类型和大小必须匹配。
- 参数:
src:包含要复制的数据的源张量。
- 返回值:如果成功则返回
true,如果类型或大小不匹配则返回false。
bool assign(int32_t value)
- 将值写入张量数据缓冲区。
- 仅当张量是标量时才有效。该值也会转换为张量数据类型:8、16 或 32 位整数。 在将值写入数据缓冲区之前,该值也会根据张量格式属性进行重新缩放(如果需要)。
- 参数:
value:要复制值。
- 返回值:如果成功则返回
true。
template[typename T](typename T) T *data()
- 获取指向张量数据缓冲区内数据的指针(如果可以直接访问)。
- 这仅在
T匹配张量的data_type()且不需要规范化/量化时才有效。示例用法:uint8_t* data8 = tensor.data[uint8_t](uint8_t)(); - 返回值:指向数据缓冲区内数据的指针或
nullptr。
void *data()
- 获取指向张量数据缓冲区内原始数据的指针(如果有)。
- 该方法返回一个
void指针,因为实际数据类型是data_type()方法返回的类型。 - 返回值:指向数据缓冲区内原始数据的指针,如果没有则返回
nullptr。
const float *as_float() const
- 获取指向转换为 float 的张量内容的指针。
- 该方法始终返回一个
float指针。如果张量的实际数据类型不是 float,则会在内部执行转换,因此用户不需要关心数据如何内部表示。请注意,这是一个指向浮点数据内部的张量指针:这意味着返回的指针不能释放,内存将在张量被销毁时自动释放。 - 返回值:指向
float[item_count()]数组的指针,表示转换为 float 的张量内容(如果张量没有数据则返回nullptr)。
Buffer *buffer()
- 获取指向张量当前数据
Buffer的指针(如果有)。 - 这将是张量的默认缓冲区,除非用户使用
set_buffer()分配不同的缓 冲区。 - 返回值:当前数据缓冲区或
nullptr(如果没有)。
bool set_buffer(Buffer *buffer)
- 设置张量的当前数据缓冲区。
- 缓冲区大小必须为 0 或匹配张量大小,否则将被拒绝(空缓冲区将自动调整为张量大小)。通常提供的缓冲区应至少与张量本身一样长。如果缓冲区对象在张量之前被销毁,它将自动取消设置,张量将保持缓冲区。
- 参数:
buffer:要用于此张量的缓冲区。缓冲区大小必须匹配张量大小(或为 0)。
- 返回值:如果成功则返回
true。
以下是张量中支持的所有数据类型列表:
enum class synaptics::synap::DataType
枚举值
| 枚举值 | 描述 |
|---|---|
enumerator invalid | 无效数据类型。 |
enumerator byte | 字节数据类型。 |
enumerator int8 | 8 位有符号整数。 |
enumerator uint8 | 8 位无符号整数。 |
enumerator int16 | 16 位有符号整数。 |
enumerator uint16 | 16 位无符号整数。 |
enumerator int32 | 32 位有符号整数。 |
enumerator uint32 | 32 位无符号整数。 |
enumerator float16 | 16 位浮点数。 |
enumerator float32 | 32 位浮点数。 |
缓冲区
用于存储张量数据的内存必须满足以下要求:
- 必须正确对齐
- 必须正确填充
- 在某些情况下必须是连续的
- 必须可由 NPU 硬件加速器以及 CPU 或其他硬件组件访问
使用 malloc()、new 或 std::vector 分配的内存不满足这些要求,因此不能直接用作 Network 的输入或输出。为此,Tensor 对象使用特殊的 Buffer 类来管理内存。每个张量内部都包含一个默认的 Buffer 对象,用于管理数据所用的内存。
Buffer 提供的 API 尽可能与 std::vector 的 API 相似。主要区 别在于缓冲区内容无法按索引访问,因为缓冲区只是原始内存的容器,没有数据类型。数据类型由使用缓冲区的张量持有。Buffer 还负责在销毁时释放已分配的内存(RAII),以避免内存泄漏。实际内存分配通过额外的 Allocator 对象完成,这允许在不同内存区域以不同属性分配内存。创建缓冲区对象时,除非指定了其他分配器,否则将使用默认分配器。分配器可以直接在构造函数中指定,也可以稍后使用 set_allocator() 方法指定。
图 8 Buffer 类
为了让缓冲区数据可在 CPU 和 NPU 硬件之间共享,需要执行一些额外操作以确保 CPU 缓存和系统内存正确对齐。当缓冲区内容用于网络推理时,这些操作会自动完成。在某些情况下,CPU 不会直接读写缓冲区数据,例如数据由其他硬件组件(如视频解码器)生成时。这种情况下,可以通过使用所提供的方法禁用 CPU 对缓冲区的访问来获得一定的性能提升。
可以创建引用现有内存区域的缓冲区,而不使用分配器。该内存必须已在 TrustZone 内核中注册,并且正确对齐和填充。Buffer 对象在销毁时不会释放该内存,因为内存归分配它的软件模块所有。
class synaptics::synap::Buffer
Synap 数据缓冲区。
概要
| 函数 | 描述 |
|---|---|
Buffer(Allocator *allocator = nullptr) | 创建空数据缓冲区。 |
Buffer(size_t size, Allocator *allocator = nullptr) | 创建并分配数据缓冲区。 |
Buffer(uint32_t mem_id, size_t offset, size_t size) | 引用现有内存区域。 |
Buffer(uint32_t handle, size_t offset, size_t size, bool is_mem_id) | 引用现有内存区域。 |
Buffer(const Buffer &rhs, size_t offset, size_t size) | 引用现有缓冲区内存区域的一部分。 |
Buffer(Buffer &&rhs) noexcept | 移动构造函数。 |
Buffer &operator=(Buffer &&rhs) noexcept | 移动赋值。 |
bool resize(size_t size) | 调整缓冲区大小。 |
bool assign(const void *data, size_t size) | 将数据复制到缓冲区。 |
size_t size() const | 获取实际数据大小。 |
const void *data() const | 获取实际数据。 |
bool allow_cpu_access(bool allow) | 启用/禁用 CPU 对缓冲区数据的访问。 |
bool set_allocator(Allocator *allocator) | 更换分配器。 |
公共函数
Buffer(Allocator *allocator = nullptr)
- 创建空数据缓冲区。
- 参数:
allocator:要使用的分配器(默认为基于 malloc 的分配器)。
Buffer(size_t size, Allocator *allocator = nullptr)
- 创建并分配数据缓冲区。
- 参数:
size:缓冲区大小。allocator:要使用的分配器(默认为基于 malloc 的分配器)。
Buffer(uint32_t mem_id, size_t offset, size_t size)
- 创建引用现有内存区域的数据缓冲区。
- 用户必须确保所提供的内存已正确对齐和填充。指定的内存区域在缓冲区销毁时不会被释放。调用者有责任在
Buffer销毁后释放mem_id。 - 参数:
mem_id:已在 TZ 内核中注册的现有内存区域的 ID。offset:内存区域内实际数据的偏移量。size:实际数据的大小。
Buffer(uint32_t handle, size_t offset, size_t size, bool is_mem_id)
- 创建引用现有内存区域的数据缓冲区。
- 用户必须确保所提供的内存已正确对齐和填充。指定的内存区域在缓冲区销毁时不会被释放。调用者有责任在
Buffer销毁后释放mem_id。 - 参数:
handle:现有 dmabuf 的 FD 或已在 TZ 内核中注册的mem_id。offset:内存区域内实际数据的偏移量。size:实际数据的大小。is_mem_id:如果第一个参数是mem_id则为true,如果是 FD 则为false。
Buffer(const Buffer &rhs, size_t offset, size_t size)
- 创建引用现有缓冲区内存区域一部分的数据缓冲区。
- 所提供缓冲区的内存必须已分配。为避免引用已释放内存,现有缓冲区内存在该缓冲区销毁之前不得被释放。
- 参数:
rhs:现有的Buffer。offset:Buffer内存区域内所需数据的偏移量。size:所需数据的大小。
Buffer(Buffer &&rhs) noexcept
- 移动构造函数。仅 适用于尚未被
Network使用的缓冲区。
Buffer &operator=(Buffer &&rhs) noexcept
- 移动赋值。仅适用于尚未被
Network使用的缓冲区。
bool resize(size_t size)
- 调整缓冲区大小。仅在提供了分配器时才可调整。之前的内容将丢失。
- 参数:
size:新的缓冲区大小。
- 返回值:如果成功则返回
true。
bool assign(const void *data, size_t size)
- 将数据复制到缓冲区。如果输入数据大小与当前缓冲区大小相同则始终成功;否则,在可能的情况下会调整缓冲区大小。
- 参数:
data:指向要复制数据的指针。size:要复制数据的大小。
- 返回值:如果成功则返回
true。
size_t size() const
- 获取实际数据大小。
const void *data() const
- 获取实际数据。
bool allow_cpu_access(bool allow)
- 启用/禁用 CPU 读写缓冲区数据的能力。
- 默认情况下,CPU 对数据的访问是启用的。当 CPU 不需要读写缓冲区数据时,可以禁用 CPU 访问,这在数据仅由其他硬件组件生成/使用时可带来一定性能提升。
在 CPU 访问被禁用时读写缓冲区数据可能导致缓冲区中的数据丢失或损坏。
- 参数:
allow:false表示 CPU 不会访问缓冲区数据。
- 返回值:当前设置。
bool set_allocator(Allocator *allocator)
- 更换分配器。仅在缓冲区为空时才可执行。
- 参数:
allocator:分配器。
- 返回值:如果成功则返回
true。
分配器
为缓冲区对象提供了两种分配器:
- 标准分配器:这是未显式指定分配器时缓冲区使用的默认分配器。内存是分页的(非连续的)。
- CMA 分配器:分配连续内存。某些硬件组件需要连续内存,且当输入/输出缓冲区非常大时,可提供一定的性能提升,因为处理内存页面所需的开销更少。使用时需格外谨慎,因为系统中可用的连续内存非常有限。
Allocator *standard_allocator()
- 返回指向系统标准分配器的指针。
Allocator *contiguous_allocator()
- 返回指向系统连续内存分配器的指针。
上述调用返回的是全局对象的指针,因此使用后不得删除。
进阶示例
访问张量数据
张量数据通常使用 Tensor::assign(const T* data, size_t count) 方法写入。该方法会处理从类型 T 到网络内部表示所需的数据规范化和类型转换。
类似地,输出数据通常使用 Tensor::as_float() 方法读取,该方法提供指向张量数据的指针,将其从内部表示转换为浮点值。
这些转换即使经过优化,也存在与数据大小成正比的运行时开销。对于输入数据,可以通过直接在张量数据缓冲区中生成数据来避免这一开销,但这仅在张量数据类型与输入数据类型匹配且不需要额外规范化/量化时才可行。张量提供了类型安全的 data[T](T)() 访问方法,仅在满足上述条件时才返回指向张量数据的指针,例如:
uint8_t* data_ptr = net.inputs[0].data[uint8_t](uint8_t)();
if (data_ptr) {
custom_generate_data(data_ptr, net.inputs[0].item_count());
}
如果张量中的数据不是 uint8_t,或者需要规范化/[反]量化,则返回值将为 nullptr。这种情况下,无法直接写入或读取,需要使用 assign() 或 as_float()。
始终可以使用原始 data() 访问方法绕过所有检查直接访问数据:
void* in_data_ptr = net.inputs[0].data();
void* out_data_ptr = net.outputs[0].data();
同样,也可以使用 void* 数据指针直接赋值原始数据(不进行任何转换):
const void* in_raw_data_ptr = ....;
net.inputs[0].assign(in_raw_data_ptr, size);
在这些情况下,用户有责任了解数据的表示方式及处理方法。
设置缓冲区
如果默认张量缓冲区的属性不合适,用户可以显式创建新缓冲区并替换默认缓冲区。例如,假设我们想使用具有连续内存的缓冲区:
Network net;
net.load_model("model.synap");
// 用使用连续内存的缓冲区替换默认缓冲区
Buffer cma_buffer(net.inputs[0].size(), contiguous_allocator());
net.inputs[0].set_buffer(&cma_buffer);
// 像往常一样执行推理
custom_generate_input_data(net.inputs[0].data(), net.inputs[0].size());
net.predict();
设置默认缓冲区属性
比上一节所示替换张量缓冲区更简单的替代方案,是直接更改默认张量缓冲区的属性。这只能在开始时、访问张量数据之前完成:
Network net;
net.load_model("model.synap");
// 为 input[0] 的默认缓冲区使用连续分配器
net.inputs[0].buffer()->set_allocator(contiguous_allocator());
// 像往常一样执行推理
custom_generate_input_data(net.inputs[0].data(), net.inputs[0].size());
net.predict();