From 101d1cd7ed743a0fe66d4ea2674652cce338ab95 Mon Sep 17 00:00:00 2001 From: Richard Loyer Date: Fri, 29 May 2026 16:40:56 +0200 Subject: [PATCH] tests infra HTP/HMX : verdict, le HTP n'est pas le bon outil pour TTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 tests systématiques sur les hypothèses non-explorées : A. USE_HMX=0 vs USE_HMX=1 sur sched CP : - HVX 20% plus rapide que HMX à batch=1 (tile 32×32 sous-utilisé) - Mais aucun ne bat baseline CPU (110 ms/frame) - HVX et HMX bit-exact entre eux ; les deux divergent de CPU (codes différents -> trajectoire talker diverge -> N=64 vs 33). B. op_offload=true vs false : false -16% sur CP HMX (placement strict moins coûteux). Quick win retenu (env KZTTS_CP_OFFLOAD). C. Profile fin (KZTTS_CP_PROFILE=1) par sub-step : - sched_reset+alloc : 0.1 ms (négligeable !) - tensor_set/get : 0.0 ms (opt_hostbuf=1 = ION partagé) - compute : 6-11 ms (95 % du temps !) -> L'overhead sched n'est PAS le problème. Le compute HTP lui-même est plus lent que CPU pour batch=1. Hypothèse #1 (sched_reserve + ctx persistant) INVALIDÉE. D. Talker option C (use_htp=true, GGML_HEXAGON_GDN_PREFILL=1) : - Prefill : 87 ms -> 185 ms (+112%) - Talker per-frame : 32 ms -> 81 ms (+154%) -> Pour un petit modèle (0.6B), l'overhead routing HTP par token dépasse le gain compute. Le pattern option C documenté marche pour Qwen3.5-4B, PAS pour Qwen3-TTS-Talker. VERDICT GLOBAL : le HTP est non-rentable sur TOUS nos workloads TTS. - BigVGAN : bloqué (nrows > 1024) - CP : régresse (batch=1) - Talker : régresse (trop petit pour amortir routing) CLAUDE.md le notait partiellement ('CPU NEON bat HVX 3.4× sur M=1') mais on n'avait pas tiré la conséquence globale : notre TTS n'a pas de workload HTP-friendly. LEVIERS RESTANTS (sans HTP) : 1. Streaming pipeline (1 session) : RTF inchangé mais TTFB 5s->500ms 2. Réduction overhead CP CPU (½ session) : ctx réutilisé entre sub-forwards 3. Decoder optim NEON conv1d : R&D long 4. Quantization Q8/Q4 CP : compromis qualité Code committé reste opt-in (KZTTS_CP_HTP, KZTTS_DECODER_HTP) et neutre au runtime. Le path par défaut RTF=3.0 est intact. Détails complets dans dist/PERF_INFRA_TESTS.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/PERF_INFRA_TESTS.md | 136 ++++++++++++++++++++++++++++++++++++++ dist/jni/cp_inference.cpp | 28 +++++++- 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 dist/PERF_INFRA_TESTS.md diff --git a/dist/PERF_INFRA_TESTS.md b/dist/PERF_INFRA_TESTS.md new file mode 100644 index 0000000..1f9e282 --- /dev/null +++ b/dist/PERF_INFRA_TESTS.md @@ -0,0 +1,136 @@ +# Tests infra — où HTP/HMX échoue, et pourquoi (29/05) + +Session de tests systématiques sur les hypothèses non-explorées du Phase 2b/F. +Verdict global : **le HTP n'est pas le bon outil pour notre TTS**, parce que +tous nos workloads sont des decodes batch=1 (talker, CP sub-forwards) ou des +matmul énormes (BigVGAN) au-delà de la limite backend `nrows > 1024`. + +## A — Sched HMX vs HVX (USE_HMX=0) + +Bench : CP + sched HTP, varier `GGML_HEXAGON_USE_HMX`. + +| Config | CP/frame | N frames | RTF | WAV md5 | +|---|---:|---:|---:|---| +| Baseline CPU pur | 110 ms | 33 | 2.92 | unique | +| Sched HMX=1 | 226 ms | 64 | 4.48 | A2=A3 | +| Sched HMX=0 (HVX) | 180 ms | 64 | 3.81 | A2=A3 | + +- HVX 20% plus rapide que HMX sur CP (tile 32×32 sous-utilisé à batch=1). +- HMX et HVX **produisent bit-exact le même WAV**. +- Les deux divergent du CPU baseline : N passe de 33 à 64 (le talker prend + une trajectoire différente parce que les codes CP ne sont plus bit-exact à NEON). +- Coût × 2 frames + overhead par-step = régression nette. + +**Conclusion A** : HVX > HMX sur batch=1, mais aucun ne bat NEON. + +## B — op_offload=false vs true + +| Config | CP/frame | +|---|---:| +| HMX + op_offload=true | 216 ms | +| HMX + op_offload=false | 181 ms | + +- Placement strict (false) = -16% sur CP. Moins de "magic" de placement adaptatif. +- Toujours plus lent que baseline CPU. + +**Conclusion B** : `op_offload=false` est meilleur défaut. Quick win mais +ne sauve pas la régression. + +## C — Profile fin par sub-step (KZTTS_CP_PROFILE) + +Breakdown moyen d'un sub-forward (HVX) : + +| Étape | Temps | +|---|---:| +| `sched_reset + sched_alloc_graph` | 0.1 ms | +| `ggml_backend_tensor_set` ×3 (x, pos_ids, mask) | 0.0 ms (< µs) | +| **`sched_graph_compute`** | **6-11 ms (croît avec pos_off)** | +| `ggml_backend_tensor_get` | 0.0 ms | + +- **L'overhead sched est négligeable** : ~0.1 ms/step × 15 = 1.5 ms/frame. + Les copies CPU↔HTP sont gratuites (opt_hostbuf=1 = ION partagé). +- **Le compute lui-même domine** : 95% du temps. +- CPU NEON équivalent : ~7 ms/step, HVX 10 ms/step. HVX est intrinsèquement + **plus lent que NEON sur ces matmul batch=1**. + +**Conclusion C MAJEURE** : ma hypothèse #1 (`sched_reserve` + ctx persistant) +est invalidée. L'overhead qu'on espérait éliminer n'existe pas — le compute +HTP est juste plus lent que CPU pour notre workload. + +## D — Talker en HTP option C + +Pour vérifier si le pattern HTP-prefill / CPU-decode (validé pour Qwen3.5-4B) +marche sur Qwen3-TTS-Talker (0.6B). + +| Métrique | CPU pur | HTP option C | +|---|---:|---:| +| Prefill | 87 ms | 185 ms (**+112%**) | +| Talker per-frame | 32 ms | 81 ms (**+154%**) | +| N frames | 33 | 18 | + +- Le talker TTS (0.6B) **REGRESSE sévèrement** en HTP option C. +- Pour un petit modèle, l'overhead routing HTP par token (DMA, sync, sched + inter-backend) dépasse le gain compute. +- Le pattern option C ne marche bien que pour des modèles assez gros pour + amortir le routing (Qwen3.5-4B = OK ; Qwen3-TTS 0.6B = perte). + +**Conclusion D** : le HTP n'apporte rien sur ce talker non plus. + +## Verdict cumulé + +Le HTP est non-rentable sur **tous** nos workloads TTS : + +| Composant | Workload | HTP gain réel | Raison | +|---|---|---|---| +| BigVGAN | conv1d batch ≥ 60k | ❌ rejeté | limite backend `nrows > 1024` | +| CP sub-forwards | matmul batch=1 ou 2 | ❌ régresse | compute HVX < NEON à batch=1 | +| Talker decode | batch=1 | ❌ régresse | overhead routing > gain compute (0.6B) | +| Talker prefill | batch ~10-20 | ❌ régresse | trop petit pour amortir | + +Le projet l'avait noté en partie dans CLAUDE.md : +> « CPU NEON bat le HVX 3.4× sur Q6_K M=1 » +> « decode CPU verrouillé (~16.5 tok/s), HTP perdant decode (~3.6 tok/s) » + +Mais on n'avait pas tiré la conséquence globale : **notre TTS n'a pas de +workload "prefill HTP-friendly"**. Les phrases courtes (10-20 tokens) sont +juste assez courtes que le routing coûte plus que le gain. + +## Vrais leviers restants (sans HTP) + +Avec le HTP exclu, les optims réelles sont : + +1. **Streaming pipeline** (1 session) — chevaucher talker / CP / decoder + par chunks. Le RTF total ne baisse pas mais le TTFB perçu passe de ~5 s + à ~500 ms. **Le seul gain UX significatif réaliste.** + +2. **Réduction overhead CP CPU** (½ session) — réutiliser le ctx ggml entre + les 15 sub-forwards par frame. L'overhead par-step est petit (~5 ms + d'init/free × 15 = ~75 ms) mais réel. Estimé -10 à -15% sur CP CPU. + +3. **Decoder : optim NEON conv1d** (R&D) — actuellement compute-bound à 95% + du plafond NEON. Pour gagner faut vectoriser plus agressivement ou + restructurer les conv1d en blocked matmul. Sujet R&D. + +4. **Quantization plus agressive du CP** (½ session) — Q8 ou Q4 sur les + poids matmul. Note CLAUDE.md : « Q8_0 CP perdu (76% codes vs 100% f16) » + = qualité dégrade. Donc compromis qualité/perf à explorer. + +## Ce qui est DÉJÀ committé qui marche + +- `KZTTS_CP_HTP=1` (sans SCHED) : poids CP sur HTP buffer, compute CPU pur. + Neutre en perf, prouve que la chaîne marche. +- `KZTTS_DECODER_HTP=1` (sans SCHED) : idem decoder. Neutre. +- Phase 2b refactor stage_bigvgan_sched : code propre, désactivé par défaut. +- Phase F refactor cp_forward_cached_step sched-friendly : idem. + +Tout ça est opt-in et n'affecte pas le path par défaut (RTF 3.0). + +## Recommandation finale + +Le ROI / risque dit clairement : **streaming pipeline (#1) est le seul vrai +gain accessible**. RTF<1 sur HTP demande de modifier le backend hexagon +(semaines), ce qui sort du scope. + +Si Richard accepte RTF=3 mais TTFB=500 ms comme livrable acceptable, le +chemin est clair en 1-2 sessions. Sinon, le frontier kernel HVX BigVGAN +reste sur la table mais c'est un autre projet. diff --git a/dist/jni/cp_inference.cpp b/dist/jni/cp_inference.cpp index 20ebc0a..7633313 100644 --- a/dist/jni/cp_inference.cpp +++ b/dist/jni/cp_inference.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -200,9 +201,11 @@ bool cp_load_with_backends(CPState& s, const char* gguf_path, std::vector bufts; bufts.reserve(s.backends.size()); for (auto b : s.backends) bufts.push_back(ggml_backend_get_default_buffer_type(b)); + // op_offload toggle via KZTTS_CP_OFFLOAD (default 1 = true). + const bool op_offload = !(getenv("KZTTS_CP_OFFLOAD") && atoi(getenv("KZTTS_CP_OFFLOAD")) == 0); s.sched = ggml_backend_sched_new(s.backends.data(), bufts.data(), (int)s.backends.size(), /*graph_size=*/8192, - /*parallel=*/false, /*op_offload=*/true); + /*parallel=*/false, op_offload); if (!s.sched) { fprintf(stderr, "cp_load_with_backends: sched_new FAIL\n"); return false; } fprintf(stderr, "cp_load_with_backends: %lld tensors (f16=%zu f32=%zu) sur %s, sched ON\n", (long long)n, kept_f16, conv_f32, ggml_backend_name(s.backends[0])); @@ -490,23 +493,46 @@ static void cp_forward_cached_step(CPState& s, const float* embeds_new, int n_ne out_hn.resize(N_EMBD); if (use_sched) { + // Profile fin via KZTTS_CP_PROFILE=1 : décompose alloc / set / compute / get + // pour identifier où va le temps dans la boucle sched. + const bool prof = (getenv("KZTTS_CP_PROFILE") && atoi(getenv("KZTTS_CP_PROFILE")) != 0); + auto nows = []() { + return std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()).count(); + }; + double t_a0=0, t_a1=0, t_s1=0, t_c1=0, t_g1=0; + if (prof) t_a0 = nows(); + ggml_backend_sched_reset(s.sched); if (!ggml_backend_sched_alloc_graph(s.sched, gf)) { fprintf(stderr, "cp_forward_cached_step: sched_alloc_graph FAIL\n"); ggml_free(ctx); return; } + if (prof) t_a1 = nows(); + // Inputs uploadés APRÈS sched_alloc_graph (les tensors ont maintenant leur backend buf). ggml_backend_tensor_set(x, embeds_new, 0, (size_t)N_EMBD * n_new * sizeof(float)); ggml_backend_tensor_set(pos_ids, pos_host.data(), 0, pos_host.size() * sizeof(int32_t)); ggml_backend_tensor_set(mask, mask_host.data(), 0, mask_host.size() * sizeof(float)); + if (prof) t_s1 = nows(); + if (ggml_backend_sched_graph_compute(s.sched, gf) != GGML_STATUS_SUCCESS) { fprintf(stderr, "cp_forward_cached_step: sched_graph_compute FAIL\n"); ggml_free(ctx); return; } + if (prof) t_c1 = nows(); + // hn = colonne (n_new - 1) de x [N_EMBD, n_new]. tensor_get avec offset. ggml_backend_tensor_get(x, out_hn.data(), (size_t)N_EMBD * (n_new - 1) * sizeof(float), (size_t)N_EMBD * sizeof(float)); + if (prof) { + t_g1 = nows(); + fprintf(stderr, "cp_sub n_new=%d pos=%d reset+alloc=%.2fms set=%.2fms compute=%.2fms get=%.2fms\n", + n_new, pos_off, + (t_a1-t_a0)*1000.0, (t_s1-t_a1)*1000.0, + (t_c1-t_s1)*1000.0, (t_g1-t_c1)*1000.0); + } } else { ggml_graph_compute_with_ctx(ctx, gf, s.n_threads); memcpy(out_hn.data(), (float*)x->data + (size_t)N_EMBD * (n_new - 1),