docs: add post-mortem to no-root report — issue resolved

The root cause was process-credential loss across fork+exec, not the QNN
SDK version mismatch I had hypothesized. Switching the LLM to in-process
ExecuTorch LlmModule (Zygote-forked context, accepted by adsprpcd's
FastRPC credential check) eliminated the su requirement.

The original investigation sections are kept verbatim for reference; the
new section 10 documents the actual fix, the patches applied to ExecuTorch,
the metrics validated end-to-end, and pointers to the project memory entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kazeia Team 2026-04-14 11:19:27 +02:00
parent b57719fa5e
commit 6c7746c5d0
1 changed files with 111 additions and 1 deletions

View File

@ -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 **Date :** 2026-04-14
**Device :** OnePlus Pad 3 (OPD2415, Snapdragon 8 Elite, SoC `sun`), Android 16 (OxygenOS), Magisk root **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 ## 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. 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 - Soit **la confirmation** que l'approche actuelle (root + Magisk remember) est le meilleur compromis accessible, avec éventuellement des suggestions pour minimiser les prompts
Merci. 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<uint8_t>` ou `Runner<uint16_t>` 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 `<think>…</think>` 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 <think> tokens out of the streaming TTS path)