doc: PERF_ANALYSIS.md — diagnostic post-session, leviers triés ROI

Mesures (Pad3, CPU 4t/8t, KZTTS_CP_CACHE=1, 31 frames) :
  pipeline 7.9s / 2.58s audio = RTF 3.07
  breakdown : prefill 1.3% / talker 12.6% / CP 43% / decoder 43%
  decoder breakdown via KZTTS_DECODER_PROFILE=1 :
    BigVGAN 95.3% (3.24s), upsample 2.8%, reste <1% chacun

Diagnostic :
  - CP BW-bound : 15 sub-forwards/frame stream 125 MB de poids f16 chacun,
    thread sweep 4/6/8 plat = pas compute-bound. ~75% overhead ggml_init/free.
  - BigVGAN compute-bound NEON : ~500 Gops, mesure 3.2s = ~95% du plafond
    NEON. Pas de gain multi-thread.
  - Talker pas un levier (1s total).

Leviers ROI décroissant :
  B. CP ctx réutilisé entre 15 sub-forwards (½ session, RTF 3.07->2.4, bit-exact)
  A. Streaming pipeline talker/CP/decoder (1 session, TTFB ~500ms, bit-exact)
  F. CP forward sur HMX V79 (1 session, RTF->~1.5, réutilise pattern Qwen3.5)
  E. BigVGAN HMX (conv1d via im2col+matmul fp16 tile, 2-3 semaines, RTF->~0.7)

Cible RTF<1 demande E (gros chantier kernel hexagon). Cible RTF~2 avec TTFB
500ms = B+A en 2 sessions, suffit probablement pour démo app.

Le patch decoder.cpp pour le profile (KZTTS_DECODER_PROFILE=1) est local dans
/opt/Kazeia/kazeia-tts-decoder-ggml, pas dans Kazeia-engine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Richard Loyer 2026-05-28 22:43:23 +02:00
parent 930f3c8c1f
commit 1c25c975dc
1 changed files with 172 additions and 0 deletions

172
dist/PERF_ANALYSIS.md vendored Normal file
View File

@ -0,0 +1,172 @@
# Analyse perf TTS Kazeia — 28/05 soir
État après les 4 chantiers de la session (RTF 4.99 → 3.07). Mesures sur Pad3,
CPU 4 threads talker / 8 threads decoder, KZTTS_CP_CACHE=1, phrase « Bonjour je m'appelle
Kazeia » → 31 frames audio (2.58s).
## 1. Breakdown précis (mesuré)
| Étape | Temps | %total | Par frame | RTF partiel |
|---|---:|---:|---:|---:|
| Prefill talker | 0.10 s | 1.3% | 3 ms | 0.04 |
| **Talker loop** (decode 31×1 token) | 1.00 s | 12.6% | 33 ms | 0.39 |
| **CP loop** (31× (1 prefill + 14 decode)) | 3.40 s | 43.0% | 110 ms | 1.32 |
| **Decoder** (5 stages) | 3.40 s | 43.0% | — | 1.32 |
| TOTAL | 7.90 s | 100% | | **3.06** |
**Decoder breakdown** (mesuré via `KZTTS_DECODER_PROFILE=1`) :
| Stage | Temps | % decoder |
|---|---:|---:|
| 1. Quantizer (16 embeddings lookup) | 18 ms | 0.5% |
| 2. Pre-conv (k=7) | 16 ms | 0.5% |
| 3. Pre-transformer (8 layers attn) | 30 ms | 0.9% |
| 4. Upsample (2 stages ×4) | 95 ms | 2.8% |
| **5. BigVGAN** (4 blocks + final, total ×480) | **3 240 ms** | **95.3%** |
**Conclusion immédiate** : 86% du pipeline est dans deux blocs (CP loop + BigVGAN).
Toute perf doit attaquer un de ces deux. Le reste est négligeable.
## 2. Pourquoi c'est lent (diagnostic)
### CP : 110 ms/frame, **BW-bound** sur ggml-cpu
15 sub-forwards par frame (1 prefill 2 tokens + 14 decode 1 token via KV cache).
Chaque sub-forward stream ~25 MB de poids f16 par layer × 5 layers = 125 MB.
À 77 GB/s LPDDR5x → 1.6 ms BW pur par sub-forward. **Mesuré 7 ms/sub-forward**, soit
~25% compute et ~75% overhead (ggml_init/free, graph build, alloc work buffers).
Confirmation BW-bound : thread sweep 4 vs 6 vs 8 → CP plat à ±5%. Le compute ne fait
pas la différence, c'est la mémoire qui plafonne.
### BigVGAN : 3.2 s, **compute-bound** sur ggml-cpu NEON
Estimation analytique : ~500 Gops sur le forward complet (conv1d k=7 dilatés +
convtranspose1d avec stride up_rates=[8,5,4,3]). Sur 8 threads NEON 4-wide
~150 GFlops total → ~3.3 s. **Cohérent avec la mesure (3.2 s)** → on est à ~95%
du plafond NEON. Pas de gain à attendre du multi-thread (déjà saturé).
Hot inner ops : `ggml_conv_1d` et `ggml_conv_transpose_1d` qui appellent
mul_mat NEON après im2col. Pas de path HVX/HMX dans ggml-cpu.
### Talker : 33 ms/frame, presque OK
llama.cpp standard decode 1 token. Pas un goulot. La piste HTP prefill talker existe
(`use_htp=true`) mais elle ne gagne que sur le prefill (0.1 s) — la boucle decode
reste CPU. Gain potentiel : <0.05 s. **Pas un levier.**
## 3. Plafonds théoriques par hardware
| Composant | Sur ce qu'on a | Plafond CPU NEON | Plafond HVX V79 | Plafond HMX V79 |
|---|---:|---:|---:|---:|
| CP loop | 3.4 s | ~3.0 s (BW-saturée) | ~1.5 s (×2.2) | ~0.6 s (×5.7) |
| BigVGAN | 3.2 s | ~3.0 s | ~1.7 s (×1.9) | **~0.3 s (×10)** |
**HMX (matmul fp16 tile 32×32) gagne énormément sur BigVGAN** parce que les conv1d
deviennent des im2col + matmul fp16, exactement ce que les tiles HMX font à 1 TFlops
crête par contexte. 4 contextes HMX V79 = ~5 TFlops crête, ~1.5 TFlops réel.
## 4. Leviers triés par ROI (gain × probabilité / effort)
### A. **Streaming pipeline** — gain TTFB, RTF inchangé
- Refactorer pour qu'audio commence à sortir dès que les premières frames du talker
sont disponibles, plutôt que d'attendre la fin de la phrase.
- Le decoder peut traiter par chunks (causal-ish), mais BigVGAN a un kernel k=7
→ dépendance gauche sur ~3 frames.
- Pipeline talker→CP→decoder en parallèle : temps total ≈ max des trois + overhead
de chunking → **4-5 s au lieu de 7.9 s pour la même phrase, et TTFB ~500 ms**
au lieu de ~5 s.
- **Effort** : 1 session de refactor.
- **Risque** : faible. Bit-exact possible.
- **Gain RTF perçu** : massif (du « 5× plus lent que temps réel » au « le son
commence dans une demi-seconde et continue »).
### B. **CP : ctx ggml réutilisé entre sub-forwards** — réduit overhead
- Actuellement 15× ggml_init/free + 15× graph build par frame.
- Réutiliser un seul ggml_context + ggml_gallocr persistant → graph build une fois,
re-compute 15 fois.
- Si overhead = 5 ms/sub-forward × 15 = 75 ms/frame, on espère gagner 50 ms/frame.
- **Effort** : ~½ session (~80 lignes de refactor dans cp_forward_cached_step).
- **Risque** : faible. Bit-exact attendu (mêmes ops, juste alloc différente).
- **Gain mesuré** : CP 110 ms → ~60 ms espérés = **RTF total ~2.4** au lieu de 3.07.
### C. **Decoder : ctx ggml réutilisé + buffer mem 12GB→raisonnable**
- BigVGAN alloue 12 GB par appel. Probablement minor mais à vérifier.
- Réutilisation entre frames si on chunke (couplé à A).
- **Gain estimé** : 5-15% sur decoder, soit 0.2-0.5 s.
- **Effort** : ½ session.
### D. **BigVGAN sur backend Vulkan (Adreno)** — gain raisonnable
- Adreno 750 + ggml-vulkan : conv1d devient compute shader.
- Probable gain ×3-4 sur BigVGAN si bien câblé (mais ggml-vulkan + conv1d a des
trous).
- **Effort** : ~2 sessions (build vulkan, test, fix les ops manquantes).
- **Risque** : moyen (état ggml-vulkan + conv1d mal connu).
- **Gain** : BigVGAN 3.2 s → 1 s = RTF total **~2.0**.
### E. **BigVGAN sur HMX V79** — le gros gain frontière
- Porter conv1d + convtranspose1d en kernels HMX via im2col + matmul fp16 tile.
- Existant : ggml-hexagon backend a déjà MUL_MAT HMX (utilisé pour Qwen3.5 prefill).
Manque : `supported_op` pour CONV_1D doit retourner true + le kernel im2col HVX.
- **Effort** : 1-2 semaines. Probablement bit-exact perdu (fp16 tile) mais audio
est tolérant.
- **Gain** : BigVGAN 3.2 s → 0.3-0.5 s = RTF total **~1.0**.
- **Risque** : moyen-élevé (kernel HVX bit-exact n'est pas demandé pour l'audio,
mais artefacts possibles si quant mal calibrée).
### F. **CP sur HMX V79** — symétrique
- Le CP est un transformer dense 5L 1024-dim, exactement le pattern HMX.
- Port le forward CP sur HTP. ggml-hexagon supporte déjà les matmul HMX pour Qwen3.5.
- **Effort** : 1 session (le forward CP est court, ~200 lignes).
- **Gain** : CP 3.4 s → 0.5-1 s = RTF total **~1.5**.
- **Risque** : faible (path déjà battu côté Qwen3.5).
- **Caveat** : les heads CP (15 × 2048 × 1024 = 30M params) restent CPU côté
dot-product. Coûtent ~30 M ops × 15 steps = 0.5 G ops/frame, ~5 ms/frame. OK.
### G. **B + F combinés** — chemin « ship-ready avec RTF<1.5 »
- CP overhead réduit (B) + CP sur HMX (F) → CP 3.4 s → ~0.3 s.
- Decoder reste CPU 3.2 s, talker 1 s.
- Total ≈ 4.5 s, RTF **1.7**. Pas <1 mais cap à dépasser.
### H. **B + E + F combinés** — RTF <1 ambitieux
- CP overhead + HMX + BigVGAN HMX → CP 0.3 s + BigVGAN 0.4 s + talker 1 s + prefill 0.1 s = 1.8 s.
- **RTF 0.7**. Cible atteinte.
- **Effort total** : 3-4 semaines.
## 5. Recommandation
Plan ordonné par ROI décroissant :
1. **B. CP ctx réutilisé** (½ session, RTF 3.07 → 2.4, bit-exact, faible risque).
2. **A. Streaming pipeline** (1 session, RTF perçu réduit drastiquement, bit-exact).
3. **F. CP forward sur HMX** (1 session, RTF → ~1.5, faible risque, réutilise du
travail existant).
4. **E. BigVGAN HMX** (2-3 semaines, RTF → ~0.7, risque qualité audio à valider).
Les étapes 1 et 2 sont des gains certains, courts, sans tester de nouveaux kernels.
Elles font la moitié du chemin. L'étape 3 est le prochain saut « propre », l'étape 4
est le sujet R&D qui fait franchir <1.
**Points de bascule** : si l'étape 1+2 donne RTF 2 avec streaming TTFB ~500 ms,
ça peut suffire pour shipper. RTF<1 n'est pas indispensable si l'audio commence
à jouer rapidement et continue plus vite qu'il ne se consomme **après** le démarrage
(= la queue se vide en cours de route).
## 6. Choses à vérifier / chantiers parallèles
- **HTP prefill talker** : n'a pas marché lors du test (hang 90s). Setup `ADSP_LIBRARY_PATH`
ou env supplémentaire requis. Pas urgent — gain mineur (~0.05 s).
- **Quant heads/codec_embs CP** (Q8/Q4) : 240 MB de f32 dans s.heads+s.codec_embs.
Pourrait diminuer BW si les heads (CPU dot-product, pas ggml) deviennent bw-bound.
Mesuré insignifiant (5 ms/frame) → pas un levier.
- **Decoder en f16** : à vérifier si poids déjà f16 ou f32. Si f32, bascule en f16
peut diviser BW par 2 si BW-bound. Mais BigVGAN est compute-bound (3.2 s ≈
plafond NEON), donc gain attendu faible.
## Verdict
RTF<1 atteignable mais demande ~3-4 semaines de travail kernel hexagon (étape E ou F+E).
RTF 2 avec TTFB ~500 ms est atteignable en ~2 sessions de refactor (étapes B+A).
Cela suffit pour démo et probablement app, parce que ce qui compte côté UX c'est
le démarrage rapide + le rythme soutenu, pas la latence end-to-end pure.