5.7 KiB
Guide GPU Adreno pour TTS Qwen3-TTS
ONNX Runtime QNN GPU Backend — Audio parfait, tokens identiques au CPU
1. Résumé
Le GPU Adreno 830 produit un audio TTS parfait via ONNX Runtime QNN avec le backend GPU (libQnnGpu.so). Contrairement au NPU (HTP) qui quantifie et détruit la qualité, le GPU fait du fp32/fp16 natif IEEE-754 sans quantification.
Résultat : tokens identiques au CPU (1995, 215, 212...), EOS naturel, audio impeccable.
Vitesse : 124-131ms/step (identique au CPU — pas de gain de vitesse dû à l'overhead de transfert mémoire par token).
2. Changement de code (1 ligne)
Dans Qwen3TtsEngine.kt, charger le talker avec le GPU backend :
// AVANT (CPU)
val talkerOpts = OrtSession.SessionOptions()
talkerKv = ortEnv!!.createSession(cpuOnnx.absolutePath, talkerOpts)
talkerUsesInt64Pos = true
// APRÈS (GPU Adreno)
val gpuPath = "$nativeLibDir/libQnnGpu.so"
val opts = OrtSession.SessionOptions()
opts.addQnn(mapOf("backend_path" to gpuPath))
talkerKv = ortEnv!!.createSession(cpuOnnx.absolutePath, opts)
talkerUsesInt64Pos = false // CRITIQUE: GPU QNN exige int32, pas int64
Point critique : talkerUsesInt64Pos = false
Le GPU QNN backend n'accepte pas les tenseurs int64 pour position_ids. L'erreur sinon :
ORT_INVALID_ARGUMENT: Unexpected input data type. Actual: (tensor(int64)), expected: (tensor(int32))
Le CPU ONNX accepte int64, le HTP aussi, mais le GPU non. Il faut envoyer int32.
Code de création du tenseur position (dans runTalkerStep) :
val posTensor = if (talkerUsesInt64Pos) {
OnnxTensor.createTensor(env, LongBuffer.wrap(longArrayOf(pos.toLong())), longArrayOf(1))
} else {
OnnxTensor.createTensor(env, IntBuffer.wrap(intArrayOf(pos)), longArrayOf(1))
}
3. Bibliothèques nécessaires
Dans app/src/main/jniLibs/arm64-v8a/ :
| Fichier | Source | Taille |
|---|---|---|
libQnnGpu.so |
QNN SDK lib/aarch64-android/ |
6.1 MB |
libQnnGpuNetRunExtensions.so |
QNN SDK lib/aarch64-android/ |
~1 MB |
libQnnGpuProfilingReader.so |
QNN SDK lib/aarch64-android/ |
~0.5 MB |
Commande pour copier depuis le QNN SDK :
QNN_SDK=/opt/Kazeia/qnn_sdk_242/qairt/2.42.0.251225
cp $QNN_SDK/lib/aarch64-android/libQnnGpu*.so \
kazeia-android/app/src/main/jniLibs/arm64-v8a/
Dépendances système (déjà présentes sur Android) :
libEGL.so— OpenGL ES contextlibGLESv2.so— OpenGL ES 2.0libOpenCL.so— déjà dans/vendor/etc/public.libraries.txtsur OnePlus Pad 2
4. Modèle ONNX
Aucune re-exportation nécessaire. Le même modèle ONNX CPU fonctionne sur GPU :
talker_kv_cpu/model.onnx(1.77 GB) — utilisé tel quel- Le GPU backend d'ONNX Runtime QNN compile le graph ONNX à la volée
Pas de cache GPU
Le context caching (qnn_context_cache_enable) ne fonctionne PAS avec le backend GPU d'ONNX Runtime (contrairement au HTP). Chaque session recompile le graph (~2.5s de chargement).
5. Pourquoi le GPU fonctionne mais pas le NPU
| Aspect | NPU (HTP) | GPU (Adreno) |
|---|---|---|
| Précision | INT8/INT16 quantifié | FP16/FP32 natif IEEE-754 |
| Quantification | Automatique, destructive | Aucune |
| Codebook argmax | Changé par la quantification | Identique au CPU |
| Audio TTS | Bruit / silence / inintelligible | Parfait |
| Vitesse | ~20ms/step (inutilisable) | ~130ms/step |
Le TTS sélectionne des codebooks par argmax sur 2048 valeurs. La moindre erreur de quantification (NPU) change le codebook sélectionné et cascade dans l'autoregression. Le GPU fait du vrai fp32 → mêmes codebooks → même audio.
6. Performance
| Métrique | CPU fp32 | GPU fp16/fp32 |
|---|---|---|
| Chargement | 2.5s | 2.8s (+graph compile) |
| Talker/step | 130ms | 124-131ms |
| Audio | Parfait | Parfait |
| RTF | 7.0 | 7.0 |
Pas de gain de vitesse. Le GPU est memory-bound pour les petits batch sizes (1 token). L'overhead de transfert CPU→GPU→CPU par step annule le gain de calcul parallèle du GPU.
Utilité : le GPU libère les cœurs CPU pour d'autres tâches (CP, UI, audio playback). En mode streaming, le talker sur GPU + CP sur CPU fonctionneraient en parallèle.
7. Dépendances Gradle
// Déjà présent pour le reste du pipeline TTS
implementation("com.microsoft.onnxruntime:onnxruntime-android-qnn:1.24.3")
Pas de dépendance supplémentaire. Le QNN GPU backend est inclus dans l'AAR ONNX Runtime.
8. Erreurs courantes
Unexpected input data type. Actual: (tensor(int64)), expected: (tensor(int32))
→ Mettre talkerUsesInt64Pos = false pour envoyer position_ids en int32.
Cannot Open QNN library libQnnGpu.so
→ Copier libQnnGpu.so dans jniLibs/arm64-v8a/.
Execution failed for method: forward (ExecuTorch)
→ Le JNI ExecuTorch ne fonctionne PAS avec le GPU QNN depuis l'app (problème de contexte GPU). Utiliser ONNX Runtime à la place.
Pas de cache GPU
→ Normal. Le QNN GPU backend ne supporte pas le context caching. Le graph est recompilé à chaque session (~2.5s).
9. Architecture finale recommandée
LLM Qwen3-0.6B → NPU HTP (93 tok/s, INT4 calibré) — ExecuTorch
Whisper STT → NPU HTP (INT8 calibré) — ONNX Runtime QNN
TTS Talker → GPU Adreno (fp32 natif) — ONNX Runtime QNN
TTS CP → CPU fp32 — ONNX Runtime
TTS Decoder → NPU HTP — ONNX Runtime QNN
Silero VAD → CPU (1.8 Mo) — ONNX Runtime
Chaque composant sur le backend optimal : NPU pour ce qui tolère la quantification, GPU pour le TTS qui exige la précision, CPU pour le reste.