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.
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>
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>
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>
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>
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>
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>
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>
Reconfirmé device froid 90% (les chiffres throttlés d'hier soir étaient faux):
- KV f16 >> q8_0 au decode: q35-lmq4 f16=10.9 vs q8_0=6.5 (-40%, idem d=512). Le déquant KV
flash-attn coûte plus que le BW. JNI corrigé type_k/v=F16 (rebuild .so requis avant ship;
le .so livré=q8_0 validé, inchangé).
- q35-lmq4 prefill HTP=189 ~= 2x Qwen3.5-Q4_0 (98): embeds/output Q4 restent sur NPU vs Q6_K
fallback CPU. Argument fort de plus pour q35-lmq4 (+5% decode ET ~2x prefill HTP).
- decode sain: q35-lmq4 10.9, dense 11.7. prefill CPU JNI=14 (t4).
Decision KV résolue; decision prefill CPU-only vs HTP ouverte (189 motive le câblage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>