ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

高通QNN SDK实战:从模型转换到C++推理全流程指南

高通QNN SDK实战:从模型转换到C++推理全流程指南 在骁龙平台做AI推理最忌讳的是把模型拿来直接在CPU上跑一遍就宣布完事硬件加速能力一点都没用上。Qualcomm QNN SDK本来就提供了一整套从模型转换、量化到C/C运行时调用的工具链大多数人卡住的地方其实就两个模型不知道怎么转成QNN格式C代码不知道从哪个接口下手。这篇文章把这条路完整走一遍从环境准备到代码解析最后再到排坑你可以把它当成一份能照着跑的作业。1. 项目全景与方案设计先从QNN的整体架构说起动手写代码之前先把QNN这套东西的地图画出来。很多人被QNN劝退不是因为代码难写而是因为概念不熟。一头扎进去看到一堆Handle、Descriptor、Profile直接就懵了。其实QNN的架构分得非常清晰理解了那几条主线后面所有代码都只是在跟这几条主线打交道。1.1 QNN SDK到底是什么为什么选它QNN SDK是高通提供的神经网络推理SDK跑在骁龙平台上时它能把模型调度到Hexagon DSP或者HTPHexagon Tensor Processor上执行。HTP是高通专门为AI推理设计的硬件加速单元比CPU省电吞吐能力高出好几个量级。常见的分类、检测、分割模型经过合理量化之后在HTP上都能跑得飞快。从架构上看QNN分两层一层是Host侧也就是你的C应用跑在CPU上的部分另一层是Device侧也就是跑在DSP/HTP上的部分。Host侧通过QNN提供的Backend接口跟Device侧打交道。像libQnnHtp.so就是HTP的Backend实现库你的程序通过dlopen或者运行时链接的方式加载它然后调用统一的QNN接口剩下的调度工作全部交给SDK完成。很多人会问既然有TFLite、ONNX Runtime这些跨平台框架为什么还要折腾QNN原因很简单模型要跑得够快够省电就必须直接调用硬件能力。跨平台框架为了兼容性往往走的是通用优化路径对特定硬件的利用程度有限。QNN能直接操作DSP/HTP上的张量缓冲区和执行流水线量化模型跑起来经常比CPU快一个数量级。如果你做的是端侧摄像头、语音助手、手势识别这类对延迟和功耗极度敏感的场景QNN几乎绕不开。1.2 完整链路拆解从模型转换到C推理一个模型要跑在QNN上链路其实可以拆成五段准备模型文件PyTorch导出ONNX或者直接拿TensorFlow/TFLite模型。使用qnn-onnx-converter把ONNX转成QNN的图描述文件.serialized。使用qnn-context-binary-generator把图描述文件和HTP Backend绑定生成一个Context Binary。编写C程序加载Backend库和Context Binary。创建输入输出张量执行推理读取结果。很多人会直接跳到第4步结果发现怎么都跑不通原因就是没有理解Context Binary的作用。Context Binary可以理解为一份提前构建好的执行计划它把模型的算子调度、内存分配、常量数据全部打包在一起。运行时只需要把这个二进制文件加载进去SDK就能直接在HTP上创建对应的执行上下文不用再一条一条去解析算子、做图优化。这一点非常重要。如果你在运行时才去构建图每次启动都要重新做一遍算子的挑选和内存规划冷启动时间可能多出几百毫秒甚至几秒。而Context Binary方案把重活全部放在离线阶段完成运行时就是纯加载速度极快。这也是为什么高通官方在端侧部署时推荐的生产路径就是Context Binary。1.3 方案取舍用Graph API还是直接用Context BinaryQNN其实提供了两条运行路径一条是直接用Graph API在运行时装图另一条就是上面说的加载Context Binary。从我实际用下来的感受来看除非你在做的是需要动态修改网络结构的实验场景否则生产环境一定要走Context Binary。Graph API在运行时构建图的好处是比较灵活可以在代码里动态指定张量维度、插入算子适合原型验证。但坏处也很明显每次初始化都要完成完整的图构建流程而且需要在目标设备上具备完整的算子库和转换工具链。这会让程序的启动时间变长还容易因为运行环境和转换环境不一致导致各种奇奇怪怪的算子兼容问题。Context Binary则是把模型在开发机上转换完毕把校验也做掉之后再把二进制文件放进设备里。运行时的代码路径极大简化就变成几行固定的调用标准流程。就算产品发布了多个模型版本只要替换二进制文件就行C代码基本不用动。我建议做产品落地的朋友直接采用这条路径下面所有的代码也都是按这个方案来写的。2. 环境准备与模型转换这一步值80%的调试时间说实话QNN项目里真正耗时间的往往不是写代码而是环境搭建和模型转换。很多人编译报错、运行崩溃最后发现都是SDK版本不匹配、交叉编译工具链不对、模型转换时埋了雷。环境准备这一章认真看能帮你省下大量排坑时间。2.1 SDK版本与交叉编译工具链准备我用的是高通发布的QNN SDK 2.x版本不同版本接口细节略有差异但整体思路是一致的。拿到SDK压缩包之后解压到某个目录然后设置环境变量export QNN_SDK_ROOT/path/to/qnn-sdk export LD_LIBRARY_PATH$QNN_SDK_ROOT/lib/aarch64-unknown-linux-gnu:$LD_LIBRARY_PATH export PATH$QNN_SDK_ROOT/bin/aarch64-unknown-linux-gnu:$PATH注意SDK里的lib目录下通常会区分x86_64-linux-clang和aarch64-unknown-linux-gnu等多个平台目录。你这台开发机用的转换工具比如qnn-onnx-converter一般是x86的Python脚本但运行时加载的libQnnHtp.so必须选目标设备对应的架构版本。如果程序跑在ARM64的Linux板子上就一定要把aarch64-unknown-linux-gnu目录下的库优先加进LD_LIBRARY_PATH而不是用x86的库去跑否则直接报无法加载动态库。交叉编译时还是要用aarch64的GCC工具链。我这边用的是aarch64-linux-gnu-g版本建议在10以上太老的编译器对C17支持不友好后面代码里的智能指针、lambda写起来会比较别扭。提前在板子上装好对应的依赖库比如libstdc、libc这种基础运行库避免把编译产物拷过去之后才发现缺符号。如果你跑的是Windows on Snapdragon平台那工具链又不太一样要用MSVC或者Clang配合高通提供的Windows库。今天这篇主要讲Linux环境但代码逻辑在Windows上一样能套用关键路径都是那些接口函数。2.2 从PyTorch导出ONNX的操作要点我们在QNN里最常见的第一步是把PyTorch模型导出成ONNX。导出命令很简单import torch model torch.load(model.pth, map_locationcpu) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, model.onnx, opset_version13, input_names[input_0], output_names[output_0], dynamic_axes{input_0: {0: batch}, output_0: {0: batch}} )这里有几个细节非常关键。第一opset_version尽量选13或更高QNN转换器对高版本opset的支持更完善低版本有些算子表达过于简陋转换器反而容易踩坑。第二dynamic_axes一定要设置成动态batch因为QNN转换器在解析ONNX时会对张量维度做静态推理如果你把batch固定为1后续想换不同batch就得重新转换非常麻烦。第三导出的模型里尽量不要包含自定义算子、数据依赖的循环结构这类ONNX算子QNN不一定认识一旦遇到就只能改成标准算子或者提前把逻辑拆到C里做预处理。我踩过最大的坑是模型里有一个torch.where的条件分支导出ONNX之后转换器总是报算子不支持。后来把网络结构里的动态条件逻辑挪到前处理步骤中用mask加法代替条件分支才顺利转换成功。所以导出模型之前最好先检查一遍网络里有没有比较冷门的高级操作。2.3 使用qnn-onnx-converter的完整转换命令环境变量配置好之后用转换器把ONNX转成QNN模型描述文件python $QNN_SDK_ROOT/bin/qnn-onnx-converter \ --input_network model.onnx \ --output_qnn_path ./qnn_model \ --input_list ./input_list.txt \ --quantize_full_type_uint16input_list.txt是用于校准的数据列表每一行写一个数据文件的路径。这些数据文件是原始输入通常是二进制格式或者npy格式。如果你要做全整型量化校准数据必须覆盖真实场景的分布比如输入是图片最好从验证集里随机抽200~500张尽量包含各种光照、角度、遮挡情况。校准数据如果太单调量化后的模型精度会有明显崩塌。--quantize_full_type_uint16表示权重激活都用uint16量化这是HTP上常用的一种方案精度比uint8高一些性能差异也不大。如果你追求极致性能可以试试uint8但精度掉得比较厉害。这块可以多测几组找一个准确率和速度的平衡点。转换完会在qnn_model目录下生成一个model.serialized描述文件。接下来用Context Binary生成器把描述文件和HTP Backend绑定qnn-context-binary-generator \ --backend $QNN_SDK_ROOT/lib/aarch64-unknown-linux-gnu/libQnnHtp.so \ --model qnn_model/model.serialized \ --binary_file model_context.bin生成的model_context.bin就是最终要部署到设备上的文件。注意这里指定的libQnnHtp.so是开发机上的x86版本还是目标板子的ARM版本命令行里的库路径实际上会被运行时路径替代生成的Context Binary里记录的是一些后端图信息最终执行还是在设备的HTP上所以选哪个版本不是最核心的问题。但为了保险我一般还是用目标架构的路径去生成省得后续出现莫名其妙的ABI不兼容。2.4 用qnn-model-tool检查模型信息转换完之后不要急着写C代码先用高通自带的工具检查一下生成结果qnn-model-tool --model model_context.bin --print_info这个命令会打印模型的输入输出张量名称、维度和类型。我会把输入张量的名字记下来后面C代码里创建张量时要用这个名字去匹配。如果名字写错了graphExecute的时候通常会报张量未绑定或者数据填充失败的错。另外还可以用qnn-model-tool --model model_context.bin --print_buffers查看模型内所有缓冲区的布局方便确认输入数据应该按什么形状填充。这一步虽然简单但能避免后面代码里到处猜维度。3. 手写C推理代码核心调用步骤逐段拆解环境搞定、模型转换完成现在进入重头戏C调用QNN SDK。我用一个最简单的图像分类模型做例子把从加载模型到推理输出的整个流程写成代码。这个结构也可以直接套用到检测、分割等更复杂的模型上。3.1 核心接口说明QNN SDK的C接口核心其实就是一组函数指针表通过QnnInterface_getProviders()拿到接口提供者再从物理设备加载Backend实现初始化之后就能使用它提供的能力。QNN最大的特点是把Backend和Context分开抽象Backend是物理能力层负责管理NPU/GPU/DSP资源Context是逻辑执行层负责持有模型图和内部状态。整个调用序列可以分成四步获取QNN接口初始化Backend。用导入的Context Binary创建Context。从Context中获取Graph并绑定输入输出张量。执行推理读取结果。代码写起来会有一些宏和版本差异但接口骨架非常稳定。下面我会拆成几个代码块来解析。3.2 从QNN Backend初始化到加载Context Binary先看接口获取和初始化的代码#include QnnInterface.h #include QnnTypes.h #include QnnContext.h #include QnnGraph.h #include QnnTensor.h #include iostream #include vector #include cstring #include fstream #include dlfcn.h using QnnFunctionTable QNN_INTERFACE_VER_TYPE; QnnFunctionTable* g_qnn nullptr; Qnn_BackendHandle_t g_backend nullptr; Qnn_ContextHandle_t g_context nullptr; bool loadQnnInterface(const char* backendLibPath) { void* handle dlopen(backendLibPath, RTLD_NOW | RTLD_GLOBAL); if (!handle) { std::cerr dlopen failed: dlerror() std::endl; return false; } auto getProviders (Qnn_ErrorHandle_t (*)(const QnnInterface_t***, uint32_t*)) dlsym(handle, QnnInterface_getProviders); if (!getProviders) { std::cerr cannot find QnnInterface_getProviders std::endl; return false; } const QnnInterface_t** providers nullptr; uint32_t numProviders 0; if (getProviders(providers, numProviders) ! QNN_SUCCESS || numProviders 0) { std::cerr no QNN providers found std::endl; return false; } g_qnn providers[0]-QNN_INTERFACE_VER_TYPE; return true; }dlopen那一步就相当于在运行时加载HTP驱动的前端库。为什么要用dlopen而不是直接链接因为你可以通过命令行参数传入不同的Backend库路径程序变得更灵活。想切到GPU Backend时只需要换个路径即可不用重新编译。拿到接口之后开始初始化Backend和Contextbool initBackendAndContext(const std::string modelPath) { if (!g_qnn) return false; // 初始化后端 if (g_qnn-backendInitialize(nullptr) ! QNN_SUCCESS) { std::cerr backend initialize failed std::endl; return false; } // 创建后端句柄 Qnn_Backend_Config_t* backendConfig nullptr; if (g_qnn-backendCreate(nullptr, backendConfig, g_backend) ! QNN_SUCCESS) { std::cerr backend create failed std::endl; return false; } // 读取Context Binary内容 std::ifstream binFile(modelPath, std::ios::binary); std::vectoruint8_t buffer((std::istreambuf_iteratorchar(binFile)), std::istreambuf_iteratorchar()); if (buffer.empty()) { std::cerr model binary is empty std::endl; return false; } // 从二进制直接创建Context Qnn_Context_Config_t contextConfig; memset(contextConfig, 0, sizeof(contextConfig)); Qnn_ContextCreateFromBinary_Config_t* ctxCreateConfig nullptr; if (g_qnn-contextCreateFromBinary( g_backend, nullptr, buffer.data(), buffer.size(), g_context, ctxCreateConfig) ! QNN_SUCCESS) { std::cerr context create from binary failed std::endl; return false; } return true; }这里要解释一下为什么用contextCreateFromBinary而不是contextCreate。contextCreate要求传入网络描述符运行时再从描述符构建图。而contextCreateFromBinary接收的是离线生成好的二进制执行计划所有算子的选择、内存分配、依赖关系都已经确定。路径短、开销小、稳定可靠这就是上一章极力推荐Context Binary的原因。执行完以上代码模型其实已经在HTP上初步建立了执行上下文但还缺输入输出张量这块拼图。3.3 创建输入输出Tensor并填充数据张量在QNN里是一个非常重要的抽象它描述了一块数据区以及它的维度、数据类型、量化参数等信息。创建张量的代码如下bool createInputTensor(const std::string name, const std::vectoruint32_t dims, Qnn_Tensor_t tensor) { tensor QNN_TENSOR_INIT; tensor.type QNN_TENSOR_TYPE_APP_WRITE; // 输入张量由应用写入 tensor.dataFormat QNN_TENSOR_DATA_FORMAT_FLOAT_32; Qnn_TensorData_t td tensor.tensorData; td.name name.c_str(); td.rank static_castuint32_t(dims.size()); td.dimensions const_castuint32_t*(dims.data()); td.dataType QNN_DATATYPE_FLOAT_32; td.quantizeParams.encodingDefinition QNN_TENSOR_QUANTIZATION_NONE; if (g_qnn-tensorCreate(g_context, tensor) ! QNN_SUCCESS) { std::cerr tensor create failed: name std::endl; return false; } return true; }QNN_TENSOR_TYPE_APP_WRITE表示这块内存在设备端由应用侧负责写入。还有一种常见类型是QNN_TENSOR_TYPE_APP_READ用于输出张量表示应用需要从设备端读取结果。当然也有QNN_TENSOR_TYPE_NATIVE这类直接绑定设备内存的用法但那是性能优化阶段才需要考虑的事第一次跑通流程时先用APP_WRITE/APP_READ最省心。创建完输入张量后要把图像数据填进去void fillInputTensor(Qnn_Tensor_t tensor, const std::vectorfloat imageData) { // 先让QNN获取一块可写入的数据指针 void* tensorData nullptr; g_qnn-tensorGetData(tensor, tensorData); if (tensorData nullptr) { std::cerr input tensor data pointer is null std::endl; return; } memcpy(tensorData, imageData.data(), imageData.size() * sizeof(float)); }输出张量的创建逻辑类似但type要改成QNN_TENSOR_TYPE_APP_READ。这里的核心概念是QNN的输入输出张量本质上是共享内存的通道你往输入张量绑定的指针里写数据模型执行时直接读这块内存执行结束后从输出张量绑定的指针里取数据。把数据搬进去和取出来是整个流程唯一需要显式操作内存的地方。3.4 执行推理和结果读取执行推理是整个项目最爽的一步代码反而是所有环节里最简短的bool runInference(Qnn_Tensor_t inputTensor, Qnn_Tensor_t outputTensor) { // 获取模型中的图句柄 Qnn_GraphHandle_t graphHandle nullptr; Qnn_GraphHandle_t* graphList nullptr; uint32_t numGraphs 0; if (g_qnn-contextGetGraphs(g_context, graphList, numGraphs) ! QNN_SUCCESS) { std::cerr context get graphs failed std::endl; return false; } if (numGraphs 0) { std::cerr no graph in context std::endl; return false; } graphHandle graphList[0]; // 将输入输出张量绑定到执行 Qnn_Tensor_t tensors[2] { inputTensor, outputTensor }; if (g_qnn-graphExecute(graphHandle, tensors, 2, nullptr, nullptr) ! QNN_SUCCESS) { std::cerr graph execute failed std::endl; return false; } // 读取输出数据 void* outData nullptr; g_qnn-tensorGetData(outputTensor, outData); float* floatOut reinterpret_castfloat*(outData); std::cout Inference done. First 10 outputs: std::endl; for (int i 0; i 10; i) { std::cout floatOut[i] ; } std::cout std::endl; return true; }graphExecute的执行是同步的调用返回时推理就已经完成了。如果你做的是视频流或者连续多帧处理建议在创建Context时打开异步执行相关的配置改成边采集边推理的方式提升吞吐。后面章节会展开说。一个容易忽略的细节是contextGetGraphs拿到的图句柄列表是Context内部的不需要自行释放。如果你加载的Context Binary里包含了多个模型比如一个检测模型加一个特征模型遍历这个数组就能拿到所有图句柄分别执行。实际项目中很有用。3.5 资源释放的正确顺序资源释放顺序有讲究别小看这最后一步。一个常见的错误是先把Backend销毁了再销毁Context然后程序崩溃。正确的顺序是void cleanup() { if (g_context) { g_qnn-contextFree(g_context); g_context nullptr; } if (g_backend) { g_qnn-backendFree(g_backend); g_backend nullptr; } // 后端terminate放最后 g_qnn-backendTerminate(nullptr); }先释放Context再释放Backend最后调用backendTerminate。原因很简单Context还持有Backend内部的设备资源如果先把Backend释放掉Context在销毁的时候会访问到一块已经失效的句柄区域轻则告警重则段错误。我早期做这块时都是直接不释放进程退出让系统回收后来做长时间运行的服务才发现释放顺序混乱会导致内存持续上涨最后只能把进程重启。养成正确的释放习惯写出来的服务才敢跑上几天几夜。4. 编译运行与踩坑实录从代码到真正跑起来代码写完之后编译和运行阶段才是真正开始“打仗”的时候。这一章我把高频踩坑点整理出来希望能帮你从报错大海里快速爬出来。4.1 编译期错误怎么排查编译命令大致是这样aarch64-linux-gnu-g -stdc17 qnn_runner.cpp -o qnn_runner \ -I$QNN_SDK_ROOT/include \ -L$QNN_SDK_ROOT/lib/aarch64-unknown-linux-gnu \ -Wl,-rpath,$QNN_SDK_ROOT/lib/aarch64-unknown-linux-gnu这里重点说一下-Wl,-rpath的作用。编译时链接的库在运行时也要能被找到如果你不在编译时指定rpath运行前就必须手动设置LD_LIBRARY_PATH。跑服务的时候很容易忘记设置然后看到一连串error while loading shared libraries排查半天才发现是动态库路径问题。把它编进二进制里一劳永逸。编译期最常遇到的第一类报错是找不到头文件比如fatal error: QnnType.h: No such file or directory。这通常是-I路径写错了检查一下SDK里的include目录结构把路径对准包含QnnInterface.h的那一层。第二类报错是链接时找不到QnnInterface_getProviders符号报undefined reference。多半是因为你用了C编译器但接口头文件里的函数没有用extern C包裹。QNN头文件有的版本自带兼容处理有的版本则需要在包含前手动加上extern C { #include QnnInterface.h }这个问题很隐蔽因为编译器报错行号往往指向你的源文件而不是头文件让人误以为是自己代码写错了。后来我在项目规则里固定了一条所有QNN相关头文件一律用extern C包含从此再没遇到过这类链接问题。4.2 运行时错误速查表运行时错误五花八门把几个高频场景列成表格方便对照排查现象常见原因处理建议dlopen失败libQnnHtp.so: cannot open shared object fileLD_LIBRARY_PATH没指对或者库架构不匹配先确认当前运行的机器架构再设置对应的库路径backendCreate返回错误没有更多日志HTP固件版本和SDK版本不匹配检查设备的DSP固件版本升级Hexagon SDK或更换QNN版本contextCreateFromBinary报RPC_ERRORHTP驱动没加载或者权限不足确认设备端是否已经启动QNN的RPC服务用高通提供的run工具初始化环境输入张量数据写不进去tensorGetData返回空指针张量类型设置错误用了NATIVE类型却没有绑定设备内存首次跑通流程用QNN_TENSOR_TYPE_APP_WRITEgraphExecute崩溃提示张量名称不匹配代码里创建张量的名字和模型里不一致用qnn-model-tool --print_info查看模型真实张量名推理结果全是0输入数据没写进张量或者量化参数设置不对先用float模型跑通再切量化模型对比结果这里补充一个容易忽略的点如果设备上跑的是没有root权限的用户态进程访问HTP可能会被SELinux或者权限策略拦下来报一些看起来完全莫名其妙的错误。遇到这类问题先检查日志看有没有权限相关的关键字。没有权限就用系统管理员协助调整配置不要试图硬绕权限限制。4.3 打开日志和调试技巧QNN内部有一套完整的日志机制调试的时候打开日志能帮你少走很多弯路。export QNN_LOG_LEVELVERBOSE ./qnn_runner日志级别从低到高通常是ERROR、WARN、INFO、VERBOSE。平时跑服务用WARN就行检查问题时开到VERBOSE。开启后你会看到SDK内部每一步在做什么比如加载后端、创建Context、绑定张量、执行算子。有一次模型执行结果不对打开日志才发现是某些算子在HTP上走了低精度分支数值精度下降导致结果偏差但整个流程本身没有报错。这种问题不开日志根本无从查起。还有一个小技巧在代码里加一个getElapsedTime的计时模块分别统计模型加载耗时和推理耗时。模型加载耗时可以用来判断Context Binary是否真的被完整加载推理耗时可以判断模型是否真的跑在了HTP上。如果推理耗时和纯CPU实现差不多那就要怀疑是不是量化没生效或者Backend配置里禁用了HTP加速。4.4 如何确认推理真的跑在HTP上这里插一个重要验证步骤怎么确认模型不是跑在CPU上最简单的方法是看执行时间。一个MobileNetV2量化的模型在骁龙8系列平台上跑推理时间通常只有几毫秒如果代码配置正确肉眼可见地比纯CPU快。另一种方式是在QNN日志里搜索HtpGraph之类的关键字VERBOSE日志会打印出后端执行时的算子调度信息。更严谨的做法是实测功耗。把设备插上功率计跑100次推理对比CPU版和HTP版的功耗差异。HTP版本通常明显更低。做功耗对比的时候建议把屏幕亮度、无线模块等因素固定住否则数据会被干扰带偏。日志确认和功耗测量都做过之后基本可以放心你的模型已经真正跑在高通NPU上了。5. 性能优化与落地建议到这里整个流程已经跑通了模型能够在C程序里被加载和推理。但跑通只是第一步要想真正落地到产品里还有几个关于性能和稳定性的问题需要处理。这部分不是必选项但对于做长期项目的人来说非常值得参考。5.1 Tensor内存分配与对齐前面提到APP_WRITE/APP_READ张量类型最省心但在追求极致性能时可以考虑使用QNN_TENSOR_TYPE_NATIVE并把输入输出缓冲区直接绑到设备端。这种做法的好处是省去了Host和Device之间的一次内存拷贝。如果决定用原生内存绑定的方式就必须注意内存对齐。QNN的HTP后端对缓冲区有对齐要求通常是256字节或者更高具体值可以在SDK的文档中查到。用posix_memalign分配内存设置QNN_TENSOR_MEM_TYPE_DMA等类型再通过tensorCreateCustom绑定能明显降低单次推理的负载。不过这属于进阶优化第一次做项目不建议直接上。先用APP_WRITE/APP_READ把功能做对之后再用性能剖析工具找出真正的瓶颈点再有针对性地替换成原生内存方案。5.2 Context复用与多模型切换在一个长期运行的应用里不要每推理一帧就创建一个新Context这是非常浪费的。一个Context可以复用很多次graphExecute是线程安全还是需要加锁取决于具体后端实现但通常建议一个Context对应一个推理线程保持稳定的调用频率。多线程场景下要么给Context加锁要么创建多个Context每个线程持有一个后者吞吐更好。如果你有多个模型需要切换推理可以一次性把所有模型的Context Binary都加载进来每个模型对应一个Context和一组输入输出张量。执行时按需调用对应Context的图切换开销很小。这种设计比每次切换都创建销毁Context要稳得多内存占用也更好预估。5.3 我的个人维护习惯最后分享一个我自己的习惯每跑通一个新模型就在项目目录下新建一个model_config.txt把模型版本、输入尺寸、量化方案、Backend库版本、生成Context Binary的命令全部记录下来。这东西看机器折腾久了半年后回来看项目如果没有一份配置记录你会对着一个陌生模型文件发半天呆。我看过太多人拿着别人给的Context Binary直接接进业务代码出了问题不知道是后端不兼容还是模型转换有误只能从头排查。提前把版本信息记录好配合上面说的日志分级机制排查问题基本能砍掉一半时间。QNN整套东西上手之后会发现它的接口设计一旦理顺剩下的就是机械式调用。真正决定项目上限的反而是在模型转换阶段对算子兼容性的把握以及在C侧对张量生命周期的管理能力。第一次跑通别着急上性能优化先让整个链路稳定转起来再一步步打磨细节。
返回列表