Drei weitere Kandidaten geprueft, alle drei abgelehnt

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
This commit is contained in:
2026-09-29 19:59:04 +02:00
co-authored by Claude Opus 5
parent 9e9958fb01
commit 3a2e8144f7
4 changed files with 176 additions and 11 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ import { pool } from '../db.js';
const OUT = '/opt/callrec/lab/ab-llm'; const OUT = '/opt/callrec/lab/ab-llm';
const API = 'http://127.0.0.1'; const API = 'http://127.0.0.1';
const POLL_MS = 5000; 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 }); fs.mkdirSync(OUT, { recursive: true });
+32 -10
View File
@@ -14,6 +14,24 @@
#include <string> #include <string>
#include <iostream> #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 std::string g_out;
static bool g_error = false; 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 // 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 // rambles generic filler. prefix/postfix wrap each user prompt; <bos> is
// added by the runtime (add_bos_token=True at conversion). // added by the runtime (add_bos_token=True at conversion).
// Qwen3.5 denkt per Default und ignoriert dabei die JSON-Vorgabe -- es // Das Chat-Template kommt aus der Umgebung, weil rkllm das HF-Template NICHT
// antwortet mit Gedankengang ("Wait, looking at line ..."), und der // selbst anwendet: ohne passende Marker bekommt das Modell den nackten Prompt
// JSON-Parser in llm.js sieht nichts Brauchbares. RKLLMInput.enable_thinking // und faselt generisches Fuellmaterial. Defaults = ChatML (Qwen, Gemma3).
// allein schaltet das NICHT ab; das Modell-Template tut es, indem es den // Phi-4-mini und SmolLM3 setzen eigene Werte per Drop-in.
// Assistenten-Zug mit einem LEEREN Denkblock vorbefuellt. Genau den haengen // RKLLM_SUPPRESS_THINK=1 haengt einen LEEREN Denkblock an: Denk-Modelle wie
// wir hier an, wenn RKLLM_SUPPRESS_THINK=1 gesetzt ist (Qwen3, Gemma3 und // Qwen3.5 antworten sonst mit Gedankengang statt JSON, und
// Qwen2.5 brauchen es nicht und bekommen unveraendert das alte Template). // RKLLMInput.enable_thinking allein genuegt dafuer nicht.
std::string assist = "<|im_end|>\n<|im_start|>assistant\n"; 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"); const char* sup = getenv("RKLLM_SUPPRESS_THINK");
if (sup && *sup == '1') assist += "<think>\n\n</think>\n\n"; if (sup && *sup == '1') user_post += "<think>\n\n</think>\n\n";
rkllm_set_chat_template(h, "", "<|im_start|>user\n", assist.c_str()); 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); printf("READY\n"); fflush(stdout);
fprintf(stderr, "rkllm model loaded\n"); fflush(stderr); fprintf(stderr, "rkllm model loaded\n"); fflush(stderr);
+29
View File
@@ -391,6 +391,29 @@ flowchart LR
Die Konvertierung eines neuen Modells passiert **nicht** auf dem Board (kein CUDA), sondern auf dem PC 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. 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/<repo>/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) ### Denk-Modelle (Qwen3.5 und jünger)
Modelle mit Denk-Modus liefern sonst Gedankengang statt JSON. `RKLLMInput.enable_thinking = false` genügt 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 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, 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. 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).
+114
View File
@@ -3,6 +3,120 @@
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
Was, Warum, betroffene Dateien/Backups). 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/<repo>/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 `<think>\n\n</think>\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 ## 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, **Durchgeführt:** Qwen3.5-4B selbst konvertiert (RTX 3090, rkllm-toolkit 1.3.0 im Container,