kazeia/TTS_REPORT.md

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 :

  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