refonte(llm): NPU .pte in-app sans root (ADSP_LIBRARY_PATH) + normalisation think-block
Validation device (OPD2415 rooté, mais .pte sans root via PD signé) : - GGUF défaut Qwen3.5-4B : ~18 tok/s, FR cohérent (chemin session cache-préfixe) - qwen3-4b .pte NPU : 18.6 tok/s | guard4b .pte NPU : 21 tok/s — FR cohérent - DJL tokenizer arm64 (tokenizer-native:0.33.0) OK on-device Fixes : - KazeiaApplication : pose ADSP_LIBRARY_PATH (dossier natif extrait + chemins DSP) AVANT toute session QNN. Sans ça le CDSP ne trouve pas libQnnHtpV79Skel.so (errno 2) → device_handle 14001 → .pte renvoie handle 0. Absent de la spec engine (validée via qnn_llama_runner CLI, où le shell exporte ADSP_LIBRARY_PATH). - UnifiedLlmAdapter : strip du bloc <think>…</think> en tête de sortie .pte (pas des special tokens => skipSpecialTokens ne les retire pas ; le GGUF le strippe nativement). Sinon le TTS le lit et la cascade parse faux. - BenchLlmHarness : réécrit pour valider UnifiedLlmAdapter sur GGUF + chaque .pte. Connu (hors périmètre intégration) : - qwen3-8b .pte (export 05-23, sharding=2) : blob contexte incompatible runtime QAIRT 2.42 (magic 0x5678abcd attendu, 0x2000000 lu ; "Context group 1") → re-export 2.42 requis côté engine. - qwen2.5-7b .pte absent du device (seul le GGUF est présent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0a72654093
commit
0108f47c09
|
|
@ -27,6 +27,21 @@ class KazeiaApplication : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
createNotificationChannel()
|
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
|
// Note: Unity native lib preloading was removed because Unity 6 GameActivity
|
||||||
// requires its own initialization sequence. Loading libs out of order causes
|
// requires its own initialization sequence. Loading libs out of order causes
|
||||||
// native crashes. Unity handles lib loading internally in onCreate().
|
// native crashes. Unity handles lib loading internally in onCreate().
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
// Harness LLM ADB-pilotable, mirror de BenchQnnHarness STT.
|
// Harness LLM ADB-pilotable, mirror de BenchQnnHarness STT.
|
||||||
// Déclenchement : `touch <files>/bench_llm.trigger`, relance app.
|
// Déclenchement : `touch <files>/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
|
package com.kazeia.llm
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kazeia.KazeiaApplication
|
import com.kazeia.config.ModelRegistry
|
||||||
|
import com.kazeia.core.SamplingParams
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
object BenchLlmHarness {
|
object BenchLlmHarness {
|
||||||
private const val TAG = "BenchLlm"
|
private const val TAG = "BenchLlm"
|
||||||
|
|
@ -16,30 +21,52 @@ object BenchLlmHarness {
|
||||||
"Je n'arrive plus à parler à ma femme"
|
"Je n'arrive plus à parler à ma femme"
|
||||||
)
|
)
|
||||||
private val SYS = "Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : " +
|
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) = 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 =====")
|
||||||
|
|
||||||
fun runBench(ctx: Context) {
|
|
||||||
val gguf = "${KazeiaApplication.MODELS_DIR}/q35-lmq4.gguf"
|
|
||||||
Log.i(TAG, "=== load q35-lmq4 ===")
|
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
val eng = try {
|
val eng = try {
|
||||||
EngineLlmEngine(gguf, ctx = 2048, nThreads = 6)
|
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) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "load FAIL: ${e.message}"); return
|
Log.e(TAG, " LOAD FAIL ${m.id}: ${e.message}", e); continue
|
||||||
}
|
}
|
||||||
Log.i(TAG, "load OK (${System.currentTimeMillis() - t0}ms)")
|
Log.i(TAG, " load OK (${System.currentTimeMillis() - t0}ms)")
|
||||||
// warmup
|
|
||||||
try { eng.generate(SYS, "Bonjour", 16) } catch (_: Exception) {}
|
// warmup (1 tour court, hors mesure)
|
||||||
|
try { eng.generate("Bonjour", SamplingParams(maxNewTokens = 16)) { true } } catch (_: Throwable) {}
|
||||||
|
|
||||||
for ((i, p) in PROMPTS.withIndex()) {
|
for ((i, p) in PROMPTS.withIndex()) {
|
||||||
val ts = System.currentTimeMillis()
|
val ts = System.currentTimeMillis()
|
||||||
val out = try { eng.generate(SYS, p, 96) } catch (e: Exception) { "[ERR ${e.message}]" }
|
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
|
val ms = System.currentTimeMillis() - ts
|
||||||
val nWords = out.trim().split(Regex("\\s+")).size
|
Log.i(TAG, " [${i + 1}/${PROMPTS.size}] '$p' → ${ms}ms (${"%.1f".format(r.tokensPerSecond)} tok/s)")
|
||||||
val tps = if (ms > 0) nWords * 1000f / ms else 0f
|
Log.i(TAG, " « ${r.text} »")
|
||||||
Log.i(TAG, "[${i+1}/${PROMPTS.size}] '${p}' → ${ms}ms (~${"%.2f".format(tps)} mots/s) :")
|
|
||||||
Log.i(TAG, " '${out.trim()}'")
|
|
||||||
}
|
}
|
||||||
eng.release()
|
eng.release()
|
||||||
Log.i(TAG, "=== Bench end ===")
|
Log.i(TAG, "===== fin ${m.id} =====")
|
||||||
|
}
|
||||||
|
Log.i(TAG, "########## BENCH LLM end ##########")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ class UnifiedLlmAdapter(
|
||||||
private val onLog: ((String) -> Unit)? = null
|
private val onLog: ((String) -> Unit)? = null
|
||||||
) : com.kazeia.core.LlmEngine {
|
) : 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*<think>.*?</think>\s*""", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
}
|
||||||
|
|
||||||
private enum class Fmt { GGUF, PTE }
|
private enum class Fmt { GGUF, PTE }
|
||||||
@Volatile private var fmt: Fmt? = null
|
@Volatile private var fmt: Fmt? = null
|
||||||
private var gguf: EngineLlmEngine? = null
|
private var gguf: EngineLlmEngine? = null
|
||||||
|
|
@ -105,7 +110,12 @@ class UnifiedLlmAdapter(
|
||||||
}
|
}
|
||||||
Fmt.PTE -> {
|
Fmt.PTE -> {
|
||||||
val e = pte ?: error("UnifiedLlmAdapter .pte non chargé")
|
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 <think>…</think> (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 « <think> </think> » et la cascade parse faux).
|
||||||
|
out = THINK_BLOCK.replace(raw, "").trim()
|
||||||
onToken?.invoke(out) // streaming .pte = un bloc (génération bloquante, doc §10)
|
onToken?.invoke(out) // streaming .pte = un bloc (génération bloquante, doc §10)
|
||||||
stats = e.lastStats()
|
stats = e.lastStats()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue