为Qualcomm NPU(QNN HTP)编译模型
这是一个流程,而非脚本:每个模型都需要自己的导出和检查。它遵循sherpa-onnx编译其已发布的NPU模型的方式(k2-fsa/sherpa-onnx,.github/workflows/export-*-qnn.yaml,.github/scripts/export-qnn/,scripts/qnn/),这些方式已知是有效的。在开始之前,请阅读那里的匹配工作流;进行适配,而非发明。
0. 判断NPU是否值得使用
- 先在目标手机上测量: 模型当前的CPU占用率和墙钟时间(
/proc/<pid>/stat每2秒滴答一次,top -H -p <pid>每线程),以及如果目标是功耗的话,在固定任务中的电池消耗。NPU在大型、稳定的矩阵计算(编码器、长批量传递)中收益明显;对于在1-2个线程上运行的小模型,收益可能很小。 - 仅支持静态形状。 NPU运行固定大小的图:固定批次(1)、固定输入长度(分块大小,或Whisper编码器的30秒)。输入长度变化的模型需要固定分块、填充或多个图。
- 每个构建对应一个芯片家族。 上下文二进制文件是为一种Hexagon架构(v73、v75、v79、v81…)编译的,只能在该架构上运行;其他手机(以及Exynos/Tensor/MediaTek)保持CPU路径。始终计划CPU回退。
- 大小限制。 非常大的模型(超过约1B参数,fp16)可能无法放入一个上下文:它们需要8位权重(w8a16)和/或分割成多个图。sherpa-onnx自己的Whisper构建止步于medium(769M)。
芯片(来自sherpa-onnx scripts/qnn/device_info.py):SM8550 Snapdragon 8 Gen 2 (v73),SM8650 8 Gen 3 (v75),
SM8750 8 Elite (v79),SM8850 (v81),QCS9100 (v73)。SM8350 (v68) 没有fp16:跳过它。通过 adb shell getprop ro.soc.model 查找手机的芯片。
1. 工具(仅Linux x86-64)
QNN SDK(QAIRT)运行在Linux x86-64(或Windows)上,不支持macOS。在Mac上:使用Linux虚拟机/云盒子,或使用 --platform linux/amd64 的Docker(模拟,速度慢)。sherpa-onnx为其构建了一个Docker镜像(Dockerfile-2.40,qnn-sdk-docker-build.yaml)。
- QAIRT SDK,例如2.40.0.251030:来自Qualcomm(免费账户),或sherpa-onnx的CI下载的副本(
huggingface.co/csukuangfj/qnn-toolkit,v2.40.0.251030.zip)。检查其许可证是否允许你的使用。 source qairt/<version>/bin/envsetup.sh(设置QNN_SDK_ROOT),然后sudo ${QNN_SDK_ROOT}/bin/check-linux-dependency.sh和python3 ${QNN_SDK_ROOT}/bin/check-python-dependency。- Python 3.10 venv,包含
numpy<2 onnx==1.17.0 onnxruntime onnxsim以及模型自身的导出依赖。 - Android NDK在
PATH中(ndk-build),用于aarch64模型库;LDFLAGS="-Wl,-z,max-page-size=16384"(16 KB页,Android 15+需要)。
2. 为NPU导出ONNX
从原始检查点(PyTorch)重新导出,而不是从为CPU制作的ONNX导出:
--dynamic-batch 0(批次1),ONNX中不使用int8量化(--enable-int8-quantization 0:改为在QNN中量化),--use-int32-inputs 1(没有int64张量),固定分块/长度。- 简化(
onnxsim)并检查每个输入/输出都有固定形状(sherpa-onnxscripts/pyannote/segmentation/show-onnx.py)。 - 在一些真实输入上,使用ONNX Runtime(onnxruntime)在CPU上运行ONNX,与原始结果对比:输出相同(或文本相同)。
示例:流式Zipformer2(icefall zipformer/export-onnx-streaming.py,参见sherpa-onnx scripts/zipformer-transducer/x-asr/qnn/export-streaming.sh);Whisper(sherpa-onnx scripts/whisper/qnn/export_onnx.py,编码器和解码器作为单独的图)。
3. 转换、构建、编译(每个图,每个芯片)
对于每个ONNX图 $m(例如编码器、解码器、joiner):
# a) ONNX → QNN C++ 图 + 权重。fp16激活:--act_bitwidth 16 --bias_bitwidth 32。
# 对于8位权重(大模型):通过带有真实输入的校准 --input_list 添加量化。
qnn-onnx-converter --input_network $m.onnx --output_path out/$m.cpp --act_bitwidth 16 --bias_bitwidth 32
# b) 为芯片生成HTP配置(sherpa-onnx scripts/qnn/generate_config.py:soc_id,dsp_arch,vtcm)。
python3 generate_config.py --soc SM8750 --graph-name $m --output-dir out/config-$m --qnn-sdk-root $QNN_SDK_ROOT
# c) 模型库:x86_64(在主机上编译上下文)和aarch64-android。
python3 $QNN_SDK_ROOT/bin/x86_64-linux-clang/qnn-model-lib-generator -c out/$m.cpp -b out/$m.bin -o out/model_libs
# d) 为芯片生成上下文二进制文件:手机加载的内容。
$QNN_SDK_ROOT/bin/x86_64-linux-clang/qnn-context-binary-generator \
--backend $QNN_SDK_ROOT/lib/x86_64-linux-clang/libQnnHtp.so \
--model out/model_libs/x86_64-linux-clang/lib$m.so \
--output_dir out/binary --binary_file $m \
--config_file out/config-$m/htp_backend_extensions.json
保留转换器和生成器的日志:不支持的操作和量化警告会显示在那里。
4. 在手机上运行
- 为Android aarch64运送QNN运行时库(
libQnnHtp.so,libQnnHtpV79Skel.so以及芯片对应的存根,libQnnSystem.so;sherpa-onnx将其发布为qnn-libs-<version>.tar.bz2)。Skel库在DSP上运行:sherpa-onnx的OnlineRecognizer.prependAdspLibraryPath(dir)(或ADSP_LIBRARY_PATH)必须指向其文件夹。 - sherpa-onnx:在模型配置上使用
QnnConfig(backendLib = ".../libQnnHtp.so",contextBinary = ".../$m.bin",systemLib = ".../libQnnSystem.so")(例如OnlineTransducerModelConfig(...,qnnConfig = ...)),provider = "qnn"。 - ONNX Runtime替代方案:QNN执行提供程序(
QNNExecutionProvider,backend_path=libQnnHtp.so),它也可以自行缓存上下文二进制文件。 - 按设备选择:如果
ro.soc.model匹配编译过的芯片且库加载成功,则使用NPU;否则使用CPU。绝不要假设。
5. 验证
- 相同输出: 在CPU和NPU上运行相同的音频/输入;对于语音,在10个以上的真实片段上计算词错误率。16位激活通常无损;8位权重必须检查。
- 速度: 手机上的实时因子和首次输出延迟。
- 功耗: 进程的CPU占用率,以及在固定30-60分钟任务中电池的下降情况(CPU vs NPU,屏幕关闭,相同的亮度和网络)。重复测试;热状态会改变结果。
- 在模型旁边记录结果(芯片、SDK版本、模型提交、量化方式、数值)。
常见陷阱
- 动态形状、int64输入、不支持的操作(某些注意力或控制流操作):在导出中修复(包装模型,固定长度),而不是在QNN中。
- 上下文二进制文件与SDK版本家族绑定:使用与运行时库相同的QAIRT版本构建。
- 模型若有部分操作回退到CPU,会无声地失去大部分收益:检查日志中的CPU分区。
- 许可证:模型许可证(重新编译的形态分发相当于分发模型)和QNN SDK许可证。
- 不使用本地Linux的替代路线:Qualcomm AI Hub(云端编译并在真实设备上分析;需要账户;检查其条款是否适用于你的应用)。