From d1baacd096d41f70cf023f4afbd50eab9fe261c0 Mon Sep 17 00:00:00 2001 From: Kazeia Team Date: Wed, 17 Jun 2026 23:28:14 +0200 Subject: [PATCH] =?UTF-8?q?refonte(llm):=20Phase=203=20=E2=80=94=20archiva?= =?UTF-8?q?ge=20code=20mort=20(Genie=20+=20EngineLlmAdapter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Après analyse de dépendances : l'essentiel d'ExecuTorch/QNN est VIVANT via le TTS Qwen3TtsEngine "prod" (org.pytorch.executorch.Module → libexecutorch.so, + libQnnGpu.so backend Adreno, + QnnHtp). NE PAS archiver (stack TTS figée). Réellement mort (refonte LLM) → archivé dans /opt/Kazeia/archive/2026-06-17_refonte-engine/ : - EngineLlmAdapter.kt (ancien adaptateur GGUF, remplacé par UnifiedLlmAdapter ; références restantes = commentaires uniquement) - GenieJni.kt + genie_jni.cpp + libGenie.so (backend Genie mort ; libgenie_jni.so n'était même plus shipé → GenieJni ne pouvait plus se charger) - cible CMake genie_jni retirée ; manifest jniLibs re-figé (27 libs, -libGenie) Aussi : ConfigStore.llmEngine documenté comme VESTIGIAL (le dispatch passe par UnifiedLlmAdapter magic-byte depuis la refonte ; modèle choisi par speaker.modelId). Build release vert ; LLM device non-régressé (GGUF 18 / qwen3-4b .pte 18.6 / guard4b .pte 21 tok/s) ; aucune lib genie dans l'APK. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/kazeia/config/ConfigStore.kt | 11 +- .../java/com/kazeia/llm/EngineLlmAdapter.kt | 97 --------- .../src/main/java/com/kazeia/llm/GenieJni.kt | 53 ----- .../app/src/main/jni/CMakeLists.txt | 11 +- kazeia-android/app/src/main/jni/genie_jni.cpp | 201 ------------------ .../scripts/jnilibs.MANIFEST.sha256 | 1 - 6 files changed, 8 insertions(+), 366 deletions(-) delete mode 100644 kazeia-android/app/src/main/java/com/kazeia/llm/EngineLlmAdapter.kt delete mode 100644 kazeia-android/app/src/main/java/com/kazeia/llm/GenieJni.kt delete mode 100644 kazeia-android/app/src/main/jni/genie_jni.cpp diff --git a/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt b/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt index 5a1e665..e68c934 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt @@ -40,12 +40,11 @@ class ConfigStore private constructor(private val file: File) { * (Kazeia-Engine unifié, A/B identique sur set 6 audios mais à valider plus large). * Pilotable depuis l'app admin / provider. Defaults safe = "prod". */ val sttEngine: String = "prod", - /** Backend LLM : "lib" = EngineLlmAdapter (libkazeia_engine + GGUF Qwen3.5, - * q35-lmq4 par défaut) — défaut depuis 2026-06-15 : on n'utilise PLUS le - * .pte avec kazeia-engine (directive). "prod" = ExecuTorchLlmEngine (.pte) — - * conservé en repli d'urgence uniquement ; sur cette plateforme le runner QNN - * du .pte échoue au load (« Failed to load llm runner [1] »), donc à éviter. - * "lib" valide device : load q35-lmq4.gguf 2.6s + génération FR cohérente. */ + /** Backend LLM : VESTIGIAL depuis la refonte 2026-06-17. Le dispatch ne passe + * plus par cette valeur : KazeiaService charge tout LLM via UnifiedLlmAdapter + * qui auto-détecte le format par MAGIC-BYTE (GGUF→CPU i8mm, .pte→NPU V79). Le + * modèle effectif est choisi par `speaker.modelId` (ModelRegistry). Champ + * conservé pour compat config/télémétrie. */ val llmEngine: String = "lib", /** Backend TTS : "lib" = libkazeia_tts unifié (SEUL compatible avec le * libllama.so fork ql embarqué depuis 2026-06-02), "prod" = Qwen3TtsEngine diff --git a/kazeia-android/app/src/main/java/com/kazeia/llm/EngineLlmAdapter.kt b/kazeia-android/app/src/main/java/com/kazeia/llm/EngineLlmAdapter.kt deleted file mode 100644 index a0d265a..0000000 --- a/kazeia-android/app/src/main/java/com/kazeia/llm/EngineLlmAdapter.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.kazeia.llm - -import com.kazeia.core.GenerationResult -import com.kazeia.core.LlmConfig -import com.kazeia.core.LlmEngine -import com.kazeia.core.SamplingParams -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -/** - * Adapter qui expose `com.kazeia.llm.EngineLlmEngine` (façade dist Kazeia-Engine, - * GGUF Qwen3.5/Qwen3 via libkazeia_engine.so + libllama fork ql + Hexagon) - * derrière l'interface prod `com.kazeia.core.LlmEngine`. Sert au swap réversible - * piloté par le flag config `llmEngine` ("prod" = ExecuTorch .pte | "lib" = engine GGUF). - * - * Pré-requis runtime : `libllama.so` fork ql + `libggml*.so` cohérents dans jniLibs. - * Si la chaîne libllama est encore en legacy (f86d), `System.loadLibrary("kazeia_engine")` - * jette UnsatisfiedLinkError au 1er accès à `EngineJni` → KazeiaService doit catch - * et tomber en repli sur le path prod. - * - * Modèle par défaut : Qwen3.5-4B `q35-lmq4.gguf` (upgrade vs Qwen3 dense en prod). - */ -class EngineLlmAdapter( - private val modelPath: String, - private val systemPrompt: String, - private val ctxLen: Int = 4096, - private val nThreads: Int = 6, - // true = ChatML standard sans (Qwen2.5…) via generateRaw ; - // false = bloc thinking-off Qwen3.5 injecté côté natif (generate). - private val plainChatml: Boolean = false, - private val onLog: ((String) -> Unit)? = null -) : LlmEngine { - private var engine: EngineLlmEngine? = null - // Session = cache de préfixe KV : le system prompt n'est prefillé QU'UNE fois - // (à la création), puis chaque tour ne prefille que le message user → ~2,5× - // moins de latence/tour (mesuré ~2991→~1190 ms sur q35-lmq4) + streaming. - // Pas créée pour plainChatml (Qwen2.5 = template ChatML différent → generateRaw). - // Stateless : reset() avant chaque ask (comportement historique sans mémoire - // LLM) — retirer ce reset activerait la mémoire conversationnelle. - private var session: LlmSession? = null - - override suspend fun load(modelPath: String, config: LlmConfig) = withContext(Dispatchers.IO) { - if (engine != null) return@withContext - val t0 = System.currentTimeMillis() - val e = EngineLlmEngine(this@EngineLlmAdapter.modelPath, ctx = ctxLen, nThreads = nThreads) - engine = e - if (!plainChatml) session = e.newSession(systemPrompt) // prefille le system 1× - onLog?.invoke("[ENGINE] load OK (${System.currentTimeMillis() - t0}ms) path=${this@EngineLlmAdapter.modelPath} t=${nThreads} session=${session != null}") - } - - override fun isLoaded(): Boolean = engine != null - - override suspend fun generate( - prompt: String, - params: SamplingParams, - onToken: ((String) -> Boolean)? - ): GenerationResult = withContext(Dispatchers.IO) { - val e = engine ?: throw IllegalStateException("EngineLlmAdapter not loaded") - val t0 = System.currentTimeMillis() - val s = session - val out: String = if (s != null) { - // Cache de préfixe + MÉMOIRE conversationnelle : le system reste prefillé, - // chaque ask ne prefille que le user et conserve l'historique en KV. - // (NB : on NE reset PAS par tour — sessionReset+ask renvoie vide, - // bug natif n_past non restauré ; reset boundary à rétablir côté engine.) - val sb = StringBuilder() - s.ask(prompt, params.maxNewTokens) { piece -> - sb.append(piece); onToken?.invoke(piece) ?: true - } - sb.toString().trim() - } else { - // plainChatml (Qwen2.5) : ChatML manuel, generateRaw bloquant. - val p = buildString { - append("<|im_start|>system\n").append(systemPrompt).append("<|im_end|>\n") - append("<|im_start|>user\n").append(prompt).append("<|im_end|>\n") - append("<|im_start|>assistant\n") - } - e.generateRaw(p, params.maxNewTokens).trim().also { onToken?.invoke(it) } - } - // Timing réel fourni par l'engine (prefill/decode séparés). tps = débit - // DÉCODE (fini l'artefact "prefill inclus" qui donnait ~3 tok/s). - val st = e.lastStats() - val n = if (st.nTokens > 0) st.nTokens.toInt() else out.split(Regex("\\s+")).size - val ms: Long = (st.prefillMs + st.decodeMs).takeIf { it > 0 } ?: (System.currentTimeMillis() - t0) - onLog?.invoke("[ENGINE] prefill=${st.prefillMs}ms decode=${st.decodeMs}ms tok=${st.nTokens} (${"%.1f".format(st.decodeTokPerSec)} tok/s decode)") - GenerationResult(out, n, ms, st.decodeTokPerSec.toFloat()) - } - - /** Démarre une conversation VIERGE (garde le system prefillé). À appeler aux - * bornes de conversation (changement de patient, effacement du chat) — sinon - * la mémoire d'un patient fuiterait vers le suivant. sessionReset natif corrigé - * 2026-06-15 (clear + re-prefill system sur archi hybride où le rewind partiel - * échoue), donc on l'utilise directement. */ - fun resetSession() { session?.reset() } - - override fun release() { session = null; engine?.release(); engine = null } -} diff --git a/kazeia-android/app/src/main/java/com/kazeia/llm/GenieJni.kt b/kazeia-android/app/src/main/java/com/kazeia/llm/GenieJni.kt deleted file mode 100644 index d07d627..0000000 --- a/kazeia-android/app/src/main/java/com/kazeia/llm/GenieJni.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.kazeia.llm - -/** - * JNI bridge to Qualcomm Genie SDK (libGenie.so). - * Native implementation in jni/genie_jni.cpp - */ -object GenieJni { - - init { - System.loadLibrary("Genie") - System.loadLibrary("genie_jni") - } - - /** - * Initialize Genie dialog from a JSON config file. - * @param configPath path to genie_config.json - * @return handle (pointer) to the dialog, or 0 on failure - */ - external fun createDialog(configPath: String): Long - - /** - * Send a query to the dialog and get a response. - * @param dialogHandle handle from createDialog - * @param prompt the text prompt - * @param callback called with each decoded token; return false to stop - * @return full response string - */ - external fun query(dialogHandle: Long, prompt: String, callback: TokenCallback?): String - - /** - * Set a stop sequence for the dialog. - * @param dialogHandle handle from createDialog - * @param stopSequence the stop sequence string - */ - external fun setStopSequence(dialogHandle: Long, stopSequence: String) - - /** - * Free the dialog resources. - * @param dialogHandle handle from createDialog - */ - external fun freeDialog(dialogHandle: Long) - - /** - * Get Genie API version. - * @return "major.minor" - */ - external fun getVersion(): String - - interface TokenCallback { - /** Called for each generated token. Return false to stop generation. */ - fun onToken(token: String): Boolean - } -} diff --git a/kazeia-android/app/src/main/jni/CMakeLists.txt b/kazeia-android/app/src/main/jni/CMakeLists.txt index 0ef2e7d..f1eb6a3 100644 --- a/kazeia-android/app/src/main/jni/CMakeLists.txt +++ b/kazeia-android/app/src/main/jni/CMakeLists.txt @@ -7,14 +7,9 @@ set(JNILIBS_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}) set(KAZEIA_LLM_DIR /opt/Kazeia/kazeia-llm) set(LLAMA_UPSTREAM /opt/Kazeia/llama-upstream) -# --- Genie JNI bridge (legacy QNN backend, kept for fallback) --- -add_library(genie_jni SHARED genie_jni.cpp) - -add_library(Genie SHARED IMPORTED) -set_target_properties(Genie PROPERTIES IMPORTED_LOCATION ${JNILIBS_DIR}/libGenie.so) - -target_link_libraries(genie_jni Genie android log) -target_compile_options(genie_jni PRIVATE -std=c++17 -O2) +# Genie JNI bridge supprimé 2026-06-17 (refonte engine) : backend Genie mort, +# remplacé par UnifiedLlmAdapter (GGUF + .pte). genie_jni.cpp + libGenie.so +# archivés dans /opt/Kazeia/archive/2026-06-17_refonte-engine/. # Whisper.cpp JNI fallback supprimé 2026-05-02 (frozen stack — STT = NPU only). # Les libggml IMPORTED restent ci-dessous : utilisées par tts_talker_cpu et tts_cp_cpu. diff --git a/kazeia-android/app/src/main/jni/genie_jni.cpp b/kazeia-android/app/src/main/jni/genie_jni.cpp deleted file mode 100644 index 9ee1ddd..0000000 --- a/kazeia-android/app/src/main/jni/genie_jni.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include -#include -#include - -#define TAG "GenieJNI" -#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) -#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) - -// Genie C API declarations (from libGenie.so) -extern "C" { - // Opaque types - typedef void* GenieDialogConfig; - typedef void* GenieDialog; - typedef void* GenieTokenizer; - typedef void* GenieSampler; - - // Version - int Genie_getApiMajorVersion(); - int Genie_getApiMinorVersion(); - int Genie_getApiPatchVersion(); - - // DialogConfig - GenieDialogConfig GenieDialogConfig_createFromJson(const char* jsonPath); - void GenieDialogConfig_free(GenieDialogConfig config); - - // Dialog - GenieDialog GenieDialog_create(GenieDialogConfig config); - void GenieDialog_free(GenieDialog dialog); - const char* GenieDialog_query(GenieDialog dialog, const char* prompt); - void GenieDialog_setStopSequence(GenieDialog dialog, const char* stopSeq); - void GenieDialog_reset(GenieDialog dialog); - void GenieDialog_signal(GenieDialog dialog); - GenieTokenizer GenieDialog_getTokenizer(GenieDialog dialog); - GenieSampler GenieDialog_getSampler(GenieDialog dialog); - - // Sampler callback - typedef bool (*GenieSamplerCallback)(const char* token, void* userData); - void GenieSampler_registerUserDataCallback( - GenieSampler sampler, - GenieSamplerCallback callback, - void* userData - ); - - // Tokenizer - const char* GenieTokenizer_decode(GenieTokenizer tokenizer, const int* tokens, int numTokens); - int* GenieTokenizer_encode(GenieTokenizer tokenizer, const char* text, int* numTokens); -} - -// Check if a pointer looks like a valid heap pointer (not an error code) -// Genie SDK returns small negative int values as error codes (e.g. -5 = 0xfffffffb) -// On ARM64 Android, valid heap pointers are always > 0x100000000 (above 4GB) -static bool isValidPointer(void* ptr) { - uintptr_t addr = reinterpret_cast(ptr); - if (ptr == nullptr) return false; - // Any value that fits in 32 bits is likely an error code, not a heap pointer - if (addr <= 0xFFFFFFFFULL) return false; - return true; -} - -// Callback context for token streaming -struct CallbackContext { - JNIEnv* env; - jobject callback; - jmethodID onTokenMethod; - std::string fullResponse; - bool shouldStop; -}; - -static bool samplerCallback(const char* token, void* userData) { - auto* ctx = reinterpret_cast(userData); - if (ctx->shouldStop || token == nullptr) return false; - - ctx->fullResponse += token; - - if (ctx->callback != nullptr) { - JNIEnv* env = ctx->env; - jstring jToken = env->NewStringUTF(token); - jboolean continueGen = env->CallBooleanMethod( - ctx->callback, ctx->onTokenMethod, jToken - ); - env->DeleteLocalRef(jToken); - - if (!continueGen) { - ctx->shouldStop = true; - return false; - } - } - return true; -} - -extern "C" { - -JNIEXPORT jlong JNICALL -Java_com_kazeia_llm_GenieJni_createDialog(JNIEnv* env, jobject, jstring configPath) { - const char* path = env->GetStringUTFChars(configPath, nullptr); - LOGI("Creating dialog from config: %s", path); - - GenieDialogConfig config = GenieDialogConfig_createFromJson(path); - env->ReleaseStringUTFChars(configPath, path); - - if (!isValidPointer(config)) { - LOGE("Failed to create dialog config (returned %p, likely error code %ld)", - config, reinterpret_cast(config)); - return 0; - } - - LOGI("Dialog config created: %p", config); - GenieDialog dialog = GenieDialog_create(config); - GenieDialogConfig_free(config); - - if (!isValidPointer(dialog)) { - LOGE("Failed to create dialog (returned %p, likely error code %ld)", - dialog, reinterpret_cast(dialog)); - return 0; - } - - LOGI("Dialog created successfully: %p", dialog); - return reinterpret_cast(dialog); -} - -JNIEXPORT jstring JNICALL -Java_com_kazeia_llm_GenieJni_query( - JNIEnv* env, jobject, jlong dialogHandle, jstring prompt, jobject callback -) { - auto* dialog = reinterpret_cast(dialogHandle); - - if (!isValidPointer(dialog)) { - LOGE("Invalid dialog handle: %p", dialog); - return env->NewStringUTF("[Erreur: modèle LLM non chargé]"); - } - - const char* promptStr = env->GetStringUTFChars(prompt, nullptr); - - CallbackContext ctx; - ctx.env = env; - ctx.callback = callback; - ctx.shouldStop = false; - - if (callback != nullptr) { - jclass cbClass = env->GetObjectClass(callback); - ctx.onTokenMethod = env->GetMethodID( - cbClass, "onToken", "(Ljava/lang/String;)Z" - ); - - // Register the sampler callback - GenieSampler sampler = GenieDialog_getSampler(dialog); - if (isValidPointer(sampler)) { - GenieSampler_registerUserDataCallback(sampler, samplerCallback, &ctx); - } else { - LOGE("Invalid sampler pointer: %p", sampler); - } - } - - LOGI("Query: %.80s...", promptStr); - const char* response = GenieDialog_query(dialog, promptStr); - env->ReleaseStringUTFChars(prompt, promptStr); - - // Use callback accumulated response if available, otherwise use direct response - std::string result; - if (!ctx.fullResponse.empty()) { - result = ctx.fullResponse; - } else if (response != nullptr) { - result = response; - } - - LOGI("Response length: %zu chars", result.size()); - return env->NewStringUTF(result.c_str()); -} - -JNIEXPORT void JNICALL -Java_com_kazeia_llm_GenieJni_setStopSequence( - JNIEnv* env, jobject, jlong dialogHandle, jstring stopSequence -) { - auto* dialog = reinterpret_cast(dialogHandle); - if (!isValidPointer(dialog)) return; - const char* seq = env->GetStringUTFChars(stopSequence, nullptr); - GenieDialog_setStopSequence(dialog, seq); - env->ReleaseStringUTFChars(stopSequence, seq); -} - -JNIEXPORT void JNICALL -Java_com_kazeia_llm_GenieJni_freeDialog(JNIEnv*, jobject, jlong dialogHandle) { - auto* dialog = reinterpret_cast(dialogHandle); - if (isValidPointer(dialog)) { - GenieDialog_free(dialog); - LOGI("Dialog freed"); - } -} - -JNIEXPORT jstring JNICALL -Java_com_kazeia_llm_GenieJni_getVersion(JNIEnv* env, jobject) { - int major = Genie_getApiMajorVersion(); - int minor = Genie_getApiMinorVersion(); - int patch = Genie_getApiPatchVersion(); - std::string version = std::to_string(major) + "." + - std::to_string(minor) + "." + - std::to_string(patch); - return env->NewStringUTF(version.c_str()); -} - -} // extern "C" diff --git a/kazeia-android/scripts/jnilibs.MANIFEST.sha256 b/kazeia-android/scripts/jnilibs.MANIFEST.sha256 index 669db0e..1c90549 100644 --- a/kazeia-android/scripts/jnilibs.MANIFEST.sha256 +++ b/kazeia-android/scripts/jnilibs.MANIFEST.sha256 @@ -9,7 +9,6 @@ d523468d62d9b603cb3354294d70d4b2feabf2c3f1e43b0c96c9aabf32813708 libc++_shared. dd0506873aeb45e103ef0023678a7f14938a0f147f2348d1efc366f1dce40863 libexecutorch_jni.so 0b6f86e114f0cf9f2940194797f06ded576e6b3272a5ffbb2c4b8cdc02850ef8 libexecutorch.so 34a0af206f660461e3da687f64172830cc61a32a93732fd7e984c43996d6fd91 libfbjni.so -d49b53137f478b7b4ccbd58b9166d83e832c299b681609c70628291c61ae7204 libGenie.so 616fa830f6fd3396ea7f0ca92f348c2146efb0b1154ab6ed2e6709655905ead3 libggml-base.so 66ea0c948385d8060a7296285d7a85738c21318a88ef767986444a3d9683aeea libggml-cpu.so 7aeb209ec5c1da8857779b8f3b4c3e7519ed69c6211491948d3c87349dbced57 libggml.so