diff --git a/kazeia-no-root-report.md b/kazeia-no-root-report.md index fc44fea..67482d8 100644 --- a/kazeia-no-root-report.md +++ b/kazeia-no-root-report.md @@ -1,4 +1,4 @@ -# Kazeia Android — Problème d'élimination de root pour le LLM +# Kazeia Android — Élimination du root pour le LLM (résolu) **Date :** 2026-04-14 **Device :** OnePlus Pad 3 (OPD2415, Snapdragon 8 Elite, SoC `sun`), Android 16 (OxygenOS), Magisk root @@ -6,6 +6,13 @@ --- +> **🟢 Statut : RÉSOLU.** Pipeline complet STT + LLM + TTS tourne in-process sans +> aucun appel à `su`. Voir la section **Résolution** en bas du document pour le +> détail du fix. Le reste du document décrit l'investigation initiale et garde +> sa valeur historique. + +--- + ## 1. Contexte général L'app Kazeia (Android / Kotlin + Jetpack Compose) orchestre un pipeline **STT → LLM → TTS** entièrement on-device sur le Hexagon HTP (V79) du Snapdragon 8 Elite. @@ -224,3 +231,106 @@ Je cherche soit : - Soit **la confirmation** que l'approche actuelle (root + Magisk remember) est le meilleur compromis accessible, avec éventuellement des suggestions pour minimiser les prompts Merci. + +--- + +## 10. Résolution (post-mortem) + +Une seconde opinion technique a identifié la **vraie cause racine** que +l'investigation locale avait mal diagnostiquée. + +### 10.1 Vraie cause + +Les processus Android forkés par Zygote (l'app elle-même, ses Services +`android:process=":xxx"`, etc.) héritent des **GIDs supplémentaires** +configurés à l'init pour `untrusted_app`. Ces GIDs incluent l'autorisation +`/dev/cdsprpc-smd` et d'autres canaux fastrpc. + +Quand `Runtime.exec("su"…)` ou `ProcessBuilder` font un `fork()` + `exec()` +classique, le `exec()` ne préserve pas tous les credentials utilisés par le +driver fastrpc Qualcomm pour authentifier le client. Le driver retourne +**error 4000 "Failed to load skel"** car il refuse de créer une session DSP +pour ce process. + +C'est pour ça que : +- ORT-QNN (Whisper) marchait in-process : chargé via `System.loadLibrary` dans + l'app, qui est Zygote-forked → credentials valides. +- `su -c qnn_llama_runner` marchait : root bypasse les checks fastrpc. +- `ProcessBuilder` du même runner échouait : ni Zygote-forked, ni root. + +Le "conflit de version QNN v2.31 vs v2.37" que j'avais soupçonné n'était +**pas le vrai problème**. Les libs étaient déjà unifiées en v2.42 dans jniLibs. + +### 10.2 La solution : `LlmModule` JNI in-process + +ExecuTorch fournit `org.pytorch.executorch.extension.llm.LlmModule`, un +wrapper JNI autour du même C++ `example::Runner` que le binaire +`qnn_llama_runner`. En l'invoquant depuis l'app (process Zygote-forked), le +DSP fastrpc accepte la session — pas de root nécessaire. + +### 10.3 Étapes réelles du fix + +1. **Build ExecuTorch Android** avec `EXECUTORCH_BUILD_LLAMA_JNI=ON`, + `EXECUTORCH_BUILD_QNN=ON`, `QNN_SDK_ROOT=/opt/Kazeia/qnn_sdk_242/qairt/2.42.0.251225` → + produit `libexecutorch_jni.so` 192 MB qui inclut le runner LLM + le backend QNN. +2. **Patches sources** dans `/opt/Kazeia/executorch-patches/llm_in_process_jni.patch` : + - `backends/qualcomm/CMakeLists.txt` : gate `PyQnnManagerAdaptor` sur `NOT ANDROID` + (le guard original sur `CMAKE_SYSTEM_PROCESSOR MATCHES x86_64` se déclenche + dans des sous-scopes du cross-compile Android). + - `extension/android/jni/jni_layer_llama.cpp`, branche `MODEL_TYPE_QNN_LLAMA` : + - `decoder_model = "qwen3"` (au lieu de `"llama3"` hardcodé) + - `temperature = 0.0f`, `eval_mode = 0` (kKVCached), `shared_buffer = true` + - **Crucial** : choisir `Runner` ou `Runner` selon + `module->get("get_kv_io_bit_width")` (mirror du `qnn_llama_runner.cpp main()`). + Hardcoder la mauvaise largeur produit du gibberish déterministe + comme `blocked罩ug darkestSOLEQuotes作者本人 humanity` — la KV cache + est lue/écrite à la mauvaise largeur de byte. +3. **Bundling jniLibs** : + - `libexecutorch.so` / `libexecutorch_jni.so` (build du 13-april avec LlmModule) + - `libqnn_executorch_backend.so` (assorti) + - `libQnnHtp.so`, `libQnnHtpPrepare.so`, `libQnnHtpV79Stub.so`, `libQnnSystem.so`, + `libQnnHtpV79Skel.so` (tous v2.42 depuis `/opt/Kazeia/qnn_sdk_242/`) +4. **JAR avec `LlmModule.class`** : compilation manuelle via `javac` (le build + gradle de l'AAR demandait android-34 platform non installée). +5. **Réécriture `ExecuTorchLlmEngine.kt`** : + - Constructeur : `LlmModule(MODEL_TYPE_QNN_LLAMA=4, ptePath, tokPath, 0.7f)` puis `.load()` + - `generate(prompt, seqLen, callback, echo=false)` — sinon le callback échoue à + stripper les tokens du prompt + - Template ChatML Qwen3 buildé en Kotlin, mirror exact de + `qnn_llama_runner.cpp::get_formatted_prompt()` pour `kQwen3` (user-first puis + system optionnel puis `<|im_start|>assistant`) + - Filtre inline `` dans le callback avec lookahead pour les tags + fragmentés sur plusieurs pieces + +### 10.4 Métriques validées + +| Métrique | Valeur | +|---|---| +| LlmModule.load() | 4.2 s (one-time à l'init de l'app) | +| LLM gen | ~17 tok/s (kv-only) | +| LLM TTFT | ~4 s pour 77 tokens prompt (prefill séquentiel kKVCached) | +| TTS Talker(PTE) | 37 ms/step (vs 45-65 avant) | +| TTS CP(PTE) | 73 ms/step | +| Pipeline e2e | "Bonjour, comment vas-tu ?" → audio en ~7 s | +| Magisk prompts | **0** | + +### 10.5 Optimisations restantes (non bloquantes) + +- **TTFT** : ré-exporter le `.pte` en `--model_mode hybrid` pour avoir un + `prefill_forward` parallèle → TTFT passerait de ~4 s à <1 s. Pas nécessaire + pour le use case conversationnel actuel. +- **Cosmétique** : le statusbar de l'app affiche encore "Hexagon NPU" pour le + TTS alors que c'est désormais le chemin .pte (label hérité du temps où c'était + ggml-hexagon). + +### 10.6 Mémoire projet + +État complet documenté dans +`/home/alf/.claude/projects/-opt-Kazeia/memory/project_llm_npu_plan.md`. +Backup git : branche `backup/pre-no-root-migration` + commit `6e6a2d9`. +Backup disk : `/home/alf/kazeia_backup_20260414/`. + +### 10.7 Commits clés + +- `f32b5dd` (LLM no-root: validate end-to-end pipeline, fix kv_io_bit_width detection) +- `b57719f` (LLM: filter tokens out of the streaming TTS path)