152 lines
5.3 KiB
Markdown
152 lines
5.3 KiB
Markdown
# 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.
|