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:
parent
0108f47c09
commit
d1baacd096
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 où le rewind partiel
|
||||
* échoue), donc on l'utilise directement. */
|
||||
fun resetSession() { session?.reset() }
|
||||
|
||||
override fun release() { session = null; engine?.release(); engine = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue