# 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 ```bash /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 ```python 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.