dist: JNI générique (auto-détecte archi) — tout LLM, sans dégrader la 3.5

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) <noreply@anthropic.com>
This commit is contained in:
Richard Loyer 2026-05-27 12:11:44 +02:00
parent 771d64a998
commit eeffbc350f
3 changed files with 60 additions and 38 deletions

9
dist/HANDOFF.md vendored
View File

@ -3,6 +3,15 @@
Point d'entrée unique. Remplace le LLM ExecuTorch/Genie par llama.cpp (fork ql) + Hexagon. 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é). 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 ## 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 - **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é par défaut → 2.38 GB, decode +5%, qualité ≈ Q4_0 mesurée). Choisi sur l'éval qualité

View File

@ -1,8 +1,9 @@
// kazeia_engine_jni.cpp — bridge LLM Kazeia-Engine (llama.cpp fork ql + Hexagon). // 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 -> // GÉNÉRIQUE: fait tourner N'IMPORTE QUEL LLM. Auto-détecte l'archi au load :
// decode CPU (ngl0, t4+fa, KV f16). prefill ~110-180 t/s (vs ~14 CPU), decode CPU ~7-10. 2 instances (~4.7GB). // - qwen35 / qwen3next (hybride GDN) -> OPTION C : prefill HTP (ngl99) -> transfert KV -> decode CPU.
// API: load -> generate(sys,usr) [mono-tour] OU generateRaw(prompt) [multi-tour, l'app/Kotlin formate] -> reset/free. // - tout le reste (qwen3 dense, etc.) -> CPU pur (universel, le prefill HTP dense crashe 0x2e).
// Sans état entre appels (l'app passe l'historique complet dans le prompt -> re-prefill HTP). STT reste ORT-QAIRT. // 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 <jni.h> #include <jni.h>
#include <cstdlib> #include <cstdlib>
#include <string> #include <string>
@ -12,8 +13,8 @@
#include "ggml-backend.h" #include "ggml-backend.h"
struct KEngine { struct KEngine {
llama_model* m_h; llama_context* c_h; // prefill HTP llama_model* m_h; llama_context* c_h; // prefill HTP (nullptr si CPU-only)
llama_model* m_c; llama_context* c_c; // decode CPU llama_model* m_c; llama_context* c_c; // decode/prefill CPU (toujours présent)
const llama_vocab* v; llama_sampler* s; 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); 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) { 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); int n = -llama_tokenize(k->v, p.c_str(), p.size(), nullptr, 0, true, true);
std::vector<llama_token> t(n); std::vector<llama_token> t(n);
llama_tokenize(k->v, p.c_str(), p.size(), t.data(), n, true, true); 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_context* pf = k->c_h ? k->c_h : k->c_c; // contexte de prefill
llama_memory_clear(llama_get_memory(k->c_h), true); llama_memory_clear(llama_get_memory(pf), true);
llama_batch b = llama_batch_get_one(t.data(), n); 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 if (k->c_h) { // mode HTP : transfert KV vers le contexte CPU
size_t sz = llama_state_seq_get_size(k->c_h, 0); size_t sz = llama_state_seq_get_size(k->c_h, 0);
std::vector<uint8_t> buf(sz); std::vector<uint8_t> buf(sz);
llama_state_seq_get_data(k->c_h, buf.data(), sz, 0); llama_state_seq_get_data(k->c_h, buf.data(), sz, 0);
llama_memory_clear(llama_get_memory(k->c_c), true); llama_memory_clear(llama_get_memory(k->c_c), true);
llama_state_seq_set_data(k->c_c, buf.data(), sz, 0); 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, pf, -1); // 1er token depuis les logits de prefill
llama_token id = llama_sampler_sample(k->s, k->c_h, -1);
std::string out; char zbuf[256]; int pos = n; std::string out; char zbuf[256]; int pos = n;
for (int i = 0; i < maxTok; ++i) { for (int i = 0; i < maxTok; ++i) {
if (llama_vocab_is_eog(k->v, id)) break; 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_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); 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 = &lg; sb.n_tokens = 1; sb.token = &tok; sb.pos = &pp; sb.n_seq_id = &ns; sb.seq_id = &spd; sb.logits = &lg;
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); pos++; id = llama_sampler_sample(k->s, k->c_c, -1);
} }
return out; return out;
@ -62,26 +64,35 @@ static std::string kengine_run(KEngine* k, const std::string& p, int maxTok) {
extern "C" JNIEXPORT jlong JNICALL extern "C" JNIEXPORT jlong JNICALL
Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx) { Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx) {
const char* p = e->GetStringUTFChars(path, 0); const char* p = e->GetStringUTFChars(path, 0);
// Posé AVANT l'init du backend hexagon. GDN prefill sur HTP. Le fallback CPU multi-token du setenv("GGML_HEXAGON_GDN_PREFILL", "1", 1); // sans effet sur les modèles sans GDN
// conv1d (SSM_CONV) est intégré au backend (ggml_hexagon_supported_ssm_conv), pas d'OPFILTER.
setenv("GGML_HEXAGON_GDN_PREFILL", "1", 1);
llama_backend_init(); llama_backend_init();
// 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);
if (!m_c) { e->ReleaseStringUTFChars(path, p); return 0; }
// 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 }; static ggml_backend_dev_t devs[2] = { nullptr, nullptr };
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) { for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
auto d = ggml_backend_dev_get(i); auto d = ggml_backend_dev_get(i);
if (!strcmp(ggml_backend_dev_name(d), "HTP0")) devs[0] = d; 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 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); m_h = llama_model_load_from_file(p, mp_h);
auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0; if (m_h) c_h = make_ctx(m_h, nctx, 8); // prefill t8
auto m_c = llama_model_load_from_file(p, mp_c); // si le chargement HTP échoue, on retombe proprement sur CPU-only (c_h = nullptr)
}
e->ReleaseStringUTFChars(path, p); e->ReleaseStringUTFChars(path, p);
if (!m_h || !m_c) return 0;
auto* k = new KEngine{ m_h, make_ctx(m_h, nctx, 8), // prefill t8 auto* k = new KEngine{ m_h, c_h, m_c, make_ctx(m_c, nctx, 4), // decode/prefill CPU t4
m_c, make_ctx(m_c, nctx, 4), // decode t4 llama_model_get_vocab(m_c), llama_sampler_init_greedy() };
llama_model_get_vocab(m_h), llama_sampler_init_greedy() };
return (jlong) k; 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()); 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 extern "C" JNIEXPORT jstring JNICALL
Java_com_kazeia_llm_EngineJni_generateRaw(JNIEnv* e, jobject, jlong h, jstring prompt, jint maxTok) { Java_com_kazeia_llm_EngineJni_generateRaw(JNIEnv* e, jobject, jlong h, jstring prompt, jint maxTok) {
auto* k = (KEngine*) h; 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 extern "C" JNIEXPORT void JNICALL
Java_com_kazeia_llm_EngineJni_reset(JNIEnv*, jobject, jlong h){ Java_com_kazeia_llm_EngineJni_reset(JNIEnv*, jobject, jlong h){
auto* k = (KEngine*) 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); llama_memory_clear(llama_get_memory(k->c_c), true);
} }
extern "C" JNIEXPORT void JNICALL extern "C" JNIEXPORT void JNICALL
Java_com_kazeia_llm_EngineJni_free(JNIEnv*, jobject, jlong h){ Java_com_kazeia_llm_EngineJni_free(JNIEnv*, jobject, jlong h){
auto* k = (KEngine*) h; auto* k = (KEngine*) h;
llama_sampler_free(k->s); llama_sampler_free(k->s);
llama_free(k->c_h); llama_free(k->c_c); if (k->c_h) llama_free(k->c_h);
llama_model_free(k->m_h); llama_model_free(k->m_c); llama_free(k->c_c);
if (k->m_h) llama_model_free(k->m_h);
llama_model_free(k->m_c);
delete k; delete k;
} }

Binary file not shown.