定义 UDO 包
UDO 包主要由一个注册库和一个或多个实现库组成。其主要思想是,注册库包含有关操作性质的信息,而实现库包含执行操作所需的内核。UDO 包可以通过 UDO 配置来定义,该配置包含操作和路径的文本规范,最终有助于定义表示 UDO 包的目录结构。本节将讨论 UDO 包的定义,而定义后的包的创建将推迟到“创建 UDO 包”部分进行讨论。
UDO 包配置说明
UDO 配置字段描述中的所有字段 均已复制到 UDO 软件包配置规范中。因此,上述 UDO 配置文件的细分是该部分中的描述与两个软件包特定字段的组合。下文包含一个通用配置,其中解释了软件包特定字段。用户应参阅UDO 配置字段描述, 以获取有关复制字段的更详细说明,并查看相关说明。
{
"UdoPackage_0":
{
"Operators": [
{
"type": "Softmax",
"inputs":[
{"name":"Placeholder", "per_core_data_types": {"CPU":"FLOAT_32", "GPU":"FLOAT_32", "DSP":"UINT_8"},
"quantization_mode": "TF"}
],
"outputs":[
{"name":"Output","per_core_data_types": {"CPU":"FLOAT_32", "GPU":"FLOAT_32", "DSP":"UINT_8"},
"quantization_mode": "TF"}
],
"core_types": ["CPU", "GPU", "DSP"],
"dsp_arch_types": ["v68"]
}
],
"UDO_PACKAGE_NAME": "SoftmaxUdoPackage"
}
}
附加字段解释如下:
-
UDO_PACKAGE_NAME:包含 UDO 的包的名称。
-
UDO_PACKAGE_PATH: UDO 包的保存路径。如果未提供,则使用当前目录。
-
SNPE_UDO_ROOT:这是一个可选变量,用于让工具知道 SnpeUdo API 目录在用户环境中的位置。可以在此处设置,也可以将其设置为环境变量。
笔记
-
配置中指定的信息用于实例化信息数据结构,这些数据结构对于 Qualcomm® 神经处理 SDK 如何执行包含 UDO 的模型至关重要。这意味着 UDO 包与包含 UDO 的模型之间存在协同作用,因此,建议使用相同的配置来定义 UDO 及其对应的包。
-
包名称的定义将决定生成的包中的源文件和实现库的名称。
-
操作类型将决定生成的包中包含的方法、类和源文件的名称。
-
包 core-types 是包中每个运算符提到的所有核心类型的集合。
创建 UDO 包
本节介绍如何使用 snpe-udo-package-generator ,根据用户定义操作的简单文本规范创建 UDO 包。从 Qualcomm® 神经处理 SDK API 的角度来看,UDO 包由一个注册库和一个或多个实现库组成。因此,虽然用户可以独立于此规范创建 UDO 包,但本节介绍了创建部分定义的 UDO 包的过程,该包可以轻松实现和编译以生成相关的库。
生成 UDO 骨架代码
要使用 Qualcomm® 神经处理 SDK 工具生成包,需要创建一个描述操作和包详细信息的 UDO 配置。指定一个能够充分表示所需 UDO 的配置后,即可将其作为参数提供给 Qualcomm® 神经处理 SDK UDO 包生成器工具(详见 snpe-udo-package-generator)。该工具旨在生成部分框架代码,以辅助快速原型设计。本节介绍包生成器工具的用法及其生成的工件。
要运行 snpe-udo-package-generator,用户需要按照 Qualcomm (R) Neural Processing SDK Setup中的设置说明进行操作。该工具还依赖于 Mako Template Library,可在此处找到: Mako Templates for Python。此外,我们需要一个解压后的 Qualcomm® AI Direct SDK(无需设置 Qualcomm® AI Direct SDK)来生成骨架代码。有关 Qualcomm® AI Direct SDK 的详细信息,请参阅 Qualcomm® AI Direct SDK 文档$QNN_SDK_ROOT/docs/index.html
,其中QNN_SDK_ROOT
是 Qualcomm® AI Direct SDK 的安装位置。将 设置$QNN_SDK_ROOT
为解压后的 Qualcomm® AI Direct SDK 位置。设置完成后,可以使用以下命令生成包:
snpe-udo-package-generator -p $SNPE_ROOT/examples/SNPE/NativeCpp/UdoExample/Softmax/config/Softmax_Htp.json -o <my-dir>
上述命令将创建一个 UDO 包,该包是一个由框架代码和构建文件组成的目录,可用于将包内容编译为独立的共享库。UDO教程中引用的配置文件已用于生成以下 udo 包内容:
|-- Makefile
|-- common.mk
|-- config
| `-- Softmax_Htp.json
|-- include
| `-- utils
| |-- IUdoOpDefinition.hpp
| |-- UdoMacros.hpp
| `-- UdoUtil.hpp
`-- jni
|-- Android.mk
|-- Application.mk
`-- src
|-- CPU
| |-- Makefile
| |-- makefiles
| | |-- Android.mk
| | |-- Application.mk
| | `-- Makefile.linux-x86_64
| `-- src
| |-- CpuCustomOpPackage.cpp
| |-- SoftmaxUdoPackageInterface.cpp
| |-- ops
| | `-- Softmax.cpp
| `-- utils
| |-- BackendUtils.hpp
| |-- CPU
| | |-- CpuBackendUtils.cpp
| | `-- CpuBackendUtils.hpp
| `-- CustomOpUtils.hpp
|-- DSP_V68
| |-- Makefile
| `-- src
| |-- SoftmaxUdoPackageInterface.cpp
| `-- ops
| `-- Softmax.cpp
|-- GPU
| |-- Makefile
| |-- include
| | |-- GpuCustomOpPackage.hpp
| | `-- Operation.hpp
| |-- makefiles
| | |-- Android.mk
| | `-- Application.mk
| `-- src
| |-- GpuCustomOpPackage.cpp
| |-- SoftmaxUdoPackageInterface.cpp
| `-- ops
| `-- Softmax.cpp
|-- reg
| |-- Makefile
| `-- SoftmaxUdoPackageRegLib.cpp
`-- utils
`-- UdoUtil.cpp
UDO 包的内容
-
您可以使用 Linux 主机的 make 构建系统或 Android 设备的 Android-NDK 构建系统来编译软件包。简而言之,make 系统使用顶层Makefile、common.mk以及每个运行时目录中的各个 makefile 进行配置。android-build 系统使用jni/Android.mk和 jni/Application.mk进行配置。
-
config 目录包含用于创建包的 JSON 配置。
-
包含目录包含三种文件:来自 Qualcomm® 神经处理 SDK UDO API 的头文件、特定于 UDO 包及其操作的头文件,以及一个包含 C++ 辅助实用程序的目录,该目录封装了 Qualcomm® 神经处理 SDK UDO API 调用。用户应注意,包含实用程序 API 只是为了方便创建实现源代码。使用这些实用程序并非构建或执行 UDO 包的先决条件。
-
该软件包的相关源文件位于jni/src目录下。配置中指定的每个核心类型都会有一个子目录。注册 (reg) 目录包含创建注册库所需的文件,该库通常是 Qualcomm® 神经处理 SDK API 的入口点。此外,还包含前面提到的 C++ 辅助实用程序的源代码。通常,用户只需编辑特定于运行时或注册目录中的代码。
生成的源代码
本节及其后续小节介绍使用生成 UDO 框架代码中显示的包内容在包中生成的源代码。最终完成的 UDO 包预计包含一个注册库和一个或多个实现库。为了生成注册库, 需要编译jni/src/reg中的源代码。实现库使用每个核心类型特定目录中的源代码进行编译。回想一下,该工具创建的包仍然需要实现。后续小节将介绍需要实现的文件。所有生成的源代码的标头中都带有“自动生成”标签。源代码在生成阶段被视为部分完成,用户有责任根据需要实现某些文件,以确保与 Qualcomm® 神经处理 SDK API 的正确兼容性和功能性。所有待实现的代码主体中都带有“在此处添加代码”标签,以指示需要实现。请注意,所有库都链接到 C++ 实用程序源代码。
完成注册骨架代码
如前所述,注册库是从jni/src/reg中的源代码创建的。该目录包含一个用于编译软件包的 Makefile 以及软件包特定文件: SoftmaxUdoPackageRegLib.cpp,该文件包含打开库时由 Qualcomm® 神经处理 SDK UDO API 解析的函数符号。注册库文件包含 API 调用,这些调用向 Qualcomm® 神经处理 SDK UDO API 提供有关模型中操作性质及其所属实现库的信息。
完成实施骨架代码
实现库是按核心类型创建的,其源代码位于jni/src目录下,对应核心类型。以 CPU 运行时为例, jni/src/CPU目录包含一个用于构建 CPU 实现库的 Makefile、一个特定于软件包的源文件 SoftmaxUdoPackageInterface.cpp(用于库中包含的所有操作)以及一个每个操作的源文件 Softmax.cpp(包含运行时实现)。与注册示例一样,特定于软件包的源文件通常不应编辑。同样,此文件包含一些方法,用于返回实现库中包含的操作的信息,以及一些方法,它们充当最终在每个操作文件中执行的代码之上的间接层。对于 CPU 示例,Softmax.cpp中的三个方法(即finalize、execute和free)由用户负责编辑。请注意,这些方法分别用于创建操作、执行其实现以及释放操作。因此,这些操作完全由用户决定。实现库的示例生成版本如下所示:
Qnn_ErrorHandle_t execute(CustomOp* operation) {
/**
* Add code here
**/
return QNN_SUCCESS;
}
Qnn_ErrorHandle_t finalize(const CustomOp* operation) {
QNN_CUSTOM_BE_ENSURE_EQ(operation->numInput(), 1, QNN_OP_PACKAGE_ERROR_VALIDATION_FAILURE)
QNN_CUSTOM_BE_ENSURE_EQ(operation->numOutput(), 1, QNN_OP_PACKAGE_ERROR_VALIDATION_FAILURE)
/**
* Add code here
**/
return QNN_SUCCESS;
}
Qnn_ErrorHandle_t free(CustomOp& operation) {
/**
* Add code here
**/
return QNN_SUCCESS;
}
为了获得良好的性能和稳定性,需要避免在已完成的 op 执行函数(即DSP V68 及以上版本、DSP V66 / V65、GPU、CPU 分别执行的<op_name>Impl、 <op_name>_executeOp、<op_name>Operation和 execute函数)中分配堆内存。堆内存分配包括但不限于调用malloc
、、构造带有默认分配器的 STL 容器对象 等,以及添加对带有默认分配器的 STL 容器对象的 调用 等 。operator new
std::vector
std::vector::push_back
避免堆内存分配的原因是,完成堆内存分配的时间不受限制,并且可能存在巨大的差异。尤其对于 DSP 和 HTP 而言,堆内存分配在某些情况下会触发 CPU 请求,并显著影响推理速度。此外,堆内存分配可能会失败并返回空指针或引发异常。在这种情况下,通常没有好的方法来继续执行。在对功能安全有严格要求的应用程序中,甚至不允许在初始化后分配堆内存。
如果需要工作缓冲区来执行操作计算,这里有一些潜在的替代方案:
-
为局部变量构造 std::array 而不是 std::vector:与 不同
std::vector
,std::array
它使用堆栈内存。如果可以预先知道最大内存大小且大小不大,则此方法有效。 -
使用输出张量空间作为临时内存:每个执行函数至少有一个输出张量。在填充实际输出数据之前,你可以使用输出张量的空间作为临时缓冲区。请注意,输出张量空间只能安全地写入拥有该输出张量的执行函数中。
笔记
-
一般情况下,包只需要进行一些能够正常执行的功能性修改即可。初始未实现的包可以保证编译通过。
-
一个细微的区别是,生成的 DSP V65 或 DSP V66 实现源代码要求每个实现库包含一个操作。而在 CPU、GPU 和 DSP V68 及更高版本的情况下,一个库中可以包含任意数量的操作。
-
每个运行时的实现源文件之间存在差异。对于 GPU 而言,执行 工作流已实现,用户只需实现<OpName>Operation和 setKernelInfo方法。与 CPU 和 GPU 不同,DSP 使用的 API 不依赖于“生成的源代码” 部分中讨论的 C++ 辅助工具。这意味着某些辅助方法和构造函数在 DSP 情况下可能不可用。对于 DSP 而言,用户需要实现softmaxImpl 方法。