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秒)。入力長が可変のモデルは、固定チャンク、パディング、または複数グラフが必要です。
- チップファミリごとに1ビルド。コンテキストバイナリは1つのHexagonアーキテクチャ(v73、v75、v79、v81…)向けにコンパイルされ、そのアーキテクチャでのみ動作します。他の端末(Exynos / Tensor / MediaTek)はCPUパスを維持します。常にCPUフォールバックを計画してください。
- サイズ制限。非常に大きなモデル(fp16で約1Bパラメータ以上)は1つのコンテキストに収まらない場合があります。その場合は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 VM/クラウドボックス、または--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"(Android 15以降で必須の16KBページ)を設定します。
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をCPU(onnxruntime)で元のモデルといくつかの実入力に対して実行し、同じ出力(または同じテキスト)になることを確認します。
例:ストリーミング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(エンコーダ、デコーダ、ジョイナーなど)に対して:
# 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:モデル設定(例:
OnlineTransducerModelConfig(..., qnnConfig = ...))にQnnConfig(backendLib = ".../libQnnHtp.so", contextBinary = ".../$m.bin", systemLib = ".../libQnnSystem.so")を指定し、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(実デバイス上でのクラウドコンパイルとプロファイリング。アカウントが必要。アプリへの利用規約を確認)。