4、高通AI Engine Direct (QNN) 入门:QNN SDK的架构概览,理解QNN Context、Graph、Tensor的概念,编写第一个QNN推理程序。
好,咱们进入正题。前面几章我们把高通平台的开发环境、编译工具链都捋了一遍。从这章开始,终于要碰最核心的东西——QNN (Qualcomm Neural Networks) SDK。
说实话,我第一次接触QNN的时候,也被它那一堆概念搞得有点晕。什么Context、Graph、Tensor,听着像深度学习框架,但又不太一样。别急,咱们一个一个拆开看。
4.1 QNN SDK 整体架构
先看一张图,把QNN的骨架搭起来。
这张图我建议你多看两眼。QNN的架构其实很清晰:上层API → 核心抽象 → 后端调度 → 硬件执行。你写代码的时候,主要跟中间两层打交道。
关键点:QNN 不是直接操作硬件的。它通过一套统一的 API,把不同硬件(HTP、GPU、DSP)的差异屏蔽掉了。你写的推理代码,在骁龙 8 Gen 2 和 8 Gen 3 上都能跑,只是后端自动选择最优硬件。
4.2 三个核心概念:Context、Graph、Tensor
这三个东西,是QNN编程的基石。我当年刚上手时,花了整整两天才把它们的生命周期理清楚。咱们一个一个说。
4.2.1 QNN Context(上下文)
Context 是什么? 说白了,它是整个推理任务的容器。你加载模型、创建计算图、分配张量,都得在某个 Context 里做。
我个人习惯把 Context 想象成一个沙箱。每个 Context 有自己的资源、自己的后端配置、自己的生命周期。你可以在一个进程里创建多个 Context,它们互不干扰。
| 属性 | 说明 |
|---|---|
| 生命周期 | 从 QnnContext_create() 到 QnnContext_free() |
| 后端绑定 | 每个 Context 可以指定使用哪个后端(如 HTP、GPU) |
| 资源隔离 | 不同 Context 的 Graph 和 Tensor 互不共享 |
| 典型用途 | 一个 Context 跑一个模型,或者多个模型共享一个 Context |
我的经验:如果你同时跑多个模型,建议每个模型单独创建 Context。这样某个模型崩溃时,不会把其他模型也带崩。我曾经在一个项目里把所有模型塞进一个 Context,结果一个模型内存泄漏,整个进程挂了……从那以后,我再也不偷懒了。
4.2.2 QNN Graph(计算图)
Graph 就是模型的计算流程。它由一系列节点(算子)和边(张量流动)组成。你从 ONNX 或 TFLite 导出的模型,加载到 QNN 后,就会变成一个 Graph。
Graph 有几个关键操作:
- 创建:从模型文件或内存中加载,生成计算图
- 配置:设置输入输出张量的形状、数据类型
- 执行:传入输入数据,触发前向推理
- 销毁:释放图占用的资源
嗯,这里要注意:Graph 是只读的。你创建好之后,不能动态修改它的结构。想改?重新加载模型吧。
4.2.3 QNN Tensor(张量)
Tensor 就是数据容器。在 QNN 里,Tensor 分为三种:
| 类型 | 作用 | 典型数据 |
|---|---|---|
| Input Tensor | 存放输入数据 | 图像、音频、文本向量 |
| Output Tensor | 存放推理结果 | 分类概率、检测框、特征向量 |
| Intermediate Tensor | 中间计算结果(内部使用) | 卷积特征图、激活值 |
Tensor 的属性包括:名称、维度、数据类型、内存地址。你写代码时,主要操作 Input 和 Output Tensor。
避坑指南:我曾经在设置 Tensor 维度时,把 CHW 和 HWC 搞反了。模型跑出来的结果全是乱码。排查了整整一个下午才发现是维度顺序的问题。记住:QNN 默认使用 NHWC(Batch, Height, Width, Channels),和 TensorFlow 一致。如果你从 PyTorch(NCHW)转模型,一定要做维度转换。
4.3 编写第一个 QNN 推理程序
理论说完了,咱们动手写代码。下面是一个完整的 QNN 推理流程,我把它拆成 5 步。
4.3.1 初始化 QNN 后端
#include "QnnInterface.h"
#include "QnnTypes.h"
// 1. 加载后端库
QnnInterface_t* qnnInterface = nullptr;
Qnn_ErrorHandle_t err = QnnInterface_loadFromPath(
"/usr/lib/libQnnHtp.so", // HTP 后端库路径
&qnnInterface
);
if (err != QNN_SUCCESS) {
// 处理错误
}
4.3.2 创建 Context
// 2. 创建 Context
Qnn_ContextHandle_t context = nullptr;
QnnContext_createConfig_t config = QNN_CONTEXT_CREATE_CONFIG_INIT;
config.profileEvents = false; // 关闭性能分析
err = qnnInterface->contextCreate(
&context,
&config
);
if (err != QNN_SUCCESS) {
// 处理错误
}
4.3.3 加载模型并创建 Graph
// 3. 从文件加载模型,创建 Graph
QnnGraph_Handle_t graph = nullptr;
QnnGraph_createConfig_t graphConfig = QNN_GRAPH_CREATE_CONFIG_INIT;
err = qnnInterface->graphCreate(
context,
"my_first_graph",
&graphConfig,
&graph
);
// 加载序列化模型(假设是 QNN 的 .bin 格式)
err = qnnInterface->graphLoadFromBinary(
graph,
modelBuffer, // 模型数据指针
modelSize // 模型数据大小
);
4.3.4 准备输入输出 Tensor
// 4. 获取输入输出 Tensor 信息
Qnn_Tensor_t inputTensor, outputTensor;
uint32_t inputCount = 0, outputCount = 0;
qnnInterface->graphGetInputTensors(graph, &inputTensor, &inputCount);
qnnInterface->graphGetOutputTensors(graph, &outputTensor, &outputCount);
// 假设输入是 224x224 的 RGB 图像
inputTensor.type = QNN_TENSOR_TYPE_INPUT;
inputTensor.rank = 4;
inputTensor.dimensions[0] = 1; // batch
inputTensor.dimensions[1] = 224; // height
inputTensor.dimensions[2] = 224; // width
inputTensor.dimensions[3] = 3; // channels
inputTensor.dataType = QNN_DATATYPE_FLOAT_32;
// 分配内存
float* inputData = (float*)malloc(1 * 224 * 224 * 3 * sizeof(float));
inputTensor.clientBuf.data = inputData;
inputTensor.clientBuf.dataSize = 1 * 224 * 224 * 3 * sizeof(float);
4.3.5 执行推理并获取结果
// 5. 执行推理
err = qnnInterface->graphExecute(
graph,
&inputTensor,
1, // 输入数量
&outputTensor,
1 // 输出数量
);
if (err == QNN_SUCCESS) {
// 读取输出结果
float* outputData = (float*)outputTensor.clientBuf.data;
// 处理 outputData...
}
// 6. 清理资源
free(inputData);
qnnInterface->graphDestroy(graph);
qnnInterface->contextFree(context);
qnnInterface->interfaceFree();
完整流程总结:加载后端 → 创建 Context → 创建 Graph → 准备 Tensor → 执行推理 → 清理资源。这个流程在所有 QNN 项目里都是通用的,只是模型加载方式和 Tensor 配置会变。
4.4 常见问题与调试技巧
写第一个程序时,你大概率会遇到下面这些问题。我帮你提前踩过坑了。
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 加载后端失败 | 库路径不对,或缺少依赖 | 检查 LD_LIBRARY_PATH,确认 libQnnHtp.so 存在 |
| Graph 创建失败 | 模型文件损坏或不兼容 | 用 qnn-model-tool 验证模型文件 |
| 推理结果全零 | 输入数据未正确填充,或 Tensor 维度错误 | 打印输入数据前几个值,确认数据有效 |
| 内存泄漏 | 未释放 Tensor 或 Context | 使用 valgrind 检查,确保每个 create 对应一个 free |
调试小技巧:QNN 提供了 QNN_LOG_LEVEL 环境变量。设置 export QNN_LOG_LEVEL=3 可以打印详细的调试日志,包括每个算子的执行时间、内存分配情况。我第一次调通推理程序,就是靠这个日志定位到 Tensor 维度设置错误的。
好了,这一章的内容就到这里。你理解了 Context、Graph、Tensor 这三个核心概念,也亲手写了一个完整的推理程序。接下来,你可以试着换一个模型(比如 MobileNet 或 YOLO),按照同样的流程跑一遍。相信我,多跑几个模型,这些概念就刻在脑子里了。