178 lines
8.3 KiB
Markdown
178 lines
8.3 KiB
Markdown
# Rapport complet TTS Qwen3-TTS — Kazeia
|
|
## 2026-04-01
|
|
|
|
---
|
|
|
|
## 1. Modele choisi
|
|
|
|
**Qwen3-TTS-12Hz-0.6B-Base** (Alibaba/Qwen)
|
|
- 0.6B parametres, 12 Hz codec (12 tokens/seconde audio)
|
|
- 16 codebooks par frame audio (hierarchiques)
|
|
- Architecture : Talker (28 layers, 1024 dim) + Code Predictor (5 layers, 1024 dim) + Speech Decoder (conv)
|
|
- Voice cloning obligatoire via x-vector (le modele Base n'a aucune voix built-in)
|
|
|
|
---
|
|
|
|
## 2. Architecture du pipeline
|
|
|
|
```
|
|
Texte → Tokenizer → [Prefill] → Talker → CB0 token
|
|
↓
|
|
Code Predictor → CB1-CB15
|
|
↓
|
|
16 codebooks → VQ Decode → Audio PCM
|
|
```
|
|
|
|
### Etapes detaillees :
|
|
|
|
1. **Prefill** (10 tokens) : `<|im_start|>assistant\n` + 4 tokens controle (think, think_bos, lang_fr, think_eos) + speaker embedding + bos + premier token texte
|
|
|
|
2. **Generation interleaved** (boucle autoregressive) :
|
|
- Talker forward → logits CB0 + hidden state
|
|
- Code Predictor (hidden, CB0_emb) → CB1-CB15 autoregressivement (15 steps)
|
|
- Somme 16 embeddings codebooks + texte trailing → input suivant du talker
|
|
- Sampling (temp=0.9, top_k=50, repetition_penalty=1.05)
|
|
- Arret sur EOS (token 2150)
|
|
|
|
3. **Decodage** : VQ lookup → pre_conv → preprocessor → conv_decoder → audio 24kHz
|
|
|
|
---
|
|
|
|
## 3. Reussites
|
|
|
|
### 3.1 Pipeline fonctionnel complet
|
|
- Audio de bonne qualite, voix clonee reconnaissable
|
|
- EOS naturel (le modele s'arrete seul)
|
|
- Fonctionne pour des phrases de longueur variable
|
|
- **RTF 7.1** (28s pour 4s audio)
|
|
|
|
### 3.2 Export ONNX valide
|
|
- **Talker KV-cache** : 1.77 GB, 28 layers, shapes fixes (KV=199), valide identique a PyTorch
|
|
- **CP fullseq** : 420 MB, 5 layers, shapes dynamiques, causal mask, 15/15 match vs PyTorch
|
|
- **CP KV-cache** : 420 MB, shapes fixes (KV=16), valide 15/15 match vs PyTorch
|
|
- **Decoder** (pre_conv + preprocessor + conv_decoder) : fonctionne sur NPU via ONNX Runtime QNN EP
|
|
|
|
### 3.3 Decodeur sur NPU
|
|
- pre_conv, preprocessor, conv_decoder : tous sur QNN NPU
|
|
- ~3s pour decoder un chunk de 60 tokens
|
|
- Pas de degradation de qualite
|
|
|
|
### 3.4 Bug critique trouve et corrige : tts_pad
|
|
- **Decouverte** : apres epuisement des tokens texte, le modele Python ajoute `tts_pad_embed` (pas des zeros)
|
|
- **Impact** : sans tts_pad, le modele ne converge JAMAIS vers EOS (100+ tokens sans arret)
|
|
- **Correction** : une ligne changee dans la boucle de generation
|
|
- C'etait LE bug qui empechait le pipeline de fonctionner correctement
|
|
|
|
### 3.5 Bug CP corrige : QK normalization
|
|
- Le CP utilise RMSNorm sur Q et K avant le rotary embedding (`q_norm`, `k_norm`)
|
|
- Notre premiere implementation manuelle de l'attention les oubliait → divergence totale au step 2
|
|
- Correction : ajout de `attn.q_norm()` et `attn.k_norm()` dans le wrapper
|
|
|
|
### 3.6 Export ExecuTorch .pte
|
|
- CP KV-cache exporte en .pte avec QNN fp16 backend (SM8750)
|
|
- **55ms pour 17 steps NPU** (vs 5.5s CPU) via le runner C++ standalone
|
|
- Pipeline d'export : torch.export → to_edge_transform_and_lower_to_qnn → .pte
|
|
- Contournement du bug `WrapWithSetGradEnabled` : pre-calcul des rotary cos/sin
|
|
|
|
### 3.7 JNI ExecuTorch integre dans l'app
|
|
- `libexecutorch.so` (49MB) compile pour arm64 avec QNN backend
|
|
- Classes Java ExecuTorch compilees dans un JAR local
|
|
- Dependances fbjni + soloader resolues
|
|
- **CP NPU via JNI : 79ms/step** (vs 353ms CPU = 4.5x plus rapide)
|
|
|
|
---
|
|
|
|
## 4. Echecs et limitations
|
|
|
|
### 4.1 NPU Talker (ONNX Runtime QNN EP)
|
|
- **Quantification par defaut (int8/int16)** : le talker diverge apres ~10 steps, produit du bruit
|
|
- **Options fp16** (`htp_precision`, `enable_htp_fp16_precision`) : aucun effet observable, probablement ignorees par le HTP backend
|
|
- **Resultat** : EOS premature (1.4-2.2s au lieu de 4s) ou degeneration
|
|
- **Cause** : la quantification automatique de ONNX Runtime QNN EP est trop agressive pour un modele autoregistratif
|
|
|
|
### 4.2 NPU CP (ONNX Runtime QNN EP)
|
|
- Meme probleme que le talker : les codebooks secondaires sont corrompus
|
|
- Cause une pause audible entre les mots (les embeddings de codebooks sont faux)
|
|
- Le modele ne converge pas vers EOS (185 tokens sans arret)
|
|
|
|
### 4.3 NPU CP (ExecuTorch fp16)
|
|
- Le runner standalone produit des codes en 55ms → rapide
|
|
- Mais les codes sont **completement differents** du CPU (0/15 match)
|
|
- L'audio genere est du bruit
|
|
- **Cause** : le fp16 change suffisamment les logits pour que l'argmax donne des codebooks differents, et l'autoregression amplifie
|
|
|
|
### 4.4 CP KV-cache avec buffer fixe
|
|
- Approche : KV padding a 16 positions, shift (drop oldest) a chaque step
|
|
- Fonctionne sur PC (valide 15/15 vs PyTorch)
|
|
- Sur tablette : degeneration au step 53+ (token 1894 x10)
|
|
- **Cause** : apres 16 steps, la position 0 (hidden state du talker) est perdue du cache → le modele perd le contexte initial
|
|
- **Solution adoptee** : revenir au CP fullseq (re-run la sequence complete a chaque step)
|
|
|
|
### 4.5 Subprocess NPU via su
|
|
- Le NPU (Hexagon DSP) necessite root (`su`) pour l'acces
|
|
- `su -c 'command'` ne transmet pas stdin/stdout au process enfant dans Java
|
|
- Named pipes (FIFO) causent un deadlock (blocking open bidirectionnel)
|
|
- **Solution** : JNI natif (elimine le besoin de subprocess)
|
|
|
|
### 4.6 Sampling vs Greedy
|
|
- **Greedy** : le modele ne genere JAMAIS EOS (gap logit de -19 a -28 entre EOS et top codec)
|
|
- **Sampling** : produit des resultats variables, mais converge vers EOS grace a la stochasticite
|
|
- La repetition penalty (1.05x par token unique) n'est pas suffisante seule pour pousser vers EOS
|
|
- C'est le sampling + tts_pad qui permet l'EOS naturel
|
|
|
|
---
|
|
|
|
## 5. Etat actuel du pipeline
|
|
|
|
| Composant | Backend | Temps/step | Total (~50 tok) | Statut |
|
|
|-----------|---------|-----------|---------|--------|
|
|
| Talker (28 layers, KV-cache) | CPU fp32 | 130ms | 6.5s | ✅ Fonctionne |
|
|
| CP fullseq (5 layers, seq 2→17) | CPU fp32 | 353ms | 17.7s | ✅ Fonctionne |
|
|
| Decoder (VQ + conv) | NPU QNN | — | 3.0s | ✅ Fonctionne |
|
|
| **Total** | | **483ms** | **~28s** | **RTF 7.1** |
|
|
|
|
---
|
|
|
|
## 6. Fichiers cles
|
|
|
|
### Sur le PC (/opt/Kazeia/)
|
|
- `models_qnn/qwen3-tts-onnx/` — tous les ONNX exports
|
|
- `models_qnn/cp_kv_fp16.pte` — CP ExecuTorch NPU (pret mais qualite insuffisante)
|
|
- `models_qnn/cp_data/` — embeddings + rotary pour le runner
|
|
- `executorch/build-android/` — libs compilees + cp_runner
|
|
- `executorch/examples/qualcomm/executor_runner/cp_runner.cpp` — source du runner
|
|
|
|
### Sur la tablette (/data/local/tmp/kazeia/)
|
|
- `models/qwen3-tts-npu/` — modeles ONNX + embeddings + codebooks
|
|
- `models/cp_kv_fp16.pte` — CP ExecuTorch
|
|
- `cp_runner` — binaire runner C++ (ARM64)
|
|
- `cp_data/` — donnees statiques pour le runner
|
|
|
|
### Dans l'app (kazeia-android/)
|
|
- `app/src/main/java/com/kazeia/tts/Qwen3TtsEngine.kt` — moteur TTS complet
|
|
- `app/src/main/jniLibs/arm64-v8a/libexecutorch.so` — JNI ExecuTorch (49MB)
|
|
- `app/src/main/jniLibs/arm64-v8a/libfbjni.so` — dependance JNI
|
|
- `app/src/main/jniLibs/arm64-v8a/libqnn_executorch_backend.so` — backend QNN
|
|
- `app/libs/executorch.jar` — classes Java ExecuTorch
|
|
|
|
---
|
|
|
|
## 7. Pistes d'optimisation (non explorees)
|
|
|
|
1. **Quantification calibree** (use_16a8w avec donnees de calibration) — pourrait preserver la qualite sur NPU
|
|
2. **Export talker en .pte fp32** — ExecuTorch CPU pourrait etre plus rapide que ONNX Runtime
|
|
3. **Streaming decode** — decoder le premier chunk pendant la generation du deuxieme
|
|
4. **NNAPI EP** — backend GPU Adreno (pas HTP) pour le talker/CP
|
|
5. **Modele TTS plus petit** — SpeechT5 ou VITS pour un RTF < 1 au detriment de la qualite
|
|
6. **KV-cache CP correct** — augmenter CP_KV_LEN a 17 (au lieu de 16) pour ne pas perdre la position 0
|
|
|
|
---
|
|
|
|
## 8. Lecons apprises
|
|
|
|
1. **Les modeles TTS sont beaucoup plus sensibles a la precision que les LLM** — le LLM Qwen3 tourne a 90 tok/s en int4 sur NPU, mais le TTS ne supporte meme pas le fp16
|
|
2. **L'autoregression amplifie les erreurs** — une petite erreur au step N se propage et s'amplifie aux steps N+1, N+2...
|
|
3. **Le debug embedding-level est essentiel** — sans comparer tensor par tensor avec PyTorch, impossible de trouver les bugs (tts_pad, q_norm/k_norm)
|
|
4. **ONNX Runtime QNN EP != ExecuTorch QNN** — deux stacks completement differentes avec des comportements de quantification differents
|
|
5. **Le `su` Android est un cauchemar pour l'IPC** — stdin/stdout ne passent pas, il faut du JNI natif ou des fichiers
|