Qualcomm® AI Engine Direct 使用手册(11)

150 篇文章 19 订阅
50 篇文章 3 订阅

Qualcomm® AI Engine Direct 使用手册(11)


5 操作包

Qualcomm® AI Engine Direct中的操作包(也称为“Op 包”)是指可供后端使用的操作集合,以便用于创建和执行表示网络模型的 Qualcomm® AI Engine Direct图形。

Qualcomm® AI Engine Direct软件架构旨在允许使用 QNN 的应用程序的内存占用进行高度定制,具体取决于运行模型所需的后端和操作。操作包通过编译成与后端核心库不同的共享库而与后端分离。这允许用户通过将用例所需的任意数量的操作打包到 Op 包中来编译精益应用程序。这在深度嵌入的物联网类型用例中尤其理想,这些用例在严格的内存限制内运行,并且仅针对少数针对其需求定制的网络模型。

Qualcomm® AI Engine Direct后端通过 API QnnBackend_registerOpPackage()注册用户提供的 Op 包。

多个 Op 包可以在后端注册,并在后端内创建的上下文和图形之间共享,作为单独的、不同的实例。它们的生命周期和范围与它们注册的后端的范围相匹配,因此它们可以比任何需要它们的图表更持久。至关重要的是,这也意味着外部各方可以使用 Qualcomm® AI Engine Direct Op 包使用的相同机制轻松创建其自定义 Op 包并将其与Qualcomm® AI Engine Direct集成。所有 Op 包都需要实现QnnOpPackage.h中定义的接口。

请参阅教程,了解简单的示例操作包,该包演示了外部各方如何在 Qualcomm® AI Engine Direct框架内创建和使用自定义操作。

请参阅以下部分,了解有关生成 Op 包框架代码的信息,然后可以将其实现为成熟的 Qualcomm® AI Engine Direct Op 包。

5.1 生成操作包

本节包含相关的子节,可用于使用XML OpDef 配置文件通过qnn-op-package-generator工具生成自定义 op 包的框架代码:

5.1.1 QNN Op 包代码生成

本节定义了演示如何使用qnn-op-package-generator生成框架代码以及用于编译的 makefile 的步骤,这两个步骤一起执行以创建 QNN Op Package 共享库。该工具接受描述包属性的 XML 配置输入文件,并生成 QNN Op 包目录结构。

创建 QNN Op 包骨架

对于以下部分,我们将假设安装指令已运行并且qnn-op-package-generator 可以在命令行上访问。1 创建包骨架的第一步是定义一个 XML OpDef 配置文件,该文件描述包信息,例如包名称、版本和域,以及包包含的操作。包信息和操作是根据预定义的 XML 模式进行描述的,该模式主要需要有关操作的输入、输出和参数的信息。有关定义 XML Op Def 的信息,请参阅 XML OpDef 架构细分。

示例配置也可以在XML Op Def 配置示例和 SDK 中找到:

${QNN_SDK_ROOT}/examples/QNN/OpPackageGenerator

一旦根据规范完全定义了 XML,就可以使用–config_path 或 -p选项将其作为参数传递给工具。要生成多个包,-p还可以使用不同的配置多次指定该选项。

该工具可以在命令行上使用单个 XML 配置来运行,如下所示:

qnn-op-package-generator -p <QNN_SDK_ROOT>/examples/QNN/OpPackageGenerator/ExampleOpPackageHtp.xml -o <output_dir>

笔记
可以指定多个-p命令行选项来生成不同的包,前提是每个包名称不同。如果包名称不明确,该工具会将每个配置中定义的所有操作合并到单个包目录中。

目录结构
上一节中的示例命令在指定的输出路径输出一个名为ExampleOpPackage的包骨架目录。2在此上下文中,包名称为ExampleOpPackage。3该包还包含两个操作:Conv2D和Softmax。

包目录树如下所示并展开:

|-- Makefile
|-- makefiles
    |-- Android.mk
    |-- Application.mk
|-- config
|   `-- ExampleOpPackage.xml
|-- include
`-- src
    |-- ExamplePackageInterface.cpp
    |-- utils
    `-- ops
        |-- Conv2D.cpp
        `-- Softmax.cpp

  • Makefile:此文件包含为各种已知体系结构编译包源文件的 make 目标和规则。请注意,不同后端的 make 命令是不同的,并且 CPU 目标需要在android 目标的makefiles目录中添加额外的 makefile。

  • config:此目录包含传递给工具的所有 XML OpDef 配置。

  • include:此目录是用户可能需要编译的任何其他包含文件的占位符。

  • src:该目录包含配置中定义的每个操作生成的源文件,以及生成的接口源文件。

    • ExamplePackageInterface:此文件实现 QNN API 加载和执行包所需的函数指针。该文件始终命名为 <package_name>Interface.cpp。有关所有其他所需函数指针的更多信息,请参阅Op 包。

    • utils:帮助实用程序,可以轻松跨后端使用。用户应注意,该目录当前仅适用于 CPU 后端。为了简洁起见,此处省略了内容,一般来说,大多数用户不需要对所包含的文件进行更改。

    • ops:每个源文件都命名为 <op_name>.cpp。源文件实现 QNN 后端所需的 API 接口方法,以启用操作初始化、操作销毁和内核执行。

骨架代码概述

在本节中,我们将介绍两种生成的源文件:接口文件和特定于操作的文件。该接口一般不需要额外的实现,而源文件只包含应由用户完成的空函数体。本节中使用的代码引用了目录结构中生成的包。

笔记
生成的特定于操作的文件的内容可能因后端而异。

笔记
为了获得良好的性能和稳定性,需要避免在已完成的操作执行函数中分配堆内存,即<op_name>Impl、 <op_name>_executeOp以及分别在 HTP、DSP 和 CPU 期间执行的执行函数图形执行。堆内存分配包括但不限于调用malloc、使用默认分配器构造STL容器对象 、添加使用 默认分配器调用STL容器对象等项。operator newstd::vectorstd::vector::push_back
避免堆内存分配的原因是因为完成堆内存分配的时间是无限的,并且可能有巨大的方差。特别是对于 DSP 和 HTP,堆内存分配在某些情况下会触发 CPU 请求并显着影响推理速度。此外,堆内存分配可能会失败并返回空指针或引发异常。在这种情况下,通常没有什么好的方法可以继续执行。在功能安全要求严格的应用中,甚至不允许初始化后分配堆内存。
如果需要暂存缓冲区来执行操作计算,以下是一些潜在的替代方案:
对于局部变量构造 std::array 而不是 std::vector:不同的是 std::vector,std::array使用堆栈内存。如果可以提前知道最大内存大小并且大小不大,则此方法有效。
使用输出张量空间作为暂存存储器:每个执行函数至少有一个输出张量。在填充实际输出数据之前,您可以使用输出张量的空间作为暂存缓冲区。请注意,输出张量空间只能安全地写入拥有输出张量的执行函数中。

接口文件
从生成的接口文件中获取的接口提供程序函数的片段如下所示:

Qnn_ErrorHandle_t ExamplePackageInterfaceProvider(QnnOpPackage_Interface_t* interface) {
   interface->interfaceVersion   = {1,3,0};
   interface->v1_3.init          = ExamplePackageInit;
   interface->v1_3.terminate     = ExamplePackageTerminate;
   interface->v1_3.createKernels = ExamplePackageCreateKernels;
   interface->v1_3.getInfo       = ExamplePackageGetInfo;
......

该文件包含根据后端确定的 Op Package API 生成的函数。所有后端都需要接口提供程序作为qnn-net-run工具的输入。第 1 行的函数始终 使用从生成时的配置中获取的信息 命名为<package_name>InterfaceProvider 。用户应注意,包名称必须始终是有效的 C++ 标识符,这意味着它只能包含字母数字字符和下划线。此外,第 5-7 行之后的所有其他函数也都以包名称为前缀。

操作源文件
本节介绍该工具为可用后端生成的源文件。下面的示例突出显示了示例配置中定义的 Conv2D 运算的输出。

HTP Conv2D.cpp 示例

```c
/* execute functions for ops */
 2
 3 template<typename TensorType,typename TensorType1>
 4 GraphStatus conv2dImpl(TensorType& out_0,
 5                       const TensorType& in_0,
 6                       const TensorType& filter,
 7                       const TensorType1 &bias,
 8                       const Tensor& stride,
 9                       const Tensor& pad_amount,
10                       const Tensor& group,
11                       const Tensor& dilation) {
12  /*
13   * add code here
14   * */
15
16  return GraphStatus::Success;
17}
18
19__attribute__((unused)) static float conv2dCostFunc(const Op *op) {
20  /*
21  * add code here
22  * */
23
24  float cost = 0.0;  // add cost computation here
25  return cost;
26}

上面显示的函数由 QNN HTP 后端用于执行和成本分析。这两个函数始终分别命名为<op_name>Impl和<op_name>CostFunc。用户应注意,操作名称必须始终是有效的 C++ 标识符,这意味着它只能包含字母数字字符和下划线。每个函数均使用 QNN HTP API 宏向 HTP 后端注册,并且应由用户完成以启用准确的功能。每个要完成的函数都在函数体中包含了add code here注释。4

用户还应该注意,模板类型是从 XML OpDef 配置中推导出来的,以便能够简单地创建多个执行函数,并且 QNN HTP 后端并不严格要求。每个函数签名都可以由用户自行决定专门化。

此外,用户应该注意自动生成到源代码中的DEF_PACKAGE_PARAM_ORDER宏。请注意,该宏是可选的,它只是列出传递给执行函数的参数的顺序及其相应的默认值(如果有)。重要的是,用户应注意,此宏中定义的所有张量和字符串参数始终设置为强制,并具有默认的空指针值,无论可选性如何。因此,用户可能需要手动更改张量参数值以确保准确执行。

DSP Conv2D.cpp 示例

 1 Udo_ErrorType_t
 2 conv2d_createOpFactory (QnnOpPackage_GlobalInfrastructure_t globalInfra,
 3   Udo_CoreType_t udoCoreType, void *perFactoryInfrastructure,
 4   Udo_String_t operationType, uint32_t numOfStaticParams,
 5   Udo_Param_t *staticParams, Udo_OpFactory_t *opFactory)
 6{
 7   if(operationType == NULL || opFactory == NULL) {
 8      return UDO_INVALID_ARGUMENT;
 9   }
10   if(strcmp(operationType, g_conv2dOpType) == 0) {
11      conv2dOpFactory_t* thisFactory = (conv2dOpFactory_t *)(*(globalInfra->dspGlobalInfra->hexNNv2Infra.udoMalloc))(sizeof(conv2dOpFactory_t));
12      int size = strlen(operationType) + 1; // +1 to hold the '\0' character
13      thisFactory->opType = (Udo_String_t)(*(globalInfra->dspGlobalInfra->hexNNv2Infra.udoMalloc))(size);
14      strlcpy((thisFactory->opType), operationType, size);
15      thisFactory->numOfStaticParams = numOfStaticParams;
16      /*
17       * if this op has static params, add code here
18       */
19      *opFactory = (Udo_OpFactory_t)thisFactory;
20   } else {
21      return UDO_INVALID_ARGUMENT;
22   }
23   return UDO_NO_ERROR;
24}
25
26 Udo_ErrorType_t
27 conv2d_releaseOpFactory(QnnOpPackage_GlobalInfrastructure_t globalInfra,
28                                             Udo_OpFactory_t opFactory)
29{
30   if(opFactory == NULL) {
31      return UDO_INVALID_ARGUMENT;
32   }
33   conv2dOpFactory_t* thisFactory = (conv2dOpFactory_t *)(opFactory);
34   (*(globalInfra->dspGlobalInfra->hexNNv2Infra.udoFree))((thisFactory->opType));
35   (*(globalInfra->dspGlobalInfra->hexNNv2Infra.udoFree))(thisFactory);
36   /*
37    * if this op has static params, add code here
38    */
39   return UDO_NO_ERROR;
40}
41
42 Udo_ErrorType_t
43 conv2d_validateOperation (Udo_String_t operationType, uint32_t numOfStaticParams,
44   const Udo_Param_t *staticParams) {
45   if(strcmp(operationType, g_conv2dOpType) == 0) {
46      if (numOfStaticParams != g_conv2dStaticParamsNum) {
47            return UDO_INVALID_ARGUMENT;
48      }
49      /*
50       * If this op should validate others, add code here
51       */
52   } else {
53      return UDO_INVALID_ARGUMENT;
54   }
55   return UDO_NO_ERROR;
56}
57
58 Udo_ErrorType_t
59 conv2d_executeOp (QnnOpPackage_GlobalInfrastructure_t globalInfra,
60   Udo_Operation_t operation, bool blocking, const uint32_t ID,
61   Udo_ExternalNotify_t notifyFunc) {
62   if(operation == NULL) {
63      return UDO_INVALID_ARGUMENT;
64   }
65   OpParams_t* m_Operation = (OpParams_t*) operation;
66   const char* opType = ((conv2dOpFactory_t*)(m_Operation->opFactory))->opType;
67   if(opType == NULL) {
68      return UDO_INVALID_ARGUMENT;
69   }
70   if(strcmp(opType, g_conv2dOpType) == 0) {
71      /*
72       * add code here
73       */
74      return UDO_NO_ERROR;
75   } else {
76      return UDO_INVALID_ARGUMENT;
77   }
78}

上面显示的函数由 QNN DSP 后端用于 createOpFactory、releaseOpFactory、validateOperation、executeOp。这些函数始终分别命名为<op_name>_createOpFactory、<op_name>_releaseOpFactory、 <op_name>_validateOperation和<op_name>_executeOp。用户应注意,操作名称必须始终是有效的 C++ 标识符,这意味着它只能包含字母数字字符和下划线。 每个功能都在 DSP 后端使用,并且应由用户完成以实现准确的功能。每个要完成的函数都在函数体中包含了add code here注释。

 1typedef struct OpParams {
 2   Udo_OpFactory_t opFactory;
 3   uint32_t numInputParams;
 4   Udo_TensorParam_t *InputParams;
 5   uint32_t numOutputParams;
 6   Udo_TensorParam_t *outputParams;
 7   Udo_HexNNv2OpInfra_t opInfra;
 8} OpParams_t;
 9
10typedef struct conv2dOpFactory {
11   Udo_String_t opType;
12   uint32_t numOfStaticParams;
13   Udo_Param_t* staticParams;
14} conv2dOpFactory_t;
conv2dOpFactory_t 和 OpParams_t 在include/DspOp.hpp中定义。

CPU Conv2D.cpp 示例

 1Qnn_ErrorHandle_t validateOpConfig(Qnn_OpConfig_t opConfig) {
 2   QNN_CUSTOM_BE_ENSURE_EQ(
 3       strcmp(opConfig.typeName, "Conv2D"), 0, QNN_OP_PACKAGE_ERROR_INVALID_ARGUMENT)
 4
 5   QNN_CUSTOM_BE_ENSURE_EQ(opConfig.numOfInputs, 3, QNN_OP_PACKAGE_ERROR_VALIDATION_FAILURE)
 6   QNN_CUSTOM_BE_ENSURE_EQ(opConfig.numOfOutputs, 1, QNN_OP_PACKAGE_ERROR_VALIDATION_FAILURE)
 7
 8   return QNN_SUCCESS;
 9}
10
11Qnn_ErrorHandle_t execute(CustomOp* operation) {
12   /**
13    * Add code here
14    **/
15
16  return QNN_SUCCESS;
17}
18
19CustomOpRegistration_t* register_Conv2DCustomOp() {
20   using namespace conv2d;
21   static CustomOpRegistration_t Conv2DRegister = {execute, finalize, free, validateOpConfig, populateFromNode};
22   return &Conv2DRegister;
23}
24
25REGISTER_OP(Conv2D, register_Conv2DCustomOp);

上面显示的注册结构与自定义 op 包对象相关联,该对象由上一节中显示的接口函数间接调用。注册结构在下面定义,也可以位于<QNN_SDK_ROOT>/share/QNN/OpPackageGenerator/CustomOp/CustomOpRegister .hpp中。

 1typedef struct _CustomOpRegistration_t {
 2   Qnn_ErrorHandle_t (*execute)(utils::CustomOp* operation);
 3   Qnn_ErrorHandle_t (*finalize)(const utils::CustomOp* operation);
 4   Qnn_ErrorHandle_t (*free)(utils::CustomOp& op);
 5
 6   QnnOpPackage_ValidateOpConfigFn_t validateOpConfig;
 7
 8   Qnn_ErrorHandle_t (*initialize)(const QnnOpPackage_Node_t opNode,
 9                                   QnnOpPackage_GraphInfrastructure_t graphInfrastructure,
10                                   utils::CustomOp* operation);
11} CustomOpRegistration_t;

自动生成的骨架代码包含要注册的函数的自由函数定义。请注意,用户可以通过完成函数体来自定义这些函数的行为。一旦函数被完全定义,每个注册函数都需要使用REGISTER_OP宏与一个 op 包实例相关联。op 包实例是一个单例,它保存所有注册结构并根据 QNN API(通过接口)基于 op 类型调用适当的函数。

例如,上一节中显示的createKernels函数指针会触发对op包注册中定义的initialize函数的调用。请注意,每个函数的调用方式可以在接口文件或共享源代码中轻松观察到。我们鼓励感兴趣的用户探索 API 以获取更详细的信息。但是,用户应该意识到修改源代码可能会对成功的包加载和/或执行产生不利影响。

最后,用户应注意上面显示的CustomOp对象。这是一个简单的类,可以在初始化、执行和终结阶段之间存储和检索输入、输出和参数数据。这是一个帮助实用程序,用户可以自由修改以满足自己的需要。生成包后,实用程序始终包含在包中,也可以在<QNN_SDK_ROOT>/share/QNN/OpPackageGenerator/CustomOp/utils中找到。

编译说明

以下部分描述了每个支持的后端的编译。

HTP 说明

  1. QNN HTP 的路径包含标头和hexagon 安装,使用以下命令设置:
$ source <QNN_SDK_ROOT>/bin/x86_64-linux-clang/envsetup.sh
$ source <HEXAGON_SDK_PATH>/setup_sdk_env.source

(可选)用户可以导出其他环境HEXAGON_TOOLS_VERSION,以覆盖 Makefile 中的默认hexagon 工具版本。

默认 HEXAGON_SDK_ROOT 版本:
x86:QNN_HEXAGON_SDK_5.0.0 v68:QNN_HEXAGON_SDK_4.2.0 v69:QNN_HEXAGON_SDK_4.3.0 v73:QNN_HEXAGON_SDK_5.0.0 v75:QNN_HEXAGON_SDK_5.4.0

如果打算制作不同的变体,则需要再次导出 HEXAGON_SDK_ROOT。

默认 HEXAGON_TOOLS_VERSION:
x86:8.6.02 v68:8.4.09 v69:8.5.03 v73:8.6.02 v75:8.7.03

  1. x86 所需:确保在您的路径中可以发现 clang 编译器,或者在您的环境中设置 X86_CXX 以指向有效的 clang 编译器路径。5

  2. ARM 准备所需ARM 和 hexagon 版本的 op 包都应编译并注册以用于 ARM 准备

  3. 然后可以使用以下任意命令为各种目标编译该包:

  • 要生成 hexagon 和 linux 目标:
    make all
    
    

注意:“make all”包括 htp_v68 作为默认的hexagon 目标。对于 v69 或更高版本,用户可以替换 htp_v68,或使用以下单独的命令。

  • 仅生成hexagon 目标:

    make htp_v68
    make htp_v69
    make htp_v73
    make htp_v75
    
  • 仅生成 Linux 目标:

    make htp_x86
    
    
  • 仅生成 ARM 目标:

    make htp_aarch64
    
    

在步骤 4 中选择任何 make 目标后,将在以下位置生成共享库:<current_dir>/build//lib<package_name>.so。

DSP指令

  1. QNN DSP 的路径包括标头和hexagon 安装,使用以下命令设置:

    $ source <QNN_SDK_ROOT>/bin/envsetup.sh
    $ source <HEXAGON_SDK_ROOT>/setup_sdk_env.source
    
  2. 然后可以使用以下命令为 DSP 目标编译该包:…代码块:

    $ make
    
    

步骤 2 之后,将在以下位置生成共享库:<current_dir>/build/DSP/libQnn<package_name>.so。

CPU指令

  1. 设置QNN环境:

    $ source <QNN_SDK_ROOT>/bin/envsetup.sh
    
    
  2. x86 所需:确保在您的路径中可以发现 clang 编译器,或者在您的环境中设置 CXX 以指向有效的 clang 编译器路径。5

  3. android 所需:确保 android ndk-build 编译器在您的路径中可发现,并将 ANDROID_NDK_ROOT 设置为指向可执行文件的位置。

  4. 然后可以使用以下任意命令为各种目标编译该包:

    • 要生成 android 和 x86 目标:

      make all
      
      
    • 仅生成 x86 目标:

      make cpu_x86
      
      
    • 仅生成 android 目标:

      make cpu_android
      
      

在步骤 3 中选择任何 make 目标后,将在以下位置生成共享库:<current_dir>/libs//lib<package_name>Cpu.so。

笔记
步骤 1-2 也可以在 makefile 中手动设置,或者作为命令行选项进行设置,而无需使用脚本。
步骤 3 也可以手动设置或作为选项传递。

1
有关 QNN 工具设置的说明,请参阅设置。

2
如果该目录已存在,该工具将仅生成新文件并尝试附加到现有文件。要强制生成新包,请使用–force- Generation选项。

3
该工具目前仅支持 HTP 和 CPU 后端的生成。

4
可能会为可能需要完成的每个操作生成附加函数,以及可能需要专门化的宏。用户应该观察生成的包以查看所有生成的函数和宏。

5 ( 1 , 2 )
如果不存在,脚本 <QNN_SDK_ROOT>/bin/check-linux-dependency.sh 也可用于下载适当的 clang 版本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值