乐于分享
好东西不私藏

使用 QAI AppBuilder API 在高通平台上部署 AI 模型

使用 QAI AppBuilder API 在高通平台上部署 AI 模型

1. 写在前面

在之前 QAI AppBuilder 的系列文章中,我们已经学习了:

在本篇及后续的文章中,我将进一步介绍如何使用 QAI AppBuilder API 适配新模型以及解决方案。


2. QAI AppBuilder API 介绍

QAI AppBuilder 提供 python 和 C++ 两种 API 接口,开发者可以根据自己需要使用的编程语言来选择。其中,python 接口是通过 pybind 来调用 C++ 接口,两者在性能上基本上是等价的。

2.1 Python API 介绍

2.1.1 核心类概览

  • QNNConfig:全局配置管理

  • QNNContext:标准模型上下文(核心类)

  • GenieContext:大语言模型专用上下文

  • LogLevel:日志级别控制

  • PerfProfile:性能模式管理


2.1.2 QNNConfig

QNNConfig 用于配置 QNN 运行环境,必须在使用其他 API 之前调用。

API 签名:

class QNNConfig:    @staticmethod    def Config(        qnn_lib_path: str = "None",                    # QAIRT运行 库路径        runtime: str = Runtime.HTP,                    # 运行时        log_level: int = LogLevel.ERROR,               # 日志级别        profiling_level: int = ProfilingLevel.OFF,     # 性能分析级别        log_path: str = "None"                         # 日志文件路径    ) -> None

2.1.3 QNNContext

QNNContext 是最常用的类,用于加载模型、执行推理和管理模型生命周期。

构造函数:

class QNNContext:    def __init__(        self,        model_name: str = "None",                      # 模型名称(唯一标识)        model_path: str = "None",                      # 模型文件路径        backend_lib_path: str = "None",                # 后端库路径(可选)        system_lib_path: str = "None",                 # 系统库路径(可选)        is_async: bool = False,                        # 是否异步执行        input_data_type: str = DataType.FLOAT,         # 输入数据类型        output_data_type: str = DataType.FLOAT         # 输出数据类型    ) -> None

核心方法:

  • Inference – 执行推理

  • 模型信息查询方法:

    • getInputShapes – 获取输入形状

    • getOutputShapes – 获取输出形状

    • getInputDataType – 获取输入数据类型

    • getOutputDataType – 获取输出数据类型

    • getGraphName – 获取图名称

    • getInputName – 获取输入张量名称

    • getOutputName – 获取输出张量名称


2.1.4 使用流程示例(不完整):

from qai_appbuilder import (    QNNContext, QNNConfig, Runtime, LogLevel,     ProfilingLevel, PerfProfile)# 配置 QNN 运行环境QNNConfig.Config(    qnn_lib_path=str(qnn_dir),  # 此参数从 v2.0.0 开始可以不进行设置,留空即可。runtime=Runtime.HTP,    log_level=LogLevel.WARN,    profiling_level=ProfilingLevel.BASIC)# 定义模型类class YourModel(QNNContext):    """Your Model"""    def Inference(self, input_data):        input_datas = [input_data]        output_data = super().Inference(input_datas)[0]        return output_data# 初始化模型your_model = YourModel()# 预处理# 执行推理output_data = your_model.Inference(input_data)# 后处理# 清理资源del your_model

2.2 C++ API 介绍

LibAppBuilder 是 C++ API 的核心类,定义(部分)如下:

classLIBAPPBUILDER_APILibAppBuilder{public:    bool ModelInitialize(const std::string& model_name, const std::string& model_path,                               const std::string& backend_lib_path, const std::string& system_lib_path,                               bool async = false, const std::string& input_data_type="float", const std::string& output_data_type="float", uint32_t deviceID=0, std::string coreIdsStr="");    bool ModelInference(std::string model_name, std::vector<uint8_t*>& inputBuffers,                         std::vector<uint8_t*>& outputBuffers, std::vector<size_t>& outputSize,                        std::string& perfProfile, size_t graphIndex = 0, size_t share_memory_size = 0);    bool ModelApplyBinaryUpdate(const std::string model_name, std::vector<LoraAdapter>& lora_adapters);    bool ModelDestroy(std::string model_name);    bool CreateShareMemory(std::string share_memory_name, size_t share_memory_size);    bool DeleteShareMemory(std::string share_memory_name);    std::vector<std::vector<size_t>> getInputShapes(std::string model_name);    std::vector<std::string> getInputDataType(std::string model_name);    std::vector<std::string> getOutputDataType(std::string model_name);    std::vector<std::vector<size_t>> getOutputShapes(std::string model_name);    std::string getGraphName(std::string model_name);    std::vector<std::string> getInputName(std::string model_name);    std::vector<std::string> getOutputName(std::string model_name);    std::vector<std::vector<size_t>> getInputShapes(std::string model_name, std::string proc_name);    std::vector<std::string> getInputDataType(std::string model_name, std::string proc_name);    std::vector<std::string> getOutputDataType(std::string model_name, std::string proc_name);    std::vector<std::vector<size_t>> getOutputShapes(std::string model_name, std::string proc_name);    std::string getGraphName(std::string model_name, std::string proc_name);    std::vector<std::string> getInputName(std::string model_name, std::string proc_name);    std::vector<std::string> getOutputName(std::string model_name, std::string proc_name);                                                          ModelInfo_t getModelInfo(std::string model_name, std::string input);    ModelInfo_t getModelInfo(std::string model_name, std::string proc_name, std::string input);    ModelInfo_t getModelInfoExt(std::string model_name, std::string input);  };

2.2.1 主要方法:

  • ModelInitialize – 初始化模型:

    具体实现在 ModelInitializeEx 函数:

    https://github.com/Francis235/ai-engine-direct-helper/blob/main/src/LibAppBuilder.cpp#L359-L512

  • ModelInference – 执行推理:

    具体实现在 ModelInferenceEx 函数:

    https://github.com/Francis235/ai-engine-direct-helper/blob/main/src/LibAppBuilder.cpp#L512-L548)

  • ModelDestroy – 销毁模型:

    具体实现在 ModelDestroyEx 函数:

    https://github.com/Francis235/ai-engine-direct-helper/blob/main/src/LibAppBuilder.cpp#L551-L604


2.2.2 使用流程示例(不完整):

#include "LibAppBuilder.hpp"const std::string MODEL_NAME = "your model name"// 初始化日志和性能分析SetLogLevel(QNN_LOG_LEVEL_WARN);SetProfilingLevel(QNN_PROFILING_LEVEL_BASIC);// 创建 LibAppBuilder 实例并初始化模型LibAppBuilder libAppBuilder;int ret = libAppBuilder.ModelInitialize(    MODEL_NAME,    model_path.string(),    backend_lib_path.string(),    system_lib_path.string());// 预处理和推理前的准备操作// 如分配输入缓冲区,填充inputBuffers等// 执行推理ret = libAppBuilder.ModelInference(    MODEL_NAME,    inputBuffers,    outputBuffers,    outputSize,    perfProfile);// 后处理和推理后的清理工作// 如释放输出缓冲区等// 销毁模型libAppBuilder.ModelDestroy(MODEL_NAME);

2.3 C++ API 到 Python API 的包装机制

在深度学习应用中,开发者通常使用相对简洁、易用的 Python 语言进行模型开发与推理;而在 Qualcomm HTP 等底层硬件上,高性能计算往往依赖复杂的 C/C++ 实现,并涉及大量指针操作。这种语言与抽象层级上的差异,为应用开发带来了较高的门槛。

为此,QAI AppBuilder 在 Python 环境中提供了高性能的 AI 模型推理能力,使开发者能够直接使用熟悉的 Python 接口和 NumPy 数组完成模型调用,而无需深入了解底层复杂的 QNN SDK 细节。同时,该能力在语义和性能上与底层 C++ 实现保持等价或近似一致。具体而言,QAI AppBuilder 的 C++ API 通过 pybind11 进行封装,对外暴露统一的 Python API。

在推理流程上,系统采用了三层委托架构: 首先,用户调用 Python 层的高阶接口,该接口负责自动完成输入张量的 reshape 等预处理操作;随后,Python 接口调用由 pybind11 封装的 C++ 对象,该对象进一步将请求委托给全局的 inference 函数;在这一过程中,pybind11 层能够将 NumPy 数组高效映射为 C++ 的 uint8_t* 指针,从而避免不必要的数据拷贝。最终,推理请求进入底层 C++ 核心推理引擎,并在 Qualcomm HTP/DSP 硬件上完成实际的模型计算。

在性能优化方面,输入数据在 float 模式下可能会触发一次必要的数据拷贝,而输出结果则通过 py::capsule 机制实现零拷贝返回。由于 Python API 与 C++ API 共用同一套核心推理代码路径,Python 层引入的额外开销主要集中在数据格式转换上。对于典型的深度学习推理场景而言,这部分开销可以忽略不计,从而在易用性与高性能之间实现了良好的平衡。

整个包装体系分为三层,层层委托:

  • Layer 1 – 核心 C++ 库(src/LibAppBuilder):这是最底层的 C++ 实现。LibAppBuilder 类声明了原生接口,直接操作 uint8_t* 裸指针缓冲区。真正的模型初始化、推理、销毁逻辑在 src/LibAppBuilder.cpp 中的 ModelInitializeEx、ModelInferenceEx、ModelDestroyEx 等内部函数实现。

  • Layer 2 – pybind11 绑定层:

    • 绑定层在 pybind/AppBuilder.h 中引入 pybind11 相关头文件,并声明一个全局 LibAppBuilder 实例 g_LibAppBuilder

    • 关键数据转换:py::array → uint8_t*

    • PYBIND11_MODULE 注册

  • Layer 3 – 高层封装:qnncontext.py 中的 Python 类(QNNContext, QNNContextProc, QNNLoraContext)是用户直接使用的 API,它们持有 pybind11 暴露的 appbuilder.QNNContext 对象。

关于这部分的详细解释可以参考 deepwiki 的介绍:https://deepwiki.com/search/qai-appbuilder-c-apipybindpyth_b4da9288-c302-4268-b0bf-8aae73e2ee42


3. yolo26n 部署

我们这里使用最新的 yolo 模型 – yolo26 来作为 demo,使用QAI AppBuilder Python API 在9075上部署。yolo26 简化了设计,是一个原生端到端模型 ,直接产生预测结果,而无需非最大抑制(NMS),专为边缘和低功耗设备设计。

从ultralytics yolo26的官方文档,我们还了解到,yolo26 采用双头架构,为不同部署场景提供了灵活性:

  • 一对一头(默认): 无需 NMS 即可生成端到端预测,输出 (N, 300, 6), 每张图像最多 300 次检测。该头为快速推理和简化部署进行了优化。

  • 一对多头 :生成需要 NMS 后处理的传统 YOLO 输出,输出 (N, nc + 4, 8400), 其中 nc 是类别数。该头通常能实现略高的精度,但代价是需要额外的后处理。可以在推理时通过指定end2end=False来启用该头。

官方测试结果:

模型来自:

https://github.com/ultralytics/assets/releases/download/v8.4.0/yolo26n.pt

我们首先导出 .onnx 模型,然后使用 QAIRT(QNN) SDK 中的工具转换成 .bin 模型:

$QNN_SDK_ROOT/bin/x86_64-linux-clang/qnn-onnx-converter -i ./sim_yolo26n.onnx --preserve_io layout --preserve_io datatype --output_path ./sim_yolo26n.cpp$QNN_SDK_ROOT/bin/x86_64-linux-clang/qnn-model-lib-generator -c ./sim_yolo26n.cpp -b ./sim_yolo26n.bin -t x86_64-linux-clang -o ./$QNN_SDK_ROOT/bin/x86_64-linux-clang/qnn-context-binary-generator --model ./x86_64-linux-clang/libsim_yolo26n.so --soc_model 77 --backend ${QNN_SDK_ROOT}/lib/x86_64-linux-clang/libQnnHtp.so --binary_file cntx_sim_yolo26n_9100_fp16

在转换模型这里需要注意模型的输入格式,这里我们使用 –preserve_io layout 参数来保证转换后模型的输入 layout 与原始模型相同,这里都是 NCHW,我们可以通过模型转换过程中产生的 *.json 文件查看模型结构。我在刚开始适配的时候,由于输入数据的结构与模型输入 layout 不匹配,导致最后画出来的目标检测框一直不对,花了很多时间在思考后处理部分,最后才意识到可能是输入的数据结构不对,调整了输入数据结构后,才得出正确的检测结果。

预处理部分相关代码如下:

def preprocess_PIL_image(image: Image) -> torch.Tensor:    """Convert a PIL image into a pyTorch tensor with range [0, 1] and shape NCHW."""    transform = transforms.Compose([transforms.PILToTensor()])  # bgr image    img: torch.Tensor = transform(image)  # type: ignore    img = img.float().unsqueeze(0) / 255.0  # int 0 - 255 to float 0.0 - 1.0    return img# ...# Read and preprocess the image.image = Image.open(input_image_path)image = image.resize((IMAGE_SIZE, IMAGE_SIZE))outputImg = Image.open(input_image_path)image = preprocess_PIL_image(image) # transfer raw image to torch tensor format# image  = image.permute(0, 2, 3, 1)image = image.numpy()

如果是从参考 yolov8 代码而来,image 这里是否需要 permute,要怎么 permute 都要根据自己模型的实际 layout 而来。

后处理部分,可以参考 yolov8 的部分代码,这里有几点点需要注意:

  • 输出数据的 shape 为 [bs, 300, 6],每个检测目标有 6 个浮点数据表示,布局与 yolov8 类似,都是 xyxy,socre,class_idx

  • 输出 score 已经按照降序排列,所以不需要再排序

  • 如果在导出模型时设置了 end2end=True(default),则不需要进行 nms;否则仍需进行 nms。

# Run the inference.model_output = yolo26.Inference(image)pred_boxes = torch.tensor(model_output[0][:, :, :4])pred_scores = torch.tensor(model_output[0][:, :, 4])pred_class_idx = torch.tensor(model_output[0][:, :, 5])

至此,使用 QAI AppBuilder API 在高通 9075 上适配 yolo26n 就基本完成了,如果需要使用 yolo26 其他 size 的模型,只需要配置相应的模型即可。后续我会把 yolo26 适配的代码 pr 到官方 github 仓库,有问题欢迎评论区交流。


4. 总结

本文系统地介绍了 QAI AppBuilder API 的整体设计与使用方法。首先分别对 Python API 与 C++ API 的接口结构和使用方式进行了介绍,帮助读者建立对 AppBuilder API 的整体认知;随后从 C++ API 到 Python API 的封装机制入手,分析了 Python API 在保持高性能的同时兼顾易用性的设计思路。最后,以 yolo26n 模型为具体示例,详细说明了基于 Python API 的模型适配流程,并总结了实际适配过程中常见的问题及注意事项,为后续在真实项目中使用 QAI AppBuilder API 提供了参考和经验借鉴。

5. 参考

https://github.com/quic/ai-engine-direct-helper/tree/main

https://github.com/quic/ai-engine-direct-helper/blob/main/docs/guide_zh.md

https://docs.ultralytics.com/models/yolo26/#overview

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 使用 QAI AppBuilder API 在高通平台上部署 AI 模型

评论 抢沙发

1 + 2 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮