diff --git a/app/lab/ab-llm.mjs b/app/lab/ab-llm.mjs index 96407b2..5fab3c7 100644 --- a/app/lab/ab-llm.mjs +++ b/app/lab/ab-llm.mjs @@ -22,7 +22,7 @@ import { pool } from '../db.js'; const OUT = '/opt/callrec/lab/ab-llm'; const API = 'http://127.0.0.1'; const POLL_MS = 5000; -const TIMEOUT_MS = 45 * 60 * 1000; // eine Analyse darf lange dauern (Whole-Pass) +const TIMEOUT_MS = 90 * 60 * 1000; // 45 min war zu knapp: #114 brauchte 60,8 min fs.mkdirSync(OUT, { recursive: true }); diff --git a/app/rkllm_server.cpp b/app/rkllm_server.cpp index 9b28727..825e57f 100644 --- a/app/rkllm_server.cpp +++ b/app/rkllm_server.cpp @@ -14,6 +14,24 @@ #include #include +// 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; @@ -75,17 +93,21 @@ int main(int argc, char** argv) { // without this the model gets the raw prompt with no turn markers and just // rambles generic filler. prefix/postfix wrap each user prompt; is // added by the runtime (add_bos_token=True at conversion). - // Qwen3.5 denkt per Default und ignoriert dabei die JSON-Vorgabe -- es - // antwortet mit Gedankengang ("Wait, looking at line ..."), und der - // JSON-Parser in llm.js sieht nichts Brauchbares. RKLLMInput.enable_thinking - // allein schaltet das NICHT ab; das Modell-Template tut es, indem es den - // Assistenten-Zug mit einem LEEREN Denkblock vorbefuellt. Genau den haengen - // wir hier an, wenn RKLLM_SUPPRESS_THINK=1 gesetzt ist (Qwen3, Gemma3 und - // Qwen2.5 brauchen es nicht und bekommen unveraendert das alte Template). - std::string assist = "<|im_end|>\n<|im_start|>assistant\n"; + // 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') assist += "\n\n\n\n"; - rkllm_set_chat_template(h, "", "<|im_start|>user\n", assist.c_str()); + if (sup && *sup == '1') user_post += "\n\n\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); diff --git a/docs/05-llm-analyse.md b/docs/05-llm-analyse.md index 417fdc3..4559fa0 100644 --- a/docs/05-llm-analyse.md +++ b/docs/05-llm-analyse.md @@ -391,6 +391,29 @@ flowchart LR Die Konvertierung eines neuen Modells passiert **nicht** auf dem Board (kein CUDA), sondern auf dem PC mit `tools/rkllm-qwen35/` — rkllm-toolkit-Version muss exakt zur Runtime auf dem Board passen. +### Bevor ein Kandidat geladen wird: Architektur prüfen + +Das rkllm-Toolkit 1.3.0 kennt nur diese Architekturen (aus `loader.so`/`common.so` extrahiert): + +``` +Baichuan, ChatGLM, Deepseek, DeepseekOCR, Gemma/2/3/3n/4, InternLM2, Llama, +MiniCPM/3, Phi/Phi3, Qwen2/2VL/2.5VL/3/3VL/3.5, RWKV7, SmolLM3, TeleChat/2 +``` + +`curl -sL https://huggingface.co//raw/main/config.json` und `architectures` dagegenhalten — +Sekunden statt Gigabyte. **Gelistet heißt aber nicht lauffähig:** Phi-4-mini trägt `Phi3ForCausalLM`, +scheitert aber an der Geometrie (partielle Rotary Embeddings, 48 statt 64 LongRoPE-Faktoren), weil +Rockchips Phi3-Pfad auf Phi-3-mini zugeschnitten ist. Ebenso zu prüfen: `quantization_config` — +FP8-Gewichte (z. B. Ministral 3) kann das Toolkit nicht lesen. + +### Chat-Template aus der Umgebung + +rkllm wendet das HF-Template **nicht** selbst an. Ohne passende Marker bekommt das Modell den nackten +Prompt und faselt generisches Füllmaterial. Seit 2026-09-29 kommen die Marker aus der Umgebung: +`RKLLM_TMPL_SYSTEM`, `RKLLM_TMPL_USER_PRE`, `RKLLM_TMPL_USER_POST` (Defaults = ChatML, also unverändert +für Qwen und Gemma3). `\n`/`\t` werden im Server dekodiert, weil systemd-`Environment=` keine echten +Umbrüche kann — und aus demselben Grund beim Schreiben des Drop-ins **zitiertes Heredoc statt `printf`**. + ### Denk-Modelle (Qwen3.5 und jünger) Modelle mit Denk-Modus liefern sonst Gedankengang statt JSON. `RKLLMInput.enable_thinking = false` genügt @@ -405,3 +428,9 @@ langsamer**, Zusammenfassungen auf ein Drittel bis Viertel gekürzt, und von 61 hält die JSON-Vorgabe nicht durch (13 Parse-Fehler mit Wiederholungen). Die Prosa selbst war nicht schlechter. **Qwen3-4B bleibt produktiv.** Lehre: bei diesem Aufbau entscheidet nicht der Benchmark, sondern ob ein Modell über lange deutsche Transkripte hinweg sauberes JSON liefert. + +Am selben Tag zusätzlich geprüft: **Ministral-3-3B** (Architektur `mistral3` nicht unterstützt, dazu +FP8-Gewichte — nie konvertiert), **Phi-4-mini** (konvertiert, aber `rkllm_init failed` wegen partieller +Rotary Embeddings) und **SmolLM3-3B** (läuft, stellenweise schneller als Qwen3, scheitert aber inhaltlich: +13 statt 61 Todos, JSON-Schlüssel als Listeneinträge, Englisch-Drift). Drei Kandidaten, drei Absagen — +Details im Changelog 2026-09-29 (4). diff --git a/docs/08-changelog.md b/docs/08-changelog.md index 7da06c7..bf52ce6 100644 --- a/docs/08-changelog.md +++ b/docs/08-changelog.md @@ -3,6 +3,120 @@ Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Was, Warum, betroffene Dateien/Backups). +## 2026-09-29 (4) — Zwei weitere Kandidaten geprüft: Ministral, Phi-4-mini, SmolLM3 + +Nach dem Qwen3.5-Ergebnis systematisch auf Hugging Face gesucht. Drei Kandidaten, **alle drei gescheitert** +— zwei davon, bevor überhaupt gemessen werden konnte. + +### Vorab: welche Architekturen das Toolkit überhaupt kennt + +Aus `rkllm_toolkit-1.3.0` (`loader.so`/`common.so`) extrahiert — **vor** dem nächsten Download prüfen: + +``` +Baichuan, ChatGLM, Deepseek, DeepseekOCR, Gemma/2/3/3n/4, InternLM2, Llama, +MiniCPM/3, Phi/Phi3, Qwen2/2VL/2.5VL/3/3VL/3.5, RWKV7, SmolLM3, TeleChat/2 +``` + +Prüfbefehl: `architectures` aus der `config.json` des HF-Repos gegen diese Liste halten +(`curl -sL https://huggingface.co//raw/main/config.json`). Das kostet Sekunden statt Gigabyte. + +### Ministral-3-3B-Instruct-2512 — gar nicht erst konvertierbar + +`Mistral3ForConditionalGeneration` / `model_type: mistral3` steht **nicht** in der Liste; der einzige +Mistral-Treffer im Toolkit ist ein Tokenizer-Detail namens `fix_mistral_regex`. Dazu sind die +veröffentlichten Gewichte **FP8-quantisiert** (`quant_method: fp8`), das Toolkit erwartet bf16/fp16. +Die `config.json` auf `LlamaForCausalLM` umzubiegen wäre möglich gewesen — bewusst **nicht** gemacht: +Mistral 3 hat Sliding-Window-Attention und einen multimodalen Wrapper, ein falsch eingelesenes Modell +hätte plausiblen Unsinn produziert und den A/B wertlos gemacht. + +### Phi-4-mini-instruct — konvertiert, lädt aber nicht auf der NPU + +Konvertierung gelang erst im dritten Anlauf (5,06 GB, 244 s): +1. `cannot import name 'LossKwargs' from 'transformers.utils'` — der mitgelieferte `modeling_phi3.py` ist + gegen ein älteres transformers geschrieben, das Toolkit verlangt 5.8.0. +2. `auto_map` auf die native Implementierung umgebogen → `Not found AutoModelForCausalLM in the auto_map`. +3. `auto_map` **komplett** entfernt (nur `architectures: Phi3ForCausalLM` bleibt) → Konvertierung sauber. + +Auf dem Board dann: + +``` +E rkllm: Tensor rope_factors_long.weight has wrong shape, expected 64, got 48 +rkllm_init failed +``` + +**Ursache:** Phi-4-mini nutzt **partielle** Rotary Embeddings (`partial_rotary_factor: 0.75`). Bei +`head_dim` 128 werden nur 96 Dimensionen rotiert → **48** LongRoPE-Faktoren. Das Modell ist stimmig; +die rkllm-Runtime rechnet in ihrem Phi3-Pfad starr mit `head_dim/2 = 64` und kennt nur die Geometrie von +Phi-3-mini. **Rockchips „Phi3" meint Phi-3, nicht Phi-4-mini.** Die LongRoPE-Skalierung auszubauen hätte +den Kontext auf die Basislänge zurückgeworfen — wir brauchen 12288. Abgebrochen. + +### SmolLM3-3B — gemessen und verloren + +Konvertierung sauber (3,74 GB, 181 s; Warnung „pad token has two ids 128012/128004" stammt aus dem Modell, +ohne Folgen für Einzelgenerierung). Template: ChatML **plus** `/no_think` in der Systemzeile **plus** +leerer Denkblock — SmolLM3 denkt sonst per Default. + +| Aufnahme | Zeichen | Qwen3-4B | SmolLM3-3B | Tempo | Summary | Todos | +|---|---|---|---|---|---|---| +| #86 | 8 390 | 10,5 min | **8,8 min** | −17 % | 2 896 → 1 167 | 6 → 2 | +| #123 | 19 275 | 24,5 min | 27,6 min | +13 % | 6 071 → 1 988 | 18 → 5 | +| #114 | 34 843 | 41,8 min | **38,1 min** | −9 % | 9 288 → 2 386 | 37 → 6 | + +**SmolLM3 ist stellenweise schneller** (2 von 3 Fällen) — erwartbar bei 3B gegen 4B. Es scheitert am Inhalt: + +- **Todos strukturell kaputt:** `["task","Melanie trifft sich …","user","Philipp","status"]` — das Modell + schreibt die JSON-**Schlüssel** als Listeneinträge. Über alle drei Gespräche 13 statt 61 Einträge, und + die vorhandenen sind teils Schrott. +- **Englisch-Drift:** „Their Plan ist nun: am 28., Freitag …" mitten im deutschen Fließtext. +- **Platzhalter-Artefakte:** `[...]` als Auslassungszeichen im Ergebnistext. +- Zeilenumbrüche landen als literales `\n` bzw. `\\n` im Text (wie schon bei Qwen3.5). + +### Fazit nach drei Kandidaten an einem Tag + +**Qwen3-4B-Instruct-2507 bleibt, und der Abstand ist größer als gedacht.** Die eigentliche Erkenntnis ist +nicht „Modell X ist schlechter", sondern: **die Menge der Modelle, die auf dieser NPU überhaupt laufen, ist +deutlich kleiner als Rockchips Unterstützungsliste suggeriert** — eine Architektur kann gelistet sein und +trotzdem an der Geometrie scheitern (Phi-4-mini). Und von denen, die laufen, scheitert bisher jedes an +derselben Stelle: **sauberes JSON über lange deutsche Transkripte hinweg**, nicht am Sprachverständnis. + +### Code-Änderung, die bleibt: Chat-Template aus der Umgebung + +`rkllm_server.cpp` liest das Template jetzt aus der Umgebung, weil rkllm das HF-Template **nicht** selbst +anwendet (ohne passende Marker faselt das Modell generisches Füllmaterial): + +| Variable | Default (= bisheriges Verhalten) | +|---|---| +| `RKLLM_TMPL_SYSTEM` | leer | +| `RKLLM_TMPL_USER_PRE` | `<\|im_start\|>user\n` | +| `RKLLM_TMPL_USER_POST` | `<\|im_end\|>\n<\|im_start\|>assistant\n` | +| `RKLLM_SUPPRESS_THINK` | aus; `1` hängt `\n\n\n\n` an USER_POST | + +`\n` und `\t` werden im Server dekodiert, weil systemd-`Environment=`-Zeilen keine echten Umbrüche +enthalten können. Ohne die Variablen ist das Verhalten unverändert — mit Qwen3 gegengeprüft (97,9 s, +`chat template: sys=0 user_pre=17 user_post=33 bytes`). Backups `rkllm_server{,.cpp}.bak-tmplenv`. +Der nächste Kandidat ist damit eine Drop-in-Zeile statt einer Neuübersetzung. + +**GOTCHA:** `printf` mit `\n` schreibt **echte** Zeilenumbrüche in die Unit-Datei und zerlegt die +`Environment=`-Zeile. Zitiertes Heredoc nehmen, damit das literale `\n` stehen bleibt: + +```bash +cat > /etc/systemd/system/callrec.service.d/gemma-model.conf <<'EOF' +[Service] +Environment="RKLLM_TMPL_SYSTEM=<|im_start|>system\n/no_think<|im_end|>\n" +EOF +``` + +### Aufgeräumt + +Beide Testmodelle vom Board gelöscht, Drop-in zurück auf Qwen3, Analysen von #86/#114/#123 per `restore` +wiederhergestellt (19,4 min/7 Todos, 58,5 min/32, 38,4 min/16). + +**#112 brauchte den Umweg über das Backup:** der Rauchtest-Schnappschuss hatte den *Fehlerzustand* des +Phi-Versuchs eingefangen, `restore` hätte den zurückgeschrieben. Stattdessen `db-callrec.sql.gz` aus der +Sicherung in eine Hilfsdatenbank `callrec_restore` geladen, die eine Zeile per JOIN zurückkopiert, +Hilfsdatenbank gelöscht — #112 steht wieder auf 124,0 s / 118 Zeichen. **Lehre: `snapshot` nie auf einer +Aufnahme laufen lassen, die gerade einen fehlgeschlagenen Lauf hinter sich hat.** + ## 2026-09-29 (3) — A/B-Ergebnis: Qwen3.5-4B verliert, Qwen3-4B bleibt produktiv **Durchgeführt:** Qwen3.5-4B selbst konvertiert (RTX 3090, rkllm-toolkit 1.3.0 im Container,