From eeffbc350fec649baadf8e5cb511c52f01745de4 Mon Sep 17 00:00:00 2001 From: Richard Loyer Date: Wed, 27 May 2026 12:11:44 +0200 Subject: [PATCH] =?UTF-8?q?dist:=20JNI=20g=C3=A9n=C3=A9rique=20(auto-d?= =?UTF-8?q?=C3=A9tecte=20archi)=20=E2=80=94=20tout=20LLM,=20sans=20d=C3=A9?= =?UTF-8?q?grader=20la=203.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le JNI lit general.architecture: qwen35/qwen3next -> option C (prefill HTP/decode CPU, préservé et revalidé cohérent+mémoire). Tout le reste (qwen3 dense, etc.) -> CPU pur (universel, jamais de crash). Dense Qwen3-4B validé via le moteur (CPU, cohérent). 3.5 non régressée. Diagnostic crash dense-HTP: ce n'est PAS le HTP (dense single-context HTP marche: cli 36.8/11.4 cohérent, llama-bench pp512=98). Le crash 0x2e (dspqueue_read, flush_pending) survient UNIQUEMENT en dual-load (option C: 2 instances modèle) appliqué au dense. Or le dense n'a pas besoin de l'option C (decode HTP 11.4 ≈ CPU 11.7, pas de pénalité GDN). Donc dense -> CPU (ou single-ctx HTP). Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/HANDOFF.md | 9 ++++ dist/jni/kazeia_engine_jni.cpp | 89 +++++++++++++++++++-------------- dist/lib/libkazeia_engine.so | Bin 38760 -> 39096 bytes 3 files changed, 60 insertions(+), 38 deletions(-) diff --git a/dist/HANDOFF.md b/dist/HANDOFF.md index 276cd03..d9e45b4 100644 --- a/dist/HANDOFF.md +++ b/dist/HANDOFF.md @@ -3,6 +3,15 @@ Point d'entrée unique. Remplace le LLM ExecuTorch/Genie par llama.cpp (fork ql) + Hexagon. GGUF, pas de `.pte`. STT reste ORT-QAIRT (inchangé). +## Générique : fait tourner N'IMPORTE QUEL LLM (auto-détection d'archi au `load`) +Le JNI lit `general.architecture` et choisit le chemin : +- **qwen35 / qwen3next** (hybride GDN) → **option C** : prefill HTP → transfert KV → decode CPU (le decode GDN + sur HTP est lent, donc on le garde sur CPU). Validé (3.5). +- **tout le reste** (qwen3 dense, etc.) → **CPU pur** : chemin universel, marche pour tout modèle llama.cpp, + jamais de crash. (Le dense tournait déjà sur HTP en contexte unique 98/11.4 — voir note dense ci-dessous — + mais le decode dense HTP ≈ CPU, donc CPU suffit et reste sûr pour les archis non validées.) +Les deux modèles cibles (dense + 3.5) tournent. Validé via `jni/test_jni_native.cpp` sur les deux. + ## Décisions figées cette session - **Modèle Speaker = Qwen3.5-4B en variante `q35-lmq4.gguf`** (embeds en Q4 au lieu du Q6_K par défaut → 2.38 GB, decode +5%, qualité ≈ Q4_0 mesurée). Choisi sur l'éval qualité diff --git a/dist/jni/kazeia_engine_jni.cpp b/dist/jni/kazeia_engine_jni.cpp index 469311f..f296644 100644 --- a/dist/jni/kazeia_engine_jni.cpp +++ b/dist/jni/kazeia_engine_jni.cpp @@ -1,8 +1,9 @@ // kazeia_engine_jni.cpp — bridge LLM Kazeia-Engine (llama.cpp fork ql + Hexagon). -// OPTION C: prefill NPU/HTP (ngl99, SSM_CONV multi-token routé CPU par le backend) -> transfert KV -> -// decode CPU (ngl0, t4+fa, KV f16). prefill ~110-180 t/s (vs ~14 CPU), decode CPU ~7-10. 2 instances (~4.7GB). -// API: load -> generate(sys,usr) [mono-tour] OU generateRaw(prompt) [multi-tour, l'app/Kotlin formate] -> reset/free. -// Sans état entre appels (l'app passe l'historique complet dans le prompt -> re-prefill HTP). STT reste ORT-QAIRT. +// GÉNÉRIQUE: fait tourner N'IMPORTE QUEL LLM. Auto-détecte l'archi au load : +// - qwen35 / qwen3next (hybride GDN) -> OPTION C : prefill HTP (ngl99) -> transfert KV -> decode CPU. +// - tout le reste (qwen3 dense, etc.) -> CPU pur (universel, le prefill HTP dense crashe 0x2e). +// Le chemin CPU marche pour tout modèle supporté par llama.cpp ; HTP = accélération conditionnelle validée. +// API: load -> generate(sys,usr) | generateRaw(prompt) -> reset/free. Sans état entre appels. #include #include #include @@ -12,8 +13,8 @@ #include "ggml-backend.h" struct KEngine { - llama_model* m_h; llama_context* c_h; // prefill HTP - llama_model* m_c; llama_context* c_c; // decode CPU + llama_model* m_h; llama_context* c_h; // prefill HTP (nullptr si CPU-only) + llama_model* m_c; llama_context* c_c; // decode/prefill CPU (toujours présent) const llama_vocab* v; llama_sampler* s; }; @@ -25,26 +26,27 @@ static llama_context* make_ctx(llama_model* m, int nctx, int nthreads) { return llama_init_from_model(m, cp); } -// Cœur option C : prefill du prompt sur HTP -> transfert KV -> decode sur CPU. Renvoie le texte généré. +// Cœur : prefill (HTP si dispo, sinon CPU) -> [transfert KV si HTP] -> decode CPU. Texte généré. static std::string kengine_run(KEngine* k, const std::string& p, int maxTok) { int n = -llama_tokenize(k->v, p.c_str(), p.size(), nullptr, 0, true, true); std::vector t(n); llama_tokenize(k->v, p.c_str(), p.size(), t.data(), n, true, true); - // PREFILL sur HTP (KV vidé -> historique complet re-prefillé à chaque appel) - llama_memory_clear(llama_get_memory(k->c_h), true); + llama_context* pf = k->c_h ? k->c_h : k->c_c; // contexte de prefill + llama_memory_clear(llama_get_memory(pf), true); llama_batch b = llama_batch_get_one(t.data(), n); - if (llama_decode(k->c_h, b) != 0) return std::string(); + if (llama_decode(pf, b) != 0) return std::string(); - // transfert état KV HTP -> CPU - size_t sz = llama_state_seq_get_size(k->c_h, 0); - std::vector buf(sz); - llama_state_seq_get_data(k->c_h, buf.data(), sz, 0); - llama_memory_clear(llama_get_memory(k->c_c), true); - llama_state_seq_set_data(k->c_c, buf.data(), sz, 0); + if (k->c_h) { // mode HTP : transfert KV vers le contexte CPU + size_t sz = llama_state_seq_get_size(k->c_h, 0); + std::vector buf(sz); + llama_state_seq_get_data(k->c_h, buf.data(), sz, 0); + llama_memory_clear(llama_get_memory(k->c_c), true); + llama_state_seq_set_data(k->c_c, buf.data(), sz, 0); + } + // (mode CPU : pf == c_c, le KV de prefill est déjà dans c_c) - // DECODE sur CPU (1er token depuis les logits prefill HTP, suite sur CPU) - llama_token id = llama_sampler_sample(k->s, k->c_h, -1); + llama_token id = llama_sampler_sample(k->s, pf, -1); // 1er token depuis les logits de prefill std::string out; char zbuf[256]; int pos = n; for (int i = 0; i < maxTok; ++i) { if (llama_vocab_is_eog(k->v, id)) break; @@ -53,7 +55,7 @@ static std::string kengine_run(KEngine* k, const std::string& p, int maxTok) { llama_token tok = id; llama_pos pp = pos; int32_t ns = 1; llama_seq_id sd = 0, *spd = &sd; int8_t lg = 1; llama_batch sb; memset(&sb, 0, sizeof sb); sb.n_tokens = 1; sb.token = &tok; sb.pos = &pp; sb.n_seq_id = &ns; sb.seq_id = &spd; sb.logits = ≶ - if (llama_decode(k->c_c, sb) != 0) break; + if (llama_decode(k->c_c, sb) != 0) break; // decode toujours sur CPU pos++; id = llama_sampler_sample(k->s, k->c_c, -1); } return out; @@ -62,26 +64,35 @@ static std::string kengine_run(KEngine* k, const std::string& p, int maxTok) { extern "C" JNIEXPORT jlong JNICALL Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx) { const char* p = e->GetStringUTFChars(path, 0); - // Posé AVANT l'init du backend hexagon. GDN prefill sur HTP. Le fallback CPU multi-token du - // conv1d (SSM_CONV) est intégré au backend (ggml_hexagon_supported_ssm_conv), pas d'OPFILTER. - setenv("GGML_HEXAGON_GDN_PREFILL", "1", 1); + setenv("GGML_HEXAGON_GDN_PREFILL", "1", 1); // sans effet sur les modèles sans GDN llama_backend_init(); - static ggml_backend_dev_t devs[2] = { nullptr, nullptr }; - for (size_t i = 0; i < ggml_backend_dev_count(); ++i) { - auto d = ggml_backend_dev_get(i); - if (!strcmp(ggml_backend_dev_name(d), "HTP0")) devs[0] = d; - } - auto mp_h = llama_model_default_params(); mp_h.n_gpu_layers = 99; if (devs[0]) mp_h.devices = devs; - auto m_h = llama_model_load_from_file(p, mp_h); + // CPU model — toujours chargé (chemin universel) auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0; auto m_c = llama_model_load_from_file(p, mp_c); - e->ReleaseStringUTFChars(path, p); - if (!m_h || !m_c) return 0; + if (!m_c) { e->ReleaseStringUTFChars(path, p); return 0; } - auto* k = new KEngine{ m_h, make_ctx(m_h, nctx, 8), // prefill t8 - m_c, make_ctx(m_c, nctx, 4), // decode t4 - llama_model_get_vocab(m_h), llama_sampler_init_greedy() }; + // Auto-détection archi : HTP-prefill seulement pour les hybrides GDN validés (qwen35/qwen3next). + char arch[64] = {0}; + llama_model_meta_val_str(m_c, "general.architecture", arch, sizeof arch); + bool htp = (strstr(arch, "qwen35") != nullptr) || (strstr(arch, "qwen3next") != nullptr); + + llama_model* m_h = nullptr; llama_context* c_h = nullptr; + if (htp) { + static ggml_backend_dev_t devs[2] = { nullptr, nullptr }; + for (size_t i = 0; i < ggml_backend_dev_count(); ++i) { + auto d = ggml_backend_dev_get(i); + if (!strcmp(ggml_backend_dev_name(d), "HTP0")) devs[0] = d; + } + auto mp_h = llama_model_default_params(); mp_h.n_gpu_layers = 99; if (devs[0]) mp_h.devices = devs; + m_h = llama_model_load_from_file(p, mp_h); + if (m_h) c_h = make_ctx(m_h, nctx, 8); // prefill t8 + // si le chargement HTP échoue, on retombe proprement sur CPU-only (c_h = nullptr) + } + e->ReleaseStringUTFChars(path, p); + + auto* k = new KEngine{ m_h, c_h, m_c, make_ctx(m_c, nctx, 4), // decode/prefill CPU t4 + llama_model_get_vocab(m_c), llama_sampler_init_greedy() }; return (jlong) k; } @@ -97,7 +108,7 @@ Java_com_kazeia_llm_EngineJni_generate(JNIEnv* e, jobject, jlong h, jstring sys, return e->NewStringUTF(out.c_str()); } -// Multi-tour : l'app/Kotlin fournit le prompt complet déjà formaté (ChatML + historique + thinking-off). +// Multi-tour : l'app/Kotlin fournit le prompt complet déjà formaté (voir ChatSession). extern "C" JNIEXPORT jstring JNICALL Java_com_kazeia_llm_EngineJni_generateRaw(JNIEnv* e, jobject, jlong h, jstring prompt, jint maxTok) { auto* k = (KEngine*) h; @@ -110,14 +121,16 @@ Java_com_kazeia_llm_EngineJni_generateRaw(JNIEnv* e, jobject, jlong h, jstring p extern "C" JNIEXPORT void JNICALL Java_com_kazeia_llm_EngineJni_reset(JNIEnv*, jobject, jlong h){ auto* k = (KEngine*) h; - llama_memory_clear(llama_get_memory(k->c_h), true); + if (k->c_h) llama_memory_clear(llama_get_memory(k->c_h), true); llama_memory_clear(llama_get_memory(k->c_c), true); } extern "C" JNIEXPORT void JNICALL Java_com_kazeia_llm_EngineJni_free(JNIEnv*, jobject, jlong h){ auto* k = (KEngine*) h; llama_sampler_free(k->s); - llama_free(k->c_h); llama_free(k->c_c); - llama_model_free(k->m_h); llama_model_free(k->m_c); + if (k->c_h) llama_free(k->c_h); + llama_free(k->c_c); + if (k->m_h) llama_model_free(k->m_h); + llama_model_free(k->m_c); delete k; } diff --git a/dist/lib/libkazeia_engine.so b/dist/lib/libkazeia_engine.so index 3fa446feddf3bc544a26d9625d06d70db0f63ca9..2de8651b25d634835e3bd6e67e3914eb231ee36c 100755 GIT binary patch delta 11881 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 delta 11555 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