Modelle für Qualcomm NPUs kompilieren (QNN HTP)
Eine Anleitung, kein Skript: Jedes Modell benötigt seinen eigenen Export und seine eigenen Überprüfungen. Sie folgt der Vorgehensweise, die sherpa-onnx für seine veröffentlichten NPU-Modelle verwendet (k2-fsa/sherpa-onnx, .github/workflows/export-*-qnn.yaml, .github/scripts/export-qnn/, scripts/qnn/), welche bekanntermaßen funktionieren. Lies den passenden Workflow dort, bevor du beginnst; passe an, erfinde nicht.
0. Entscheide, ob sich die NPU lohnt
- Messe zuerst auf dem Ziel-Handy: CPU-Auslastung und Wandzeit des Modells im aktuellen Betrieb (
/proc/<pid>/statTicks alle 2 s,top -H -p <pid>pro Thread) und Akkuverbrauch über eine festgelegte Aufgabe, wenn Energie das Ziel ist. Die NPU lohnt sich für große, stabile Matrix-Arbeiten (Encoder, lange Batch-Durchläufe); für ein kleines Modell auf 1–2 Threads kann der Gewinn gering sein. - Nur statische Shapes. Die NPU führt Graphen mit fester Größe aus: feste Batch-Größe (1), feste Eingabelänge (Chunk-Größe oder 30 s für Whisper-Encoder). Ein Modell mit variabler Eingabelänge benötigt einen festen Chunk, Padding oder mehrere Graphen.
- Eine Chip-Familie pro Build. Ein Kontext-Binary wird für eine Hexagon-Architektur (v73, v75, v79, v81…) kompiliert und läuft nur dort; andere Handys (und Exynos / Tensor / MediaTek) behalten den CPU-Pfad. Plane immer einen CPU-Fallback ein.
- Größenbeschränkungen. Sehr große Modelle (über etwa 1B Parameter in fp16) passen möglicherweise nicht in einen Kontext: Sie benötigen 8-Bit-Gewichte (w8a16) und/oder Aufteilung in mehrere Graphen. sherpa-onnxs eigene Whisper-Builds enden bei medium (769M).
Chips (aus 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) hat kein fp16: überspringe es. Finde den Chip eines Handys mit
adb shell getprop ro.soc.model.
1. Werkzeuge (nur Linux x86-64)
Das QNN SDK (QAIRT) läuft auf Linux x86-64 (oder Windows), nicht auf macOS. Auf einem Mac: Eine Linux-VM/Cloud-Box oder Docker mit
--platform linux/amd64 (langsam: emuliert). sherpa-onnx baut ein Docker-Image dafür (Dockerfile-2.40,
qnn-sdk-docker-build.yaml).
- QAIRT SDK, z. B. 2.40.0.251030: von Qualcomm (kostenloses Konto) oder die Kopie, die sherpa-onnxs CI herunterlädt
(
huggingface.co/csukuangfj/qnn-toolkit,v2.40.0.251030.zip). Überprüfe, ob seine Lizenz deine Nutzung erlaubt. source qairt/<version>/bin/envsetup.sh(setztQNN_SDK_ROOT), dannsudo ${QNN_SDK_ROOT}/bin/check-linux-dependency.shundpython3 ${QNN_SDK_ROOT}/bin/check-python-dependency.- Python 3.10 venv mit
numpy<2 onnx==1.17.0 onnxruntime onnxsimplus die eigenen Export-Abhängigkeiten des Modells. - Android NDK im
PATH(ndk-build), für die aarch64-Modellbibliotheken;LDFLAGS="-Wl,-z,max-page-size=16384"(16-KB-Seiten, erforderlich ab Android 15+).
2. Exportiere ONNX für die NPU
Exportiere neu aus dem originalen Checkpoint (PyTorch), nicht aus einem für CPUs erstellten ONNX:
--dynamic-batch 0(Batch-Größe 1), keine int8-Quantisierung im ONNX (--enable-int8-quantization 0: quantisiere in QNN stattdessen),--use-int32-inputs 1(keine int64-Tensoren), fester Chunk/Länge.- Vereinfache (
onnxsim) und überprüfe, ob jede Ein-/Ausgabe eine feste Form hat (sherpa-onnxscripts/pyannote/segmentation/show-onnx.py). - Führe das ONNX auf der CPU (onnxruntime) gegen das Original mit einigen realen Eingaben aus: gleiche Ausgabe (oder gleicher Text).
Beispiele: Streaming Zipformer2 (icefall zipformer/export-onnx-streaming.py, siehe sherpa-onnx
scripts/zipformer-transducer/x-asr/qnn/export-streaming.sh); Whisper (sherpa-onnx scripts/whisper/qnn/export_onnx.py,
Encoder und Decoder als separate Graphen).
3. Konvertieren, bauen, kompilieren (pro Graph, pro Chip)
Für jeden ONNX-Graphen $m (z. B. encoder, decoder, joiner):
# a) ONNX → QNN C++ Graph + Gewichte. fp16-Aktivierungen: --act_bitwidth 16 --bias_bitwidth 32.
# Für 8-Bit-Gewichte (große Modelle): füge Quantisierung mit einer Kalibrierung --input_list mit realen Eingaben hinzu.
qnn-onnx-converter --input_network $m.onnx --output_path out/$m.cpp --act_bitwidth 16 --bias_bitwidth 32
# b) HTP-Konfiguration für den Chip (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) Modellbibliotheken: x86_64 (um den Kontext auf dem Host zu kompilieren) und 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) Kontext-Binary für den Chip: das, was das Handy lädt.
$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
Behalte die Konverter- und Generator-Logs: Nicht unterstützte Operationen und Quantisierungswarnungen werden dort angezeigt.
4. Auf dem Handy ausführen
- Übertrage die QNN-Laufzeitbibliotheken für Android aarch64 (
libQnnHtp.so,libQnnHtpV79Skel.sound Stub für den Chip,libQnnSystem.so; sherpa-onnx veröffentlicht sie alsqnn-libs-<version>.tar.bz2). Die Skel-Bibliothek läuft auf dem DSP: sherpa-onnxsOnlineRecognizer.prependAdspLibraryPath(dir)(oderADSP_LIBRARY_PATH) muss auf ihren Ordner zeigen. - sherpa-onnx:
QnnConfig(backendLib = ".../libQnnHtp.so", contextBinary = ".../$m.bin", systemLib = ".../libQnnSystem.so")in der Modellkonfiguration (z. B.OnlineTransducerModelConfig(..., qnnConfig = ...)),provider = "qnn". - ONNX Runtime Alternative: der QNN Execution Provider (
QNNExecutionProvider,backend_path=libQnnHtp.so), welcher auch selbst Kontext-Binaries cachen kann. - Wähle pro Gerät: Wenn
ro.soc.modelzu einem kompilierten Chip passt und die Bibliotheken laden, NPU; sonst CPU. Gehe niemals davon aus.
5. Überprüfen
- Gleiche Ausgabe: Führe denselben Audio-/Eingabedatensatz auf CPU und NPU aus; für Sprache, Wortfehlerrate auf 10+ echten Clips. 16-Bit- Aktivierungen sind normalerweise verlustfrei; 8-Bit-Gewichte müssen überprüft werden.
- Geschwindigkeit: Echtzeitfaktor und Latenz der ersten Ausgabe auf dem Handy.
- Energie: CPU-Auslastung des Prozesses und Akkuverbrauch über eine feste Aufgabe von 30–60 min, CPU vs. NPU, Bildschirm aus, gleiche Helligkeit und Netzwerk. Wiederhole; der thermische Zustand beeinflusst die Ergebnisse.
- Zeichne Ergebnisse (Chip, SDK-Version, Modell-Commit, Quantisierung, Zahlen) neben dem Modell auf.
Fallstricke
- Dynamische Shapes, int64-Eingaben, nicht unterstützte Operationen (einige Attention- oder Kontrollfluss-Ops): Behebe sie im Export (umwickele das Modell, korrigiere Längen), nicht in QNN.
- Kontext-Binaries sind an die SDK-Versionsfamilie gebunden: Baue mit derselben QAIRT-Version wie die Laufzeitbibliotheken.
- Ein Modell, das für einige Operationen auf die CPU zurückfällt, verliert still den Großteil des Gewinns: Überprüfe die Logs auf CPU-Partitionen.
- Lizenzen: Die des Modells (das Verteilen einer kompilierten Form ist das Verteilen des Modells) und die des QNN SDKs.
- Alternativer Weg ohne lokales Linux: Qualcomm AI Hub (Cloud-Kompilierung und Profilerstellung auf echten Geräten; benötigt ein Konto; überprüfe seine Bedingungen für deine App).