# 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 là 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 là..." | ~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)*