From 9e9958fb0148db40df29bfb5c754ac316a313f8e Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 29 Sep 2026 17:19:47 +0200 Subject: [PATCH] A/B-Ergebnis: Qwen3.5-4B verliert, Qwen3-4B bleibt produktiv Qwen3.5-4B ist auf der RK3588-NPU 21-45% langsamer und haelt die JSON-Vorgabe nicht durch: von 61 extrahierten Todos ueber drei Gespraeche blieben 2 uebrig, dazu 13 Parse-Fehler mit Wiederholungen. Die Prosa war nicht schlechter, teils genauer -- ausschlaggebend ist die Formatdisziplin. Modell-Drop-in zurueckgesetzt, Originalanalysen per restore wiederhergestellt. rkllm_server.cpp: neuer Schalter RKLLM_SUPPRESS_THINK=1 haengt einen leeren Denkblock an das Chat-Template. Ohne ihn antwortet Qwen3.5 mit Gedankengang statt JSON; RKLLMInput.enable_thinking reicht dafuer nicht. Ohne die Variable bleibt das Template unveraendert. Doku: Changelog 2026-09-29 (3) mit Messwerten, Bewertung und der Einschaetzung zu rknn-llm v1.3.1 (fuer uns wirkungslos, weil wir keep_history=0 fahren), plus Kapitel 05. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017A91sWU5aWrqZ2DWNTLVph --- app/rkllm_server.cpp | 13 +++++++++- docs/05-llm-analyse.md | 15 +++++++++++ docs/08-changelog.md | 59 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/app/rkllm_server.cpp b/app/rkllm_server.cpp index b6b94f0..9b28727 100644 --- a/app/rkllm_server.cpp +++ b/app/rkllm_server.cpp @@ -8,6 +8,7 @@ // keep_history=0 → every request is independent (Node sends full context each time). #include // rkllm.h uses size_t without including it itself #include "rkllm.h" +#include // getenv #include #include #include @@ -74,7 +75,17 @@ 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). - rkllm_set_chat_template(h, "", "<|im_start|>user\n", "<|im_end|>\n<|im_start|>assistant\n"); + // 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"; + 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()); 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 a2e6633..417fdc3 100644 --- a/docs/05-llm-analyse.md +++ b/docs/05-llm-analyse.md @@ -390,3 +390,18 @@ 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. + +### Denk-Modelle (Qwen3.5 und jünger) + +Modelle mit Denk-Modus liefern sonst Gedankengang statt JSON. `RKLLMInput.enable_thinking = false` genügt +**nicht** — der Assistenten-Zug muss mit einem leeren Denkblock vorbefüllt werden. Dafür gibt es +`RKLLM_SUPPRESS_THINK=1`: dann hängt `rkllm_server.cpp` `\n\n\n\n` an das Chat-Template. +Ohne die Variable bleibt alles wie bisher. + +### Ergebnis des ersten Modellvergleichs (2026-09-29) + +Qwen3.5-4B gegen das produktive Qwen3-4B, gleiche Konvertierungsparameter, drei Aufnahmen: **21-45 % +langsamer**, Zusammenfassungen auf ein Drittel bis Viertel gekürzt, und von 61 Todos blieben 2 — das Modell +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. diff --git a/docs/08-changelog.md b/docs/08-changelog.md index 749a85b..7da06c7 100644 --- a/docs/08-changelog.md +++ b/docs/08-changelog.md @@ -3,6 +3,65 @@ Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Was, Warum, betroffene Dateien/Backups). +## 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, +`w8a8_g128`, `max_context 16384`, 3 NPU-Kerne — identisch zum produktiven Modell), 5,53 GB, Konvertierung +308 s. Übertragung aufs Board per md5 verifiziert. Danach beide Modelle über `lab/ab-llm.mjs` gegen +dieselben drei Aufnahmen. + +| Aufnahme | Zeichen | Qwen3-4B | Qwen3.5-4B | Tempo | Summary | Todos | +|---|---|---|---|---|---|---| +| #86 | 8 390 | 10,5 min | 13,1 min | **+25 %** | 2 896 → 848 | 6 → **0** | +| #123 | 19 275 | 24,5 min | 29,7 min | **+21 %** | 6 071 → 3 064 | 18 → **2** | +| #114 | 34 843 | 41,8 min | 60,8 min | **+45 %** | 9 288 → 2 292 | 37 → **0** | + +**Urteil: Qwen3-4B-Instruct-2507 bleibt.** Qwen3.5 ist auf unserer NPU durchgängig langsamer, und der +Ausschlag ist nicht das Tempo, sondern die **Todos**: von 61 extrahierten Aufgaben über die drei Gespräche +bleiben 2 übrig. Die Aufgabenliste ist eine Kernfunktion — damit ist das Modell für uns unbrauchbar, +unabhängig von Benchmark-Werten (Artificial-Analysis-Index 27 vs. 18). + +**Fair gegenüber Qwen3.5:** die *Prosa* ist nicht schlechter, eher dichter. Bei #123 nennt Qwen3.5 Philipp +korrekt als neues Mitglied, während Qwen3 ihn zur „neuen Teilnehmerin" macht. Das Modell scheitert nicht +am Verstehen, sondern an der **Formatdisziplin**: 13 JSON-Parse-Fehler mit Wiederholungen während der +Läufe (`chunk 12 unparseable -> retry 3`), im Ergebnis leere `todos`-Arrays, und die Zeilenumbrüche landen +teils als literales `\n` im Text. + +### Neu: `RKLLM_SUPPRESS_THINK` in `rkllm_server.cpp` + +Erster Versuch scheiterte komplett: Qwen3.5 antwortete mit Gedankengang statt JSON +(`llm: combined JSON unparseable`, Auszug: „Wait, looking at line …"), Analyse brach ab. +**`RKLLMInput.enable_thinking = false` reicht bei diesem Modell NICHT.** Das Denken schaltet erst ab, +wenn der Assistenten-Zug mit einem **leeren Denkblock** vorbefüllt wird — so macht es das +`chat_template.jinja` des Modells (Zeile 149-152): `{{- '\n\n\n\n' }}`. + +`rkllm_server.cpp` hängt diesen Block jetzt an das Chat-Template an, wenn `RKLLM_SUPPRESS_THINK=1` in der +Umgebung steht; ohne die Variable bleibt das Template unverändert (Qwen3, Gemma3, Qwen2.5 brauchen es +nicht). Neu übersetzt auf dem Board: +`g++ -O2 -std=c++17 -o rkllm_server rkllm_server.cpp -I/usr/include -L/lib -lrkllmrt`. +Backups: `rkllm_server.cpp.bak-think`, `rkllm_server.bak-think`. **Diese Änderung bleibt** — sie kostet +nichts und macht jeden künftigen Denk-Modell-Test möglich. + +### Zur Frage rknn-llm v1.3.1 (erschienen am selben Tag) + +Enthält u. a. eine *linear attention cache reuse strategy*, die auf Issue #519 zielt (unter 1.3.0 meldet +RKLLM bei Qwen3.5 „Current model does not support kv cache reuse"). **Für uns wirkungslos:** unser Server +fährt bewusst `keep_history = 0` und ruft `rkllm_clear_kv_cache()` vor jedem Lauf, damit kein Kontext +zwischen Aufnahmen durchsickert — es gibt nichts wiederzuverwenden, und die Warnung taucht in unserem +Journal gar nicht auf. Der gemessene Rückstand liegt zudem beim Decode (weniger Text in mehr Zeit), nicht +beim Prefill, den ein Cache-Reuse entlasten würde. Update daher nicht durchgeführt. + +### Zustand nach dem Test + +Modell-Drop-in auf `qwen3-4b-w8a8g128-16k.rkllm` zurückgesetzt, `restore` hat die Originalanalysen von +#86, #112, #114 und #123 zurückgeschrieben (geprüft: 19,4 min / 7 Todos, 58,5 min / 32 Todos, +38,4 min / 16 Todos). Das konvertierte `qwen35-4b-w8a8g128-16k.rkllm` (5,9 GB) bleibt unter +`/opt/rkllm-models/` liegen — für einen späteren Versuch mit angepasstem Prompt oder neuerer Runtime. + +**GOTCHA für künftige Läufe:** `lab/ab-llm.mjs` hat ein Zeitlimit von 45 Min pro Aufnahme; #114 brauchte +60,8 Min und wurde deshalb in `run-qwen35.json` als `pending` eingefroren, obwohl die Analyse später sauber +durchlief. Die echten Werte standen in der DB. Bei langen Gesprächen `TIMEOUT_MS` hochsetzen. + ## 2026-09-29 (2) — A/B-Aufbau Qwen3.5-4B (Sicherung, Messstrecke, Konvertierungs-Kit) **Anlass:** Recherche zur Frage, ob es inzwischen ein besseres LLM gibt als das produktive