Ministral-3-3B: Architektur mistral3 steht nicht in der Liste des rkllm-Toolkits, dazu FP8-Gewichte -- nie konvertiert. Config auf Llama umbiegen waere moeglich gewesen, aber ein falsch eingelesenes Modell haette plausiblen Unsinn produziert. Phi-4-mini: konvertiert (5,06 GB), laedt aber nicht auf der NPU. partial_rotary_factor 0.75 ergibt 48 LongRoPE-Faktoren, die Runtime erwartet starr head_dim/2 = 64. Rockchips "Phi3" meint Phi-3-mini. SmolLM3-3B: laeuft, in 2 von 3 Faellen schneller als Qwen3, scheitert aber inhaltlich -- 13 statt 61 Todos, JSON-Schluessel als Listeneintraege, Englisch-Drift im deutschen Text. rkllm_server.cpp: Chat-Template kommt jetzt aus der Umgebung (RKLLM_TMPL_SYSTEM / _USER_PRE / _USER_POST), Defaults = bisheriges ChatML-Verhalten, mit Qwen3 gegengeprueft. Damit ist der naechste Kandidat eine Drop-in-Zeile statt einer Neuuebersetzung. Doku: Changelog 2026-09-29 (4) mit Messwerten und allen Fallstricken, Kapitel 05 um Architektur-Vorpruefung und Template-Konfiguration erweitert. Messstrecke: Zeitlimit 45 -> 90 Minuten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017A91sWU5aWrqZ2DWNTLVph
138 lines
6.2 KiB
C++
138 lines
6.2 KiB
C++
// Persistent RKLLM inference server for the CallRec app.
|
|
// Loads a .rkllm model onto the RK3588 NPU ONCE, then answers prompts over a
|
|
// simple line protocol on stdin/stdout so Node can spawn it a single time:
|
|
// stdin : one request per line = base64(prompt) (base64 has no newlines)
|
|
// stdout: "READY" once after model load, then per request:
|
|
// "OK <base64(completion)>" on success
|
|
// "ERR <message>" on failure
|
|
// keep_history=0 → every request is independent (Node sends full context each time).
|
|
#include <cstddef> // rkllm.h uses size_t without including it itself
|
|
#include "rkllm.h"
|
|
#include <cstdlib> // getenv
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <iostream>
|
|
|
|
// Liest einen Umgebungswert und wandelt die Zwei-Zeichen-Folgen \n und \t in
|
|
// echte Steuerzeichen. systemd-Environment-Zeilen koennen keine Zeilenumbrueche
|
|
// enthalten, die Chat-Marker brauchen aber welche.
|
|
static std::string env_or(const char* name, const char* fallback) {
|
|
const char* v = getenv(name);
|
|
std::string s = (v && *v) ? v : fallback;
|
|
std::string out;
|
|
for (size_t i = 0; i < s.size(); ++i) {
|
|
if (s[i] == '\\' && i + 1 < s.size()) {
|
|
if (s[i+1] == 'n') { out += '\n'; ++i; continue; }
|
|
else if (s[i+1] == 't') { out += '\t'; ++i; continue; }
|
|
else if (s[i+1] == '\\') { out += '\\'; ++i; continue; }
|
|
}
|
|
out += s[i];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static std::string g_out;
|
|
static bool g_error = false;
|
|
|
|
static int cb(RKLLMResult* r, void* /*ud*/, LLMCallState st) {
|
|
if (st == RKLLM_RUN_NORMAL) { if (r && r->text) g_out += r->text; }
|
|
else if (st == RKLLM_RUN_ERROR) { g_error = true; }
|
|
return 0;
|
|
}
|
|
|
|
static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
static std::string b64enc(const std::string& in) {
|
|
std::string out; int val = 0, bits = -6;
|
|
for (unsigned char c : in) {
|
|
val = (val << 8) + c; bits += 8;
|
|
while (bits >= 0) { out.push_back(B64[(val >> bits) & 0x3F]); bits -= 6; }
|
|
}
|
|
if (bits > -6) out.push_back(B64[((val << 8) >> (bits + 8)) & 0x3F]);
|
|
while (out.size() % 4) out.push_back('=');
|
|
return out;
|
|
}
|
|
static std::string b64dec(const std::string& in) {
|
|
int T[256]; for (int i = 0; i < 256; i++) T[i] = -1;
|
|
for (int i = 0; i < 64; i++) T[(unsigned char)B64[i]] = i;
|
|
std::string out; int val = 0, bits = -8;
|
|
for (unsigned char c : in) {
|
|
if (T[c] == -1) continue;
|
|
val = (val << 6) + T[c]; bits += 6;
|
|
if (bits >= 0) { out.push_back((char)((val >> bits) & 0xFF)); bits -= 8; }
|
|
}
|
|
return out;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc < 2) { fprintf(stderr, "usage: %s model.rkllm\n", argv[0]); return 1; }
|
|
RKLLMParam p = rkllm_createDefaultParam();
|
|
p.model_path = argv[1];
|
|
p.max_context_len = 12288;
|
|
p.max_new_tokens = 1536;
|
|
// Gemma-3-4B (w8a8) degenerates into repetition loops ("X, X, X…") and echoes
|
|
// the prompt under pure-greedy decode → needs real sampling + strong repeat/
|
|
// frequency penalties. (Qwen was fine at top_k=1/1.1; Gemma is not.)
|
|
p.top_k = 20;
|
|
p.top_p = 0.9f;
|
|
p.temperature = 0.5f;
|
|
p.repeat_penalty = 1.3f;
|
|
p.frequency_penalty = 0.5f;
|
|
p.is_async = false; // rkllm_run blocks until finish → buffer complete on return
|
|
// Pin the runtime's CPU-side threads to the big A76 cores (4-7). The 1.2.1
|
|
// default mask targets the little A55 cores, which starves de/quant and
|
|
// sampling between NPU calls.
|
|
p.extend_param.enabled_cpus_num = 4;
|
|
p.extend_param.enabled_cpus_mask = (1u<<4)|(1u<<5)|(1u<<6)|(1u<<7);
|
|
p.skip_special_token = true;
|
|
|
|
LLMHandle h = nullptr;
|
|
RKLLMCallback cbs; memset(&cbs, 0, sizeof(cbs)); cbs.result_callback = cb;
|
|
if (rkllm_init(&h, &p, &cbs) != 0) { fprintf(stderr, "rkllm_init failed\n"); return 2; }
|
|
// Gemma-3 turn format. rkllm does NOT auto-apply the HF chat template, so
|
|
// without this the model gets the raw prompt with no turn markers and just
|
|
// rambles generic filler. prefix/postfix wrap each user prompt; <bos> is
|
|
// added by the runtime (add_bos_token=True at conversion).
|
|
// Das Chat-Template kommt aus der Umgebung, weil rkllm das HF-Template NICHT
|
|
// selbst anwendet: ohne passende Marker bekommt das Modell den nackten Prompt
|
|
// und faselt generisches Fuellmaterial. Defaults = ChatML (Qwen, Gemma3).
|
|
// Phi-4-mini und SmolLM3 setzen eigene Werte per Drop-in.
|
|
// RKLLM_SUPPRESS_THINK=1 haengt einen LEEREN Denkblock an: Denk-Modelle wie
|
|
// Qwen3.5 antworten sonst mit Gedankengang statt JSON, und
|
|
// RKLLMInput.enable_thinking allein genuegt dafuer nicht.
|
|
std::string sys_pre = env_or("RKLLM_TMPL_SYSTEM", "");
|
|
std::string user_pre = env_or("RKLLM_TMPL_USER_PRE", "<|im_start|>user\n");
|
|
std::string user_post = env_or("RKLLM_TMPL_USER_POST", "<|im_end|>\n<|im_start|>assistant\n");
|
|
const char* sup = getenv("RKLLM_SUPPRESS_THINK");
|
|
if (sup && *sup == '1') user_post += "<think>\n\n</think>\n\n";
|
|
fprintf(stderr, "chat template: sys=%zu user_pre=%zu user_post=%zu bytes\n",
|
|
sys_pre.size(), user_pre.size(), user_post.size());
|
|
rkllm_set_chat_template(h, sys_pre.c_str(), user_pre.c_str(), user_post.c_str());
|
|
printf("READY\n"); fflush(stdout);
|
|
fprintf(stderr, "rkllm model loaded\n"); fflush(stderr);
|
|
|
|
std::string line;
|
|
while (std::getline(std::cin, line)) {
|
|
if (line.empty()) continue;
|
|
std::string prompt = b64dec(line);
|
|
g_out.clear(); g_error = false;
|
|
|
|
RKLLMInput in; memset(&in, 0, sizeof(in));
|
|
in.role = "user";
|
|
in.enable_thinking = false;
|
|
in.input_type = RKLLM_INPUT_PROMPT;
|
|
in.prompt_input = prompt.c_str();
|
|
|
|
RKLLMInferParam ip; memset(&ip, 0, sizeof(ip));
|
|
ip.mode = RKLLM_INFER_GENERATE;
|
|
ip.keep_history = 0;
|
|
|
|
rkllm_clear_kv_cache(h, 0, nullptr, nullptr); // hard reset: no cross-recording KV bleed
|
|
int rc = rkllm_run(h, &in, &ip, nullptr);
|
|
if (rc != 0 || g_error) { printf("ERR run rc=%d\n", rc); fflush(stdout); continue; }
|
|
printf("OK %s\n", b64enc(g_out).c_str()); fflush(stdout);
|
|
}
|
|
rkllm_destroy(h);
|
|
return 0;
|
|
}
|