高通跃龙IQ-9075平台的开发记录(3): AI部署的SDK选择
设备: 高通跃龙 IQ-9075 EVK(SA8775P,Hexagon v73)
运行时: Qualcomm Genie(QAIRT 2.42 附带示例源码 / 设备侧 Genie 1.14.0)
模型: Qwen2.5-7B-Instruct(本地编译)
依据: Qualcomm AI Hub Models 导出代码;QAIRT SDK 中 Geniequalla引擎源码;板上实测
引言:高通 AI 部署时的SNPE、QNN、Genie 怎么选
了解 QAIRT SDK 的读者都知道,高通 AI 部署有三套推理 SDK:SNPE、QNN、Genie。三者在文档里并列出现,功能有重叠,但适用场景差异很大。需要根据实际应用场景选用合适的。
QAIRT官方文档对三者的定位描述是:
- SNPE 是更简单的 API,允许模型在多种处理器上执行,代价是文件更大、对单个算子的控制粒度更粗;
- QNN 面向特定处理器,提供对每个算子执行方式的精细控制;
- Genie 扩展 QNN,专门服务于生成式 AI 场景。但文档没有展开讲的是,实际部署中这三个 SDK 的适用边界到底在哪里。
一、三者的定位
SNPE是高通较早推出的 AI 推理 SDK,全称 Snapdragon Neural Processing SDK,品牌命名沿用的是骁龙平台的早期体系。模型格式是 DLC,转换工具
snpe-onnx-to-dlc和snpe-tensorflow-to-dlc负责把 ONNX 和 TensorFlow 模型转成 DLC。运行时 API 层次较高,调用snpe-net-run或 SNPE C++/Java API,框架会自动在 CPU/GPU/DSP 之间调度算子。文档里能找到的教程和示例集中在 2D CNN:图像分类、目标检测、语义分割。算子不支持的部分需要写 UDO(User Defined Op),文档有专门一章讲 UDO 的写法,但开发成本不低。QNN全称 Qualcomm AI Engine Direct,是后来推出的底层推理引擎,定位上更接近硬件。不依赖 DLC 格式,直接面向 HTP(Hexagon Tensor Processor),提供 7 种硬件后端(CPU、GPU、HTP 都有)。算子以 Op Package 的形式组织,粒度比 SNPE 细。量化方案支持 INT8、INT16、FP16,以及 w8a16(INT8 权重 + FP16 激活)这类混合精度模式。工具链包括
qnn-model-lib-generator(编译模型库)和qnn-context-binary-generator(生成 Context Binary),最终产物是.bin文件,运行时用qnn-net-runCLI 或 QNN C/C++ API 加载执行。文档里对 HTP 架构、量化 Schema、算子包开发都有独立章节,细节程度明显高于 SNPE。Genie构建在 QNN 之上,专门为 LLM 推理设计。不参与模型编译——编译仍然走 QNN 工具链。Genie 消费编译好的 Context Binary,在上面封装了 tokenizer、KV Cache 管理、采样(temperature、top-k、top-p)、流式输出。运行时 API 以 Dialog 为核心:创建 Dialog、发送 Query、通过回调接收 token。文档里预置了 16 种主流 LLM 的配置模板,覆盖 Llama v2/v3、Qwen2/Qwen2.5、Mistral、Phi-3.5 等。
概括地说,SNPE 是较早的高层 API,QNN 是后来推出的底层引擎,Genie 是 QNN 上的 LLM 专用层。从 QAIRT 2.x 版本的演进来看,新算子、新量化方案优先在 QNN 侧提供,SNPE 侧的更新活跃度明显下降。下面以几个实际案例说明选用SDK的原则。
二、跑 LLM:首选 Genie
以 QCS9075 上部署千问 2.5 大语言模型为例,SDK 选 Genie,不是 QNN 直接调用,更不是 SNPE。
Genie 封装了 LLM 推理的全部运行时逻辑。C++ 推理服务器通过dlopen动态加载libGenie.so,核心调用就几行:
g_genie.config_create(config_str.c_str(),&g_config_handle);g_genie.dialog_create(g_config_handle,&g_dialog);g_genie.dialog_query(g_dialog,prompt.c_str(),GENIE_DIALOG_SENTENCE_COMPLETE,stream_callback,state);dialog_query一调,token 通过回调逐个返回,前端 SSE 实时显示。模型加载一次常驻内存,后续请求只需重置 KV Cache(dialog_reset),首 token 延迟从子进程方案的 ~20 秒降到了 ~176ms。
如果不用 Genie,直接用 QNN C API 跑 LLM当然也可以,但是很太东西需要自己实现:tokenizer 的加载和编码、KV Cache 的分配和轮转、位置编码(RoPE)的计算、采样策略、多分片模型的串联调度。千问 2.5 的 7B 模型被分成 6 个 Context Binary,每片包含一部分 Transformer 层,片间数据传递和 KV Cache 同步都得自己管。而Genie 恰好以最优方式或最佳实践实现了这些。genie_config.json里写好ctx-bins列表和positional-encoding参数,运行时自动处理这些。
编译阶段的细节仍然属于 QNN。必须设置soc_model=77和dsp_arch="v73",否则会出现各种不可预期的异常,比如输出乱码。--float_bitwidth 32不能设成 16(QAIRT 2.35 有 bug),weights_packing=True必须打开(否则模型体积膨胀 2 倍)。这些参数走qnn-context-binary-generator,Genie 运行时不参与编译,只消费编译好的.bin文件。
如果用 SNPE 跑 LLM,基本走不通。SNPE 没有为 LLM 设计的运行时接口,DLC 格式不支持 LLM 所需的动态形状(sequence length 可变)和 KV Cache 机制。SNPE 的推理 API 是"给定输入张量 → 得到输出张量"的一次性模式,没有"多轮对话 + 增量推理"的概念。
高通官方的应用 demo 里,LLM 相关的 ChatApp(Android 和 Windows)也都用 Genie,模型是 Llama 3.2 3B,分成 3 个 Context Binary。genie_config.json配好ctx-bins、positional-encoding、sampler参数,htp_config.json指定soc_model和dsp_arch。两个平台做法一致。
三、跑扩散模型:QNN 是唯一现实选项
在 Linux 上部署 Stable Diffusion 2.1,目标平台 QCS9075 / QCS9100,整条推理链路用 QNN 的qnn-net-run驱动。三个子模型(Text Encoder、U-Net、VAE)都是预编译的 QNN Context Binary,量化方案 w8a16,后端libQnnHtp.so。
核心调用长这样:
qnn-net-run\--retrieve_context"unet_w8a16.bin"\--backend"libQnnHtp.so"\--input_list"input_list.txt"\--output_dir"tmp/"Python 脚本负责 tokenizer、DPM-Solver 调度器、CFG 噪声组合这些轻量计算,重活全部交给 HTP。每次推理通过os.system()调qnn-net-run,输入输出走临时.raw文件。整个项目是一个 370 行的 Python 单文件,没有 C/C++ 代码,没有 Makefile。
为什么不用 SNPE?模型转换这一步就卡住了。SNPE 的snpe-onnx-to-dlc对 Stable Diffusion 2.1 这种大模型的算子覆盖不全,w8a16 量化在 SNPE 工具链里没有直接对应的选项。就算转换成功,运行时性能也很难达到 HTP 的最佳状态——SNPE 的调度逻辑会多一层抽象,对 HTP 的直接控制力不如 QNN。
为什么不用 Genie?Genie 是 LLM 专用运行时,API 为对话和文本生成设计。Stable Diffusion 的推理逻辑是"文本编码 → 多步去噪 → 图像解码",跟 Genie 的 Dialog/Query 模型对不上。用 Genie 跑扩散模型,相当于拿聊天框架去做图像生成,API 不匹配。
高通官方 demo 里 Windows 端的 Stable Diffusion 走的是 ONNX Runtime + QNN Execution Provider,底层仍然是 QNN。路径不同,到达 HTP 的方式一样。
四、跑 3D 视觉模型:QNN 的算子覆盖优势
部署 3D ResNet 做视频动作识别,输入 16 帧 112×112 RGB,输出 Kinetics-400 的 400 类分类。目标平台 QCS6490(RB3 Gen 2),HTP v68。
这个案例走了 Edge Impulse 的 Linux Runner 做封装,但模型文件名里的qnn已经说明了一切。部署时仍然需要把 QAIRT SDK 的 QNN 库拷到设备上:
scp/opt/qcom/aistack/qairt/2.31.0.250130/lib/aarch64-ubuntu-gcc9.4/* user@device:/usr/libscp.../lib/hexagon-v68/unsigned/libQnnHtpV68Skel.so user@device:/usr/lib/rfsa/adsp/Edge Impulse Runner 底层调用的就是 QNN 的 HTP 后端,.eim文件是 QNN Context Binary 的封装。Runner 在外面包了一层 HTTP 服务,Node.js 应用通过端点发推理请求。架构上分两层:Node.js Express 服务负责视频解码和预处理,Edge Impulse Runner 负责模型推理。
选 QNN 而不是 SNPE,原因和扩散模型类似但不完全相同。3D ResNet 的算子(3D 卷积、时序池化)在 QNN 的算子包里覆盖更好。SNPE 文档里能找到的教程和示例都是 2D CNN,3D 卷积在 SNPE 里需要写 UDO,开发成本高。QNN 的 Op Package 机制同样支持自定义算子,但 3D ResNet 的标准算子在 QNN 里已经有了,不需要额外开发。
如果用 SNPE,3D 卷积算子大概率要手写 UDO,而且 UDO 跑在 HTP 上的性能调优是个黑洞——需要自己管量化、内存布局、skel 库编译。Edge Impulse 选 QNN 后端而不是 SNPE 后端,也是这个原因。
五、传统 2D 视觉模型:上层框架 + QNN Delegate
高通官方 demo 里,传统的 2D 视觉应用(图像分类、目标检测、语义分割、超分辨率)没有直接用 QNN C API,而是通过上层框架对接 QNN:
- Android 端:TFLite + QNN Delegate。Delegate 优先级是 QNN_NPU → GPU → XNNPack(CPU),自动降级:
publicstaticfinalDelegateType[][]delegatePriorityOrder={{QNN_NPU,GPUv2},// NPU + GPU + XNNPack{GPUv2},// GPU + XNNPack{}// XNNPack only (CPU)};- Windows 端:ONNX Runtime + QNN EP:
qnn_options["backend_path"]="QnnHtp.dll";// NPUsession_options.AppendExecutionProvider("QNN",qnn_options);- Ubuntu 端:TFLite + QNN Delegate,Python 调用:
delegate=Delegate("libQnnTFLiteDelegate.so",{"backend_type":"htp","htp_performance_mode":"2",# burst})interpreter=Interpreter("PalmDetector.tflite",experimental_delegates=[delegate])这些路径最终都经过 QNN 到达 HTP,只是上层封装不同。用 TFLite 或 ONNX Runtime 的好处是模型不用转格式,直接用.tflite或.onnx,Delegate/EP 负责把支持的算子放到 NPU 上跑,不支持的留在 CPU。对 2D CNN 来说,这种方式开发效率最高。
SNPE 在这个场景下理论上可行——2D CNN 的算子覆盖没有问题,DLC 转换工具也成熟。但 TFLite Delegate 和 ONNX Runtime EP 的生态对接都在 QNN 侧,用 SNPE 意味着脱离这条路径,自己走 DLC 转换 + SNPE API 的老路。高通官方示例库里完全没有 SNPE 的身影,说明官方的推荐方向已经明确。
六、SNPE 为什么逐渐被跳过
从 QAIRT 文档的更新重心和实际项目的选择来看,SNPE 被跳过有几点原因。
模型格式限制。SNPE 用 DLC 格式,转换工具对复杂模型(扩散模型、LLM、3D CNN)的算子覆盖不全。QNN 用 Context Binary,编译工具直接面向 HTP 架构,算子支持更广。
运行时 API 差距。SNPE 的推理 API 是静态的:固定输入、固定输出、一次性执行。LLM 需要的 KV Cache 轮转、流式输出、动态 sequence length,SNPE 都不原生支持。QNN 的 Graph/Context 模型更灵活,Genie 在 QNN 之上补齐了 LLM 运行时。
工具链演进方向。QAIRT SDK 2.x 版本的更新重心在 QNN 和 Genie。新算子、新量化方案(如 w8a16、w4a16)优先在 QNN 侧提供。文档里 QNN 部分有约 87 页,SNPE 约 86 页,页数接近,但 QNN 的内容更新频率明显更高。实际项目中用到的 QNN 版本从 2.31 到 2.44,不到一年跳了好几版,每次都带来新的算子支持和 bug 修复。
生态对接。Edge Impulse 的 Linux Runner 选了 QNN 后端,AI Hub 的模型导出工具支持 QNN 运行时,TFLite 的 QNN Delegate 和 ONNX Runtime 的 QNN EP 都对接 QNN。SNPE 在这些生态接口里没有位置。
七、什么情况下还会考虑 SNPE
这里并不是说 SNPE 没用。满足这几个条件,SNPE 仍然是最佳选择:
- 模型是标准的 2D CNN(图像分类、目标检测、语义分割),算子在 SNPE 支持列表内
- 模型已经用 DLC 格式部署过,迁移成本高
- 需要 SNPE 的多处理器自动调度功能(SNPE 会自动在 CPU/GPU/DSP 之间分配算子,QNN 需要手动指定后端)
- 目标设备的 HTP 架构较老(如 v65 以下),QNN 对老架构的支持可能不如 SNPE 成熟
但如果是新项目,模型涉及 LLM、扩散模型、3D CNN,或者需要 w8a16 等新量化方案,直接上 QNN 或 Genie,不要在 SNPE 上花时间。
八、选型决策
把上面几个案例的经验归纳成几条规则。
跑 LLM → Genie。Genie 封装了 LLM 运行时的全部逻辑,直接调 QNN 会陷入 KV Cache 和采样策略的实现细节。在 9075 上部署千问 2.5 和高通官方 ChatApp 都验证了这条路径。动态加载libGenie.so避免了编译时对专有头文件的依赖,是一个值得参考的开源兼容做法。
跑扩散模型 → QNN。Genie 不支持图像生成场景。QNN 的 Context Binary 能直接控制 HTP 的量化精度和算子调度。Linux 上部署 Stable Diffusion 用qnn-net-runCLI 就完成了端到端推理,不需要写 C++ 代码。如果后续需要更低延迟,可以改为 QNN C API 直接调用,去掉进程创建和文件 I/O 的开销。
跑 3D 视觉模型 → QNN。3D 卷积在 SNPE 里要手写 UDO,QNN 算子包里已有标准 3D 算子。不管上层用 Edge Impulse Runner 还是直接调 QNN C API,底层都是 QNN HTP 后端。
跑传统 2D 视觉模型 → 看平台。Android 上用 TFLite + QNN Delegate,Delegate 链自动处理 NPU/GPU/CPU 降级。Windows 上用 ONNX Runtime + QNN EP。Ubuntu 上用 TFLite + QNN Delegate 或直接用 QNN CLI。这些路径最终都经过 QNN 到达 HTP。
不要用 SNPE 起新项目。如果已有 DLC 模型且运行正常,维持现状。新模型走 QNN 工具链,LLM 走 Genie。