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:
parent
b57719fa5e
commit
6c7746c5d0
|
|
@ -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<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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue