kazeia/TTS_CALIBRATION_GUIDE.md

5.3 KiB
Raw Permalink Blame History

Guide de calibration TTS Qwen3-TTS pour NPU

ExecuTorch + QNN quantification calibrée


1. Pourquoi la calibration

Le fp16 brut (sans calibration) échoue sur les modèles TTS :

  • CP fp16 : produit du bruit (codebooks complètement faux)
  • Talker fp16 : produit du silence (tokens dans un mauvais régime)
  • Cause : l'autoregression amplifie les erreurs de précision

La calibration observe les plages d'activation réelles du modèle et ajuste la quantification pour minimiser la distorsion. C'est la technique utilisée pour le LLM Qwen3-0.6B (93 tok/s sur NPU).


2. Phrases de calibration

Fichier : models_qnn/calibration_phrases.json

10 langues × 5 phrases = 50 phrases couvrant :

  • Chinois, Anglais, Allemand, Espagnol, Japonais
  • Français, Coréen, Russe, Italien, Portugais

Chaque phrase couvre des phonèmes variés, prosodie (questions, exclamations), et cas difficiles.


3. Collecte des tenseurs de calibration

Prérequis

  • Python 3.10 dans /opt/Kazeia/qnn_venv/
  • Modèle Qwen3-TTS dans le cache HuggingFace
  • Speaker embedding Damien dans models_qnn/qwen3-tts-embeddings/

Commande

/opt/Kazeia/qnn_venv/bin/python3 /opt/Kazeia/models_qnn/collect_calibration.py

Script : models_qnn/collect_calibration.py

Le script :

  1. Charge le modèle Qwen3-TTS complet en fp32
  2. Hook le talker.model.forward et code_predictor.model.forward
  3. Pour chaque phrase, lance model.generate() avec le pipeline complet (sampling, tts_pad, voice cloning)
  4. Sauvegarde les inputs_embeds de chaque forward pass

Sortie

models_qnn/calibration_data/
├── talker_inputs/     # ~2500 tenseurs .pt ([1, 1, 1024])
│   ├── french_0_step0.pt
│   ├── french_0_step1.pt
│   └── ...
└── cp_inputs/         # ~37000 tenseurs .pt ([1, 2..17, 1024])
    ├── french_0_call0.pt
    └── ...

Durée : ~30-60 minutes sur CPU


4. Export avec calibration (étape suivante)

Pipeline ExecuTorch

from torch.ao.quantization.quantize_pt2e import prepare_pt2e, convert_pt2e
from executorch.backends.qualcomm.quantizer import QnnQuantizer, QuantDtype

# 1. Créer le wrapper (même que pour l'export fp16)
wrapper = TalkerKVWrapper(model.talker.model, model.talker.codec_head)

# 2. torch.export
exported = torch.export.export(wrapper, example_inputs, strict=False)

# 3. Préparer la quantification
quantizer = QnnQuantizer()
quantizer.set_quant_config(QuantDtype.use_16a8w)  # 16-bit activations, 8-bit weights
prepared = prepare_pt2e(exported, quantizer)

# 4. Calibration : rejouer les tenseurs collectés
for tensor_file in calibration_files:
    inputs = torch.load(tensor_file)
    prepared(*rebuild_full_inputs(inputs))

# 5. Convertir
quantized = convert_pt2e(prepared)

# 6. Export QNN
edge = to_edge_transform_and_lower_to_qnn(quantized, ...)
pte = edge.to_executorch()

Schémas de quantification à tester

Schéma Poids Activations KV cache Taille estimée
use_16a8w 8-bit 16-bit 16-bit ~900 MB
use_16a4w 4-bit 16-bit 16-bit ~500 MB
use_8a8w 8-bit 8-bit 8-bit ~450 MB

Recommandation : commencer par use_16a8w (le plus conservateur), puis tester use_16a4w si la qualité est bonne.


5. Fichiers clés

Fichier Description
models_qnn/calibration_phrases.json 50 phrases en 10 langues
models_qnn/collect_calibration.py Script de collecte
models_qnn/calibration_data/ Tenseurs de calibration
models_qnn/qwen3-tts-onnx/talker_rotary_cos.npy M-RoPE pré-calculé talker
models_qnn/qwen3-tts-onnx/talker_rotary_sin.npy M-RoPE pré-calculé talker
models_qnn/qwen3-tts-onnx/cp_rotary_cos.npy RoPE pré-calculé CP
models_qnn/qwen3-tts-onnx/cp_rotary_sin.npy RoPE pré-calculé CP

6. Wrappers PyTorch validés

Talker wrapper (28 layers, M-RoPE)

  • Inputs : [emb(1,1,1024), mask(1,1,1,200), cos(1,1,128), sin(1,1,128), 56×kv(1,8,199,128)]
  • Outputs : [hidden(1,1,1024), logits(1,1,3072), 56×kv(1,8,200,128)]
  • Validé identique à PyTorch (diff logits < 0.00006)
  • M-RoPE pré-calculé avec apply_interleaved_rope pour les 3 axes (identiques en TTS)

CP wrapper (5 layers, RoPE standard)

  • Inputs : [emb(1,1,1024), mask(1,1,1,17), cos(1,1,128), sin(1,1,128), 10×kv(1,8,16,128)]
  • Outputs : [hidden(1,1,1024), head_logits(1,15,2048), 10×kv(1,8,17,128)]
  • Validé 15/15 match vs PyTorch
  • Inclut projection + 15 heads dans le même modèle

Points critiques (bugs trouvés et corrigés)

  1. q_norm / k_norm : RMSNorm sur Q et K avant rotary — obligatoire
  2. M-RoPE interleaved : le talker utilise un rotary multimodal à 3 axes
  3. WrapWithSetGradEnabled : contourné en pré-calculant cos/sin
  4. tts_pad après texte : le modèle attend tts_pad_embed (pas des zeros) après l'EOS texte

7. Résultats NPU sans calibration (pour référence)

Config Vitesse Qualité EOS
Talker fp16 .pte 67ms/step Silence Oui (step 60)
CP fp16 .pte 55ms/17 steps Bruit Non (degeneration)
Talker ONNX QNN int8 ~20ms/step EOS prématuré 1.4-2.2s
CP ONNX QNN int8 ~4ms/step Pause/bruit Non

La calibration vise à obtenir la vitesse NPU avec la qualité CPU.