8.3 KiB
8.3 KiB
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 :
-
Prefill (10 tokens) :
<|im_start|>assistant\n+ 4 tokens controle (think, think_bos, lang_fr, think_eos) + speaker embedding + bos + premier token texte -
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)
-
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()etattn.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 exportsmodels_qnn/cp_kv_fp16.pte— CP ExecuTorch NPU (pret mais qualite insuffisante)models_qnn/cp_data/— embeddings + rotary pour le runnerexecutorch/build-android/— libs compilees + cp_runnerexecutorch/examples/qualcomm/executor_runner/cp_runner.cpp— source du runner
Sur la tablette (/data/local/tmp/kazeia/)
models/qwen3-tts-npu/— modeles ONNX + embeddings + codebooksmodels/cp_kv_fp16.pte— CP ExecuTorchcp_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 completapp/src/main/jniLibs/arm64-v8a/libexecutorch.so— JNI ExecuTorch (49MB)app/src/main/jniLibs/arm64-v8a/libfbjni.so— dependance JNIapp/src/main/jniLibs/arm64-v8a/libqnn_executorch_backend.so— backend QNNapp/libs/executorch.jar— classes Java ExecuTorch
7. Pistes d'optimisation (non explorees)
- Quantification calibree (use_16a8w avec donnees de calibration) — pourrait preserver la qualite sur NPU
- Export talker en .pte fp32 — ExecuTorch CPU pourrait etre plus rapide que ONNX Runtime
- Streaming decode — decoder le premier chunk pendant la generation du deuxieme
- NNAPI EP — backend GPU Adreno (pas HTP) pour le talker/CP
- Modele TTS plus petit — SpeechT5 ou VITS pour un RTF < 1 au detriment de la qualite
- KV-cache CP correct — augmenter CP_KV_LEN a 17 (au lieu de 16) pour ne pas perdre la position 0
8. Lecons apprises
- 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
- L'autoregression amplifie les erreurs — une petite erreur au step N se propage et s'amplifie aux steps N+1, N+2...
- Le debug embedding-level est essentiel — sans comparer tensor par tensor avec PyTorch, impossible de trouver les bugs (tts_pad, q_norm/k_norm)
- ONNX Runtime QNN EP != ExecuTorch QNN — deux stacks completement differentes avec des comportements de quantification differents
- Le
suAndroid est un cauchemar pour l'IPC — stdin/stdout ne passent pas, il faut du JNI natif ou des fichiers