乐于分享
好东西不私藏

使用 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 = falseconst 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::stringgetInputDataType(std::string model_name);    std::vector<std::stringgetOutputDataType(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::stringgetInputName(std::string model_name);    std::vector<std::stringgetOutputName(std::string model_name);    std::vector<std::vector<size_t>> getInputShapes(std::string model_name, std::string proc_name);    std::vector<std::stringgetInputDataType(std::string model_name, std::string proc_name);    std::vector<std::stringgetOutputDataType(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::stringgetInputName(std::string model_name, std::string proc_name);    std::vector<std::stringgetOutputName(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 中的 ModelInitializeExModelInferenceExModelDestroyEx 等内部函数实现。

  • Layer 2 – pybind11 绑定层

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

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

    • PYBIND11_MODULE 注册

  • Layer 3 – 高层封装qnncontext.py 中的 Python 类(QNNContextQNNContextProcQNNLoraContext)是用户直接使用的 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 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮