// kazeia_engine_jni.cpp — bridge LLM Kazeia-Engine (llama.cpp fork ql + Hexagon). // 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 #include #include #include "llama.h" #include "ggml-backend.h" struct KEngine { 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; }; static llama_context* make_ctx(llama_model* m, int nctx, int nthreads) { auto cp = llama_context_default_params(); cp.n_ctx = nctx; cp.n_batch = nctx; cp.n_threads = nthreads; cp.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED; // t4+fa optimum decode cp.type_k = GGML_TYPE_F16; cp.type_v = GGML_TYPE_F16; // KV f16 (q8_0 = -40% decode, mesuré) return llama_init_from_model(m, cp); } // 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); 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(pf, b) != 0) return std::string(); 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) 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; int l = llama_token_to_piece(k->v, id, zbuf, sizeof zbuf, 0, true); if (l > 0) out.append(zbuf, l); 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; // decode toujours sur CPU pos++; id = llama_sampler_sample(k->s, k->c_c, -1); } return out; } 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); setenv("GGML_HEXAGON_GDN_PREFILL", "1", 1); // sans effet sur les modèles sans GDN 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 }; 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; } // Mono-tour : construit le ChatML (system + 1 tour user) + thinking-off, puis infère. extern "C" JNIEXPORT jstring JNICALL Java_com_kazeia_llm_EngineJni_generate(JNIEnv* e, jobject, jlong h, jstring sys, jstring usr, jint maxTok) { auto* k = (KEngine*) h; const char* sp = e->GetStringUTFChars(sys, 0); const char* up = e->GetStringUTFChars(usr, 0); std::string p = "<|im_start|>system\n"; p += sp; p += "<|im_end|>\n<|im_start|>user\n"; p += up; p += "<|im_end|>\n<|im_start|>assistant\n\n\n\n\n"; e->ReleaseStringUTFChars(sys, sp); e->ReleaseStringUTFChars(usr, up); std::string out = kengine_run(k, p, maxTok); return e->NewStringUTF(out.c_str()); } // 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; const char* pp = e->GetStringUTFChars(prompt, 0); std::string out = kengine_run(k, std::string(pp), maxTok); e->ReleaseStringUTFChars(prompt, pp); return e->NewStringUTF(out.c_str()); } extern "C" JNIEXPORT void JNICALL Java_com_kazeia_llm_EngineJni_reset(JNIEnv*, jobject, jlong h){ auto* k = (KEngine*) h; 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); 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; }