Kazeia-engine/dist/jni/kazeia_engine_jni.cpp

177 lines
8.4 KiB
C++

// kazeia_engine_jni.cpp — bridge LLM Kazeia-Engine (llama.cpp fork ql + Hexagon).
// GÉNÉRIQUE : fait tourner N'IMPORTE QUEL LLM. Au load, détecte la STRUCTURE du modèle
// (llama_model_is_hybrid/is_recurrent) et route :
// - HYBRIDE GDN (qwen3.5 / qwen3next) -> OPTION C : prefill HTP -> transfert KV -> decode CPU
// (le decode GDN sur HTP est lent ; le split le garde sur CPU).
// - DENSE (qwen3, llama, ...) -> HTP contexte-unique : prefill + decode sur HTP
// (decode dense HTP ~= CPU, pas de pénalité GDN ; prefill ~98 vs 14 CPU ; pas de dual-load => pas de crash 0x2e).
// - pas de device HTP -> CPU pur (repli universel).
// API : load -> generate(sys,usr) | generateRaw(prompt) -> reset/free. Sans état entre appels.
#include <jni.h>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include <cstring>
#include "llama.h"
#include "ggml-backend.h"
#include "gguf.h"
// Lit general.architecture dans l'en-tête GGUF SANS init backend (pour décider USE_HMX avant).
static std::string read_arch(const char* path) {
struct gguf_init_params gp = { /*no_alloc=*/true, /*ctx=*/nullptr };
gguf_context* g = gguf_init_from_file(path, gp);
if (!g) return "";
int64_t kid = gguf_find_key(g, "general.architecture");
std::string arch = (kid >= 0) ? gguf_get_val_str(g, kid) : "";
gguf_free(g);
return arch;
}
struct KEngine {
llama_model* m_h; llama_context* c_h; // prefill HTP (nullptr si pas de HTP)
llama_model* m_c; llama_context* c_c; // decode CPU (nullptr si HTP contexte-unique)
const llama_vocab* v; llama_sampler* s;
};
static llama_context* make_ctx(llama_model* m, int nctx, int nthreads, enum llama_flash_attn_type fa) {
auto cp = llama_context_default_params();
cp.n_ctx = nctx; cp.n_batch = 2048; cp.n_threads = nthreads;
cp.flash_attn_type = fa; // ENABLED (decode CPU) / DISABLED (prefill HTP dense)
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);
}
static ggml_backend_dev_t find_htp() {
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")) return d;
}
return nullptr;
}
// Cœur : prefill (HTP si dispo) -> [transfert KV si dual-ctx] -> decode (CPU en option C, sinon HTP).
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<llama_token> 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_context* dec = k->c_c ? k->c_c : k->c_h; // contexte de decode
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 && k->c_c) { // option C : transfert KV HTP -> CPU
size_t sz = llama_state_seq_get_size(k->c_h, 0);
std::vector<uint8_t> 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);
}
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 = &lg;
if (llama_decode(dec, sb) != 0) break;
pos++; id = llama_sampler_sample(k->s, dec, -1);
}
return out;
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_kazeia_llm_EngineJni_load(JNIEnv* e, jobject, jstring path, jint nctx) {
const char* path_c = e->GetStringUTFChars(path, 0);
std::string p(path_c);
e->ReleaseStringUTFChars(path, path_c);
// Détection d'archi AVANT init backend (USE_HMX est latché à l'init).
std::string arch = read_arch(p.c_str());
const bool hybrid = (arch.find("qwen3next") != std::string::npos) || (arch == "qwen35");
// Le matmul HMX faute (0x2e) sur les activations réelles du DENSE (massive activations > plage fp16).
// -> HMX OFF pour le dense (matmul HVX, stable, ~3x CPU). L'hybride (3.5) garde HMX (prefill rapide, OK).
if (!hybrid) setenv("GGML_HEXAGON_USE_HMX", "0", 1);
setenv("GGML_HEXAGON_GDN_PREFILL", "1", 1); // sans effet sur les modèles sans GDN
llama_backend_init();
llama_model* m_h = nullptr; llama_context* c_h = nullptr;
llama_model* m_c = nullptr; llama_context* c_c = nullptr;
ggml_backend_dev_t htp = find_htp();
if (htp) {
ggml_backend_dev_t devs[2] = { htp, nullptr };
auto mp_h = llama_model_default_params(); mp_h.n_gpu_layers = 99; mp_h.devices = devs;
m_h = llama_model_load_from_file(p.c_str(), mp_h);
}
if (m_h && hybrid) {
// qwen3.5-like (GDN) -> OPTION C : prefill HTP (HMX) + instance CPU pour le decode (decode GDN HTP = lent).
c_h = make_ctx(m_h, nctx, 8, LLAMA_FLASH_ATTN_TYPE_ENABLED);
auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0;
m_c = llama_model_load_from_file(p.c_str(), mp_c);
if (m_c) c_c = make_ctx(m_c, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED);
fprintf(stderr, "kazeia-engine: HYBRIDE GDN (%s) -> option C, prefill HTP+HMX / decode CPU\n", arch.c_str());
} else if (m_h) {
// dense -> HTP contexte-unique, matmul HVX (HMX off) : prefill+decode HTP, stable, ~3x CPU.
c_h = make_ctx(m_h, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED);
fprintf(stderr, "kazeia-engine: DENSE (%s) -> HTP contexte-unique, matmul HVX (HMX off)\n", arch.c_str());
} else {
// pas de HTP (ou échec du load HTP) -> CPU pur universel.
auto mp_c = llama_model_default_params(); mp_c.n_gpu_layers = 0;
m_c = llama_model_load_from_file(p.c_str(), mp_c);
if (!m_c) return 0;
c_c = make_ctx(m_c, nctx, 4, LLAMA_FLASH_ATTN_TYPE_ENABLED);
fprintf(stderr, "kazeia-engine: CPU pur (%s, pas de HTP)\n", arch.c_str());
}
auto* k = new KEngine{ m_h, c_h, m_c, c_c,
llama_model_get_vocab(m_h ? m_h : 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<think>\n\n</think>\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);
if (k->c_c) 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);
if (k->c_c) llama_free(k->c_c);
if (k->m_h) llama_model_free(k->m_h);
if (k->m_c) llama_model_free(k->m_c);
delete k;
}