diff --git a/kazeia-android/app/src/main/java/com/kazeia/KazeiaApplication.kt b/kazeia-android/app/src/main/java/com/kazeia/KazeiaApplication.kt index a51d8d4..a81a06e 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/KazeiaApplication.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/KazeiaApplication.kt @@ -27,6 +27,21 @@ class KazeiaApplication : Application() { override fun onCreate() { super.onCreate() createNotificationChannel() + + // QNN HTP sur DSP (.pte LLM ExecuTorch + TTS) : le skel `libQnnHtpV79Skel.so` + // est relayé au CDSP par FastRPC, qui le cherche via ADSP_LIBRARY_PATH. Sans + // ça, le DSP ne trouve pas le skel (errno 2) → device_handle échoue (14001) → + // chargement .pte renvoie handle 0. On pointe sur le dossier natif extrait de + // l'APK (où vit le skel) + chemins DSP système. Doit être posé AVANT toute + // ouverture de session QNN (bench, KazeiaService, TTS). Cf. Qwen3TtsEngine. + try { + val nativeLibDir = applicationInfo.nativeLibraryDir + android.system.Os.setenv( + "ADSP_LIBRARY_PATH", + "$nativeLibDir;/vendor/dsp/cdsp;/vendor/lib/rfsa/adsp;/vendor/dsp", + true + ) + } catch (_: Throwable) {} // Note: Unity native lib preloading was removed because Unity 6 GameActivity // requires its own initialization sequence. Loading libs out of order causes // native crashes. Unity handles lib loading internally in onCreate(). diff --git a/kazeia-android/app/src/main/java/com/kazeia/llm/BenchLlmHarness.kt b/kazeia-android/app/src/main/java/com/kazeia/llm/BenchLlmHarness.kt index d52f5e5..427f352 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/llm/BenchLlmHarness.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/llm/BenchLlmHarness.kt @@ -1,11 +1,16 @@ // Harness LLM ADB-pilotable, mirror de BenchQnnHarness STT. // Déclenchement : `touch /bench_llm.trigger`, relance app. -// Charge EngineLlmEngine (lib CPU-only) + génère 3 prompts FR, log texte + tps + nThreads honoré. +// Valide le chemin refonte 2026-06-17 : UnifiedLlmAdapter (magic-byte) sur +// 1) le GGUF par défaut (Qwen3.5-4B, CPU i8mm + session cache-préfixe) +// 2) CHAQUE .pte présent sur disque (NPU V79 : libkazeia_pte + tokeniseur DJL arm64) +// Log : texte généré + prefill/decode + tok/s, pour chaque modèle. package com.kazeia.llm import android.content.Context import android.util.Log -import com.kazeia.KazeiaApplication +import com.kazeia.config.ModelRegistry +import com.kazeia.core.SamplingParams +import kotlinx.coroutines.runBlocking object BenchLlmHarness { private const val TAG = "BenchLlm" @@ -16,30 +21,52 @@ object BenchLlmHarness { "Je n'arrive plus à parler à ma femme" ) private val SYS = "Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : " + - "UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle simple." + "UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle simple. /no_think" - fun runBench(ctx: Context) { - val gguf = "${KazeiaApplication.MODELS_DIR}/q35-lmq4.gguf" - Log.i(TAG, "=== load q35-lmq4 ===") - val t0 = System.currentTimeMillis() - val eng = try { - EngineLlmEngine(gguf, ctx = 2048, nThreads = 6) - } catch (e: Throwable) { - Log.e(TAG, "load FAIL: ${e.message}"); return + fun runBench(ctx: Context) = runBlocking { + Log.i(TAG, "########## BENCH LLM (UnifiedLlmAdapter, refonte 2026-06-17) ##########") + + // Modèles à tester : GGUF par défaut + tous les .pte présents sur disque. + val models = ModelRegistry.availableOnDisk() + if (models.isEmpty()) { Log.e(TAG, "aucun modèle sur disque (ni GGUF ni .pte)"); return@runBlocking } + + for (m in models) { + val path = if (m.backend == ModelRegistry.Backend.GGUF) m.ggufPath() else m.ptePath() + val tkPath = if (m.backend == ModelRegistry.Backend.GGUF) null else m.tokenizerPath() + Log.i(TAG, "===== ${m.id} (${m.backend}, seq=${m.maxSeqLen}) : $path =====") + + val t0 = System.currentTimeMillis() + val eng = try { + UnifiedLlmAdapter( + modelPath = path, + systemPrompt = SYS, + ctx = m.maxSeqLen, + nThreads = 6, + tokenizerPath = tkPath, + onLog = { Log.i(TAG, it) } + ).also { it.load(path, com.kazeia.core.LlmConfig()) } + } catch (e: Throwable) { + Log.e(TAG, " LOAD FAIL ${m.id}: ${e.message}", e); continue + } + Log.i(TAG, " load OK (${System.currentTimeMillis() - t0}ms)") + + // warmup (1 tour court, hors mesure) + try { eng.generate("Bonjour", SamplingParams(maxNewTokens = 16)) { true } } catch (_: Throwable) {} + + for ((i, p) in PROMPTS.withIndex()) { + val ts = System.currentTimeMillis() + val r = try { + eng.generate(p, SamplingParams(maxNewTokens = 96, temperature = 0.7f)) { true } + } catch (e: Throwable) { + Log.e(TAG, " [${i + 1}] gen FAIL: ${e.message}", e); continue + } + val ms = System.currentTimeMillis() - ts + Log.i(TAG, " [${i + 1}/${PROMPTS.size}] '$p' → ${ms}ms (${"%.1f".format(r.tokensPerSecond)} tok/s)") + Log.i(TAG, " « ${r.text} »") + } + eng.release() + Log.i(TAG, "===== fin ${m.id} =====") } - Log.i(TAG, "load OK (${System.currentTimeMillis() - t0}ms)") - // warmup - try { eng.generate(SYS, "Bonjour", 16) } catch (_: Exception) {} - for ((i, p) in PROMPTS.withIndex()) { - val ts = System.currentTimeMillis() - val out = try { eng.generate(SYS, p, 96) } catch (e: Exception) { "[ERR ${e.message}]" } - val ms = System.currentTimeMillis() - ts - val nWords = out.trim().split(Regex("\\s+")).size - val tps = if (ms > 0) nWords * 1000f / ms else 0f - Log.i(TAG, "[${i+1}/${PROMPTS.size}] '${p}' → ${ms}ms (~${"%.2f".format(tps)} mots/s) :") - Log.i(TAG, " '${out.trim()}'") - } - eng.release() - Log.i(TAG, "=== Bench end ===") + Log.i(TAG, "########## BENCH LLM end ##########") } } diff --git a/kazeia-android/app/src/main/java/com/kazeia/llm/UnifiedLlmAdapter.kt b/kazeia-android/app/src/main/java/com/kazeia/llm/UnifiedLlmAdapter.kt index 21822c5..89c46ec 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/llm/UnifiedLlmAdapter.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/llm/UnifiedLlmAdapter.kt @@ -29,6 +29,11 @@ class UnifiedLlmAdapter( private val onLog: ((String) -> Unit)? = null ) : com.kazeia.core.LlmEngine { + private companion object { + // Bloc de raisonnement Qwen3.5 en tête de réponse (thinking-off => vide). À strip côté .pte. + val THINK_BLOCK = Regex("""^\s*.*?\s*""", RegexOption.DOT_MATCHES_ALL) + } + private enum class Fmt { GGUF, PTE } @Volatile private var fmt: Fmt? = null private var gguf: EngineLlmEngine? = null @@ -105,7 +110,12 @@ class UnifiedLlmAdapter( } Fmt.PTE -> { val e = pte ?: error("UnifiedLlmAdapter .pte non chargé") - out = e.generate(systemPromptOverride ?: systemPrompt, prompt, params.maxNewTokens) + val raw = e.generate(systemPromptOverride ?: systemPrompt, prompt, params.maxNewTokens) + // Le décode .pte conserve le bloc (thinking-off => bloc vide) : + // ce ne sont PAS des special tokens Qwen3.5, donc skipSpecialTokens ne les retire pas. + // Le moteur GGUF, lui, le strippe nativement. On normalise ici pour que les 2 backends + // se comportent pareil (sinon le TTS lit « » et la cascade parse faux). + out = THINK_BLOCK.replace(raw, "").trim() onToken?.invoke(out) // streaming .pte = un bloc (génération bloquante, doc §10) stats = e.lastStats() }