Kazeia-engine/dist/PERF_ANALYSIS.md

8.5 KiB
Raw Permalink Blame History

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.