kazeia/kazeia-android/RAPPORT_TTS_QWEN3_TESTS.md

136 lines
5.8 KiB
Markdown

# Tests Qwen3-TTS sur OnePlus Pad 3 — Journal
**Date** : 29 mars 2026
---
## Environnement
- **Tablette** : OnePlus Pad 3 (Snapdragon 8 Elite, 16 GB RAM)
- **Runtime** : Termux + Python 3.12 + PyTorch 2.9.0 (Termux native ARM)
- **Modèle** : Qwen3-TTS-12Hz-0.6B-Base (local, `/data/local/tmp/kazeia/models/qwen3-tts/`)
- **Dépendances** : transformers 4.57.3, torchaudio 2.9.0, soundfile, einops
- **Mocks** : librosa (soundfile+scipy), soxr (scipy), sox, onnxruntime
## Résultats des tests
### Test 1 : float32 complet
- **Résultat** : OOM (killed) — le modèle 1.7 GB + speech tokenizer 651 MB + overhead dépassent la RAM disponible
- **RAM utilisée** : >10 GB avant crash
### Test 2 : float16
- **Résultat** : NaN dans le code predictor (`RuntimeError: probability tensor contains either inf, nan or element < 0`)
- **Cause** : float16 n'a pas assez de précision pour le softmax du code predictor (5 couches)
### Test 3 : float16 + code predictor float32
- **Résultat** : dtype mismatch (`RuntimeError: expected m1 and m2 to have the same dtype, but got: float != c10::Half`)
- **Cause** : le code predictor en float32 reçoit des tenseurs float16 du talker les types ne sont pas automatiquement castés dans le forward couplé
### Test 4 : bfloat16 ✅
- **Résultat** : **Fonctionne**
- **"Bonjour."** : 39.5s pour 1.0s d'audio (RTF 39.5x)
- **"Bonjour, je suis pour vous écouter."** : 109.4s pour 2.6s d'audio (RTF 41.5x)
- **Explication** : bfloat16 a le même range que float32 (8 bits d'exposant) mais moins de mantisse. Le code predictor ne produit plus de NaN.
- **RAM** : ~3.8 GB (modèle) + ~1-2 GB (inference) = ~5-6 GB total
### Test 5 : INT8 dynamic quantization
- **Résultat** : Échec (`NoQEngine` le backend quantization n'est pas compilé dans la version Termux de PyTorch)
### Test 6 : torch.compile
- **Résultat** : OOM l'overhead de compilation consomme trop de RAM
### Test 7 : Speaker encoder timing
- **Sur PC** : 2-10s selon la voix
- **Sur tablette CPU** : **688s (11 min)** inutilisable
- **Solution** : Pré-calculer les embeddings sur PC, les stocker en .npy (4 KB chacun), les charger instantanément
## Architecture validée
```
[PC - pré-calcul]
Voix WAV → Speaker Encoder → embedding .npy (1024 floats, 4 KB)
[Tablette - runtime]
embedding .npy (instantané)
+ texte
Talker LM (28 couches, bfloat16 CPU) → speech tokens
Code Predictor (5 couches, bfloat16) → 15 codebooks
Speech Decoder (Transformer + VQ + ConvNet) → audio WAV
```
## Performances actuelles (CPU bfloat16, 6 threads)
| Phrase | Tokens | Temps | Audio | RTF |
|--------|--------|-------|-------|-----|
| "Bonjour." | ~20 | 39.5s | 1.0s | 39.5x |
| "Bonjour, je suis ..." | ~50 | 109.4s | 2.6s | 41.5x |
**Goulot d'étranglement** : Le talker (28 couches transformer autorégressif) représente ~90% du temps.
## Estimation avec NPU
Le talker .pte a été testé à **90.7 tok/s** sur le NPU Hexagon (rapport précédent).
Sur CPU bfloat16, le talker fait ~0.5 tok/s (estimé d'après les temps).
| Composant | CPU actuel | NPU estimé |
|-----------|-----------|------------|
| Talker (50 tokens) | ~100s | **~0.6s** |
| Code predictor | ~3s | ~3s (CPU) |
| Speech decoder | ~6s | ~6s (CPU) |
| **Total** | **~109s** | **~10s** |
## Blocages pour l'intégration NPU
1. **`qnn_llama_runner` incompatible** : Le runner prend du texte brut et utilise un TEXT tokenizer. Le talker TTS attend des embeddings texte pré-calculés (via `text_projection`) + un speaker embedding. Les entrées/sorties ne correspondent pas.
2. **ExecuTorch Python pas dispo sur Termux** : Le package pip `executorch` n'a pas de wheel ARM. La compilation locale nécessiterait le NDK + CMake cross-compilation.
3. **Couplage talker ↔ code predictor** : Le code predictor est appelé à CHAQUE step du talker (pas après). Ses sorties (15 codebooks) sont ré-injectées dans le talker comme embeddings pour le step suivant.
## Solutions en cours d'exploration
### A. Service TTS résident (CPU bfloat16)
Script Python (`tts_service.py`) qui reste en mémoire avec le modèle chargé. L'app Android écrit une requête JSON, le service génère le WAV.
- **Avantage** : Fonctionne maintenant (validé)
- **Inconvénient** : ~40-110s par phrase (inutilisable en production)
### B. Compiler ExecuTorch Python pour Termux/ARM
Cross-compiler le binding Python ExecuTorch pour aarch64-android. Permettrait de charger le `.pte` et faire les forward passes sur NPU directement depuis Python.
- **Avantage** : Garderait le couplage talker code predictor
- **Difficulté** : Compilation cross-platform complexe
### C. Runner C++ custom pour le talker TTS
Modifier `qnn_llama_runner` pour accepter des embeddings pré-calculés au lieu de texte, et sortir des token IDs bruts.
- **Avantage** : Réutilise l'infra ExecuTorch existante
- **Difficulté** : Modification C++ du runner
### D. Pipeline découplé (talker NPU → code predictor CPU)
Accepter une qualité légèrement réduite en découplant : le talker NPU génère codebook 0, puis le code predictor génère codebooks 1-14 en un seul pass (pas step-by-step).
- **Avantage** : Plus simple à implémenter
- **Inconvénient** : Qualité potentiellement dégradée
## Fichiers déployés sur la tablette
```
/data/local/tmp/kazeia/
├── models/qwen3-tts/
│ ├── config.json, model.safetensors (1.7 GB)
│ ├── speech_tokenizer/model.safetensors (651 MB)
│ ├── tokenizer_config.json, vocab.json, merges.txt
│ └── voice_embeddings/
│ ├── damien_spk_embedding.npy (4 KB)
│ ├── elodie_spk_embedding.npy
│ └── ... (8 voix)
├── tts_service.py
├── tts_test.wav (dernier test)
└── kazeia-et/
└── hybrid_llama_qnn.pte (286 MB, talker NPU)
```
---
*Journal de tests — Claude Code (Opus 4.6)*