Commit Graph

115 Commits

Author SHA1 Message Date
Richard Loyer 5cc8c22fc9 chantier RAG #1 : entrypoint embeddings (texte -> vecteur poolé) pour le RAG mobile
Spec /opt/Kazeia/docs/RAG_EMBEDDINGS_ENGINE_SPEC.md implémentée.

Préflight (point #2 du spec = LE bloqueur potentiel) :
 - Arch BERT/nomic-bert/gemma-embedding TOUJOURS présentes dans le fork ql
   (llama-arch.cpp + src/models/bert.cpp), malgré les mods Qwen/TTS.
 - Outil de référence examples/embedding/embedding.cpp dispo = source de vérité.
 - Confirmé : BERT encoder-only routé via llama_decode (PAS llama_encode) dans
   ce fork ; le code ne bascule sur erreur que pour encoder+decoder (T5).

Ajouts kazeia_engine_jni.cpp (handle KEmbedder SÉPARÉ du KEngine, CPU pur) :
 - loadEmbedder(path, nThreads, pooling) : embeddings=true, pooling MEAN/CLS/auto,
   n_ctx=n_batch=n_ubatch=512 (contrainte pooling = séquence dans 1 ubatch),
   n_gpu_layers=0, refus explicite des modèles encoder-decoder.
 - embedText(h, text) : tokenize add_special + batch logits=1 + llama_decode +
   llama_get_embeddings_seq + normalisation L2 -> float[n_embd] ou null.
 - freeEmbedder(h).
 - Reproduit À L'IDENTIQUE examples/embedding/embedding.cpp.

Bindings Kotlin (EngineLlmEngine.kt) : loadEmbedder/embedText/freeEmbedder +
wrapper EmbedderEngine(model, nThreads, pooling).embed(text).

Validation sur tablette SM8750 (adb shell, nomic-embed Q4_K_M, CPU) :
 - Déterminisme : max|diff| = 0.0 bit-identique sur 2 runs
 - Norme L2 = 1.0000
 - PARITÉ RÉFÉRENCE : mon embedText vs llama-embedding = max|diff| 5e-7,
   cos 1.000000 (probe rag_probe.cpp répliquant le chemin exact)
 - Cohérence sémantique : OK avec bons préfixes

FINDING modèle (point #1 du spec confirmé par mesure) : nomic-embed est
anglo-centré, marge FR fine (cos insomnie 0.556 vs tarte 0.517 = 0.04, et
ordre s'inverse avec mauvais préfixe). => SHIPPER multilingual-e5-small comme
le spec l'exige, pas nomic. Le code est agnostique au modèle (n_embd auto).

⚠ Bloqueur SELinux identique LLM/TTS : l'embedder linke libllama->libggml-hexagon
qui ouvre /dev/fastrpc-cdsp au backend_init -> crash untrusted_app. DOIT tourner
sur le build CPU-only (GGML_HEXAGON=OFF). Documenté dans le code + RAG_INTEGRATION.md.

Doc : dist/RAG_INTEGRATION.md (format des 3 autres) + overview à jour (4 sous-systèmes).
2026-06-09 22:02:30 +02:00
Richard Loyer f4c6e1290c chantier B engine #3 : TTS lib audible + E2E in-app validé
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>
2026-06-03 22:37:47 +02:00
Richard Loyer e55cd175c8 chantier B engine #2 : bloqueur SELinux découvert + guide rebuild CPU-only pour dev
Le dev Kazeia a découvert au test de dégel TTS / toggle llm_engine=lib :
SIGABRT in-app au tts_engine_load / kazeia_engine_load, jamais en standalone.

Trace dmesg :
  avc: denied { open } for path="/dev/fastrpc-cdsp"
    scontext=u:r:untrusted_app  tcontext=u:object_r:vendor_qdsp_device

Cause : libggml-hexagon.so dlopen libcdsprpc.so (FastRPC) au init backend HTP,
qui ouvre /dev/fastrpc-cdsp. Policy SELinux Oplus/Qualcomm sur SM8750 interdit
cet accès à untrusted_app (UID 10xxx). Seul shell (UID 2000) y a accès.

Conséquence : tout mon harness CLI standalone via adb shell était un FAUX
positif systématique. Le test "loadLibrary OK" + "calls C++ OK" ne révèle
rien du comportement in-app pour le backend HTP/DSP.

Pourquoi STT marche : passe par libQnnHtp.so Maven (delegation autorisée
untrusted_app), pas FastRPC direct.

Pourquoi ExecuTorch .pte marche : idem libQnn Maven.

3 voies évaluées (cf project_engine_selinux_fastrpc_blocker memoire) :
 1. Rebuild CPU-only (GGML_HEXAGON=OFF) -- réaliste, +1s/tour LLM, +0 TTS
 2. Port ORT-QAIRT pour LLM -- non réaliste (pas de Qwen QAIRT context)
 3. Statu quo .pte -- régression ambition projet

Décision user : le dev rebuilde de son côté en environnement APK réel
(évite mon harness biaisé). Reprise des sources Kazeia-Engine et build.

Livrables cette session :
 - dist/REBUILD_CPU_ONLY.md : guide complet 7 étapes (sources, flags CMake,
   build libllama/ggml CPU-only, rebuild kazeia_engine/tts, jniLibs allégé,
   tests in-app, perf attendue, points d'attention)
 - dist/LLM_INTEGRATION.md §0 : bloqueur SELinux documenté + référence
   rebuild CPU-only + chiffres perf attendus
 - dist/TTS_INTEGRATION.md §0 : idem côté TTS, noter que le clonage vocal
   (speaker encoder ECAPA-TDNN) est CPU-only de base, donc préservé sans
   impact
 - dist/KAZEIA_ENGINE_OVERVIEW.md : warning en tête + pointer REBUILD doc
 - project_engine_selinux_fastrpc_blocker.md : diagnostic complet pour
   contexte futur

Coût mesuré attendu (vs CLI HTP) :
 - LLM prefill : 0.4s -> 1.3s (+0.9s)
 - LLM decode : inchangé (déjà CPU NEON option C)
 - TTS talker prefill : 50ms -> 150ms (négligeable in-mix)
 - Tour LLM total : +1s (acceptable thérapeutique)
 - Tour TTS total : +0.1s (imperceptible)

Leçon de méthode gravée : un harness CLI via adb shell sera TOUJOURS
faux positif pour tout test impliquant FastRPC/DSP/contexte vendor.
Tout test validation engine pour intégration in-app DOIT passer par
APK debug ou androidTest instrumenté.
2026-06-02 23:12:38 +02:00
Richard Loyer 8c1e83be18 chantier B unification #1 : engine 3/3 prêt pour intégration (STT + TTS + LLM)
Reproche du user juste : j'ai livré STT complet mais pas TTS ni LLM au même
niveau. Cette session corrige : 3 docs d'intégration équivalentes en qualité,
2 bugs in-app investigués et l'un fixé.

Investigation TTS "crashe au load in-app" :
 - DT_NEEDED de libkazeia_tts.so = libllama.so + libggml.so + libggml-base.so
   + libggml-cpu.so + libc++_shared.so + libdl/libm/libc
 - Test reproduction sur tablette : lib charge OK avec les 7 deps présentes,
   crash avec "library libllama.so not found" si une dep manque
 - Conclusion : c'est juste 7 libs à pousser dans jniLibs/arm64-v8a/, pas un
   bug code. Liste exhaustive donnée dans TTS_INTEGRATION.md §2.b

Investigation LLM "ne tient pas 8 threads in-app" :
 - kazeia_engine_jni.cpp ligne 117/120/124/131 : n_threads HARDCODE à 4
   pour le decode CPU. Pas exposé côté Kotlin.
 - Fix : ajouter param nThreads à load(), N_DECODE défaut 6 (sweet spot r3
   mesuré : Qwen3.5-4B 9.8 tok/s à t=6 vs 7.4 à t=8 contention)
 - Update EngineJni.load(model, ctx, nThreads) + EngineLlmEngine(ctx, nThreads=6)
 - Rebuild libkazeia_engine.so 60 KB

Docs livrées (format identique STT_INTEGRATION.md) :
 - TTS_INTEGRATION.md (10 KB) : libs à pousser, façade, clonage vocal,
   sampling, codes erreur, bench, checklist, pièges
 - LLM_INTEGRATION.md (10 KB) : option C HTP/CPU, n_threads sweet spot,
   modèles supportés (Qwen3 dense / Qwen3.5 hybride GDN), cascade
   Speaker+Thinker, piège affinity Android (decode 9.8 -> 0.32 tok/s in-app
   si scheduler pin sur 1 cœur)
 - KAZEIA_ENGINE_OVERVIEW.md actualisé : entrée unique pointant les 3 docs,
   pattern dispatch flag (mirror STT pour LLM+TTS), état réel 3/3 prêt

Reste côté dev (cf KAZEIA_ENGINE_OVERVIEW.md):
 1. Pousser les libs jniLibs (8 fichiers pour TTS/LLM, partagées entre eux)
 2. Copier les 3 façades Kotlin
 3. Câbler 2 flags dispatch supplémentaires (llmEngine + ttsEngine, mirror STT)
 4. Valider checklist §7 de chaque doc (System.loadLibrary + 1 call)
 5. Bench A/B prod vs lib sur audios/prompts fixtures

Le pattern STT a marché parce qu'il avait tout (lib + façade + doc + checklist).
Maintenant TTS et LLM aussi.
2026-06-01 15:46:20 +02:00
Richard Loyer 4ad3b3716d chantier B STT #6 : 2 bugs critiques fixés grâce à l'A/B in-app du dev
L'A/B in-app du dev a révélé un bug grave que mon harness standalone ratait :
3/6 audios FR (elodie, richard, zelda) sortaient vides (0 tokens, EOT immédiat).

Investigation : reproduit en standalone (chaque audio dans son propre process)
- DONC PAS un bug d'enchaînement KV (mon code reset déjà self_k/v à chaque
  transcribe). Le dev avait une fausse piste qui m'a poussé à investiguer
  en profondeur côté algo decoder, ce qui a révélé les vraies causes.

Bug #1 : FFT zero-pad 512 vs DFT exact 400
- kazeia_mel utilisait next_pow2(N_FFT=400)=512 + FFT radix-2 zero-padded
- Conséquence : bins 0..200 du FFT 512 sont à fs/512=31.25 Hz/bin, alors
  que mel_filters.json est calibré pour fs/400=40 Hz/bin du DFT exact
- Spectre désaligné en fréquences -> mel cohérent sur voix graves (damien,
  jerome, sid) mais cassé sur voix avec hautes harmoniques (elodie,
  richard, zelda)
- Fix : DFT directe N=n_fft quand n_fft pas puissance de 2, FFT radix-2
  quand pow2 (TTS speaker encoder N_FFT=1024 reste rapide)
- Coût : mel 60 ms -> 220-320 ms (incontournable pour N_FFT=400)
- Fixe elodie après ce bug, mais richard/zelda restaient vides

Bug #2 : Pas de forced decoder prompt
- Sur audios borderline (énergie spectrale ambiguë), Whisper saute la
  prédiction de langue (top-1 step 0 = 50362 NOTIMESTAMPS direct au lieu
  de 50265 FR) et atteint EOT au step 1
- Trace step-0 logits richard : [50362:+25.1] [50265:+24.7] (0.4 diff)
- Solution standard HF whisper : forcer le prompt <SOT, |lang|, |task|>
  au lieu de laisser le modèle prédire les 3 tokens spéciaux
- IMPORTANT : NE PAS forcer <|notimestamps|> ; le decoder QAIRT pred
  timestamp_begin (50363) après <|transcribe|> et veut le mode timestamps.
  Forcer NOTIMESTAMPS cause un step 3 = EOT immédiat sur TOUS les audios.
- Fix : forced_prompt = [SOT, lang_tok, TRANSCRIBE_TOK], modèle décide
  timestamps vs notimestamps après. lang="auto" garde juste SOT.

Résultat après les 2 fixes : 6/6 audios FR transcrivent en texte cohérent
(damien/elodie/jerome/richard/sid/zelda).

Chiffres honnêtes (warm path, après fixes) :
- Mel : 220-320 ms (vs prod 189) — DFT plus coûteux mais correct
- Encoder : 60-86 ms (vs prod 125) — gain options QNN burst/v79/fp16
- Decoder/token : 12-13 ms (vs prod 23) — gain zero-copy + options QNN
- Total 21 tokens / 3s : ~620 ms (mesuré)
- Total estimé 22 tokens / 1.6s : ~546 ms (vs prod ~820, -33 %)

Les chiffres précédents (-45 %, mel 60 ms) étaient FAUX à cause du bug #1.
Le -33 % réel reste meilleur que prod mais l'écart s'est réduit.

A/B accuracy CER/WER vs WhisperHybridEngine reste à mesurer in-app par
le dev sur SES audios fixtures.

Modifs :
- kazeia_mel.cpp : ajout DFT directe pour n_fft non pow2
- stt_engine.cpp : forced decoder prompt + lang_to_token + flags KZSTT_QNN_*
  réglables runtime pour debug
- stt_cli.cpp : multi-audio sequence + KZSTT_DEBUG_STEPS pour trace logits

Crédit : sans le retour précis du dev sur les vides observés, ces bugs
étaient passé en checklist §9 et fait crash en prod.
2026-06-01 12:19:33 +02:00
Richard Loyer dbd4f3d944 chantier B STT #5 : Action 1 dev — patch QNN options pour la prod gelée
Suite à l'analyse du dev (commit c9e1983) : le gain mesuré (-80% encoder,
-71% decoder/token) vient des options QNN ExecutionProvider, pas de la
migration C++. Donc le patch s'applique aussi à la prod Kotlin actuelle
sans dégeler la stack STT — 4 lignes Kotlin, réversible.

Livrables Action 1 (séparée de la décision migration) :

- dist/PATCH_QNN_PROD.md : explication + diff Kotlin + chiffres attendus
  (prod 820 ms -> ~560 ms = -32 % gratuit) + script bench audio Pierre

- dist/patches/whisper_hybrid_engine_qnn.diff : patch précis sur
  WhisperHybridEngine.kt (lignes 101, 109, 134 du fichier prod). Ajoute
  htp_performance_mode=burst + htp_arch=79 + enable_htp_fp16_precision=1
  + setOptimizationLevel(ALL_OPT) sur les 3 sites encOpts/decOpts.

- dist/patches/bench_qnn_before_after.kt : harness Android pour mesurer
  le gain réel sur audios Pierre. 3 runs/audio, médiane, RTF pondéré.
  Compare 2 builds app (avant patch / après patch) sur le même corpus.

Décision migration libkazeia_stt CONDITIONNEE à :
 1. Action 1 mesurée sur audio Pierre (gain réel patch QNN seul)
 2. A/B accuracy CER/WER lib unifiée vs prod patchée (in-app uniquement,
    pas mesurable standalone)

Bénéfice résiduel migration après patch QNN prod : ~110 ms total + 168 MB
RAM + code unifié. Reste valable mais beaucoup moins fort que les chiffres
initiaux. Le dev a raison de séparer les décisions.

Mémoire à mettre à jour : project_stt_qnn_options_win.
2026-06-01 09:42:27 +02:00
Richard Loyer c9e198332d chantier B STT #4 : audit dev pris en compte — options QNN explicites + bench apples-to-apples
Critique du dev Kazeia juste : la comparaison RTF 0.18 (10s) vs 0.51 (1.6s) prod
était pommes/oranges (encoder fixe amorti sur audio long), et l'encoder +152 %
non investigué cachait un vrai problème de config QNN par défaut.

Investigation : options QNN ExecutionProvider explicites manquaient
 - htp_performance_mode = "burst" (au lieu de default = balanced)
 - htp_arch = "79" (au lieu de auto-detect)
 - enable_htp_fp16_precision = "1" (NPU fp16 natif)
 - SetGraphOptimizationLevel(ORT_ENABLE_ALL) + DisableMemPattern()

Impact mesuré (5 runs warm, audio FR continu, 12 tokens / 1.6s) :
 - Encoder : 309 ms -> 62 ms  (-80 %)
 - Decoder/token : 48 ms -> 14 ms  (-71 %)
 - Total : 962 ms -> 320 ms  (-67 %)
 - RAM peak : 377 MB inchangée

Cas réel Kazeia (1.6s, extrapolé 22 tokens) :
 - Prod Kotlin : 820 ms (mel 189 + enc 125 + 22*23)
 - C++ unifié  : ~450 ms (mel 80 + enc 62 + 22*14)
 - Gain réel : -45 % sur le workload réel (pas amortissement)

Audio long (15s, 71 tokens) : decoder 10 ms/tok, RTF 0.06.

Mode warm bench (KZSTT_RUNS=N) : exerce N transcribes successifs sur le même
engine pour distinguer cold-start (1er run) vs warm path (suivants).

Recommandation prod Kotlin : ajouter les mêmes options QNN (encOpts.addQnn
multimap) pour gagner sans migration. Documenté dans STT_INTEGRATION.md §7bis.

A/B accuracy : 10 utterances FR transcrites sortent du texte FR cohérent
(avec erreurs typiques Whisper-Small sur mots rares : "Vagons" vs "Wagons",
"zéfière" vs "zéphyr"). La comparaison CER/WER vs WhisperHybridEngine sur
les mêmes fichiers reste à faire côté app (checklist §9.2) — non faisable
standalone sans la prod Kotlin en CLI.

Remerciements : critique du dev a permis de trouver une optim qui sert
TANT la version C++ unifiée QUE la prod Kotlin actuelle.
2026-06-01 09:08:22 +02:00
Richard Loyer 970f731e01 chantier B STT #3 : decoder zero-copy + sanity vocab + checklist dev
Optim decoder identifiée après bench sur audio FR continu (39 tokens) :
le `std::vector<>::emplace_back(data)` à chaque step recopiait 12 MB de
self KV inutilement = 2.4 GB de bande passante mémoire sur le loop entier.

Refactor zero-copy :
 - Pré-allocation UNE FOIS des buffers persistants (self_k/v, mask, input_ids,
   position_ids) au début de transcribe()
 - Pré-création UNE FOIS des Ort::Value pointant sur ces buffers (les Ort::Value
   sont des "borrowing views" — ORT lit les buffers au Run())
 - Cache des indices k_self_out / v_self_out / logits_out (parsing 1 fois,
   pas N fois dans le loop)
 - Update self KV : memcpy direct depuis dec_outputs vers nos buffers persistants
   (qui sont déjà les inputs du step suivant)
 - mask_buf updaté à 1 fp16 par step (juste le slot R-to-L), pas 200

Bench zero-copy sur Snapdragon 8 Elite :
 - Decoder/token : 98 ms -> 36 ms (-63%)
 - Decoder total 39 tokens : 3831 ms -> 1390 ms (-64%)
 - RTF audio 10s FR continu : 0.42 -> 0.18 (mieux que prod 0.51)
 - RAM peak : 378 MB inchangée (zero-copy n'augmente pas la peak)

Sanity vérifié au runtime :
 - Logits decoder shape = 51865 elements (= constante VOCAB_SIZE OK,
   pas de buffer over-read sur argmax). Le vocab.json contient 50258 tokens
   texte, les 1607 manquants sont les tokens langue/timestamp non décodés.

Doc STT_INTEGRATION.md :
 - Chiffres perf à jour (RTF 0.18, decoder 36 ms/token)
 - Nouvelle section "Checklist d'intégration" (6 points à vérifier en premier
   côté app : System.loadLibrary, transcribe vs WhisperHybridEngine, null/edge,
   VAD drop-in, threading IO, cycle de vie release)
 - Roadmap optim future via Ort::IoBinding (gain estimé 30% sur decoder restant)
2026-05-31 22:26:40 +02:00
Richard Loyer 8f6253204c chantier B STT #2 : S2 + S3 livrés (stt_engine ORT QNN + JNI + façade Kotlin + doc)
S2 port complet WhisperHybridEngine.kt -> C++ :
 - stt_engine.cpp impl complète (ORT 1.24.3 + QNN EP HTP V79, decoder KV-cache
   autoregressif 200 steps, override translate->transcribe, parsers JSON inline
   pour mel_filters et vocab.json, tokenizer BPE byte-level GPT-2/Whisper)
 - kazeia_stt_cli : binaire test (1.3 MB statique)
 - Bench bit-correct vs prod : transcription FR identique sur 3 runs déterministes
   ("Elle mat est un casque de chantier connecté et modulable.")
 - Perf SM8750 : RTF 0.41-0.43 (vs prod 0.51), RAM peak 378 MB (vs 545 MB)
 - Mel C++ 60 ms (FFT) vs prod 189 ms (DFT). Decoder per-token 100 ms vs 23 ms
   prod (overhead memcpy KV; optim IO bindings ORT non-bloquant)

S3 JNI + façade + doc :
 - kazeia_stt_jni.cpp : 7 fonctions JNI exportées (nativeLoad/Transcribe/Free
   + nativeVadNew/Push/Reset/Free). Sortie pipe-séparée parseable côté Kotlin.
 - libkazeia_stt.so : 168 KB SHARED, prête pour jniLibs/arm64-v8a/
 - SttEngine.kt : façade Kotlin minimale (~100 l) parallèle à TtsEngine.kt
   (SttJni / SttEngine / SttVad / SttResult)
 - Suppression côté app : WhisperHybridEngine.kt (527L), MelExtractor.kt,
   VadStage.kt, mel_extractor.cpp, libmel_extractor.so (documentée)

Doc livrable dev :
 - STT_INTEGRATION.md (10 KB) : guide complet migration + perf + erreurs +
   bench rapide + roadmap optim
 - KAZEIA_ENGINE_OVERVIEW.md : vue globale moteur unifié (LLM + TTS + STT),
   architecture des libs, API Kotlin unifiée, empreinte cascade complète
 - CLAUDE.md : section "STT unifié engine (31/05)" + verdict A invalidé

Runtime ORT 1.24.3 + QNN libs : extraites cache Gradle Maven (cf README_ORT.md
+ README_QNN.md dans dist/lib/). Pas de tarball ext. Modèles whisper-small-sm8750
prod inchangés (545 MB QAIRT contexte conservé).
2026-05-31 22:15:58 +02:00
Richard Loyer 4b43d9e3d7 chantier B STT #1 : S1 unification engine (mel partagé + skeleton stt + VAD C++)
S1.1 mel extractor unifié (kazeia_mel.{h,cpp}) :
 - Paramétrable : window (sym/periodic), compression (log/log10), post-norm
   (none/whisper), FFT auto radix-2 (zero-pad N_FFT pas puissance de 2)
 - Configs pré-faites : config_qwen3_tts_speaker() + config_whisper()
 - speaker_encoder.cpp migré dessus : régression damien cos=0.9997 et bit-exact
   (max abs diff = 0) vs avant. E2E clonage in-process intact (RTF inchangé).

S1.2 audit ORT C++ :
 - libonnxruntime.so + headers C++ trouvés dans cache Gradle (AAR Maven
   onnxruntime-android-qnn:1.24.3). API_VERSION=24, QNN EP intégré.
 - headers/ copiés dans dist/include/onnxruntime/ (versionné, ~1 MB)
 - libonnxruntime.so copiée dans dist/lib/ (gitignore, 20 MB, README pour
   reproduire l'extraction).

S1.3 skeleton stt_engine :
 - stt_engine.{h,cpp} : API parallèle à tts_engine.h
   (SttEngineLoadCfg / SttTranscribeCfg / SttTranscribeResult)
 - VAD RMS C++ complet (port de VadStage.kt prod : frame=1600, seuil=150,
   3 frames speech, 8 frames silence)
 - stt_engine_load + stt_engine_transcribe : stubs err=-99 jusqu'au port ORT
   QNN en S2 (encoder NPU + decoder loop KV-cache + tokenizer BPE)
 - Binaire kazeia_stt_test (1.2 MB statique) valide mel Whisper [80,3000] +
   VAD sur audio FR 5s, plage de valeurs cohérente.

Reste S2 : port encoder/decoder NPU via Ort::Session + QNN EP, port tokenizer
BPE byte-level, override transcribe. Quand S2 done : S3 = JNI + façade Kotlin
+ suppression libmel_extractor.so / VadStage.kt / WhisperHybridEngine.kt.
2026-05-31 19:19:28 +02:00
Richard Loyer 82d206137f chantier B TTS #8 : speaker encoder embedded + clonage in-process
SE.3+SE.4+SE.5 : ECAPA-TDNN ggml C++ bit-exact + intégration TtsEngine.

speaker_encoder.{h,cpp} :
 - API publique : speaker_encoder_load / encode_wav / encode_waveform / free
 - mel C++ (FFT radix-2, Hann, reflect pad, librosa basis) bit-exact Python
 - ECAPA-TDNN forward : conv0 + 3 SE-Res2Net + MFA + ASP + FC -> x_vector[1024]
 - validation : cos=0.9997 vs Python ref (damien_5s.wav)
 - SPK_STANDALONE -> binaire CLI kazeia_speaker_encode

tts_engine :
 - TtsEngineLoadCfg.speaker_encoder_gguf + mel_basis_path (load-time opt-in)
 - TtsSynthesizeCfg.xvector_override (per-call, RAII restore)
 - tts_engine_encode_speaker_wav / _waveform exposés

tts_pipeline : KZTTS_SPK_GGUF + KZTTS_MEL_BASIS + KZTTS_REF_WAV -> clonage
in-process (un seul binaire, plus de subprocess swap manuel).

Pièges trouvés en route (documentés CLAUDE.md project_tts_*) :
 - ggml_conv_1d exige weights F16 -> cast au call site
 - input layout : data[c*T+t] (ggml ne[0]=T fastest) pas memcpy [T,C]
 - refs Python en bytes [C,T] = ggml [T,C] côté cpp
 - RIFF reader chunk-parsing (ffmpeg = LIST/JUNK avant data)

Test E2E : 4 voix (damien/amir/elodie/zelda) clonées à partir de WAV ref 5s,
toutes audibles et distinctes.

Reste : JNI nativeSynthesizeWithReference (refactor mécanique, prochaine session).
2026-05-31 15:54:29 +02:00
Richard Loyer e42e8f8464 SE.1+SE.2+SE.3 partiel : speaker encoder pipeline POC bout-en-bout
Chantier Option C : embarquer le speaker encoder Qwen3-TTS dans Kazeia-Engine
pour permettre le clonage vocal depuis n'importe quel WAV (5-10s) en
tout-tablette sans étape Python offline.

SE.1 Audit : speaker_encoder_weights.pt = ECAPA-TDNN classique
  - 76 tenseurs, 8.85M params (33.8 MB f32 -> 17 MB f16)
  - blocks.0 Conv1d(128->512 k=5) -> 3x SE-Res2Net (tdnn1+res2net+tdnn2+se)
    -> MFA concat(1536) -> ASP(stats pooling) -> FC(3072->1024)
  - Toutes ops supportées par ggml-cpu (Conv1d via im2col+matmul)

SE.2 Convert : dist/scripts/convert_speaker_encoder_to_gguf.py
  produit speaker_encoder.gguf (17 MB f16) + metadata mel config :
    n_mels=128, sr=24000, n_fft=1024, hop=256, win=1024, fmin=0, fmax=12000
  (bit-exact qwen_tts.core.models.modeling_qwen3_tts.mel_spectrogram)

SE.3 partiel : dist/jni/speaker_encoder.cpp + build_speaker_encoder.sh
  POC bout-en-bout sur tablette :
    WAV damien_5s (24kHz mono, 120k samples = 5s)
      -> mel 128 x 468 frames (FFT radix-2 + Hann + reflect pad + librosa basis)
      -> GGUF loaded (76 tensors)
      -> [TODO session suivante : forward ECAPA-TDNN complet, ~300-400 lignes]
      -> stub x_vector[1024] zeros écrit pour valider l'I/O

  Binaire : 53 KB statique, dépend juste de libggml dynamic.

  mel_basis librosa pré-calculé (24kHz/128 mel/0-12000Hz) dumpé en
  dist/models/mel_basis_qwen3tts.bin (262 KB f32 [128,513]) pour
  reproduire à l'identique torch.stft + librosa filter_bank.

À faire next session :
  - Valider mel C++ bit-exact vs torch.stft Python (sanity)
  - Implémenter forward ECAPA-TDNN : ggml_conv_1d, SE block (squeeze/excite),
    ASP (Attentive Statistics Pooling = stats globaux + tdnn attention),
    FC final.
  - Bit-match damien_xvector.bin référence Python.
  - JNI bridge + Kotlin cloneVoice(refWavPath): FloatArray

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 11:37:14 +02:00
Richard Loyer 19ac123afa TU sampler tuning : top_p 1.0->0.95 + rep_penalty 1.05->1.10 nouveaux défauts
Validation à l'écoute (panel A/B 4 combos sur 'Bonsoir, comment tu te sens
ce soir ?'). Verdict utilisateur : combinaison TOPP_0.95_REPP_1.10 sonne le
moins robotique.

Analyse complémentaire TU.2 (dump codes via KZTTS_DUMP_CODES=path) sur 4
phrases panel a confirmé :
  - code 690 CB0 = attracteur silence (5.9% du panel, doublé systématiquement
    en début de phrase, jusqu'à 4 répétitions consécutives mesurées)
  - rep_penalty 1.05 trop doux pour casser ces 690 690
  - rep_penalty 1.10 plus nucleus 0.95 -> top_p coupe la queue improbable
    + rep défavorise les codes récents -> moins de pause robotique

Nouveaux défauts (tts_engine.h TtsSynthesizeCfg + tts_pipeline env defaults
+ TtsEngine.kt TtsSampling) :
  cp_top_p     = 0.95f  (était 1.0)
  cp_rep_penalty = 1.10f (était 1.05)
  talker_top_p = 0.95f  (était 1.0)
  talker_rep_penalty = 1.10f (était 1.05)
  cp_temp, top_k, talker_temp/top_k inchangés (0.9, 50).

Mesure panel 4 phrases avec nouveaux défauts + decoder chraac subproc :
  'Bonjour Kazeia'                            : N=29 RTF 2.19
  'Bonsoir, comment tu te sens ce soir ?'    : N=48 RTF 2.21
  'Je rumine la nuit.'                       : N=34 RTF 2.21
  'Comment puis-je t'aider aujourd'hui ?'   : N=29 RTF 2.27

RTF stable ~2.2. Compression légère sur phrases courtes (29 vs 33 baseline),
expansion sur phrases interrogatives ('Bonsoir' N=48 vs 41). Cohérent
avec top_p resserré qui choisit des codes plus pertinents.

KZTTS_DUMP_CODES=path ajouté dans tts_engine pour analyse future
(int32 binaire [N,16] time-major).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 11:18:25 +02:00
Richard Loyer c332943181 Decoder chraac en sous-process : -20% decoder, -9% pipeline, talker intact
Architecture pour exploiter le gain chraac sans casser le talker :
  - kazeia_decoder_chraac : binaire static contre libqwen3tts-decoder.a chraac
    + ggml chraac statiques (9.8 MB autonome, pas de dép .so chraac).
    Args : <decoder.gguf> <codes.bin> <T> <out.wav>. Lit codes int32 time-major,
    transpose codebook-major, forward, écrit WAV.
  - tts_engine : si KZTTS_DECODER_SUBPROC=path + KZTTS_DECODER_GGUF=path posés,
    fork+execl au lieu de dec.forward intégré. Codes via /data/local/tmp tmpfile,
    WAV via rename. Le talker+CP continuent sur ql/ggml (qualcomm) qui marche.

Mesures Pad3 KZTTS_THREADS=6 GGML_NUM_THREADS=8 seed=42 :

  Phrase 'Bonjour Kazeia' (33 frames) :
    A baseline qualcomm : decoder 3.29s, total 6.61s, RTF 2.40
    B chraac subproc    : decoder 2.63s, total 6.06s, RTF 2.20
    -> decoder -20%, total -8.3%, audio RMS 1743 identique

  Phrase 'Bonsoir, comment tu te sens ce soir ?' (41 frames) :
    A baseline : decoder 3.95s, total 8.16s, RTF 2.39
    B subproc  : decoder 3.15s, total 7.38s, RTF 2.16
    -> decoder -20%, total -9.6%

Audio cohérent au baseline (range, RMS quasi identiques), imperceptible
à l'oreille (cf cas 'Bonsoir' envoyé).

Le gain chraac historique sur decoder seul est désormais accessible SANS
casser le talker.

Code committé opt-in (KZTTS_DECODER_SUBPROC absent par défaut = baseline intact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 18:14:09 +02:00
Richard Loyer 2809b54943 CH chraac-llama : -18% RTF (2.43 -> 2.04) via swap ggml source
Découverte historique remise au jour : la mémoire 'TTS RTF<1 atteint via
chraac-llama' (projet d'origine /opt/Kazeia, 02/05) documente que le
décodeur TTS atteint RTF 0.96 avec ggml-cpu de chraac-llama (dev-refactoring
commit 897501a) vs 1.47 avec llama-upstream (= notre ql/ggml actuel).

Notre Kazeia-Engine builde contre ql/ggml = github.com/qualcomm/llama.cpp
fork = essentiellement llama-upstream + hexagon backend. NEON heads optim
sortie cette session a déjà compensé une partie mais pas tout.

Validation reproductibilité :
  Bench historique decoder seul (T=56) sur Pad3 chargée : RTF 0.935
  (legèrement mieux que 0.961 historique, batterie pleine + device froid).

Build chraac variant :
  - libs runtime : copies de /opt/Kazeia/chraac-llama/build-android-kazeia/bin
    poussées dans /data/local/tmp/kz-engine/bin-chraac
  - libqwen3tts-decoder.a : rebuild fresh contre chraac/ggml dans
    /opt/Kazeia/kazeia-tts-decoder-ggml/build-android-chraac-fresh
    (code actuel avec load_with_backends + snake_consts)
  - tts_pipeline_chraac : linké contre ces libs, headers
    /opt/Kazeia/chraac-llama/include + ggml/include

Mesure A/B Pad3 (KZTTS_THREADS=6 GGML_NUM_THREADS=8 seed=42) :

  Phrase 'Bonsoir, comment tu te sens ce soir ?' :
    Baseline ql/ggml  : N=41 RTF 2.43  talker=31.5  cp=70.2  decoder/frame=98.1 ms
    chraac-llama      : N=64 RTF 2.04  talker=31.2  cp=57.5  decoder/frame=76.9 ms
    Delta per-frame   :       -16%      -1%      -18%      -22%

  Phrase 'Bonjour Kazeia' :
    Baseline : N=33 RTF 2.47 / Chraac : N=64 RTF 2.02
    (Différence N = trajectoire diverge — précision f32 différente entre
    stacks fait que le sampler talker produit des codes différents et
    génère une phrase plus longue avant EOS)

Gains :
  - Decoder -22% confirmé (cohérent avec ratio chraac 0.96 / llama-upstream 1.47)
  - CP -18 à -21%  (ggml_graph_compute repack/sgemm ARM optimisé chraac)
  - Talker -1% (déjà au plafond NEON via llama.cpp)
  - RTF total -16 à -18%

Bug découvert (à traiter plus tard) :
  Phrase historique 56 codes 'Bonjour, je m'appelle Kazeia, je suis encore en phase de développement' (= ~60 frames) crash ggml_sin chraac (GGML_ABORT 'unsupported types' probable). Phrases moyennes (<= 50 frames)
  passent OK. Sans doute incompat de types entre snake_consts (ctx_const dans
  Decoder) et le path ggml_sin de chraac (qui n'a peut-être pas la même
  variante f16/f32 que les ops récentes).

Path actuel ql/ggml RESTE le défaut. Le chraac est build séparé tts_pipeline_chraac
opt-in pour A/B. Libs chraac dans dist/lib-chraac/ (gitignored).

À faire next :
  - Investiger crash ggml_sin chraac sur T>= ~50 frames
  - Décider : swap définitif Kazeia-Engine sur chraac (avec lib-chraac
    comme runtime principal) OU rester sur ql/ggml et patch chraac repack
    optims dedans

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 12:38:47 +02:00
Richard Loyer d6acf378d5 POC streaming TTFB ÷4-6 (KZTTS_STREAM_CHUNK=K)
Concept validé : decoder appelé incrementalement sur codes accumulés tous
les K frames. Premier appel donne TTFB rapide.

TtsSynthesizeResult ajoute ttfb_s + n_chunks. tts_engine_synthesize
détecte KZTTS_STREAM_CHUNK > 0 et appelle decoder.forward(codes_so_far)
tous les K codes, accumule la portion nouvelle dans wav_stream.

Mesure Pad3 phrase 'Bonjour Kazeia' (33 frames) :
  baseline           : TTFB 6.80s, RTF 2.47
  KZTTS_STREAM_CHUNK=8 : TTFB 1.90s (-72%), 5 chunks, total RTF 8.67 (×3.5 cost)
  KZTTS_STREAM_CHUNK=4 : TTFB 1.13s (-83%), 9 chunks, total RTF 13.82 (×5.6 cost)

Limitations connues du POC :
1. Total time explose : recompute decoder sur N_so_far à chaque chunk =
   somme des coûts O(N_chunks × N_avg) au lieu de O(N_total).
2. WAV pas bit-exact vs baseline : decoder.forward(K) ne match pas le
   prefix de decoder.forward(N). Probable conv_transpose_1d qui regarde
   au-delà du strict causal kernel/stride. À comparer à l'oreille.

POC opt-in (KZTTS_STREAM_CHUNK=0 par défaut = path baseline intact).

Pour rendre utilisable en prod (sessions suivantes) :
- Refactor decoder avec state KV cache (pre_transformer en KV cache style
  comme cp_forward_cached_step + BigVGAN avec context buffer ~5 frames input)
- Threading 2-thread : generator (talker+CP) en parallèle de decoder
- Cible : TTFB ~500ms, total time ≈ baseline + overhead minimal

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 22:56:14 +02:00
Richard Loyer 27d893c73f Vulkan Adreno 830 testé : neutre au mieux, régresse au pire (commit aussi le code opt-in)
Re-analyse globale plateforme : le GPU n'avait JAMAIS été testé. Build
ggml-vulkan pour Android arm64 (NDK r27d + SPIRV-Headers + vulkan.hpp
téléchargés depuis github, libomp.so poussé sur tablette).

Adreno 830 détecté : uma=1 fp16=1 matrix cores=NONE (compute shaders seuls).

3 patterns testés :
  (1) Decoder weights Vulkan buft + CPU compute : NEUTRE
      RTF 2.45 -> 2.40-2.47 (bruit), WAV bit-exact
      Cause : UMA = même DRAM, pas de gain BW
  (2) Decoder + sched (GPU compute attendu) : tout CPU
      sched route UMA buffer -> CPU (logique is_host)
      Cause : sched ne déclenche pas l'offload pour buffers UMA
  (3) Talker offload 29/29 layers via llama.cpp : REGRESSION +128%
      Prefill 87->775 ms, talker 32->45 ms, decoder 3.3->6.6s
      Cause : 0.6B + batch=1 + bus contention GPU/CPU sur DRAM partagée

Verdict cumulé HTP + Vulkan : sur SM8750 ce TTS spécifique, ni HTP ni
GPU n'apportent gain net. NEON CPU optimisé reste la voie unique
réaliste sur ce hardware.

Conso énergie : Vulkan talker = pipeline ×2.3 plus long, conso ~×2.
NEON CPU = meilleure efficience.
Verdict : RESTE SUR NEON CPU pour la prod.

Code committé opt-in (KZTTS_VULKAN_LIB, KZTTS_DECODER_VULKAN,
KZTTS_TALKER_VULKAN), désactivé par défaut. Path principal RTF 2.45 intact.

Détails complets dans dist/PERF_VULKAN_TESTS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 22:28:16 +02:00
Richard Loyer a5d591a07b doc: PERF_CPU_OPTIMS.md — bilan optims CPU, RTF 2.93->2.45
Synthèse de 6 stratégies CPU testées systématiquement :
  T1.bench profile fin    : ggml_init=0ms, copies gratuites, delta 24ms = sample_head
  T1.1 thread sweep       : T=6 G=8 optimal, T=8 catastrophe (RTF 40+)
  T1.2 affinity taskset   : Android tatillon, masks restrictifs freezent
  T2.1 NEON heads 16-way  : -33% CP, -16% pipeline, BIT-EXACT  <- GROS GAIN
  T2.2 threadpool persist : estimé 0.15% gain, skipped
  T1.3 ctx réutilisé      : invalidé par profile (init=0ms)

État final RTF 2.45 stable sur 3 phrases (33/41/60 frames). Decoder
maintenant 49% du total. Plafond CPU pur atteint sans toucher kernel.

Leviers résiduels :
- Streaming pipeline (1 session) : seul gain UX disponible, TTFB ~500ms
- Decoder fine optim : < 5% potentiel additionnel
- RTF<1 = chantier kernel hexagon ou Vulkan, hors scope CPU

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 17:30:54 +02:00
Richard Loyer 9a0c63f164 T2.1 CP heads NEON 16-way: CP -33%, pipeline -16%, bit-exact
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>
2026-05-29 17:26:54 +02:00
Richard Loyer 101d1cd7ed tests infra HTP/HMX : verdict, le HTP n'est pas le bon outil pour TTS
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>
2026-05-29 16:40:56 +02:00
Richard Loyer 48c66c03e1 chantier F (CP HMX) : path sched livré, gain absent (overhead per-call)
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>
2026-05-29 16:03:41 +02:00
Richard Loyer bdd528ed25 chantier BigVGAN HMX Phase 2b: refactor sched-friendly + limite backend découverte
Refactor stage_bigvgan en variante stage_bigvgan_sched :
  - ggml_init(no_alloc=true) + mem_size réduit 12 GB -> 16 MB
  - ggml_set_input/output sur input/output
  - causal_conv1d_TC : ggml_pad_ext au lieu de memset zeros + concat
  - apply_snake_TC : lookup d.snake_consts pré-calculé (au lieu de host exp() per-frame)
  - Pattern sched_reset + alloc_graph + tensor_set + compute + tensor_get
  - Switch dans Decoder::forward : sched ? stage_bigvgan_sched : stage_bigvgan

Snake constants pré-calculées au load_with_backends :
  - 29 paires (a_eff, inv_b) stockées dans ctx_const + buf_const sur le même
    backend que buf_w. exp() host-side une fois au load au lieu de chaque frame.
  - buf_const ~0.1 MB sur HTP0 (29 × 2 × C × f32 avec C de 96 à 1536).

Mesure tablette (KZTTS_DECODER_HTP=1 KZTTS_DECODER_SCHED=1 GGML_HEXAGON_USE_HMX=1) :
  baseline CPU pur     : decoder 3.34s, bigvgan 3.19s
  sched HTP path neuf  : decoder 3.95s, bigvgan 3.76s  -> REGRESSION 18%

Cause : GGML_SCHED_DEBUG=2 révèle que TOUS les MUL_MAT tombent sur CPU.
ggml_hexagon_supported_mul_mat rejette ggml_nrows(src1) > 1024 (commentaire
'no huge batches (for now)'). BigVGAN block 3 résiduels : conv1d k=7 sur
T=59520, im2col_output [672, 59520] -> 59520 rows >> 1024. Tous les
MUL_MAT BigVGAN sont rejetés -> fallback CPU + copies inutiles -> régression.

PHASE 2b STATUT : refactor fait et stable, mais le gain HMX nécessite
soit (a) modifier ggml-hexagon pour gros batches, (b) splitter MUL_MAT
en chunks ≤ 1024, (c) reprendre via étape F (CP HMX) qui n'a pas ce
problème (nrows ≤ 16 par sub-forward).

Snapshots updatés dans dist/decoder_patches/.

Le path activé seulement si KZTTS_DECODER_SCHED=1 (opt-in, désactivé par
défaut). En mode KZTTS_DECODER_HTP=1 sans SCHED : path legacy CPU pur, perf
inchangée vs baseline (poids juste relogés sur HTP buffer via opt_hostbuf=1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:05:35 +02:00
Richard Loyer 9de6b76838 chantier BigVGAN HMX Phase 0+2a: audit + chaîne load HTP validée
PHASE 0 audit (PERF_ANALYSIS.md + cette commit):
  - BigVGAN = 95% du decoder (3.24s sur 3.36s), confirmé via KZTTS_DECODER_PROFILE=1
  - 26 conv1d (63.7 Gops) + 4 conv_transpose_1d (51.6 Gops) + 30 snake (0.4 Gops)
  - channels par block : 1024->1536->768->384->192->96->1
  - T_cur évolution : 124->992->4960->19840->59520
  - poids decoder TOUS en F16 déjà (110 MB F16 conv + 215 MB F32 snake α/β)
  - ggml_conv_1d = im2col + mul_mat en interne, le graphe expose déjà MUL_MAT
  - ggml-hexagon ql/ggml supporte : MUL_MAT (HMX fp16), UNARY (exp/silu/etc),
    SOFT_MAX, ROPE, ADD, MUL, NORM, PAD, etc.
    PAS supporté : IM2COL, CONV_1D, CONV_TRANSPOSE_1D, SIN
  - opt_hostbuf=1 default -> HTP buffer CPU-mappable via ION
  - budget RAM HTP session: 2 GB sur 3.5 GB plafond, ~1.5 GB marge

PHASE 2a (chaîne load HTP, sans gain HMX):
  - Refactor decoder.h + gguf_loader.cpp : load_with_backends(path, devs)
    crée le sched optionnellement (gated KZTTS_DECODER_SCHED).
  - tts_engine.cpp : KZTTS_DECODER_HTP=1 -> initialise backend HTP +
    backend CPU (sched), passe les deux à load_with_backends.
  - Mode KZTTS_DECODER_HTP=1 sans SCHED : poids 325 MB sur HTP, compute
    CPU pur lit via opt_hostbuf=1. WAV produit correct, ~baseline perf.
  - Mode SCHED=1 : sched créé mais sched_alloc_graph plante (buffer_id>=0)
    car les stages utilisent ggml_init(no_alloc=false) + memset, incompatible
    avec sched. Refactor stages requis pour activer.

Snapshots des modifs decoder dans dist/decoder_patches/ (le repo decoder
est externe /opt/Kazeia, fichiers untracked).

PHASE 2b à faire prochaine session (~1 session):
  - stage_bigvgan en pattern sched-friendly:
    * no_alloc=true + ggml_set_input/output
    * memset zeros -> ggml_pad_ext
    * precompute_snake host -> ggml_exp + ggml_div in-graph
    * sched_reset + alloc + tensor_set + compute + tensor_get
  - Cible: decoder 3.2s -> 0.5-0.8s (×4-6 sur MUL_MAT HMX)
  - Snake sin reste CPU (fallback sched), borné à 0.4 Gops/115 total

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:51:08 +02:00
Richard Loyer 1c25c975dc 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>
2026-05-28 22:43:23 +02:00
Richard Loyer 930f3c8c1f chantier B TTS #11: libkazeia_tts.so + Kotlin wrapper, JNI validé bout-en-bout
Refactor: tts_engine.{h,cpp} = API publique de l'engine TTS (load une fois,
synthesize N fois, free). État load-once dans struct TtsEngine (talker
model+ctx, CPState, Decoder, KzTextTokenizer, fixtures + embeds spéciaux
+ role_proj pré-calculé). KV cache talker reset via llama_memory_clear
au début de chaque synthesize -> appels indépendants.

tts_pipeline.cpp devient un thin CLI dessus (refonte sans changement de
sortie : WAV md5 identique au pré-refactor sur 'Bonjour je m'appelle Kazeia').

JNI bridge: kazeia_tts_jni.cpp expose 3 fonctions :
  Java_com_kazeia_tts_TtsJni_nativeLoad / Synthesize / Free
Signatures alignées avec TtsEngine.kt (companion loadLibrary 'kazeia_tts').
Build: build_kazeia_tts.sh -> b-jni/libkazeia_tts.so (~900 KB).

Test_engine_2calls (sans JNI) : 3 synth sur même instance, KV reset OK,
2 appels identiques -> WAV md5 identique, appel 3 différent -> codes
différents.

Test_jni_tts (harness JNI sans VM Java) : dlopen libkazeia_tts.so, JNIEnv
mock minimal (GetStringUTFChars/Release + NewIntArray/SetIntArrayRegion),
appels load + 2 synth + free. WAV md5 identiques aux runs directs. exit=0.

Empreinte tablette mesurée: ~3.5 GB par instance (talker f32 1.6 GB + CP
f16 mixte 250 MB + decoder 325 MB + vocab Qwen3 vocab_only 50 MB + fixtures
text_embed/tp_* 1.2 GB). Une instance par process.

RTF stable autour de 3.0 (CPU 6t), inchangé vs avant refonte.

Reste sur la liste initiale : tuning sampling (itératif à l'oreille).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:54:21 +02:00
Richard Loyer b27c8ef403 chantier B TTS #10: tokenizer Qwen3 BPE via llama_tokenize, texte arbitraire
Plutôt que de porter Qwen3 BPE (NFC + pre-tokenize regex Unicode + ByteLevel +
merges + ignore_merges) à la main en C++ (~500 LOC risquées), on charge
n'importe quelle Qwen3 GGUF en vocab_only=true (~50 MB RAM, pas de poids) et
on appelle llama_tokenize(parse_special=true). Bit-exact garanti par
construction.

kazeia_text_tokenizer.h/cpp : API courte
  kz_tok_load(tok, gguf_path) / kz_tok_free / kz_tok_encode
  kz_tok_encode_tts_prompt(tok, content) -> enveloppe dans le template chat
    <|im_start|>assistant\n{content}<|im_end|>\n<|im_start|>assistant\n
  (déduit du dump golden : 16 tokens pour 'Bonjour je m'appelle Kazeia').

test_tokenizer : binaire standalone vérifiant bit-exact vs input_ids_full.bin
  ./test_tokenizer Qwen3-4B-Q4_0.gguf tts_dump/input_ids_full.bin 'Bonjour je m'appelle Kazeia'
  -> OK BIT-EXACT (16/16 tokens identiques au golden HF).

tts_pipeline : si KZTTS_TEXT et KZTTS_VOCAB_GGUF posés, tokenize la phrase au
lieu de lire input_ids_full.bin. La structure attendue (3 role + Nt body + 5
trailing) est exactement ce que kz_tok_encode_tts_prompt produit, donc
input_ids_len = Nt + 8 sans rien d'autre à changer dans la construction
prefill_embeds.

Mesure tablette Pad3 (cpu 6t, KZTTS_CP_CACHE=1, seed=42) :
  A fixture        : 31 frames, total 7.866s, RTF 3.04
  B tokenizer C++  : 31 frames, total 7.842s, RTF 3.04  + WAV md5 identique
  C 'Bonsoir...'   : 41 frames audio 3.4s, total 10.5s, RTF 3.09 (phrase neuve OK)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:21:53 +02:00
Richard Loyer 1203d7ce5d chantier B TTS #9: CP weights f16 conservés, CP -52% cumulé, bit-exact
cp_load: les tenseurs F16 du gguf restent F16 en mémoire (sauf
*_norm.weight qui restent F32 — ggml_mul exige même type que xn).
ggml_mul_mat gère mul_mat(W_f16, x_f32) -> out_f32 nativement, donc
aucun changement dans cp_forward_cached_step / cp_forward_lastpos.

Mesure tablette Pad3 (KZTTS_CP_CACHE=1, seed=42, 31 frames):
  baseline f32 oracle   : CP 227.9 ms/frame, RTF 4.99
  baseline f32 cached   : CP 137.8 ms/frame, RTF 3.45
  f16 weights oracle    : CP 223.8 ms/frame, RTF 4.58  (peu de gain : oracle compute-heavy)
  f16 weights cached    : CP 108.3 ms/frame, RTF 3.07  (-52% CP cumulé vs baseline)
  codes_ref.bin == codes_cache.bin (cmp) + out_*.wav md5 identiques

Toujours BIT-EXACT contre l'oracle f32 : les sums f16->f32 dans mul_mat
restent suffisamment stables pour que les argmax post-softmax tombent
sur les mêmes indices. Bonus inattendu.

Restants pour <1 :
- Decoder RTF 1.33 dominant (~50% du temps total maintenant)
- Fusion graphes CP multi-step : incertain, ~lourd

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:14:21 +02:00
Richard Loyer 7626131d50 chantier B TTS #8: KV cache CP -40% par frame, bit-exact
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>
2026-05-28 19:51:22 +02:00
Richard Loyer d20f2b297b chantier B TTS #7: sampler HF-style top_k+top_p+rep_penalty (générique)
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>
2026-05-28 16:55:05 +02:00
Richard Loyer 9ef5e11b53 chantier B TTS #6: P3.4+P3.5 = pipeline complet 100% tablette bout-en-bout
tts_pipeline.cpp : binaire unique qui orchestre TOUT en un seul process :
  input_ids + x_vector (fixtures)
    -> text_projection + lookups + spéciaux (P3.2) -> prefill_embeds [T, 1024]
    -> Talker engine (libllama, M-RoPE IMROPE, Patch 1+2) -> logits + hidden
    -> sampling top_k+temp sur CB0 (le greedy pur tombe dans un attracteur :
       cb0 répété -> decoder produit du silence. temp=0.9 top_k=50 = défauts
       Python, débloquent l'attracteur)
    -> CP runner (cp_inference, recompute bit-exact) -> CB1..15
    -> sum 16 codecs + tts_pad -> next embed (orchestration ggml-side, bit-exact)
    -> codes [N, 16] -> decoder ggml (libqwen3tts-decoder rebuilt vs ql/ggml,
       evite conflit double-ggml) -> WAV PCM 24kHz

Sortie : Bonjour je m'appelle Kazeia, 32 frames -> 2.56s audio audible
(rms=0.040, range [-0.26, 0.29]). 100% tablette : pas de Python, pas de root,
pas de QNN. Stack unifiee ggml.

Bench tablette CPU 6 threads (sweet spot, 8 = contention scheduler) :
  load total       :  1.12 s  (talker f32 + cp + decoder + fixtures)
  Talker prefill   :  0.10 s
  Loop (N=32)      :  8.57 s   talker_decode=1.01 cp=7.55 (talker 31ms/step, cp 236ms/step)
  Decoder          :  5.32 s   (RTF dec 2.00)
  TOTAL            : 14.00 s pour 2.67s audio -> RTF 5.25

Gros leviers perf restants pour RTF<1 (P3.5 itératif, dehors session) :
- KV cache CP : 15 passes/step recompute O(L^2) -> incremental O(L). Estimation
  gain x5-8 sur CP. Tentative cette session = abandonnee (risque casser bit-exact).
- Decoder ggml : histo chraac 0.96 prouve realisable, faut tuner.
- Threads big cores via taskset (Pad3 SD8Elite 8 cores big, scheduler ne route
  pas tjrs au mieux).

Q8_0 CP teste -> perd la precision (76% codes match vs 100% f16) ; sampling et
CP en f16 reste necessaire (confirme observation rapport TTS avril : modeles RVQ
incompatibles avec quantization).

Decoder ggml rebuilte au passage avec ggml engine : kazeia-tts-decoder-ggml/
build-android-engine/ -> libqwen3tts-decoder.a sans conflit symboles. Patch
mineur build-time, pas de modif source decoder.

P3.4 (libkazeia_tts.so JNI assemblage Kotlin) : reporte en session future, le
code de tts_pipeline.cpp est l'implementation reference a wrapper en JNI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:25:00 +02:00
Richard Loyer 746bbe77cd chantier B TTS #5: P3.3 = vrai CP runner intégré (bit-exact)
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>
2026-05-28 15:11:07 +02:00
Richard Loyer 7a998dec6b chantier B TTS #4: P3.1+P3.2 = x_vector + prefill_embeds depuis input_ids (bit-exact)
P3.1 - x_vector Damien dumpe offline : tts.model.extract_speaker_embedding(damien_15s_24k.wav)
-> f32[1024] = 4 KB. Embed dans l'app comme ressource. Pas de port du speaker
encoder Python (8.9M params), voix fixe = pre-calcul trivial.

P3.2 - reproduction C++ de la construction Python des talker_input_embeds.
Reference modeling_qwen3_tts.py:2124-2233 (mode x_vector_only + non_streaming).
Sequence prefill (19 positions pour 'Bonjour je m'appelle Kazeia', 8 tokens texte):
  0..2  : text_projection(text_embed[input_ids[:3]])                  role <im_start>assistant\n
  3..7  : tts_pad + tok_embd[think/think_bos/lang_fr/think_eos/x_vec]  5 positions codec prefix
  8     : tts_bos + tok_embd[codec_pad]                                 1 position
  9..N+8: text_projection(text_embed[input_ids[3:-5]]) + tok_embd[codec_pad]   text body
  N+9   : tts_eos + tok_embd[codec_pad]
  N+10  : tts_pad + tok_embd[codec_bos]
ou tts_*_embed = text_projection(text_embed[tts_bos/eos/pad_token_id]).chunk(3).

Composants : text_embed [151936, 2048] f32 (1.21 GB, a quantiser en P3.5),
text_projection ResizeMLP(2048->2048->1024, SiLU, bias=True), tok_embd talker
[3072, 1024], x_vector [1024], constants (special token ids).

Validation host (g++ -O2) sur phrase 'Bonjour je m'appelle Kazeia' :
  mean_rmse = 3.2e-8, max_abs = 1.8e-6 sur les 19 positions vs prefill_embeds
  capture du dump Python -> noise pure du f32 (eps ~1.2e-7).
Construction bit-exact.

Reste pour usage texte arbitraire : tokenizer BPE Qwen3 en C++ (llama_vocab
supporte cela en standard, a brancher en P3.4 quand on assemble la lib finale).
Pour l'instant input_ids dumpes depuis Python pour 1 phrase fixe = suffisant
pour P3.3 (integration CP) et P3.4 (assemblage).

Fixtures /opt/Kazeia/tts_talker_dump/ : text_embed.bin, tp_fc1_w/b.bin,
tp_fc2_w/b.bin, damien_xvector.bin, input_ids_full.bin, manifest_text.txt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:05:23 +02:00
Richard Loyer 2e0b145b51 chantier B TTS #3: orchestration ggml-side bit-exact + WAV E2E sur tablette
Le binaire dist/jni/tts_orchestrate.cpp boucle :
  prefill talker (talker_f32.gguf via engine) -> greedy CB0 depuis logits engine
  -> CB1..15 teacher-forces depuis codes_golden (CP non encore integre, mais
     deja valide bit-exact standalone)
  -> next_embed = talker.tok_embd[CB0] + sum_i cp_codec_embs[i-1, CB(i)] + tts_pad
  -> talker.decodeEmbed(next_embed) -> repeat
  -> dump codes_engine [N=26, 16] sur disque

Resultats end-to-end ('Bonjour je m'appelle Kazeia', N=26 frames, ~2.08s audio) :
  pipeline ggml-side sur tablette (talker engine + decoder existant) :
  - codes engine vs codes Python : 7/26 frames divergent (CB0 only) = sampling
    stochastique Python (subtalker_dosample temp=0.9) vs greedy engine. Attendu.
  - next_embed engine vs step_inputs_py: rmse=0 cos=1.000 a chaque step ou
    CB0 matche -> orchestration sum+pad strictement bit-exact.
  - WAV decoder tablet sur codes_golden vs PY_REF : cos=0.999999, rmse=3.9e-5
    (juste du noise int16 quantization, bit-exact pratique).
  - WAV decoder tablet sur codes_engine vs PY_REF : cos=0.999200, rmse=3.8e-3
    -> divergence audio uniquement du sampling, son audible et coherent.
  - WAV tablet vs host (memes codes) : cos=1.000000 -> decoder ggml deterministe.

Le seul ecart restant (cos=0.9992 engine vs PY_REF) est purement strategique
(sampling vs greedy), pas implementation. Engine ggml-side prouve fonctionnel.

Reste pour P3 finition (sessions suivantes) :
- Integrer le vrai CP runner (cp_runner.cpp) -> remplace teacher-forcing CB1..15
- Speaker Encoder x-vector (8.9M params) OU pre-calcul offline
- BPE tokenizer Qwen3-TTS + text_projection -> production prefill_embeds depuis texte FR
- Assemblage final en JNI (libkazeia_tts ?) utilisable depuis l'app Kotlin

Dump tools: /opt/Kazeia/tts_talker_dump/dump_talker.py (step I/O + logits)
            /opt/Kazeia/tts_talker_dump/dump_talker2.py (pad/bos/eos/codes/tables)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:25:18 +02:00
Richard Loyer 056dc2831a ql: bump pour qwen3 dense IMROPE (ql:6e0edba)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 13:54:27 +02:00
Richard Loyer 12df0d0ba3 chantier B TTS #2: fix M-RoPE positions en embeds-mode + validation bit-exact
Bug subtil dans llm_graph_input_pos::set_input (llama-graph.cpp:121-123) : pour
M-RoPE (n_pos_per_embd=4), la conversion 1D->4D des positions n'est faite QUE
pour ubatch.token. En embeds-mode (ubatch.embd != nullptr), la branche else fait
un memcpy raw de ubatch.pos avec taille T*4, lisant T*3 entiers out-of-bounds
au-dela du buffer T-only que j'allouais. -> positions garbage -> sortie cassee
(cos=-0.02 vs Python avant fix, soit pire qu'aleatoire).

Fix cote JNI (chantier P1) : detecte rope_type via llama_model_rope_type et
alloue T*4 positions quand MROPE/IMROPE, en remplissant les 3 premiers axes
avec la position et le 4eme avec 0 (text-only) -- exactement ce que le framework
fait pour les tokens. Memoire : decode_embd_batch dans kazeia_engine_jni.cpp.

Smoke test bit-exact Python vs engine (test_talker_replay.cpp, teacher-forced) :
- 19 positions prefill + 26 steps decode sur talker_f32.gguf (Bonjour je m'appelle Kazeia)
- hidden : mean_cos=1.000000, mean_rmse=2.9e-3
- logits : mean_cos=1.000000, mean_rmse=1.8e-3
- argmax engine == argmax Python sur les 26 steps (info only -- la divergence
  RMSE residuelle est du f32 noise, ordre de sommation).

Le port Talker engine est valide bit-exact (precision f32) sur tablette CPU.
Reste P3 etape 3 : orchestration finale (BPE tokenizer + x-vector encoder +
cablage CP runner + decoder ggml) en mode standalone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 13:54:03 +02:00
Richard Loyer 50814f3bca chantier B TTS #1: API JNI embeds-only + M-RoPE qwen3 dense
Premières 2 pieces du chantier B (porter le Talker Qwen3-TTS sur l'engine
pour TTS standalone tablette, sans dépendre du pipeline Python).

P1 - API embeds-only dans le JNI (dist/jni/kazeia_engine_jni.cpp)
Le Talker a vocab=3072 codes audio (pas de BPE), entrée = embeds
pré-mélangés (text+x-vector au prefill, sum 16 codecs + tts_pad au decode),
sortie = logits[3072] + hidden[1024] (pour le Code Predictor).
Solution: pas un appel monolithique, 5 primitives qui laissent l'orchestration
côté caller:
  - nEmbd(h), nVocab(h) - dimensionnement des buffers Kotlin
  - resetEmbeds(h)       - KV clear + pos=0
  - prefillEmbeds(h, embds[T*n_embd], T, outHidden[n_embd])
  - decodeEmbed(h, embd[n_embd], outLogits[vocab], outHidden[n_embd])
Implementation: llama_set_embeddings(ctx, true), llama_batch.embd au lieu
de .token, logits=1 sur la derniere position seulement (economie KV).
KEngine porte un compteur pos_embd interne. Kotlin wrapper miroir dans
dist/jni/EngineLlmEngine.kt.

P2 - M-RoPE qwen3 dense (sous-module ql, commit c1609a1)
Pointeur sous-module avance pour embarquer le support.

Validation device: dist/jni/test_talker.cpp charge talker_f32.gguf, lance
llama_decode embeds-only T=4 prefill puis 1 step, recupere hidden+logits
sans crash. Log device: print_info rope_type=8, mrope sections=[24,20,20,0],
prefill OK / step OK. argmax stable (entree bidon, juste smoke).

Libs rebuiltes: dist/lib/libllama.so + libkazeia_engine.so + libggml-base.so.
Reste P3 = cablage Talker->CP->Decoder cote orchestration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 13:37:17 +02:00
Richard Loyer 9af47bf388 chantier dense-HMX #4: 0x2e décodé = AEE_ECONNRESET (reset watchdog CDSP), pas une corruption de valeurs
Connaissance retrouvée briques archivées: 0x2e = DSP-side fault/timeout, le -inf host = conséquence (buffer non fini) -> explique le non-déterminisme. Hypothèses 'valeurs' = mauvaise catégorie. Meilleure hypothèse = accès mémoire DSP invalide chemin HMX k=4096. Log-to-host-buffer prouvé mort (briques: cache non flushé post-crash). Reprise = device rooté seul.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:20:28 +02:00
Richard Loyer 97180b29a6 dist: audit intégrabilité HANDOFF — corrige passages périmés
- libkazeia_engine.so = SHARED (pas statique) -> shipper tout lib/ ensemble (ABI #277)
- KV f16 déjà rebuildé dans le .so livré (la note 'rebuild/q8_0' était stale)
- OPFILTER obsolète (fix SSM_CONV dans backend) -> libellés B/C + tableau nettoyés
- note instrumentation OPLOG/DUMP dormante dans libggml-hexagon.so (zéro-coût)
Paquet vérifié: 5 symboles JNI, source f16+routing arch conformes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:55:16 +02:00
Richard Loyer b350c2be73 chantier dense-HMX #3: uninit VTCM matmul écarté (zéro-init ne corrige pas). Source -inf non-det = amont, instrumentation DSP requise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:54:58 +02:00
Richard Loyer 464d4e69c0 chantier dense-HMX: avancement #2 (replay) — root = fault HMX non-déterministe, instrumentation DSP requise (bloquée sans root). Tools committés.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:35:14 +02:00
Richard Loyer 224b375a10 chantier dense-HMX: avancement — ni magnitude, ni NaN, ni shape pure (value/context-spécifique)
3 hypothèses éliminées par mesure: overflow fp16 (act=9.8), NaN/Inf inputs (0), shape pure (isolation
synthétique k=4096 ne crashe pas). Op fautive = o_proj MUL_MAT q4_0 k=4096 après FLASH_ATTN_EXT.
Prochaines étapes révisées (dump-and-replay value-vs-context, instrumentation DSP log-vers-buffer).
OPLOG mis à jour (nan/inf) dans dist/lib. ql -> commit OPLOG.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:09:52 +02:00
Richard Loyer 5aa4457567 chantier: instrumentation crash dense-HTP-HMX + .pte retiré (engine seul runtime)
CHANTIER_HMX_DENSE.md: analyse + outils pour le crash matmul HMX dense. Finding: PAS un overflow
fp16 (activation du matmul fautif = 9.8) — c'est un bug de tiling/buffer HMX pour k=4096 (o_proj
après FLASH_ATTN_EXT). Localisé via OPLOG CPU-side (FARF DSP bloqué sans root). Étapes + repro documentés.

HANDOFF: .pte retiré -> engine seul runtime LLM. Dense=HVX (40) sur l'engine; HMX (295)=ce chantier.
dist/lib refreshé (libggml-hexagon.so avec OPLOG dormant, htp-v79 propre). ql -> 7d4cade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:29:32 +02:00
Richard Loyer 0407ac8b71 dist: ROOT CAUSE crash dense-HTP = matmul HMX (fp16) ; fix = HVX pour le dense
Analyse complète (méthode SSM_CONV) du crash dense-HTP:
- bench HTP synthétique (pp512, +tg8) = OK ; cli/engine texte réel crashe 0x2e > ~14 tokens.
- OPFILTER bisect: SOFT_MAX/ROPE/RMS_NORM/GET_ROWS/CPY/CONT/ADD n'arrêtent PAS le crash ; MUL_MAT->CPU OUI.
- GGML_HEXAGON_USE_HMX=0 (matmul HVX) supprime le crash, =1 le reproduit. => le matmul HMX (fp16 tile)
  faute sur les activations réelles du dense (massive activations Qwen3 > plage fp16 ; bench synthétique
  ne déclenche pas l'overflow). Analogue du SSM_CONV pour la 3.5, mais ici c'est le matmul HMX coeur.

Câblage: lecture general.architecture dans le GGUF AVANT init backend -> hybride (qwen35/qwen3next):
HMX on + option C (préservé) ; dense: USE_HMX=0 + HTP contexte-unique (HVX, ~40 prefill =3x CPU, decode ~11,
stable). Repli CPU si pas de HTP. Validé test_jni_native: dense (HVX HTP) + 3.5 (option C) cohérents.
TODO backend: matmul HMX robuste aux activations hors plage fp16.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:42:15 +02:00
Richard Loyer c166391f49 dist: routage par structure (is_hybrid) — 3.5->option C, dense->CPU robuste
Détection au load via llama_model_is_hybrid/is_recurrent (structure, pas string d'archi):
- hybride GDN (qwen3.5/qwen3next) -> option C (prefill HTP/decode CPU), préservé+validé.
- dense (qwen3, etc.) / pas de HTP -> CPU pur (universel, robuste).
Charge l'instance CPU d'abord (détection + universel), n'ajoute l'instance HTP que pour l'hybride.

Diagnostic dense-HTP: le prefill dense sur HTP CRASHE (dspqueue_read 0x2e) sur prompt réaliste
(reproduit aussi en llama-cli -dev HTP0 -fa 1; le "ça marche" antérieur = prompt minuscule sans fa,
cas chanceux). Bug backend HTP attention dense multi-token, non corrigé. Dense a un .pte (451) de
toute façon -> dense=CPU robuste. Validé test_jni_native: dense (CPU) + 3.5 (option C) cohérents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:30:43 +02:00
Richard Loyer eeffbc350f dist: JNI générique (auto-détecte archi) — tout LLM, sans dégrader la 3.5
Le JNI lit general.architecture: qwen35/qwen3next -> option C (prefill HTP/decode CPU, préservé
et revalidé cohérent+mémoire). Tout le reste (qwen3 dense, etc.) -> CPU pur (universel, jamais de
crash). Dense Qwen3-4B validé via le moteur (CPU, cohérent). 3.5 non régressée.

Diagnostic crash dense-HTP: ce n'est PAS le HTP (dense single-context HTP marche: cli 36.8/11.4
cohérent, llama-bench pp512=98). Le crash 0x2e (dspqueue_read, flush_pending) survient UNIQUEMENT
en dual-load (option C: 2 instances modèle) appliqué au dense. Or le dense n'a pas besoin de
l'option C (decode HTP 11.4 ≈ CPU 11.7, pas de pénalité GDN). Donc dense -> CPU (ou single-ctx HTP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:11:44 +02:00
Richard Loyer 771d64a998 dist: multi-tour propre (generateRaw + ChatSession) + JNI validé end-to-end natif
Ferme les 2 derniers trous d'intégration:
- generateRaw(prompt) exposé (cœur option C partagé avec generate). EngineLlmEngine.kt: ChatSession
  gère l'historique + construit le ChatML Qwen3.5 validé -> multi-tour propre sans que l'app connaisse
  le template (structure identique à dual_ctx_mt).
- test_jni_native.cpp: dlopen + JNIEnv mock teste le .so shippé bout-en-bout sur device
  (load/generate/generateRaw/free). Résultat: FR cohérent + mémoire conversationnelle OK
  ("Marc, je me souviens parfaitement de ton prénom"). Marshalling JNI réel validé.
libkazeia_engine.so reconstruit (5 symboles). Paquet dist/ prêt à intégrer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:18:20 +02:00
Richard Loyer 6935089459 dist: libkazeia_engine.so rebuild option C (paquet cohérent), notes rebuild MAJ
libkazeia_engine.so reconstruit depuis le JNI option C (37KB strippé, 4 symboles JNI, SHARED,
NEEDED llama/ggml/ggml-base/log/c++_shared). Le paquet dist/ est désormais interne-cohérent:
sources + prebuilts alignés (option C + fix SSM_CONV). Docs: prebuilts à jour, rebuild app-side
recommandé pour ABI/STL. Logique validée via dual_ctx_mt (JNI non testé via Java ici).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:45:47 +02:00
Richard Loyer 2699b631a3 dist: prefill HTP corrigé (SSM_CONV gate backend), JNI option C sans OPFILTER
Fix intégré dans libggml-hexagon.so (ql e872623): ggml_hexagon_supported_ssm_conv route le
conv1d multi-token (prefill) sur ggml-cpu (le kernel HTP est faux multi-token pour d_inner
1024/2048). Plus besoin de GGML_HEXAGON_OPFILTER -> retiré du JNI. Validé: oracle SSM_CONV 18/18,
cli -dev HTP0 cohérent, multi-tour cohérent+mémoire, prefill HTP 110-180 t/s. libggml-hexagon.so
rafraîchi. Rebuild requis avant ship: libggml-hexagon.so + libkazeia_engine.so. Docs MAJ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:39:08 +02:00
Richard Loyer 2c89ec01a2 dist: JNI option C câblé (prefill HTP / decode CPU), validé multi-tour
generate() = prefill prompt sur instance HTP (ngl99, device HTP0, OPFILTER=SSM_CONV +
GDN_PREFILL posés au load) -> llama_state_seq transfère le KV -> decode sur instance CPU
(ngl0, t4+fa, KV f16). 2 instances (~4.7GB). Sans état entre appels (app passe l'historique).
Validé multi-tour via jni/dual_ctx_mt.cpp: 3 tours cohérents FR + mémoire conversationnelle
(rappelle prénom/âge du tour 1 au tour 3), prefill HTP 103-144 t/s, decode CPU ~7-9.
Compile+linke OK (4 symboles JNI, CMakeLists inchangé). Rebuild libkazeia_engine.so requis
avant ship (le .so livré = ancienne version CPU-only/q8_0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:24:55 +02:00
Richard Loyer 501abb67e5 dist: prefill HTP RÉCUPÉRABLE — charabia = 1 op (SSM_CONV) cassée, fix OPFILTER
Investigation détaillée: décomposé le prefill HTP charabia op par op (test-backend-ops -b HTP0
par op + OPFILTER bisect en génération). Coupable unique = SSM_CONV (conv1d) cassé sur HTP en
multi-token (prefill) pour d_inner=1024/2048 (oracle FAIL ERR~1.3; decode n=1 OK; 1536 passe;
bug backend hexagon, HVX+scalaire HTP faux, ggml-cpu correct). FIX immédiat: GGML_HEXAGON_OPFILTER
=SSM_CONV (conv sur CPU, reste HTP) -> prefill HTP 181 t/s + sortie FR cohérente, x13 vs CPU 14.
Renverse le "prefill HTP mort": il est récupérable. 3 configs (A CPU-only livrée / B mono-ctx
HTP+OPFILTER 181/6.4 / C dual-ctx 181/10.9 +2.3GB). TODO: corriger SSM_CONV HTP (gate oracle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:12:37 +02:00