Trois fixes pour que le pipeline engine TTS tourne dans l'app réelle :
- cp_inference: cp_keep_f16() retourne false par défaut. Garder les poids
matmul CP en F16 faisait overflow les massive activations Qwen3 (>65504)
-> NaN -> codes 0 -> audio inaudible. F32 = ref cp_runner bit-exact.
KZTTS_CP_KEEP_F16=1 réactive l'ancien path (debug perf).
- tts_engine: arming EOS naturel gated derrière KZTTS_EOS_NATURAL (off par
défaut). C'était un workaround de l'ère NaN qui tronquait la fin de phrase
(frames 33/37). Depuis le fix F32 le talker se termine seul (cb0==codec_eos),
frames 40/42. Masking + anti-repeat + hard-fallback conservés.
- kazeia_tts_jni: freopen stderr gated derrière KZTTS_STDERR_LOG (plus de
redirection /sdcard par défaut en prod).
- build_kazeia_tts.sh: speaker_encoder.cpp + kazeia_mel.cpp ajoutés aux SRCS.
Validé E2E in-app : LLM lib (q35-lmq4 GGUF) + TTS lib coexistent sans
collision libllama, réponse FR audible jouée via AudioTrack 24kHz.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Le profile fin (KZTTS_CP_PROFILE=1) a révélé que ~75% du temps CP par frame
est dans ggml_graph_compute (4-6 ms/sub-step × 15 = 80 ms) et ~22% dans le
delta CPU host (~24 ms/frame). Ce delta était entièrement le sample_head :
2048 dot products de 1024 floats SCALAIRE × 15 heads/frame = 31.5M ops/frame.
Implémentation NEON aarch64 16-way unroll (vfmaq_f32 × 4 accumulators) sur
dot_neon_1024(). Gated par KZTTS_CP_NO_NEON=1 pour fallback debug.
Mesure Pad3 (KZTTS_THREADS=6 GGML_NUM_THREADS=8, seed=42, Bonjour Kazeia) :
scalar : CP 106.8 ms/frame, total 8.05s, RTF 2.93
NEON : CP 72.1 ms/frame, total 6.75s, RTF 2.45 (-33% CP, -16% pipe)
WAV md5 identiques c3dd71cbc783013842a3dacedfefd758 (BIT-EXACT)
Le scalaire compilé par clang -O3 -march=armv8.6-a n'a apparemment pas
auto-vectorisé cette boucle malgré l'évidence (peut-être à cause des
indices non-trivials avec head_idx). NEON explicite débloque immédiatement.
Reste : decoder = 51% du total maintenant (3.34s sur 6.75s).
BigVGAN ggml-cpu déjà NEON vectorisé donc moins de marge évidente côté
host code. Cibles potentielles : text_projection 50ms one-shot au prefill,
mais marginal sur pipeline total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
cp_inference.h/.cpp : nouveau cp_load_with_backends + sched path dans
cp_forward_cached_step. KV cache aussi alloué sur backend buffer en mode sched
(no_alloc=true + ggml_backend_alloc_ctx_tensors_from_buft). Inputs (x, pos_ids,
mask) marqués ggml_set_input ; output ggml_set_output. Compute via sched_reset
+ alloc + tensor_set + sched_compute + tensor_get. Path legacy CPU pur (sans
sched) reste intact via détection s.sched.
tts_engine.cpp : KZTTS_CP_HTP=1 + KZTTS_CP_SCHED=1 active le path sched HMX.
KZTTS_CP_HTP=1 seul = poids HTP, compute CPU pur via opt_hostbuf=1 (baseline).
Mesures Pad3 (KZTTS_CP_CACHE=1, seed=42, 'Bonjour Kazeia') :
CPU baseline : CP 111 ms/frame, RTF 2.95, N=33
CP poids HTP, compute CPU : CP 108 ms/frame, RTF 2.93, N=33 (~baseline)
CP sched HMX (HTP-routé) : CP 181 ms/frame, RTF 3.87, N=64 REGRESSION
GGML_SCHED_DEBUG=2 confirme que ~95% des MUL_MAT CP tombent bien sur HTP0
(blk.0.attn_q.weight : 360 HTP / 21 CPU). Donc HMX est techniquement actif.
Cause de la régression :
- Overhead par sub-forward sched_reset + sched_alloc_graph + tensor_set/get
× 15 sub-forwards par frame. ~5 ms/appel × 15 = 75 ms ajoutés par frame
(cohérent avec 111 -> 181 ms).
- N=64 vs N=33 = trajectoire talker diverge à cause des codes CP qui
diffèrent (perte précision HMX f16 tile vs CPU NEON f32).
Pour livrer un gain CP HMX réel, il faudrait :
- (a) ctx persistant entre sub-forwards (~refactor architectural)
- (b) batcher les 15 sub-forwards en 1 graph autoregressif (~lourd)
Le path sched CP est laissé en opt-in (KZTTS_CP_SCHED=1, désactivé par défaut)
pour itérer dessus ultérieurement. KZTTS_CP_HTP=1 seul est neutre (~baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cp_inference: nouvelle voie cp_predict_cached (1 prefill 2 tokens +
14 decode 1 token via KV cache f32 persistant) à côté de cp_predict
(oracle recompute conservé pour A/B). cp_forward_cached_step écrit le
K/V post-RoPE dans s.K_cache/V_cache via ggml_view_3d + ggml_cpy, et
lit le full cache [0..T_full) pour l'attention. Ordre topo garanti
par build_forward_expand(cpy) AVANT build_forward_expand(x).
tts_pipeline: switch KZTTS_CP_CACHE=0/1 (défaut 0 = oracle bit-exact).
build_tts_pipeline.sh: rebuild reproductible (CMakeLists ne couvrait
que libkazeia_engine.so).
Mesure tablette Pad3 (talker_f32+tts_dump, CPU 6t, seed=42, 31 frames):
oracle : CP 227.9 ms/frame, total 12.90s, RTF 4.99
cached : CP 137.8 ms/frame, total 8.91s, RTF 3.45
codes_ref.bin == codes_cache.bin (cmp), out_ref.wav md5 == out_cache.wav
Speedup CP ×1.65, pas le ×8 espéré : le forward est BW-bound (~100 MB
de poids f32 streamés par sub-forward, on lit toujours 15 fois). Le
gain est sur la part compute (sum L=2..16 -> 16 token-fwd au lieu de
135). Pour aller plus loin : (a) cp_load en f16 conservé (×2 BW),
(b) fusion graphe multi-step. Decoder RTF 1.37 reste aussi à attaquer.
Bit-exactness : f32 mul_mat stable entre L=k+1 et L=1 à threads=6,
les sommations donnent les mêmes bits. Math identique, ordre identique
de fait.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sampler.{h,cpp} : module isolé reproduisant la chaine HF
LogitsProcessor :
1. RepetitionPenaltyLogitsProcessor (logit/penalty si >0, logit*penalty sinon)
2. TemperatureLogitsWarper (logit /= temp)
3. TopKLogitsWarper (garde top_k, autres -> -inf)
4. TopPLogitsWarper (nucleus : garde la masse cumulee jusqu'a top_p)
5. multinomial sample (xorshift32, deterministe via sampler_seed)
PRNG xorshift32 != torch.mt19937 donc meme seed != audio Python, mais stable
sur n'importe quel texte/voix (pas d'attracteur greedy).
Plumbing :
- CPState.sampler (defaut greedy top_k=1, temp=0 -> cp_validate continue a sortir
495/495 codes match). Pipeline override pour temp=0.9 top_k=50 rep_penalty=1.05
rep_window=16 (intra-frame, 15 codebooks max). CB1..15 utilisent
sampler_sample_local() qui prend les tokens deja samples CE step pour penaliser
repetition intra-frame.
- tts_pipeline : Sampler talker_sampler avec rep_window=64 (historique long sur
CB0). Override via env KZTTS_TEMP/TOPK/TOPP/REPP + KZTTS_CP_TEMP/TOPK/TOPP/REPP.
Validation sur 5 seeds (1, 42, 100, 777, 12345), phrase 'Bonjour je m'appelle Kazeia' :
- Tous EOS naturel (step 31-44, 2.48-3.67s audio @12Hz, proche du PY_REF 2.08s)
- silence_pct : 25-36% (vs > 60% avec greedy/sampler simple)
- cp_validate continue a sortir 495/495 codes match (greedy par defaut)
Vs WAV pipeline_v3 user-confirme 'parfait' (seed=123 temp=0.8 sans rep_penalty),
le sampler v4 est generique sur tout seed/texte, plus de besoin de chercher un
seed favorable. Trade-off : on n'a plus le PRNG torch donc l'audio diffère de
PyTorch a chaque run (different trajectoire), mais reste audio TTS coherent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cp_inference.h/cpp : extrait de cp_runner.cpp standalone (kazeia-tts-decoder-ggml/),
adapté en module réutilisable. Architecture identique (qwen3 5L, GQA 16/8, head_dim
128, RoPE NEOX theta=1e6, q/k-norm AVANT RoPE, recompute T<=16 sans KV cache).
API : cp_load() + cp_predict(hidden[1024], cb0_emb[1024]) -> int32[15] = CB1..CB15.
cp_validate.cpp : reproduit sur tablette les conditions du standalone cp_runner
-> 495/495 codes match (33/33 frames parfaits) vs cp.generate(do_sample=False)
greedy golden, sur les 33 frames du dump 'Bonjour' historique. CP intégré =
bit-exact au standalone, qui est lui-même bit-exact à PyTorch.
tts_orchestrate.cpp mis à jour : si cp_f16.gguf/cp_heads/cp_codec_embs présents
dans <dump_dir>, le CP est ENABLED et remplace le teacher-forcing CB1..15.
Hidden state Talker capturé via llama_get_embeddings_ith(ctx,-1) après chaque
decode_embeds (pas besoin de re-instrumenter le forward Talker).
Mesure sur 'Bonjour je m'appelle Kazeia' (N=26 frames, audio 2.17s) tablette CPU 4t :
Talker prefill (T=19) : 0.124 s
Loop (N=26 steps) : 7.705 s talker_decode=0.763, cp=6.942
per-step talker : 29.3 ms
per-step CP (15 pass): 267.0 ms
Total Talker+CP : 7.829 s -> RTF talker+cp = 3.61
Le CP est le nouveau goulot (267ms × 15 passes/step). Levier P3.5 = KV cache CP
(recompute -> incremental), threads 8, ou Q8_0 du CP.
CB0 match golden = 3/26 = trajectoire stochastique Python diverge rapidement vs
notre greedy ; PAS un bug CP (cp_validate prouve la conformité bit-exact).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>