kazeia/TTS_GPU_GUIDE.md

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 context
  • libGLESv2.so — OpenGL ES 2.0
  • libOpenCL.so — déjà dans /vendor/etc/public.libraries.txt sur 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.