kazeia/kazeia-android/RAPPORT_TTS_QWEN3_TESTS.md

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

  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)