kazeia/TTS_GPU_GUIDE.md

155 lines
5.7 KiB
Markdown

# 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 :
```kotlin
// 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`) :
```kotlin
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 :
```bash
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
```kotlin
// 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.