5.8 KiB
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
-
qnn_llama_runnerincompatible : Le runner prend du texte brut et utilise un TEXT tokenizer. Le talker TTS attend des embeddings texte pré-calculés (viatext_projection) + un speaker embedding. Les entrées/sorties ne correspondent pas. -
ExecuTorch Python pas dispo sur Termux : Le package pip
executorchn'a pas de wheel ARM. La compilation locale nécessiterait le NDK + CMake cross-compilation. -
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)