refonte(llm): Phase 3 — archivage code mort (Genie + EngineLlmAdapter)

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) <noreply@anthropic.com>
This commit is contained in:
Kazeia Team 2026-06-17 23:28:14 +02:00
parent 0108f47c09
commit d1baacd096
6 changed files with 8 additions and 366 deletions

View File

@ -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). * (Kazeia-Engine unifié, A/B identique sur set 6 audios mais à valider plus large).
* Pilotable depuis l'app admin / provider. Defaults safe = "prod". */ * Pilotable depuis l'app admin / provider. Defaults safe = "prod". */
val sttEngine: String = "prod", val sttEngine: String = "prod",
/** Backend LLM : "lib" = EngineLlmAdapter (libkazeia_engine + GGUF Qwen3.5, /** Backend LLM : VESTIGIAL depuis la refonte 2026-06-17. Le dispatch ne passe
* q35-lmq4 par défaut) défaut depuis 2026-06-15 : on n'utilise PLUS le * plus par cette valeur : KazeiaService charge tout LLM via UnifiedLlmAdapter
* .pte avec kazeia-engine (directive). "prod" = ExecuTorchLlmEngine (.pte) * qui auto-détecte le format par MAGIC-BYTE (GGUFCPU i8mm, .pteNPU V79). Le
* conservé en repli d'urgence uniquement ; sur cette plateforme le runner QNN * modèle effectif est choisi par `speaker.modelId` (ModelRegistry). Champ
* du .pte échoue au load (« Failed to load llm runner [1] »), donc à éviter. * conservé pour compat config/télémétrie. */
* "lib" valide device : load q35-lmq4.gguf 2.6s + génération FR cohérente. */
val llmEngine: String = "lib", val llmEngine: String = "lib",
/** Backend TTS : "lib" = libkazeia_tts unifié (SEUL compatible avec le /** Backend TTS : "lib" = libkazeia_tts unifié (SEUL compatible avec le
* libllama.so fork ql embarqué depuis 2026-06-02), "prod" = Qwen3TtsEngine * libllama.so fork ql embarqué depuis 2026-06-02), "prod" = Qwen3TtsEngine

View File

@ -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 <think> (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 le rewind partiel
* échoue), donc on l'utilise directement. */
fun resetSession() { session?.reset() }
override fun release() { session = null; engine?.release(); engine = null }
}

View File

@ -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
}
}

View File

@ -7,14 +7,9 @@ set(JNILIBS_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(KAZEIA_LLM_DIR /opt/Kazeia/kazeia-llm) set(KAZEIA_LLM_DIR /opt/Kazeia/kazeia-llm)
set(LLAMA_UPSTREAM /opt/Kazeia/llama-upstream) set(LLAMA_UPSTREAM /opt/Kazeia/llama-upstream)
# --- Genie JNI bridge (legacy QNN backend, kept for fallback) --- # Genie JNI bridge supprimé 2026-06-17 (refonte engine) : backend Genie mort,
add_library(genie_jni SHARED genie_jni.cpp) # remplacé par UnifiedLlmAdapter (GGUF + .pte). genie_jni.cpp + libGenie.so
# archivés dans /opt/Kazeia/archive/2026-06-17_refonte-engine/.
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)
# Whisper.cpp JNI fallback supprimé 2026-05-02 (frozen stack STT = NPU only). # 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. # Les libggml IMPORTED restent ci-dessous : utilisées par tts_talker_cpu et tts_cp_cpu.

View File

@ -1,201 +0,0 @@
#include <jni.h>
#include <string>
#include <android/log.h>
#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<uintptr_t>(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<CallbackContext*>(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<long>(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<long>(dialog));
return 0;
}
LOGI("Dialog created successfully: %p", dialog);
return reinterpret_cast<jlong>(dialog);
}
JNIEXPORT jstring JNICALL
Java_com_kazeia_llm_GenieJni_query(
JNIEnv* env, jobject, jlong dialogHandle, jstring prompt, jobject callback
) {
auto* dialog = reinterpret_cast<GenieDialog>(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<GenieDialog>(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<GenieDialog>(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"

View File

@ -9,7 +9,6 @@ d523468d62d9b603cb3354294d70d4b2feabf2c3f1e43b0c96c9aabf32813708 libc++_shared.
dd0506873aeb45e103ef0023678a7f14938a0f147f2348d1efc366f1dce40863 libexecutorch_jni.so dd0506873aeb45e103ef0023678a7f14938a0f147f2348d1efc366f1dce40863 libexecutorch_jni.so
0b6f86e114f0cf9f2940194797f06ded576e6b3272a5ffbb2c4b8cdc02850ef8 libexecutorch.so 0b6f86e114f0cf9f2940194797f06ded576e6b3272a5ffbb2c4b8cdc02850ef8 libexecutorch.so
34a0af206f660461e3da687f64172830cc61a32a93732fd7e984c43996d6fd91 libfbjni.so 34a0af206f660461e3da687f64172830cc61a32a93732fd7e984c43996d6fd91 libfbjni.so
d49b53137f478b7b4ccbd58b9166d83e832c299b681609c70628291c61ae7204 libGenie.so
616fa830f6fd3396ea7f0ca92f348c2146efb0b1154ab6ed2e6709655905ead3 libggml-base.so 616fa830f6fd3396ea7f0ca92f348c2146efb0b1154ab6ed2e6709655905ead3 libggml-base.so
66ea0c948385d8060a7296285d7a85738c21318a88ef767986444a3d9683aeea libggml-cpu.so 66ea0c948385d8060a7296285d7a85738c21318a88ef767986444a3d9683aeea libggml-cpu.so
7aeb209ec5c1da8857779b8f3b4c3e7519ed69c6211491948d3c87349dbced57 libggml.so 7aeb209ec5c1da8857779b8f3b4c3e7519ed69c6211491948d3c87349dbced57 libggml.so