為 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 回退方案。
- 大小限制。 非常大的模型(fp16 下超過約 1B 參數)可能無法放入一個上下文:它們需要 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 虛擬環境,需安裝
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)。 - 在 CPU(onnxruntime)上使用一些真實輸入來運行 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(例如 encoder、decoder、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 與 NPU 在固定 30-60 分鐘任務中的程序 CPU 使用率和電池消耗。重複測試;熱狀態會改變結果。
- 在模型旁記錄結果(晶片、SDK 版本、模型提交、量化方式、數據)。
常見陷阱
- 動態形狀、int64 輸入、不支援的操作(某些注意力或控制流程操作):請在匯出階段修復(包裝模型、修正長度),不要在 QNN 中處理。
- 上下文二進位檔與 SDK 版本系列綁定:請使用與執行時期程式庫相同的 QAIRT 版本進行建置。
- 若某個模型在某些操作上靜默回退到 CPU,則會失去大部分效能提升:請檢查日誌中是否有 CPU 分區。
- 授權條款:模型的(重新分編譯形式等同於分發模型)和 QNN SDK 的。
- 無本地 Linux 的替代方案:Qualcomm AI Hub(在真實裝置上進行雲端編譯和性能分析;需要帳戶;請檢查其條款是否適用於您的應用程式)。