kazeia/TTS_CALIBRATION_GUIDE.md

152 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.