244 lines
11 KiB
Markdown
244 lines
11 KiB
Markdown
# Rapport TTS On-Device NPU — Problèmes et Solutions
|
||
|
||
**Date** : 29 mars 2026
|
||
**Contexte** : Kazeia — chatbot émotionnel on-device sur OnePlus Pad 3 (Snapdragon 8 Elite, HTP V79)
|
||
**Objectif** : TTS multilingue avec voice cloning, entièrement sur NPU
|
||
|
||
---
|
||
|
||
## 1. Exigences
|
||
|
||
| Critère | Requis |
|
||
|---------|--------|
|
||
| **Multilingue** | Français + anglais minimum |
|
||
| **Voice cloning** | Cloner une voix à partir d'un échantillon WAV (~5-10s) |
|
||
| **On-device** | Aucun appel réseau, tout local |
|
||
| **NPU** | Le composant le plus lourd doit tourner sur le HTP Qualcomm |
|
||
| **Latence** | < 3s pour une phrase courte (temps réel acceptable) |
|
||
| **Qualité** | Voix naturelle, intelligible, prosodie correcte |
|
||
|
||
---
|
||
|
||
## 2. Candidats évalués
|
||
|
||
### 2.1 Chatterbox Multilingual (ONNX)
|
||
|
||
| | Détail |
|
||
|---|---|
|
||
| **Source** | `onnx-community/chatterbox-multilingual-ONNX` |
|
||
| **Architecture** | Speech Encoder (591 MB) + Embed Tokens (68 MB) + Language Model 30L (291-2000 MB) + Conditional Decoder (534 MB) |
|
||
| **Multilingue** | Oui (23 langues, tag `[fr]`, `[en]`, etc.) |
|
||
| **Voice cloning** | Oui (speaker embedding extrait de l'audio de référence) |
|
||
| **Format** | ONNX (FP32, FP16, Q4F16) |
|
||
|
||
### 2.2 Qwen3-TTS 0.6B Base (PyTorch)
|
||
|
||
| | Détail |
|
||
|---|---|
|
||
| **Source** | `Qwen/Qwen3-TTS-12Hz-0.6B-Base` |
|
||
| **Architecture** | Speaker Encoder (8.9M) + Talker LM 28L (754M) + Code Predictor 5L (141M) + Speech Decoder (114M) |
|
||
| **Multilingue** | Oui (français, anglais, etc.) |
|
||
| **Voice cloning** | Oui (x-vector du speaker encoder) |
|
||
| **Format** | PyTorch natif (le Talker LM est aussi exporté en ExecuTorch .pte) |
|
||
|
||
---
|
||
|
||
## 3. Problèmes rencontrés
|
||
|
||
### 3.1 Chatterbox — Opérateurs ONNX non standard
|
||
|
||
**Problème central** : Les modèles ONNX de Chatterbox utilisent des **opérateurs Microsoft custom** qui ne sont supportés ni par QNN (Qualcomm) ni par AI Hub :
|
||
|
||
| Opérateur | Domaine | Utilisations | Problème |
|
||
|-----------|---------|-------------|----------|
|
||
| `GroupQueryAttention` | `com.microsoft` | 30 (1 par couche) | Non supporté par QNN/AI Hub |
|
||
| `SkipSimplifiedLayerNormalization` | `com.microsoft` | 60 | Non supporté par QNN/AI Hub |
|
||
| `SimplifiedLayerNormalization` | ONNX opset 21 | 1 | Non supporté par QNN (opset trop récent) |
|
||
|
||
Ces opérateurs sont des **optimisations internes d'ONNX Runtime** (fusion GQA, skip-connection + layernorm fusionnés). Ils fonctionnent sur CPU via ORT mais ne peuvent pas être compilés pour le NPU Qualcomm.
|
||
|
||
**Conséquence** : Le language model (30 couches, ~85% du temps de calcul) tourne entièrement sur **CPU** à ~1 tok/s sur la tablette. Sur PC, il tourne à ~45 tok/s (CPU x86 plus puissant).
|
||
|
||
**Tentatives de résolution** :
|
||
1. ✅ Compilation AI Hub avec opset 21 → Échec (`SimplifiedLayerNormalization` non supporté)
|
||
2. ✅ Patch opset 21→17 + remplacement LayerNorm → Échec (`int64` non supporté)
|
||
3. ✅ Ajout `--truncate_64bit_io` → Échec (`GroupQueryAttention` non supporté)
|
||
4. ❌ Le modèle FP32 utilise aussi `GroupQueryAttention`
|
||
5. ❌ Le modèle Q4F16 utilise aussi `GroupQueryAttention`
|
||
|
||
**Solution potentielle** : Retrouver le modèle PyTorch original de Chatterbox et le ré-exporter en ONNX avec des opérateurs standard (attention multi-head classique au lieu de GQA fusionné). Le modèle source est sur HuggingFace (`resemble-ai/chatterbox-multilingual`) mais l'export ONNX standard n'a pas été publié.
|
||
|
||
**Autre problème constaté** : La variante Q4F16 (quantifiée INT4) produit de l'audio de **mauvaise qualité** sur la tablette — le son "ne correspond à rien" selon le test utilisateur. Sur PC, le même modèle Q4F16 fonctionne correctement (63 tokens, stop token atteint, 2.5s d'audio). La différence pourrait venir de la précision des opérations INT4 sur ARM vs x86.
|
||
|
||
### 3.2 Qwen3-TTS — Speech Decoder non exportable
|
||
|
||
**Problème central** : Le pipeline Qwen3-TTS est composé de 4 modules dont **seuls 2 sont exportables** :
|
||
|
||
| Module | Export ONNX | Export ExecuTorch | Bloqueur |
|
||
|--------|------------|-------------------|----------|
|
||
| **Speaker Encoder** (8.9M) | ⚠️ Non testé (probablement OK) | Non tenté | Conv1D simple |
|
||
| **Talker LM** (754M) | ❌ Échoue | ✅ **Fonctionne** (90.7 tok/s NPU) | — |
|
||
| **Code Predictor** (141M) | ✅ **Exporté** (440 MB) | Non tenté | — |
|
||
| **Speech Decoder** (114M) | ❌ **Échoue** | ❌ Échoue | `SplitResidualVectorQuantizer` + `SnakeBeta` |
|
||
|
||
Le **Speech Decoder** est le bloqueur. Il contient :
|
||
|
||
1. **`SplitResidualVectorQuantizer`** : Utilise `torch.autograd.Function` avec `vmap` — une fonctionnalité PyTorch avancée incompatible avec tout export (ONNX legacy, dynamo, jit.trace). C'est le composant qui convertit les indices de codebook en vecteurs continus.
|
||
|
||
2. **`SnakeBeta`** activation : Bien que son `forward()` soit du PyTorch standard (`x + sin²(αx)/β`), elle est utilisée dans des blocs qui contiennent aussi le VQ, rendant l'export impossible pour l'ensemble.
|
||
|
||
**Tentatives de résolution** :
|
||
1. ✅ Export ONNX legacy (`torch.onnx.export`) → `RuntimeError: unordered_map::at` (vmap)
|
||
2. ✅ Export dynamo (`torch.onnx.export(dynamo=True)`) → Échec (strict et non-strict)
|
||
3. ✅ Export TorchScript (`torch.jit.trace`) → `RuntimeError: unordered_map::at`
|
||
4. ✅ Décomposition en sous-modules (pre_conv, pre_transformer, conv_decoder) → Le VQ bloque toujours
|
||
5. ✅ Export du code predictor seul → Réussi (mais inutile sans le speech decoder)
|
||
|
||
**Solution potentielle** : Réécrire le `SplitResidualVectorQuantizer.decode()` en opérations PyTorch basiques (embedding lookups + Conv1d projections) sans utiliser `torch.autograd.Function` ni `vmap`. Les poids des codebooks ont été extraits en numpy. Cela demande de comprendre précisément le flow de données du VQ decode.
|
||
|
||
---
|
||
|
||
## 4. Résumé comparatif
|
||
|
||
| Critère | Chatterbox ONNX | Qwen3-TTS |
|
||
|---------|----------------|-----------|
|
||
| **Multilingue** | ✅ 23 langues | ✅ Multilingue |
|
||
| **Voice cloning** | ✅ | ✅ (x-vector) |
|
||
| **Fonctionne sur CPU tablette** | ✅ (très lent, ~1 tok/s) | ❌ (nécessite PyTorch = Termux) |
|
||
| **NPU compilable** | ❌ (ops Microsoft custom) | ⚠️ Partiel (Talker OK, decoder bloqué) |
|
||
| **Qualité Q4F16** | ⚠️ Mauvaise sur ARM | N/A |
|
||
| **Qualité FP16/FP32** | ✅ Bonne (PC) | ✅ Bonne (PC) |
|
||
| **Taille totale** | ~1.5 GB (Q4F16) | ~1.0 GB (Talker .pte + reste) |
|
||
| **Vitesse estimée NPU** | ~45 tok/s (si compilable) | ~90 tok/s (Talker déjà validé) |
|
||
|
||
---
|
||
|
||
## 5. Chemins de résolution
|
||
|
||
### Option A : Ré-exporter Chatterbox depuis PyTorch (recommandé)
|
||
|
||
**Principe** : Charger le modèle PyTorch original (`resemble-ai/chatterbox-multilingual`), désactiver les optimisations ORT, et exporter en ONNX standard.
|
||
|
||
**Avantages** :
|
||
- Le pipeline complet est déjà implémenté dans l'app Android (`ChatterboxTtsEngine.kt`)
|
||
- Speech encoder, embed tokens, et conditional decoder tournent déjà sur CPU (petits, rapides)
|
||
- Seul le language model a besoin du NPU
|
||
|
||
**Étapes** :
|
||
1. Charger `resemble-ai/chatterbox-multilingual` en PyTorch
|
||
2. Exporter le language model en ONNX opset 17 avec attention standard (pas GQA fusionné)
|
||
3. Compiler via AI Hub pour SM8750
|
||
4. Remplacer le `language_model_q4f16.onnx` par la version QNN precompiled
|
||
5. Les 3 autres modèles restent en ONNX CPU
|
||
|
||
**Risques** : Le modèle PyTorch original pourrait ne pas être public ou avoir une architecture différente des ONNX publiés.
|
||
|
||
**Estimation** : 2-4h de travail si le modèle PyTorch est accessible.
|
||
|
||
### Option B : Réécrire le VQ decode de Qwen3-TTS
|
||
|
||
**Principe** : Remplacer le `SplitResidualVectorQuantizer` par des opérations ONNX-compatibles (embedding lookups).
|
||
|
||
**Avantages** :
|
||
- Le Talker tourne déjà à 90 tok/s sur NPU
|
||
- Le Code Predictor est déjà exporté en ONNX
|
||
- Qualité TTS supérieure (Qwen3 est plus récent)
|
||
|
||
**Étapes** :
|
||
1. Analyser le flow de `quantizer.decode()` (codebook lookup + projection + sommation)
|
||
2. Réimplémenter en PyTorch sans `vmap` ni `autograd.Function`
|
||
3. Exporter le speech decoder complet en ONNX
|
||
4. Intégrer dans l'app Android
|
||
|
||
**Risques** : La réimplémentation du VQ pourrait introduire des différences numériques affectant la qualité audio.
|
||
|
||
**Estimation** : 4-8h de travail.
|
||
|
||
### Option C : Chatterbox CPU avec optimisations
|
||
|
||
**Principe** : Garder Chatterbox sur CPU mais optimiser :
|
||
- Utiliser NNAPI EP au lieu de CPU pur (délègue certaines ops au DSP)
|
||
- Réduire le nombre de tokens max (limiter à ~50 tokens au lieu de 512)
|
||
- Pré-encoder les voix au premier lancement (éviter le coût du speech encoder)
|
||
|
||
**Avantages** : Pas de recompilation nécessaire, fonctionne maintenant.
|
||
|
||
**Inconvénients** : Toujours lent (~1-5 tok/s), latence de 10-30s par phrase.
|
||
|
||
### Option D : TTS léger (Piper) comme solution intermédiaire
|
||
|
||
**Principe** : Utiliser Piper TTS (VITS, ~30 MB) pour avoir du TTS français fonctionnel immédiatement, en parallèle du travail sur Chatterbox/Qwen3-TTS NPU.
|
||
|
||
**Avantages** :
|
||
- Modèles ONNX standard, très légers
|
||
- Latence ~100ms
|
||
- Français disponible
|
||
- Pas de compilation NPU nécessaire
|
||
|
||
**Inconvénients** :
|
||
- Pas de voice cloning
|
||
- Qualité inférieure (voix synthétique)
|
||
- Une seule voix par modèle
|
||
|
||
---
|
||
|
||
## 6. Recommandation
|
||
|
||
**Court terme** : Option A (ré-export Chatterbox PyTorch) est la voie la plus prometteuse. Le pipeline Android est déjà prêt, seul le language model a besoin du NPU. Si le modèle PyTorch est accessible, c'est réalisable rapidement.
|
||
|
||
**Moyen terme** : Option B (Qwen3-TTS VQ rewrite) donnerait les meilleures performances (Talker déjà à 90 tok/s NPU) mais demande plus de travail d'ingénierie.
|
||
|
||
**Fallback** : Option D (Piper) comme TTS temporaire pendant le développement NPU.
|
||
|
||
---
|
||
|
||
## 7. Fichiers et ressources disponibles
|
||
|
||
### Modèles Chatterbox (sur serveur)
|
||
```
|
||
/opt/Kazeia/models_qnn/chatterbox-tts/onnx/
|
||
├── speech_encoder.onnx (+data, 591 MB)
|
||
├── embed_tokens.onnx (+data, 68 MB)
|
||
├── language_model.onnx (+data, 2081 MB FP32)
|
||
├── language_model_fp16.onnx (+data, 1040 MB)
|
||
├── language_model_q4f16.onnx (+data, 305 MB)
|
||
└── conditional_decoder.onnx (+data, 534 MB)
|
||
```
|
||
|
||
### Modèles Qwen3-TTS (sur serveur)
|
||
```
|
||
/opt/Kazeia/models_qnn/qwen3-tts-executorch/
|
||
├── hybrid_llama_qnn.pte (286 MB, Talker NPU ✅)
|
||
└── tokenizer.json
|
||
|
||
/opt/Kazeia/models_qnn/qwen3-tts-onnx/
|
||
├── code_predictor_transformer.onnx (314.8 MB ✅)
|
||
├── code_predictor_heads.onnx (125.8 MB ✅)
|
||
├── code_predictor_embeddings.npy
|
||
└── speech_decoder_pre_conv.onnx (6.3 MB ✅)
|
||
|
||
/opt/Kazeia/models_qnn/qwen3-tts-native/
|
||
├── speech_decoder_weights.pt (437 MB)
|
||
├── code_predictor_weights.pt (541 MB)
|
||
├── speaker_encoder_weights.pt (34 MB)
|
||
└── text_components.pt (1.2 GB)
|
||
```
|
||
|
||
### Voix de référence (sur tablette)
|
||
```
|
||
/data/local/tmp/kazeia/voix/
|
||
├── damien.wav, elodie.wav, jerome.wav, richard.wav
|
||
├── amir.wav, didier.wav, sid.wav, zelda.wav
|
||
```
|
||
|
||
### Code Android
|
||
```
|
||
app/src/main/java/com/kazeia/tts/
|
||
├── ChatterboxTtsEngine.kt (pipeline complet, KV-cache, voice cloning)
|
||
├── AndroidTtsEngine.kt (fallback Google TTS)
|
||
```
|
||
|
||
---
|
||
|
||
*Rapport généré par Claude Code (Opus 4.6) — Projet Kazeia*
|