diff --git a/dist/KAZEIA_ENGINE_OVERVIEW.md b/dist/KAZEIA_ENGINE_OVERVIEW.md index 8706da2..702bae1 100644 --- a/dist/KAZEIA_ENGINE_OVERVIEW.md +++ b/dist/KAZEIA_ENGINE_OVERVIEW.md @@ -1,106 +1,161 @@ -# Kazeia-Engine — vue d'ensemble du moteur unifié +# Kazeia-Engine — vue d'ensemble et entrée d'intégration -**Mai 2026**. Moteur d'inférence local-first pour la tablette Snapdragon 8 Elite (Pad3). Couvre LLM, TTS (avec clonage vocal embarqué) et STT dans une stack cohérente. +**Date** : 01/06/2026. Moteur d'inférence local-first pour Snapdragon 8 Elite (Pad3). Unifie **LLM + TTS + STT** dans une stack cohérente. Pas de cloud, pas de root, pas de Python runtime. + +## Documents d'intégration (3 indépendants) + +Pour intégrer dans l'app Kazeia, lis ces 3 docs (un par sous-système) : + +| Sous-système | Doc d'intégration | Lib `.so` | Façade Kotlin | +|---|---|---|---| +| **STT** (Whisper-Small NPU) | [`STT_INTEGRATION.md`](STT_INTEGRATION.md) | `libkazeia_stt.so` 183 KB | `SttEngine.kt` | +| **TTS** (Qwen3-TTS + clonage vocal) | [`TTS_INTEGRATION.md`](TTS_INTEGRATION.md) | `libkazeia_tts.so` 929 KB | `TtsEngine.kt` | +| **LLM** (Qwen3.5 hybride / Qwen3 dense) | [`LLM_INTEGRATION.md`](LLM_INTEGRATION.md) | `libkazeia_engine.so` 60 KB | `EngineLlmEngine.kt` | + +Chaque doc a la même structure : libs à pousser, façade Kotlin, exemples, codes d'erreur, bench standalone, checklist d'intégration, pièges connus, perf détaillée. + +--- + +## Architecture ``` -┌─────────────────────── Kazeia-Engine ───────────────────────┐ -│ │ -│ STT (Whisper-Small NPU) TTS (Qwen3-TTS in-process) │ -│ ┌──────────────────────┐ ┌────────────────────────┐ │ -│ │ libkazeia_stt.so │ │ libkazeia_tts.so │ │ -│ │ 168 KB │ │ ~5 MB │ │ -│ │ ↳ stt_engine │ │ ↳ tts_engine │ │ -│ │ ↳ kazeia_mel │ ────► │ ↳ kazeia_mel │ │ partage -│ │ ↳ VAD RMS C++ │ │ ↳ speaker_encoder │ │ mel + VAD -│ │ ↳ BPE byte-level │ │ ↳ cp_inference │ │ -│ │ ↳ JSON parser │ │ ↳ decoder ggml chraac │ │ -│ │ │ │ ↳ sampler │ │ -│ │ runtime : │ │ │ │ -│ │ libonnxruntime.so + │ │ runtime : │ │ -│ │ libQnnHtp*.so │ │ libllama + libggml* │ │ -│ └──────────────────────┘ └────────────────────────┘ │ -│ │ -│ LLM (Qwen3.5-4B GGUF) │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ libllama.so + libggml-htp-v79.so + chraac codec │ │ -│ │ ↳ prefill HTP / decode CPU NEON │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────┘ +┌────────────────────── Kazeia-Engine ──────────────────────┐ +│ │ +│ STT TTS LLM │ +│ libkazeia_stt libkazeia_tts libkazeia_eng │ +│ 183 KB 929 KB 60 KB │ +│ │ │ │ │ +│ └─ libonnxruntime └────────┬───────────┘ │ +│ libQnnHtp* libllama 35 MB │ +│ libcdsprpc libggml + ggml-cpu │ +│ libggml-hexagon │ +│ libggml-htp-v79 │ +│ (chaîne mutualisée TTS+LLM) │ +│ │ +└────────────────────────────────────────────────────────────┘ + +Côté APK : + jniLibs/arm64-v8a/ + libkazeia_stt.so + libonnxruntime.so (2 fichiers, ~20 MB) + libkazeia_tts.so + libkazeia_engine.so + libllama.so + ... (8 fichiers, ~53 MB partagés) + libc++_shared.so (commune) + + TOTAL ~75 MB pour les 3 sous-systèmes (vs ~50 MB ExecuTorch seul). ``` ## API Kotlin unifiée (côté app) ```kotlin -// 1) STT in-process (Whisper-Small NPU) -val stt = SttEngine("/data/.../whisper-small-sm8750", useHtp = true) +// STT +val stt = SttEngine(modelDir = "/data/.../whisper-small-sm8750", useHtp = true) val transcript = stt.transcribe(pcm16, language = "fr") -// 2) VAD (drop-in replacement de VadStage.kt) +// VAD (drop-in replacement de l'ancienne VadStage.kt) val vad = SttVad() -when (vad.push(chunk)) { SttVad.SPEECH -> ... ; SttVad.END_OF_SPEECH -> ... } +when (vad.push(chunk)) { + SttVad.SPEECH -> buffer.addAll(chunk.toList()) + SttVad.END_OF_SPEECH -> { stt.transcribe(buffer); vad.reset() } +} -// 3) TTS in-process (Qwen3-TTS + clonage vocal embarqué) +// TTS in-process (Qwen3-TTS + clonage vocal) val tts = TtsEngine( - talkerGguf = "/data/.../talker_f32.gguf", - vocabGguf = "/data/.../Qwen3-4B-Q4_0.gguf", - dumpDir = "/data/.../tts_dump", - useHtp = false, - nThreads = 6 + talkerGguf = "/data/.../talker_f32.gguf", + vocabGguf = "/data/.../Qwen3-4B-Q4_0.gguf", + dumpDir = "/data/.../tts_dump", + nThreads = 6 ) val r = tts.synthesize("Bonjour, comment ça va ?", "/sdcard/out.wav") -// 4) LLM via libllama (Qwen3.5-4B Q4_0 cascade Speaker+Thinker) -// -- intégration via lib séparée libkazeia_engine.so + façade existante. +// Clonage vocal embarqué (optionnel) +val pierreVoice = tts.encodeSpeakerWav("/sdcard/voix_pierre.wav") +tts.synthesize("Bonjour Pierre.", outWav, xvectorOverride = pierreVoice) + +// LLM (Qwen3.5 GGUF cascade Speaker + Thinker) +val llm = EngineLlmEngine("/data/.../Qwen3.5-4B-Q4_0.gguf", ctx = 4096, nThreads = 6) +val reply = llm.generate(sys = "Tu es Kazeia...", usr = userInput, max = 96) ``` -## Composants livrés à ce jour +## Pattern d'unification recommandé (cf demande dev Kazeia) -| Composant | État | Lib | Doc | -|---|---|---|---| -| LLM Qwen3.5-4B / Qwen3.5-9B GGUF | prod | `libllama.so` + Hexagon backend | `CLAUDE.md` | -| TTS Talker + CP + decoder ggml | prod | `libkazeia_tts.so` | `tts_engine.h` | -| Clonage vocal speaker encoder ECAPA-TDNN | **livré 31/05** | `libkazeia_tts.so` | mémoire `project_tts_voice_cloning` | -| STT Whisper-Small NPU C++ | **livré 31/05** | `libkazeia_stt.so` | **`STT_INTEGRATION.md`** | -| VAD RMS C++ | **livré 31/05** | `libkazeia_stt.so` | idem | -| Mel extractor partagé (FFT) | **livré 31/05** | `kazeia_mel.{h,cpp}` | header | +Pour permettre une bascule progressive `prod` ↔ `lib` sans dégeler les sous-systèmes, le dev a câblé un flag de dispatch côté app pour STT et propose de faire pareil pour LLM et TTS : -## Empreinte mémoire (Snapdragon 8 Elite, 16 GB RAM) +```kotlin +// Dans KazeiaService.kt (ou équivalent) +class KazeiaService { + private val sttEngine = when (flag("sttEngine", default = "prod")) { + "lib" -> SttEngine(...) // libkazeia_stt + else -> WhisperHybridEngine(...) // legacy ONNX/QNN direct + } + private val llmEngine = when (flag("llmEngine", default = "prod")) { + "lib" -> EngineLlmEngine(...) // libkazeia_engine + else -> ExecuTorchLlmEngine(...) // legacy .pte + } + private val ttsEngine = when (flag("ttsEngine", default = "prod")) { + "lib" -> TtsEngine(...) // libkazeia_tts + else -> LegacyTtsPipeline(...) // legacy à supprimer une fois libé validée + } +} +``` + +Toggle via `adb shell setprop kazeia.sttEngine lib` (ou équivalent du flag system). Bascule réversible, mesurable en CER/WER/RTF avant de flip le default. + +État réel des 3 dispatch flags (à confirmer côté dev) : +- ✅ STT : flag câblé, default `prod`, A/B in-app validé +- ⏳ LLM : flag à ajouter (mirror exact du STT). Lib + façade + doc prêts. +- ⏳ TTS : flag à ajouter une fois `libkazeia_tts.so` validée load + 1 synth in-app. + +## État livré (01/06/2026, commit en cours) + +| | Lib `.so` | Façade Kotlin | JNI source | Doc d'intégration | Bug investigué | +|---|:---:|:---:|:---:|:---:|---| +| **STT** | ✅ 183 KB | ✅ `SttEngine.kt` | ✅ `kazeia_stt_jni.cpp` | ✅ `STT_INTEGRATION.md` 15 KB | ✅ 2 bugs fixés (DFT 400 + forced prompt) | +| **TTS** | ✅ 929 KB | ✅ `TtsEngine.kt` | ✅ `kazeia_tts_jni.cpp` | ✅ **`TTS_INTEGRATION.md` 10 KB** (cette session) | ✅ Crash load = libs manquantes, liste exhaustive donnée | +| **LLM** | ✅ 60 KB | ✅ `EngineLlmEngine.kt` | ✅ `kazeia_engine_jni.cpp` | ✅ **`LLM_INTEGRATION.md` 10 KB** (cette session) | ✅ Bug threads = `nThreads=4` hardcoded, fix `nThreads=6` paramétrable | + +## Empreinte mémoire (Snapdragon 8 Elite, 16 GB RAM, mesures r3) | Composant chargé | RAM peak | |---|---:| -| LLM Qwen3.5-4B Q4_0 | 2.4 GB | -| LLM Qwen3.5-9B Q4_0 | 5.0 GB | +| STT Whisper-Small NPU (encoder + decoder QAIRT) | 379 MB | | TTS (talker + CP + decoder + vocab + xvector fixture) | 3.5 GB | -| TTS clonage vocal (speaker encoder, en plus du fixture) | +34 MB | -| STT Whisper-Small NPU (encoder + decoder QAIRT) | 378 MB | -| **Total cascade Speaker 4B + STT + TTS** | **~6.3 GB** (sur 16 GB dispo) | +| TTS clonage vocal (speaker encoder, en plus) | +34 MB | +| LLM Qwen3.5-4B Q4_0 (1 instance) | 2.4 GB | +| LLM Qwen3.5-9B Q4_0 (1 instance) | 5.0 GB | +| **Cascade typique** : STT + Speaker 4B + Guard 4B + TTS | ~9 GB / 16 GB | -## Pipeline vocal de bout en bout (latence typique) +## Perf bout-en-bout (latence typique) ``` mic 16 kHz PCM16 ↓ AudioRecord -SttVad (RMS énergie) → ~830 ms après fin de parole (silence 800 ms + overhead) +SttVad RMS → ~830 ms après fin parole (silence 800 ms + overhead) ↓ end_of_speech -SttEngine.transcribe(pcm) → ~0.4-2.0 s selon longueur audio (RTF 0.4) +SttEngine.transcribe(pcm) → ~500-2000 ms (RTF 0.18-0.40 sur audio 3-10s FR) ↓ texte FR -LLM Qwen3.5-4B cascade → ~5-10 s (Guard + Speaker, 4-9 GB total) +LLM Speaker 4B (+ Guard 4B) → ~5-10 s (cascade Speaker + Guard 4B) ↓ réponse FR -TtsEngine.synthesize(text) → ~3-10 s (RTF 2.5-3.0 selon longueur) +TtsEngine.synthesize(text) → ~3-10 s (RTF 2.5-3.0 selon longueur) ↓ audio playback ``` -Total latence voix→audio : ~10-25 s selon longueur du tour, dominée par LLM + TTS. +Latence totale voix→audio : ~10-25 s par tour, dominée par LLM + TTS. -## Documentation par sous-système +## Documentation détaillée -- **LLM** : `/opt/Kazeia-engine/CLAUDE.md` (état complet) -- **TTS** : `tts_engine.h` (API publique) + clonage vocal section §1 du STT_INTEGRATION.md -- **STT** : **`STT_INTEGRATION.md`** (guide intégration complet) -- **Mémoire interne assistant** : `/home/alf/.claude/projects/-opt-Kazeia-engine/memory/` +- **STT** : [STT_INTEGRATION.md](STT_INTEGRATION.md) — Whisper-Small NPU, VAD, options QNN, A/B accuracy +- **TTS** : [TTS_INTEGRATION.md](TTS_INTEGRATION.md) — Qwen3-TTS, clonage vocal, sampling tuning +- **LLM** : [LLM_INTEGRATION.md](LLM_INTEGRATION.md) — Qwen3.5/Qwen3, option C, threading +- **Patch QNN options prod (gratuit, sans migration)** : [PATCH_QNN_PROD.md](PATCH_QNN_PROD.md) +- **Engine global (CLAUDE.md projet)** : `/opt/Kazeia-engine/CLAUDE.md` +- **Mémoires assistant** : `/home/alf/.claude/projects/-opt-Kazeia-engine/memory/` -## Pour intégrer côté app Android +## Pour démarrer l'intégration -Lire `STT_INTEGRATION.md` — guide complet de migration depuis `WhisperHybridEngine.kt`. Les libs `libkazeia_tts.so` et `libkazeia_stt.so` partagent le même runtime (NDK r27d, arm64-v8a, API 31+, `-march=armv8.6-a+i8mm+bf16+dotprod+fp16`). +1. Lire les 3 docs d'intégration dans l'ordre : STT → TTS → LLM. +2. Pousser les libs nécessaires dans `jniLibs/arm64-v8a/` (cf §2.b de chaque doc). +3. Copier les 3 façades Kotlin (`SttEngine.kt`, `TtsEngine.kt`, `EngineLlmEngine.kt`). +4. Câbler les 3 flags dispatch côté app pour bascule progressive (cf section ci-dessus). +5. Valider la checklist §7 de chaque doc en premier (System.loadLibrary + 1 call OK). +6. Bench A/B `prod` vs `lib` sur audios/prompts/modèles fixtures représentatifs. +7. Flip default vers `lib` quand les 3 sous-systèmes passent les gates (CER/WER STT, RTF TTS, decode tok/s LLM). diff --git a/dist/LLM_INTEGRATION.md b/dist/LLM_INTEGRATION.md new file mode 100644 index 0000000..fffda31 --- /dev/null +++ b/dist/LLM_INTEGRATION.md @@ -0,0 +1,313 @@ +# Intégration LLM Kazeia-Engine — Guide développeur + +**Date** : 01/06/2026 +**Cible** : remplacer le pipeline LLM ExecuTorch `.pte` par `libkazeia_engine.so` + façade Kotlin `EngineLlmEngine.kt`. Modèle GGUF chargé directement (pas de conversion .pte), prefill HTP / decode CPU automatique (option C). + +**Runtime** : libllama (fork ggml-org/llama.cpp commit `f0fe1058b`) + libggml fork ql (Qualcomm) avec backend Hexagon HTP V79. + +**Modèles supportés** (auto-détection via `general.architecture` du GGUF) : +- **Qwen3 dense** (Qwen3-4B, Qwen3Guard-Gen-4B/8B) : HTP contexte-unique, matmul HVX (HMX off, fault 0x2e), prefill+decode HTP +- **Qwen3.5 hybride GDN** (Qwen3.5-4B, Qwen3.5-9B) : **option C** — prefill HTP avec HMX (159 tok/s) / decode CPU NEON (16.5 tok/s) avec transfert KV automatique entre contextes +- **Fallback CPU pur** si pas de HTP détecté + +**Perf prouvée** (Snapdragon 8 Elite SM8750, t=6, standalone CLI) : + +| Modèle | Prefill | Decode | RAM | Mode | +|---|---:|---:|---:|---| +| Qwen3-4B-Q4_0 | 103 tok/s (HTP) | 17 tok/s (CPU) | 2.4 GB | dense HTP | +| Qwen3.5-4B-Q4_0 | 55 tok/s (HTP) | 15 tok/s (CPU) | 2.4 GB | option C | +| Qwen3.5-9B-Q4_0 | 41 tok/s (HTP) | 8.4 tok/s (CPU) | 5.0 GB | option C | +| Qwen3Guard-Gen-4B-Q4_K_M | 89 tok/s (HTP) | 22 tok/s (CPU) | 2.5 GB | dense HTP | + +⚠️ **Note in-app perf** : le sweet spot threads in-app est `nThreads=6` (mesure `r3`). Versions précédentes du bridge utilisaient `nThreads=4` hardcoded — corrigé dans le commit ce session. + +--- + +## 1. Ce que la lib fait + +``` +libkazeia_engine.so (60 KB, SHARED, dépend de libllama + libggml*) + ├── EngineLlmEngine load(ggufPath, nCtx=4096, nThreads=6) -> handle + ├── EngineLlmEngine generate(handle, sys, usr, maxTok) -> String # mono-tour ChatML + ├── EngineLlmEngine generateRaw(handle, prompt, maxTok) -> String # multi-tour + ├── EngineLlmEngine reset(handle) # clear KV + ├── EngineLlmEngine free(handle) + └── (+ API TTS embeds-only : nEmbd, nVocab, prefillEmbeds, decodeEmbed — pour Talker TTS) +``` + +API Kotlin (`EngineLlmEngine.kt`) : + +```kotlin +val llm = EngineLlmEngine( + model = "/data/.../Qwen3.5-4B-Q4_0.gguf", + ctx = 4096, + nThreads = 6 // sweet spot Snapdragon 8 Elite r3 +) + +// Mono-tour avec system + 1 message +val reply = llm.generate( + sys = "Tu es Kazeia, un assistant psychologique bienveillant. Réponds en français bref.", + usr = "Je me sens un peu seul.", + max = 96 +) + +// Multi-tour via ChatSession (gère ChatML + thinking-off déterministe) +val chat = llm.newChat(system = "...") +val r1 = chat.ask("Bonjour") +val r2 = chat.ask("Comment vous allez ?") +chat.clear() + +llm.release() +``` + +--- + +## 2. Build (côté app Android) + +### a. Récupérer les artifacts + +```bash +cd /opt/Kazeia-engine/dist +# Recompile libkazeia_engine.so (60 KB) +NDK=/opt/Kazeia/android-ndk-r27d +$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang++ \ + -std=c++17 -O3 -fPIC \ + -march=armv8.6-a+i8mm+bf16+dotprod+fp16 \ + -I./include -shared jni/kazeia_engine_jni.cpp \ + -L./lib -lllama -lggml -lggml-base -llog -ldl -lm \ + -o b-jni/libkazeia_engine.so +``` + +### b. Libs à copier dans `app/src/main/jniLibs/arm64-v8a/` + +**8 fichiers obligatoires** (vérifiés via `readelf -d` sur la lib + dépendances transitives `libllama.so`) : + +| Fichier | Taille | Rôle | +|---|---:|---| +| `libkazeia_engine.so` | 60 KB | la lib elle-même | +| `libllama.so` | 35 MB | fork ggml-org/llama.cpp | +| `libggml.so` | 627 KB | ggml core (charge cpu+hexagon backends dynamiquement) | +| `libggml-base.so` | 6.6 MB | ggml base ops | +| `libggml-cpu.so` | 4.7 MB | backend CPU NEON ARM v8.6 | +| `libggml-hexagon.so` | 3.3 MB | backend Hexagon HTP host wrapper | +| `libggml-htp-v79.so` | 354 KB | DSP skel V79 (chargé via dlopen runtime) | +| `libc++_shared.so` | 1.8 MB | NDK r27d STL | + +**Total** : ~52 MB. **Mutualisable avec libkazeia_tts.so** — si tu intègres TTS en parallèle, ces 7 deps sont les mêmes (juste `libkazeia_engine.so` et `libkazeia_tts.so` qui s'ajoutent en plus). + +### c. Façade Kotlin + +Copie `dist/jni/EngineLlmEngine.kt` dans `app/src/main/java/com/kazeia/llm/EngineLlmEngine.kt`. Package = `com.kazeia.llm`. + +### d. Modèles sur la tablette + +``` +/data/local/tmp/kazeia/models/ +├── Qwen3.5-4B-Q4_0.gguf # 2.4 GB — speaker / thinker principal +├── Qwen3.5-9B-Q4_0.gguf # 5.0 GB — option si qualité prioritaire +├── Qwen3Guard-Gen-4B.Q4_K_M.gguf # 2.5 GB — Guard (modération) +└── Qwen3-4B-Q4_0.gguf # 2.4 GB — pour cascade Speaker dense +``` + +--- + +## 3. Migration depuis ExecuTorch (.pte) + +| Avant (ExecuTorch) | Après (Kazeia-Engine) | +|---|---| +| `model.pte` 3.3 GB | `Qwen3.5-4B-Q4_0.gguf` 2.4 GB (-30 %) | +| Conversion .pte requise (chaîne ExecuTorch + QNN compile) | Pas de conversion : GGUF chargé direct | +| Pipeline 100% NPU INT4 | Option C : prefill HTP / decode CPU NEON | +| Decode 14-21 tok/s | Decode 15-17 tok/s (4B), équivalent | +| Pas extensible à Qwen3.5 (ExecuTorch ne sait pas l'exporter) | Qwen3.5 hybride GDN supporté nativement | +| Pas de cascade multi-modèle facile | Cascade Speaker+Thinker via 2 instances `EngineLlmEngine` | + +Suppression côté app : tout le pipeline `ExecuTorchLlmEngine` et ses dépendances ExecuTorch peuvent être retirés. + +--- + +## 4. Exemples d'usage + +### Mono-tour psy bref + +```kotlin +val llm = EngineLlmEngine( + model = "$kazeiaDir/models/Qwen3.5-4B-Q4_0.gguf", + ctx = 2048, + nThreads = 6 +) +val reply = llm.generate( + sys = "Tu es Kazeia, psy bienveillant. Une phrase brève.", + usr = "Je rumine la nuit sans dormir.", + max = 64 +) +llm.release() +``` + +### Multi-tour ChatSession + +```kotlin +val llm = EngineLlmEngine("$kazeiaDir/Qwen3.5-4B-Q4_0.gguf", nThreads = 6) +val chat = llm.newChat("Tu es Kazeia, psy bienveillant.") +chat.ask("Bonsoir.") +chat.ask("Je me sens vide.") +chat.ask("Depuis le départ de ma fille.") +chat.clear() // reset historique +llm.release() +``` + +### Cascade Speaker + Thinker + +```kotlin +val speaker = EngineLlmEngine("$kazeiaDir/Qwen3.5-9B-Q4_0.gguf", nThreads = 6) +val thinker = EngineLlmEngine("$kazeiaDir/Qwen3Guard-Gen-4B.Q4_K_M.gguf", nThreads = 4) +// 4 threads sur Thinker pour laisser 4 cœurs au Speaker en parallèle + +val draft = speaker.generate(sysSpeaker, userInput, max = 96) +val safe = thinker.generate(sysGuard, "Réponse à vérifier: $draft", max = 32) +// si safe == "OK" -> envoyer draft, sinon reformuler + +speaker.release(); thinker.release() +``` + +--- + +## 5. Codes d'erreur + +`load()` renvoie `0L` (= `IllegalStateException` Kotlin via `require`) si : +- modèle introuvable ou GGUF invalide +- pas assez de RAM +- erreur ggml_backend_init (rare) + +`generate()` / `generateRaw()` renvoient un texte vide `""` si : +- tokenize fail +- llama_decode prefill fail +- KV transfer fail (option C uniquement) + +Pas de codes int dédiés (différent de TTS/STT) — l'API LLM est plus simple. Log via stderr `kazeia-engine:` pour le debug. + +--- + +## 6. Bench rapide standalone + +`llama-cli` est inclus dans `dist/` pour mesurer la perf avant intégration : + +```bash +adb shell ' +cd /data/local/tmp/kz-engine +export LD_LIBRARY_PATH=./lib +export ADSP_LIBRARY_PATH=./lib +./llama-cli -m Qwen3.5-4B-Q4_0.gguf \ + -p "Bonjour, comment vas-tu ?" \ + -n 64 -t 6 -ngl 99 --reasoning-budget 0 -dev HTP0 +' +``` + +Bench standardisé : +```bash +./llama-bench -m Qwen3.5-4B-Q4_0.gguf -t 6 -ngl 99 -p 32,128,512 -n 16 +``` + +--- + +## 7. Checklist d'intégration + +1. **`System.loadLibrary("kazeia_engine")` réussit** au démarrage. Si erreur `library "libllama.so" not found`, vérifier les 8 libs dans `jniLibs/arm64-v8a/`. + +2. **Charge un engine + 1 generate** sur un modèle fixture (Qwen3.5-4B) avec un prompt simple. Le log stderr doit indiquer le mode détecté : + ``` + kazeia-engine: HYBRIDE GDN (qwen35) -> option C, prefill HTP+HMX (t=8) / decode CPU (t=6) + ``` + ou + ``` + kazeia-engine: DENSE (qwen3) -> HTP contexte-unique, matmul HVX (HMX off) (t=6) + ``` + ou + ``` + kazeia-engine: CPU pur (qwen3, pas de HTP) (t=6) + ``` + +3. **Vérifier n_threads in-app** : si tu vois decode << 10 tok/s sur Qwen3.5-4B, c'est que les threads tombent à 1 cœur (bug Android scheduler). Solution : forcer affinity 8 grands cœurs depuis Kotlin avant de générer (cf §8). + +4. **Sanity null/edge** : + - prompt vide → texte vide + - maxTok = 0 → texte vide + - modèle inexistant → IllegalStateException au constructeur + +5. **Threading app** : `generate()` est synchrone bloquant. Appeler depuis `Dispatchers.IO` ou un thread dédié. Pas multi-thread sur le même handle (1 generate à la fois par engine). + +6. **Cycle de vie** : `release()` quand l'app sort du foreground (libère 2.4-5.0 GB de RAM selon modèle). + +--- + +## 8. Piège connu : affinity Android in-app + +**Symptôme** : decode tombe de 9.8 tok/s (CLI standalone) à 0.32 tok/s in-app sur le même modèle. + +**Cause** : le scheduler Android plante le process sur 1 petit cœur quand l'app n'est pas au foreground actif, ou quand il n'y a pas de hint thermique. `llama-cli` standalone via adb shell évite ce piège (process root-like). + +**Fix côté app** : + +```kotlin +import android.os.Process +import java.io.RandomAccessFile + +private fun pinToBigCores() { + // Snapdragon 8 Elite : 8 cores total, cores 0-3 = grands (big), 4-7 = petits (medium/little). + // En pratique sur SM8750, masque 0xFC laisse tomber les 2 petits cores et garde les 6 grands. + try { + val tid = Process.myTid() + // sched_setaffinity via /proc/self/task//cgroup ou via JNI taskset. + // Sinon, fallback : démarrer le thread avec Thread.MAX_PRIORITY. + Thread.currentThread().priority = Thread.MAX_PRIORITY + Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND) + } catch (e: Throwable) { /* best effort */ } +} + +// Dans le ViewModel/Service qui héberge l'engine : +viewModelScope.launch(Dispatchers.IO) { + pinToBigCores() + val reply = llm.generate(sys, usr, max) + withContext(Dispatchers.Main) { display(reply) } +} +``` + +Sans cette pin, `nThreads=6` côté JNI n'est pas suffisant — Android assigne quand même 1 cœur effectif. + +**Validation** : pendant un generate, faire `adb shell top -H -p ` et vérifier qu'on voit 6 threads actifs autour de 80-100 % CPU. Si on voit 1 thread à 100 % et les autres à 0, l'affinity n'est pas posée. + +--- + +## 9. Perf détaillée et tuning + +### Sweet spot threads (mesure `r3`, Snapdragon 8 Elite) + +| Modèle | t=4 | **t=6** | t=8 | +|---|---:|---:|---:| +| Qwen3.5-4B Q4_0 | 8.2 | **9.8** | 7.4 (contention) | +| Qwen3.5-9B Q4_0 | 4.4 | **5.2** | 2.1 (contention) | +| Qwen3-4B Q4_0 | 13 | **17** | 14 | + +`t=8` est mauvais à cause de contention (oversubscription + scheduler Android). **Garde 6 par défaut.** + +### Cascade : adapter les threads + +Si tu lances Speaker (4B) et Thinker (4B) en simultané : +- Speaker `nThreads = 4` +- Thinker `nThreads = 2` + +Le total = 6 grands cœurs, on laisse 2 cœurs au système Android. + +### Decode = memory-bandwidth bound + +`r3` : decode M=1 est limité par BW LPDDR5x ~77 GB/s, pas par compute. Donc Q4_0 (2 GB poids) est ~2× plus rapide que Q8_0 (4 GB) sur le decode. Pas d'intérêt à Q6_K ou plus précis sur ce hardware. + +--- + +## 10. Contacts & support + +Code source : `/opt/Kazeia-engine/dist/jni/kazeia_engine_jni.cpp` + `EngineLlmEngine.kt`. + +Bench CLI : `/opt/Kazeia-engine/dist/llama-cli` + `llama-bench`. + +Pour reproduire un bug : capture logcat tag `kazeia-engine`, et output du log stderr (mode détecté + n_threads effectif). diff --git a/dist/TTS_INTEGRATION.md b/dist/TTS_INTEGRATION.md new file mode 100644 index 0000000..796c401 --- /dev/null +++ b/dist/TTS_INTEGRATION.md @@ -0,0 +1,290 @@ +# Intégration TTS Kazeia-Engine — Guide développeur + +**Date** : 01/06/2026 +**Cible** : remplacer le pipeline TTS legacy par `libkazeia_tts.so` + façade Kotlin `TtsEngine.kt`. Inclut le **clonage vocal embarqué** via speaker encoder ECAPA-TDNN. + +**Runtime** : libllama + libggml (fork ql Qualcomm) avec backend Hexagon HTP V79 — décode ggml in-process. Pas d'ONNX, pas de Python, pas de root. + +**Composants in-process** : +- **Talker** Qwen3-TTS via `libllama.so` (texte → codes audio 16 codebooks) +- **CP** (Code Predictor) en ggml C++ avec KV-cache (codes → embeddings audio) +- **Decoder** ggml chraac intégré (embeddings → WAV PCM 24 kHz) +- **Speaker encoder** ECAPA-TDNN optionnel (WAV ref → x_vector[1024] pour clonage vocal) + +**Perf prouvée** (Snapdragon 8 Elite SM8750, audio FR 2-5 s) : +- RTF total : ~2.5-3.0 selon longueur (talker 32 ms/frame, CP 70 ms/frame, decoder RTF ~2) +- Clonage vocal : encoder per-call **190-530 ms** selon ref WAV (5-15 s) +- RAM peak : ~3.5 GB (talker f32 1.6 GB + CP 250 MB + decoder 325 MB + vocab 50 MB + fixtures 1.2 GB) +- Clonage ajoute ~34 MB RAM permanent au load + +--- + +## 1. Ce que la lib fait + +``` +libkazeia_tts.so (929 KB, SHARED, dépend de libllama + libggml*) + ├── TtsEngine load(talker_gguf, vocab_gguf, dump_dir, useHtp, nThreads, cpUseCache) + ├── TtsEngine synthesize(text, outWavPath, maxSteps, seed, sampling, xvectorOverride?) + ├── TtsEngine encode_speaker_wav(refWavPath) -> FloatArray[1024] // clonage + ├── TtsEngine encode_speaker_waveform(pcm, sampleRate) // clonage in-memory + └── TtsEngine free(handle) +``` + +API Kotlin (`TtsEngine.kt`) : + +```kotlin +val tts = TtsEngine( + talkerGguf = "/data/.../talker_f32.gguf", + vocabGguf = "/data/.../Qwen3-4B-Q4_0.gguf", + dumpDir = "/data/.../tts_dump", + useHtp = false, // true -> HTP0 prefill option C ; false -> CPU pur + nThreads = 6, + cpUseCache = true, // KV cache CP (gain RTF -39%) +) + +// Synthèse classique (voix par défaut = damien_xvector.bin fixture) +val r = tts.synthesize( + text = "Bonjour, comment allez-vous ?", + outWavPath = "/sdcard/out.wav", + maxSteps = 256, + sampling = TtsSampling() // top_p=0.95, rep_penalty=1.10 validés +) +println("WAV ${r.frames} frames, ${r.audioMs} ms audio en ${r.totalMs} ms, RTF=${r.totalMs.toFloat()/r.audioMs}") + +// Synthèse avec voix clonée +val xvec = tts.encodeSpeakerWav("/sdcard/voix_pierre.wav") // 1024 floats +val r2 = tts.synthesize(text = "...", outWavPath = "...", xvectorOverride = xvec) + +tts.release() +``` + +--- + +## 2. Build (côté app Android) + +### a. Récupérer les artifacts + +Sur le host (machine de build) : + +```bash +cd /opt/Kazeia-engine/dist +bash build_kazeia_tts.sh +# produit b-jni/libkazeia_tts.so +``` + +### b. Libs à copier dans `app/src/main/jniLibs/arm64-v8a/` + +**8 fichiers obligatoires** (vérifiés via `readelf -d` sur la lib produite) : + +| Fichier | Taille | Origine | Rôle | +|---|---:|---|---| +| `libkazeia_tts.so` | 929 KB | `dist/b-jni/` | la lib elle-même | +| `libllama.so` | 35 MB | `dist/lib/` | fork ggml-org/llama.cpp + Hexagon | +| `libggml.so` | 627 KB | `dist/lib/` | ggml core | +| `libggml-base.so` | 6.6 MB | `dist/lib/` | ggml base ops | +| `libggml-cpu.so` | 4.7 MB | `dist/lib/` | ggml CPU backend (NEON ARM v8.6) | +| `libggml-hexagon.so` | 3.3 MB | `dist/lib/` | ggml Hexagon HTP backend (host wrapper) | +| `libggml-htp-v79.so` | 354 KB | `dist/lib/` | DSP skel V79 (chargé via dlopen runtime par libggml-hexagon) | +| `libc++_shared.so` | 1.8 MB | `dist/lib/` | NDK r27d STL (peut être déjà fournie par votre projet) | + +**Total** : ~52 MB. Important côté APK mais indispensable. Pas de Maven AAR alternatif — il faut pousser ces .so. + +**Crash typique** si une lib manque (vu en bench) : +``` +dlopen failed: library "libllama.so" not found: needed by /data/app/.../libkazeia_tts.so +``` + +→ Vérifier que les 8 libs sont bien dans `jniLibs/arm64-v8a/`. + +### c. Copier la façade Kotlin + +Copie `dist/jni/TtsEngine.kt` dans `app/src/main/java/com/kazeia/tts/TtsEngine.kt`. Package = `com.kazeia.tts` (cohérent avec l'existant). + +### d. Modèles sur la tablette + +Inchangé par rapport au pipeline TTS actuel. Push dans le dir habituel : + +``` +/data/local/tmp/kazeia/tts/ +├── talker_f32.gguf # Qwen3-TTS talker 1.6 GB +├── tts_dump/ +│ ├── cp_f16.gguf # CP weights +│ ├── cp_heads.bin +│ ├── cp_codec_embs.bin +│ ├── qwen3tts_decoder.gguf # decoder ggml chraac +│ ├── text_embed.bin # ~1.2 GB +│ ├── tp_fc1_w.bin / tp_fc1_b.bin +│ ├── tp_fc2_w.bin / tp_fc2_b.bin +│ ├── talker_tok_embd.bin # tied embed reference +│ ├── damien_xvector.bin # voix par défaut (1024 floats) +│ ├── manifest_text.txt # constantes BOS/EOS/PAD +│ ├── speaker_encoder.gguf # OPTIONNEL : 17 MB clonage vocal +│ └── mel_basis_qwen3tts.bin # OPTIONNEL : 256 KB librosa mel +└── (vocab_gguf = un Qwen3-4B-Q4_0.gguf pour le tokenizer texte) +``` + +--- + +## 3. Migration depuis le pipeline TTS legacy + +Si tu avais `tts_talker_cpu.cpp` / `tts_cp_cpu.cpp` compilés en source dans le projet app contre un ancien fork ggml : + +- **Tu peux les supprimer**. Tout ce code est embarqué dans `libkazeia_tts.so` (tts_engine.cpp + cp_inference.cpp + speaker_encoder.cpp + sampler.cpp + kazeia_text_tokenizer.cpp + chraac decoder statique). +- Tu n'as plus besoin de recompiler quoi que ce soit contre les headers du fork ql — la lib est self-contained. +- Tu n'as plus besoin du tokenizer texte custom — il est dans la lib via `kazeia_text_tokenizer.cpp` qui charge le vocab d'un GGUF Qwen. + +--- + +## 4. Exemples d'usage + +### Synthèse simple + +```kotlin +val tts = TtsEngine( + talkerGguf = "$kazeiaDir/talker_f32.gguf", + vocabGguf = "$kazeiaDir/Qwen3-4B-Q4_0.gguf", + dumpDir = "$kazeiaDir/tts_dump" +) +val r = tts.synthesize("Bonjour Pierre, comment vous sentez-vous aujourd'hui ?", + outWavPath = "$cacheDir/reply.wav") +playWav(r.outWavPath) +tts.release() +``` + +### Clonage vocal — encode 1 fois, synth N fois + +```kotlin +val tts = TtsEngine(/* ... */, speakerEncoderGguf = "$kazeiaDir/tts_dump/speaker_encoder.gguf", + mel_basis_path = "$kazeiaDir/tts_dump/mel_basis_qwen3tts.bin") + +// Encode une fois (cache l'utiliser sur les N synthèses qui suivent) +val pierreVoice = tts.encodeSpeakerWav("$voicesDir/pierre_5s.wav") // ~190 ms + +for (text in messages) { + val r = tts.synthesize(text, outWavPath = nextWav(), xvectorOverride = pierreVoice) + playWav(r.outWavPath) +} + +tts.release() +``` + +### Sampling tuning + +Les défauts (`top_p=0.95`, `rep_penalty=1.10`) sont **validés à l'écoute** sur panel FR (cf mémoire `project_tts_session_28may`). Override possible : + +```kotlin +val r = tts.synthesize(text, outWavPath, sampling = TtsSampling( + cpTemp = 0.9f, cpTopK = 50, cpTopP = 0.95f, cpRepPenalty = 1.10f, + talkerTemp = 0.9f, talkerTopK = 50, talkerTopP = 0.95f, talkerRepPenalty = 1.10f +)) +``` + +--- + +## 5. Codes d'erreur (`TtsResult.err`) + +| Code | Signification | +|---|---| +| 0 | OK | +| -1 | engine nul ou texte/outWavPath nul | +| -2 | pas de tokenizer chargé (vocabGguf manquant au load) | +| -3 | tokens trop courts (texte < 8 tokens BPE) | +| -4 | prefill talker FAIL (modèle, vocab, manifest) | +| -6 | WAV write FAIL | + +Au `tts_engine_load` (renvoie `null` → exception Kotlin `IllegalStateException`) : +- talker_gguf ou dump_dir nul +- fichiers fixtures manquants (`text_embed.bin`, `tp_*`, `tok_embd.bin`, `damien_xvector.bin`) +- vocab GGUF invalide +- modèle talker invalide + +--- + +## 6. Bench rapide standalone (avant d'intégrer) + +Pour vérifier que la lib + libs sont OK avant câblage app : + +```bash +adb push dist/b-jni/tts_pipeline /data/local/tmp/kz-engine/bin/ +adb push dist/lib/lib*.so /data/local/tmp/kz-engine/bin/ + +adb shell ' +cd /data/local/tmp/kz-engine +export LD_LIBRARY_PATH=./bin +export ADSP_LIBRARY_PATH=./bin +export KZTTS_TEXT="Bonjour, comment allez-vous ?" +export KZTTS_VOCAB_GGUF=Qwen3-4B-Q4_0.gguf +./bin/tts_pipeline talker_f32.gguf tts_dump /tmp/out.wav cpu 64 +' +``` + +Sortie typique : +``` +tts_engine_load: 1.0s (talker_n_embd=1024, npe=4, kz_tok=1, cp_cache=1, spk_enc=0) +=== TTS : N=42 frames (audio 3.50s) en 8.5s (RTF 2.43) === +WAV -> /tmp/out.wav +``` + +Si la lib charge mais le synth échoue, lire la sortie stderr (codes d'erreur ci-dessus). + +--- + +## 7. Checklist d'intégration (à valider en premier in-app) + +1. **`System.loadLibrary("kazeia_tts")` réussit** au démarrage de l'app. Si erreur `library "libllama.so" not found`, vérifier qu'il y a bien les 8 libs dans `jniLibs/arm64-v8a/`. + +2. **Charge un engine + 1 synthèse** sur un texte fixture FR (« Bonjour. ») : doit produire un WAV audible. Si silence ou bruit, vérifier que `tts_dump/` est complet (notamment `text_embed.bin` 1.2 GB et `tp_fc1/2_*` qui sont obligatoires). + +3. **Sanity null/edge** : + - text vide → err -1 + - text 3 caractères → err -3 (trop court) + - outWavPath dans un dir inexistant → err -6 + +4. **Threading** : `TtsEngine.synthesize()` est synchrone, ~3-10 s selon longueur. Appeler depuis `Dispatchers.IO`. Le n_threads est paramétrable au load (défaut 6). + +5. **Cycle de vie** : `release()` à appeler quand l'app sort du foreground (3.5 GB de RAM libérée). + +6. **Clonage vocal (optionnel)** : si tu fournis `speakerEncoderGguf` + `melBasisPath` au load, tu peux appeler `encodeSpeakerWav()` ensuite. Sinon, l'engine utilise le `damien_xvector.bin` fixture (voix damien). + +--- + +## 8. Pièges connus + +- **Stack TTS legacy** : si tu avais des sources `tts_talker_cpu.cpp` / `tts_cp_cpu.cpp` compilées contre un fork ggml différent, **tu peux et dois les supprimer**. Tout est dans la lib. + +- **chraac decoder** : le decoder ggml chraac est intégré statiquement dans `libkazeia_tts.so`. Pas besoin de pousser `libqwen3tts-decoder.a` séparément, ni le binaire `decoder_subprocess` (l'ancien pattern subprocess est obsolète). + +- **HTP TTS** : `useHtp = true` n'apporte rien sur le talker actuel (decode CPU 16.5 tok/s reste meilleur que HTP 3.6 tok/s pour les modèles dense). Garder `useHtp = false` sauf si tu testes le prefill HTP gain. + +- **Sample rate** : le clonage vocal `encodeSpeakerWav()` attend du **mono 16-bit 24 kHz**. Si tu lis un WAV à un autre SR, resample externe (libAndroid AudioRecord + resampler ou ffmpeg). + +--- + +## 9. Perf détaillée + +Pour 3 s d'audio synthétisé, 36 frames (12 frames/s = 80 ms/frame) : + +``` +prefill ~150 ms (talker prompt -> KV initial) +talker_loop ~1150 ms (36 frames × 32 ms) +cp_loop ~2520 ms (36 frames × 70 ms) +decoder ~4400 ms (RTF ~1.3 sur 36 frames) +───────────────────────── +total ~8.2 s pour 3 s audio = RTF 2.7 +``` + +Le decoder (chraac) est le pic. Optimisations futures : décoder par chunks de 8 frames pour overlap, ou portage HTP (R&D ouverte). + +**Clonage** : +190-530 ms one-time par changement de voix (encode_speaker_wav), 0 ms si on garde la même voix sur plusieurs synthèses. + +--- + +## 10. Contacts & support + +Code source : `/opt/Kazeia-engine/dist/jni/tts_engine.{h,cpp}` + `cp_inference.cpp` + `speaker_encoder.cpp` + `kazeia_mel.{h,cpp}` + `sampler.{h,cpp}` + `kazeia_tts_jni.cpp`. + +Test CLI : `/opt/Kazeia-engine/dist/jni/tts_pipeline.cpp` (binaire `tts_pipeline`). + +Bench artifacts : `/opt/Kazeia-engine/dist/b-jni/libkazeia_tts.so` + `tts_pipeline`. + +Pour reproduire un bug : capture logcat avec tag `tts_engine`. diff --git a/dist/jni/EngineLlmEngine.kt b/dist/jni/EngineLlmEngine.kt index 57c4bd8..6b7227d 100644 --- a/dist/jni/EngineLlmEngine.kt +++ b/dist/jni/EngineLlmEngine.kt @@ -3,7 +3,12 @@ package com.kazeia.llm // Mono-tour: generate(sys, usr). Multi-tour: ChatSession (gère l'historique + le template ChatML). class EngineJni { - external fun load(ggufPath: String, nCtx: Int): Long + /** + * @param nThreads decode CPU threads. Défaut 0 -> 6 (sweet spot Snapdragon 8 Elite, + * mémoire r3). Recommandations : 6 pour Speaker 4B/9B in-app, 4 si cascade + * simultanée Speaker+Thinker pour laisser cœurs au Thinker. + */ + external fun load(ggufPath: String, nCtx: Int, nThreads: Int): Long external fun generate(h: Long, sys: String, usr: String, maxTok: Int): String external fun generateRaw(h: Long, prompt: String, maxTok: Int): String // prompt complet déjà formaté external fun reset(h: Long) @@ -22,9 +27,9 @@ class EngineJni { companion object { init { System.loadLibrary("kazeia_engine") } } } -class EngineLlmEngine(model: String, ctx: Int = 4096) { +class EngineLlmEngine(model: String, ctx: Int = 4096, nThreads: Int = 6) { private val jni = EngineJni() - private val h = jni.load(model, ctx) + private val h = jni.load(model, ctx, nThreads) init { require(h != 0L) { "Kazeia-Engine: échec du chargement du modèle ($model)" } } // Mono-tour : system + un message user. diff --git a/dist/jni/kazeia_engine_jni.cpp b/dist/jni/kazeia_engine_jni.cpp index 68cfee6..c55e412 100644 --- a/dist/jni/kazeia_engine_jni.cpp +++ b/dist/jni/kazeia_engine_jni.cpp @@ -87,11 +87,18 @@ static std::string kengine_run(KEngine* k, const std::string& p, int maxTok) { return out; } +// LOAD : nThreads contrôle le n_threads du decode CPU. +// Sur Snapdragon 8 Elite (SM8750), l'optimum mesuré sur r3 est t=6 pour Qwen3.5-4B +// (decode 9.8 tok/s) et t=6 pour 9B (5.2 tok/s). t=8 réduit à cause de contention +// thermique + interaction avec le scheduler Android (in-app souvent dégradé). +// Param exposé pour permettre le tuning par voix (Speaker vs Thinker). extern "C" JNIEXPORT jlong JNICALL -Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx) { +Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx, jint nThreads) { const char* path_c = e->GetStringUTFChars(path, 0); std::string p(path_c); e->ReleaseStringUTFChars(path, path_c); + const int N_DECODE = (nThreads > 0) ? nThreads : 6; // défaut sweet spot r3 + const int N_PREFILL = 8; // prefill HTP : 8 OK (HTP gère) // Détection d'archi AVANT init backend (USE_HMX est latché à l'init). std::string arch = read_arch(p.c_str()); @@ -114,22 +121,24 @@ Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx) } if (m_h && hybrid) { // qwen3.5-like (GDN) -> OPTION C : prefill HTP (HMX) + instance CPU pour le decode (decode GDN HTP = lent). - c_h = make_ctx(m_h, nctx, 8, LLAMA_FLASH_ATTN_TYPE_ENABLED); + c_h = make_ctx(m_h, nctx, N_PREFILL, LLAMA_FLASH_ATTN_TYPE_ENABLED); auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0; m_c = llama_model_load_from_file(p.c_str(), mp_c); - if (m_c) c_c = make_ctx(m_c, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED); - fprintf(stderr, "kazeia-engine: HYBRIDE GDN (%s) -> option C, prefill HTP+HMX / decode CPU\n", arch.c_str()); + if (m_c) c_c = make_ctx(m_c, nctx, N_DECODE, LLAMA_FLASH_ATTN_TYPE_ENABLED); + fprintf(stderr, "kazeia-engine: HYBRIDE GDN (%s) -> option C, prefill HTP+HMX (t=%d) / decode CPU (t=%d)\n", + arch.c_str(), N_PREFILL, N_DECODE); } else if (m_h) { // dense -> HTP contexte-unique, matmul HVX (HMX off) : prefill+decode HTP, stable, ~3x CPU. - c_h = make_ctx(m_h, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED); - fprintf(stderr, "kazeia-engine: DENSE (%s) -> HTP contexte-unique, matmul HVX (HMX off)\n", arch.c_str()); + c_h = make_ctx(m_h, nctx, N_DECODE, LLAMA_FLASH_ATTN_TYPE_ENABLED); + fprintf(stderr, "kazeia-engine: DENSE (%s) -> HTP contexte-unique, matmul HVX (HMX off) (t=%d)\n", + arch.c_str(), N_DECODE); } else { // pas de HTP (ou échec du load HTP) -> CPU pur universel. auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0; m_c = llama_model_load_from_file(p.c_str(), mp_c); if (!m_c) return 0; - c_c = make_ctx(m_c, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED); - fprintf(stderr, "kazeia-engine: CPU pur (%s, pas de HTP)\n", arch.c_str()); + c_c = make_ctx(m_c, nctx, N_DECODE, LLAMA_FLASH_ATTN_TYPE_ENABLED); + fprintf(stderr, "kazeia-engine: CPU pur (%s, pas de HTP) (t=%d)\n", arch.c_str(), N_DECODE); } auto* k = new KEngine{ m_h, c_h, m_c, c_c,