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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017A91sWU5aWrqZ2DWNTLVph
This commit is contained in:
2026-09-29 17:19:47 +02:00
co-authored by Claude Opus 5
parent e9ba48224e
commit 9e9958fb01
3 changed files with 86 additions and 1 deletions
+12 -1
View File
@@ -8,6 +8,7 @@
// keep_history=0 → every request is independent (Node sends full context each time). // 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 <cstddef> // rkllm.h uses size_t without including it itself
#include "rkllm.h" #include "rkllm.h"
#include <cstdlib> // getenv
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <string> #include <string>
@@ -74,7 +75,17 @@ 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).
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 += "<think>\n\n</think>\n\n";
rkllm_set_chat_template(h, "", "<|im_start|>user\n", assist.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);
+15
View File
@@ -390,3 +390,18 @@ 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.
### 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` `<think>\n\n</think>\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.
+59
View File
@@ -3,6 +3,65 @@
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 (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): `{{- '<think>\n\n</think>\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) ## 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 **Anlass:** Recherche zur Frage, ob es inzwischen ein besseres LLM gibt als das produktive