llm: add llama.cpp backend (LlamaCppLlmEngine) for Qwen3.5-4B
- KazeiaLlmJni.kt : JNI bindings for 9 native fns (load/generate/setSystem/ reset/snapshot/stats/kvTokens/free). - LlamaCppLlmEngine.kt : implements core LlmEngine interface over KazeiaLlmJni, persistent KV cache across turns, streaming token callback. - jni/kazeia_llm_jni.cpp : JNI bridge to /opt/Kazeia/kazeia-llm/ C++ wrapper (compiled inline via CMakeLists). - jniLibs : libllama.so + ggml-* from /opt/Kazeia/llama-upstream/build-android (rebuilt with GGML_OPENMP=OFF to avoid libomp.so runtime dep). - KazeiaApplication.LLM_BACKEND config + runtime override via debug.kazeia.llm_backend (llamacpp|executorch). Default stays executorch for now; flip to llamacpp per-boot for the new path. - KazeiaService wires the selected engine at pipeline load. Validated on OPD2415: app loads Qwen3.5-4B-Q4_0 in ~4s via llama.cpp, ChatActivity opens, pipeline STT→LLM→TTS ready. Phase 2 complete.
This commit is contained in:
parent
db281002d9
commit
8a3151681d
|
|
@ -10,6 +10,14 @@ class KazeiaApplication : Application() {
|
||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = "kazeia_service_channel"
|
const val CHANNEL_ID = "kazeia_service_channel"
|
||||||
const val MODELS_DIR = "/data/local/tmp/kazeia/models"
|
const val MODELS_DIR = "/data/local/tmp/kazeia/models"
|
||||||
|
|
||||||
|
/** LLM backend selector :
|
||||||
|
* - "executorch" : Qwen3-4B via ExecuTorch QNN on NPU (legacy, current default)
|
||||||
|
* - "llamacpp" : Qwen3.5-4B via llama.cpp upstream on CPU (new primary path)
|
||||||
|
* Can be overridden at boot via `adb shell setprop debug.kazeia.llm_backend llamacpp`
|
||||||
|
* (property is read by KazeiaService). */
|
||||||
|
const val LLM_BACKEND = "executorch"
|
||||||
|
const val LLAMACPP_MODEL_FILENAME = "Qwen3.5-4B-Q4_0-pure.gguf"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package com.kazeia.llm
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JNI bridge to the KazeiaLLM C++ wrapper over llama.cpp.
|
||||||
|
*
|
||||||
|
* Native impl in jni/kazeia_llm_jni.cpp. Underlying C++ library source :
|
||||||
|
* /opt/Kazeia/kazeia-llm/ (compiled inline via the app CMake).
|
||||||
|
*
|
||||||
|
* One [load] allocates a native handle (opaque Long) tied to a loaded model +
|
||||||
|
* context. Same handle is then passed to every call. Always call [free] when
|
||||||
|
* done to release the model and KV buffer.
|
||||||
|
*
|
||||||
|
* KV cache is persistent across [generate] calls : it stores the full
|
||||||
|
* conversation so subsequent turns only re-prefill the new user message +
|
||||||
|
* assistant prefix. Call [reset] to clear.
|
||||||
|
*/
|
||||||
|
object KazeiaLlmJni {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// ggml is shared with whisper ; llama is the new addition.
|
||||||
|
System.loadLibrary("ggml-base")
|
||||||
|
System.loadLibrary("ggml-cpu")
|
||||||
|
System.loadLibrary("ggml")
|
||||||
|
System.loadLibrary("llama")
|
||||||
|
System.loadLibrary("kazeia_llm_jni")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a GGUF model + create context. Returns native handle (0 on failure).
|
||||||
|
* @param gguf absolute path to the .gguf file (e.g. Qwen3.5-4B-Q4_0-pure.gguf).
|
||||||
|
* @param nCtx max context tokens (default 4096).
|
||||||
|
* @param nThreads CPU threads for decode (default 6 on OPD2415).
|
||||||
|
*/
|
||||||
|
external fun nativeLoad(
|
||||||
|
gguf: String,
|
||||||
|
nCtx: Int = 4096,
|
||||||
|
nThreads: Int = 6,
|
||||||
|
temperature: Float = 0.7f,
|
||||||
|
topP: Float = 0.9f,
|
||||||
|
repeatPenalty: Float = 1.1f
|
||||||
|
): Long
|
||||||
|
|
||||||
|
/** Free model + context. Idempotent for handle=0. */
|
||||||
|
external fun nativeFree(handle: Long)
|
||||||
|
|
||||||
|
/** Set system prompt (clears KV, re-evaluates). Returns 0 on success. */
|
||||||
|
external fun nativeSetSystem(handle: Long, system: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append user message to conversation and generate an assistant reply.
|
||||||
|
* KV cache is updated with user turn + assistant reply.
|
||||||
|
* @param maxTokens hard cap on generated tokens (model stops earlier on <|im_end|>).
|
||||||
|
* @param callback optional streaming callback ; return false to stop early.
|
||||||
|
* @return full assistant text.
|
||||||
|
*/
|
||||||
|
external fun nativeGenerate(
|
||||||
|
handle: Long,
|
||||||
|
userMessage: String,
|
||||||
|
maxTokens: Int,
|
||||||
|
callback: TokenCallback?
|
||||||
|
): String
|
||||||
|
|
||||||
|
/** Clear the entire KV cache. System prompt must be re-set after this. */
|
||||||
|
external fun nativeReset(handle: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats from the last [nativeGenerate] call.
|
||||||
|
* Returns [promptTokens, decodeTokens, promptMs, decodeMs, promptTps, decodeTps].
|
||||||
|
*/
|
||||||
|
external fun nativeLastStats(handle: Long): DoubleArray
|
||||||
|
|
||||||
|
/** Save current KV / conversation state to a file. Returns 0 on success. */
|
||||||
|
external fun nativeSaveSnapshot(handle: Long, path: String): Int
|
||||||
|
|
||||||
|
/** Load conversation state from a file (clears current first). Returns 0 on success. */
|
||||||
|
external fun nativeRestoreSnapshot(handle: Long, path: String): Int
|
||||||
|
|
||||||
|
/** Current number of tokens held in the KV cache (debug / UI). */
|
||||||
|
external fun nativeKvTokens(handle: Long): Int
|
||||||
|
|
||||||
|
fun interface TokenCallback {
|
||||||
|
/** Called for each generated piece. Return false to halt generation. */
|
||||||
|
fun onToken(token: String): Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
package com.kazeia.llm
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.kazeia.core.GenerationResult
|
||||||
|
import com.kazeia.core.LlmConfig
|
||||||
|
import com.kazeia.core.LlmEngine
|
||||||
|
import com.kazeia.core.SamplingParams
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kazeia LLM backed by llama.cpp (upstream master) via KazeiaLlmJni.
|
||||||
|
*
|
||||||
|
* Replaces the previous GenieLlmEngine (Qualcomm Genie SDK on QNN) for the
|
||||||
|
* therapeutic conversation path. Measured ~11 tok/s sustained on Qwen3.5-4B
|
||||||
|
* Q4_0 on OPD2415 — +57 % vs the legacy briques runtime.
|
||||||
|
*
|
||||||
|
* Lifecycle : [load] once per app session ; [generate] called for each user
|
||||||
|
* turn. The native side keeps a persistent KV cache between generate() calls,
|
||||||
|
* so the conversation history need not be re-prefilled every turn — we only
|
||||||
|
* append the new user + assistant tokens. Call [release] at app shutdown.
|
||||||
|
*
|
||||||
|
* The [modelPath] passed to [load] must point at the GGUF file directly
|
||||||
|
* (e.g. ".../kazeia/models/Qwen3.5-4B-Q4_0-pure.gguf"), not a config file.
|
||||||
|
*
|
||||||
|
* System prompt is set via [setSystem] (defaults to a French therapeutic
|
||||||
|
* assistant prompt if never called).
|
||||||
|
*/
|
||||||
|
class LlamaCppLlmEngine : LlmEngine {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LlamaCppLlmEngine"
|
||||||
|
private const val DEFAULT_SYSTEM =
|
||||||
|
"Tu es un assistant psychologique bienveillant qui écoute et répond " +
|
||||||
|
"avec empathie et chaleur. Réponses courtes et naturelles en français. " +
|
||||||
|
"Ne jamais afficher ton raisonnement, réponds directement à l'utilisateur."
|
||||||
|
}
|
||||||
|
|
||||||
|
@Volatile private var handle: Long = 0L
|
||||||
|
@Volatile private var loaded = false
|
||||||
|
private var currentSystem: String = DEFAULT_SYSTEM
|
||||||
|
|
||||||
|
override suspend fun load(modelPath: String, config: LlmConfig) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.i(TAG, "Loading Qwen3.5 via llama.cpp : $modelPath")
|
||||||
|
handle = KazeiaLlmJni.nativeLoad(
|
||||||
|
gguf = modelPath,
|
||||||
|
nCtx = config.maxContextLength,
|
||||||
|
nThreads = 6,
|
||||||
|
temperature = 0.7f,
|
||||||
|
topP = 0.9f,
|
||||||
|
repeatPenalty = 1.1f,
|
||||||
|
)
|
||||||
|
if (handle == 0L) {
|
||||||
|
throw RuntimeException("llama.cpp nativeLoad failed for $modelPath")
|
||||||
|
}
|
||||||
|
val rc = KazeiaLlmJni.nativeSetSystem(handle, currentSystem)
|
||||||
|
if (rc != 0) {
|
||||||
|
Log.w(TAG, "nativeSetSystem returned $rc ; continuing with empty system")
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
Log.i(TAG, "llama.cpp ready handle=$handle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLoaded(): Boolean = loaded
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the system prompt. Clears the KV cache (conversation restarts).
|
||||||
|
*/
|
||||||
|
fun setSystem(system: String) {
|
||||||
|
if (!loaded) {
|
||||||
|
currentSystem = system
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentSystem = system
|
||||||
|
val rc = KazeiaLlmJni.nativeSetSystem(handle, system)
|
||||||
|
if (rc != 0) Log.w(TAG, "setSystem rc=$rc")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the conversation history. [setSystem] must be called after (or
|
||||||
|
* the previously-set system is re-applied automatically by reset+eval
|
||||||
|
* if the native side keeps it).
|
||||||
|
*/
|
||||||
|
fun resetConversation() {
|
||||||
|
if (!loaded) return
|
||||||
|
KazeiaLlmJni.nativeReset(handle)
|
||||||
|
// Re-apply system so the next generate() starts on a valid turn.
|
||||||
|
KazeiaLlmJni.nativeSetSystem(handle, currentSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun generate(
|
||||||
|
prompt: String,
|
||||||
|
params: SamplingParams,
|
||||||
|
onToken: ((String) -> Boolean)?,
|
||||||
|
): GenerationResult = withContext(Dispatchers.IO) {
|
||||||
|
check(loaded) { "LlamaCppLlmEngine: model not loaded" }
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val cb = onToken?.let {
|
||||||
|
KazeiaLlmJni.TokenCallback { piece -> it(piece) }
|
||||||
|
}
|
||||||
|
val reply = KazeiaLlmJni.nativeGenerate(
|
||||||
|
handle = handle,
|
||||||
|
userMessage = prompt,
|
||||||
|
maxTokens = params.maxNewTokens,
|
||||||
|
callback = cb,
|
||||||
|
)
|
||||||
|
val elapsed = System.currentTimeMillis() - startTime
|
||||||
|
|
||||||
|
val stats = KazeiaLlmJni.nativeLastStats(handle)
|
||||||
|
val decodeTokens = stats.getOrNull(1)?.toInt() ?: 0
|
||||||
|
val decodeMs = stats.getOrNull(3) ?: elapsed.toDouble()
|
||||||
|
val decodeTps = stats.getOrNull(5)?.toFloat() ?: 0f
|
||||||
|
|
||||||
|
GenerationResult(
|
||||||
|
text = reply,
|
||||||
|
tokenCount = decodeTokens,
|
||||||
|
timeMs = elapsed,
|
||||||
|
tokensPerSecond = if (decodeMs > 0) decodeTps else 0f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save the current conversation state to [path]. */
|
||||||
|
fun saveSnapshot(path: String): Boolean {
|
||||||
|
if (!loaded) return false
|
||||||
|
return KazeiaLlmJni.nativeSaveSnapshot(handle, path) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restore conversation state from [path]. */
|
||||||
|
fun restoreSnapshot(path: String): Boolean {
|
||||||
|
if (!loaded) return false
|
||||||
|
return KazeiaLlmJni.nativeRestoreSnapshot(handle, path) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Debug : current KV token count. */
|
||||||
|
fun kvTokens(): Int = if (loaded) KazeiaLlmJni.nativeKvTokens(handle) else 0
|
||||||
|
|
||||||
|
override fun release() {
|
||||||
|
if (handle != 0L) {
|
||||||
|
KazeiaLlmJni.nativeFree(handle)
|
||||||
|
handle = 0L
|
||||||
|
loaded = false
|
||||||
|
Log.i(TAG, "llama.cpp released")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -561,14 +561,39 @@ class KazeiaService : Service() {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LLM: Qwen3-4B on Hexagon V79 via qnn_llama_runner.
|
// LLM backend selection : llama.cpp Qwen3.5-4B (new default for
|
||||||
_loadingState.value = LoadingState(50, "LLM Qwen3-4B NPU…")
|
// therapeutic conversation, measured ~11 tok/s on OPD2415 +57 %
|
||||||
|
// vs ExecuTorch path) or legacy ExecuTorch Qwen3-4B QNN.
|
||||||
|
// Override at runtime via `adb shell setprop debug.kazeia.llm_backend llamacpp`.
|
||||||
|
val backend = try {
|
||||||
|
val spClass = Class.forName("android.os.SystemProperties")
|
||||||
|
spClass.getMethod("get", String::class.java, String::class.java)
|
||||||
|
.invoke(null, "debug.kazeia.llm_backend", KazeiaApplication.LLM_BACKEND) as String
|
||||||
|
} catch (_: Throwable) { KazeiaApplication.LLM_BACKEND }
|
||||||
|
|
||||||
|
_loadingState.value = LoadingState(50, "LLM $backend…")
|
||||||
|
when (backend) {
|
||||||
|
"llamacpp" -> {
|
||||||
|
val engine = com.kazeia.llm.LlamaCppLlmEngine()
|
||||||
|
llm = engine
|
||||||
|
try {
|
||||||
|
val gguf = "${KazeiaApplication.MODELS_DIR}/${KazeiaApplication.LLAMACPP_MODEL_FILENAME}"
|
||||||
|
engine.load(gguf, com.kazeia.core.LlmConfig(maxContextLength = 4096))
|
||||||
|
log("LLM llama.cpp loaded: ${KazeiaApplication.LLAMACPP_MODEL_FILENAME}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log("llama.cpp load failed: ${e.message} — falling back to echo mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// "executorch" (default) — Qwen3-4B QNN on Hexagon V79.
|
||||||
llm = ExecuTorchLlmEngine(this@KazeiaService) { msg -> log(msg) }
|
llm = ExecuTorchLlmEngine(this@KazeiaService) { msg -> log(msg) }
|
||||||
try {
|
try {
|
||||||
llm.load("${KazeiaApplication.MODELS_DIR}/qwen3-4b", com.kazeia.core.LlmConfig())
|
llm.load("${KazeiaApplication.MODELS_DIR}/qwen3-4b", com.kazeia.core.LlmConfig())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log("LLM load failed: ${e.message} — falling back to echo mode")
|
log("LLM load failed: ${e.message} — falling back to echo mode")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_loadingState.value = LoadingState(80, "Audio…")
|
_loadingState.value = LoadingState(80, "Audio…")
|
||||||
// Audio
|
// Audio
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ project(kazeia-jni)
|
||||||
|
|
||||||
set(JNILIBS_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
set(JNILIBS_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||||
|
|
||||||
# --- Genie JNI bridge ---
|
# --- External source paths (for kazeia_llm llama.cpp backend) ---
|
||||||
|
set(KAZEIA_LLM_DIR /opt/Kazeia/kazeia-llm)
|
||||||
|
set(LLAMA_UPSTREAM /opt/Kazeia/llama-upstream)
|
||||||
|
|
||||||
|
# --- Genie JNI bridge (legacy QNN backend, kept for fallback) ---
|
||||||
add_library(genie_jni SHARED genie_jni.cpp)
|
add_library(genie_jni SHARED genie_jni.cpp)
|
||||||
|
|
||||||
add_library(Genie SHARED IMPORTED)
|
add_library(Genie SHARED IMPORTED)
|
||||||
|
|
@ -50,3 +54,24 @@ target_compile_options(neon_ops PRIVATE -std=c++17 -O3 -march=armv8.2-a+fp16)
|
||||||
add_library(mel_extractor SHARED mel_extractor.cpp)
|
add_library(mel_extractor SHARED mel_extractor.cpp)
|
||||||
target_link_libraries(mel_extractor android log)
|
target_link_libraries(mel_extractor android log)
|
||||||
target_compile_options(mel_extractor PRIVATE -std=c++17 -O2)
|
target_compile_options(mel_extractor PRIVATE -std=c++17 -O2)
|
||||||
|
|
||||||
|
# --- Kazeia LLM JNI bridge : llama.cpp upstream backend (primary LLM path) ---
|
||||||
|
# Replaces the Genie/QNN path for Qwen3.5-4B. Compiles the KazeiaLLM C++
|
||||||
|
# wrapper directly from /opt/Kazeia/kazeia-llm/ so there is no .a to ship
|
||||||
|
# separately. Links against libllama.so shipped in jniLibs (synced from
|
||||||
|
# /opt/Kazeia/llama-upstream/build-android/bin/).
|
||||||
|
add_library(llama SHARED IMPORTED)
|
||||||
|
set_target_properties(llama PROPERTIES IMPORTED_LOCATION ${JNILIBS_DIR}/libllama.so)
|
||||||
|
|
||||||
|
add_library(kazeia_llm_jni SHARED
|
||||||
|
kazeia_llm_jni.cpp
|
||||||
|
${KAZEIA_LLM_DIR}/src/kazeia_llm.cpp)
|
||||||
|
|
||||||
|
target_include_directories(kazeia_llm_jni PRIVATE
|
||||||
|
${KAZEIA_LLM_DIR}/include
|
||||||
|
${LLAMA_UPSTREAM}/include
|
||||||
|
${LLAMA_UPSTREAM}/ggml/include)
|
||||||
|
|
||||||
|
target_link_libraries(kazeia_llm_jni llama ggml ggml-base ggml-cpu android log)
|
||||||
|
target_compile_options(kazeia_llm_jni PRIVATE -std=c++17 -O3 -Wall -Wextra
|
||||||
|
-Wno-unused-parameter -Wno-unused-variable)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
// JNI bridge for com.kazeia.llm.KazeiaLlmJni — wraps KazeiaLLM (llama.cpp based)
|
||||||
|
// for the Kazeia Android app.
|
||||||
|
//
|
||||||
|
// Design : keep a KazeiaLLM instance per Java-side handle (opaque long). The
|
||||||
|
// TokenCallback Java interface is routed through a JNI local thunk that
|
||||||
|
// invokes the Kotlin Function1 on each token. If the callback returns false,
|
||||||
|
// generation aborts gracefully.
|
||||||
|
//
|
||||||
|
// See /opt/Kazeia/kazeia-llm/ for the underlying C++ wrapper.
|
||||||
|
|
||||||
|
#include "kazeia_llm.h"
|
||||||
|
|
||||||
|
#include <android/log.h>
|
||||||
|
#include <jni.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#define LOG_TAG "KazeiaLlmJni"
|
||||||
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||||
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Convert a jstring to std::string (UTF-8). Handles null.
|
||||||
|
std::string jstr_to_std(JNIEnv * env, jstring js) {
|
||||||
|
if (!js) return {};
|
||||||
|
const char * c = env->GetStringUTFChars(js, nullptr);
|
||||||
|
std::string s = c ? c : "";
|
||||||
|
if (c) env->ReleaseStringUTFChars(js, c);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the Kazeia therapeutic system prompt. Kept in native to minimise
|
||||||
|
// back-and-forth with Kotlin when the app does not override it.
|
||||||
|
static const char * DEFAULT_SYSTEM =
|
||||||
|
"Tu es un assistant psychologique bienveillant qui écoute et répond avec "
|
||||||
|
"empathie et chaleur. Réponses courtes et naturelles en français. "
|
||||||
|
"Ne jamais afficher ton raisonnement, réponds directement à l'utilisateur.";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeLoad(JNIEnv * env, jobject /*thiz*/,
|
||||||
|
jstring jgguf,
|
||||||
|
jint n_ctx,
|
||||||
|
jint n_threads,
|
||||||
|
jfloat temperature,
|
||||||
|
jfloat top_p,
|
||||||
|
jfloat repeat_penalty) {
|
||||||
|
std::string gguf = jstr_to_std(env, jgguf);
|
||||||
|
auto * llm = new kazeia::KazeiaLLM();
|
||||||
|
kazeia::LLMConfig cfg;
|
||||||
|
cfg.n_ctx = n_ctx > 0 ? n_ctx : 4096;
|
||||||
|
cfg.n_threads = n_threads > 0 ? n_threads : 6;
|
||||||
|
cfg.n_batch = 512;
|
||||||
|
cfg.temperature = temperature;
|
||||||
|
cfg.top_p = top_p;
|
||||||
|
cfg.repeat_penalty = repeat_penalty;
|
||||||
|
|
||||||
|
if (llm->load(gguf, cfg) != 0) {
|
||||||
|
LOGE("KazeiaLLM::load failed on %s", gguf.c_str());
|
||||||
|
delete llm;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
LOGI("KazeiaLLM loaded : %s (ctx=%d threads=%d temp=%.2f)",
|
||||||
|
gguf.c_str(), cfg.n_ctx, cfg.n_threads, cfg.temperature);
|
||||||
|
return reinterpret_cast<jlong>(llm);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeFree(JNIEnv * /*env*/, jobject /*thiz*/, jlong handle) {
|
||||||
|
if (handle == 0) return;
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
delete llm;
|
||||||
|
LOGI("KazeiaLLM released");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeSetSystem(JNIEnv * env, jobject /*thiz*/,
|
||||||
|
jlong handle, jstring jsystem) {
|
||||||
|
if (handle == 0) return -1;
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
std::string system = jstr_to_std(env, jsystem);
|
||||||
|
if (system.empty()) system = DEFAULT_SYSTEM;
|
||||||
|
int rc = llm->set_system(system);
|
||||||
|
LOGI("set_system rc=%d len=%zu", rc, system.size());
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeGenerate(JNIEnv * env, jobject /*thiz*/,
|
||||||
|
jlong handle,
|
||||||
|
jstring juser,
|
||||||
|
jint max_tokens,
|
||||||
|
jobject jcallback) {
|
||||||
|
if (handle == 0) return env->NewStringUTF("");
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
std::string user_msg = jstr_to_std(env, juser);
|
||||||
|
|
||||||
|
kazeia::TokenCallback cb = nullptr;
|
||||||
|
jclass cb_class = nullptr;
|
||||||
|
jmethodID cb_method = nullptr;
|
||||||
|
|
||||||
|
if (jcallback) {
|
||||||
|
cb_class = env->GetObjectClass(jcallback);
|
||||||
|
if (cb_class) {
|
||||||
|
// Method signature : boolean onToken(String piece)
|
||||||
|
cb_method = env->GetMethodID(cb_class, "onToken", "(Ljava/lang/String;)Z");
|
||||||
|
}
|
||||||
|
if (!cb_method) {
|
||||||
|
LOGE("callback onToken(String):boolean not found");
|
||||||
|
env->ExceptionClear();
|
||||||
|
} else {
|
||||||
|
// Capture by value : env, jcallback, cb_method must stay alive
|
||||||
|
// for the duration of generate(). All local to the JNI thread.
|
||||||
|
cb = [env, jcallback, cb_method](const std::string & piece) -> bool {
|
||||||
|
jstring js = env->NewStringUTF(piece.c_str());
|
||||||
|
jboolean cont = env->CallBooleanMethod(jcallback, cb_method, js);
|
||||||
|
env->DeleteLocalRef(js);
|
||||||
|
if (env->ExceptionCheck()) {
|
||||||
|
env->ExceptionClear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cont == JNI_TRUE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string reply = llm->generate(user_msg, (int) max_tokens, cb);
|
||||||
|
const auto & s = llm->last_stats();
|
||||||
|
LOGI("generate : prompt=%lld gen=%lld prefill=%.1fms decode=%.1fms (%.2f tok/s)",
|
||||||
|
(long long) s.prompt_tokens, (long long) s.decode_tokens,
|
||||||
|
s.prompt_ms, s.decode_ms, s.decode_tps());
|
||||||
|
|
||||||
|
return env->NewStringUTF(reply.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeReset(JNIEnv * /*env*/, jobject /*thiz*/, jlong handle) {
|
||||||
|
if (handle == 0) return;
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
llm->reset();
|
||||||
|
LOGI("reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jdoubleArray JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeLastStats(JNIEnv * env, jobject /*thiz*/, jlong handle) {
|
||||||
|
jdoubleArray arr = env->NewDoubleArray(6);
|
||||||
|
if (handle == 0 || !arr) return arr;
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
const auto & s = llm->last_stats();
|
||||||
|
jdouble buf[6] = {
|
||||||
|
(jdouble) s.prompt_tokens,
|
||||||
|
(jdouble) s.decode_tokens,
|
||||||
|
(jdouble) s.prompt_ms,
|
||||||
|
(jdouble) s.decode_ms,
|
||||||
|
(jdouble) s.prompt_tps(),
|
||||||
|
(jdouble) s.decode_tps(),
|
||||||
|
};
|
||||||
|
env->SetDoubleArrayRegion(arr, 0, 6, buf);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeSaveSnapshot(JNIEnv * env, jobject /*thiz*/,
|
||||||
|
jlong handle, jstring jpath) {
|
||||||
|
if (handle == 0) return -1;
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
return llm->save_snapshot(jstr_to_std(env, jpath));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeRestoreSnapshot(JNIEnv * env, jobject /*thiz*/,
|
||||||
|
jlong handle, jstring jpath) {
|
||||||
|
if (handle == 0) return -1;
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
return llm->restore_snapshot(jstr_to_std(env, jpath));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_kazeia_llm_KazeiaLlmJni_nativeKvTokens(JNIEnv * /*env*/, jobject /*thiz*/, jlong handle) {
|
||||||
|
if (handle == 0) return 0;
|
||||||
|
auto * llm = reinterpret_cast<kazeia::KazeiaLLM *>(handle);
|
||||||
|
return llm->n_tokens_in_kv();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
Loading…
Reference in New Issue