diff --git a/README.md b/README.md index 633d8cc..aa21d20 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,14 @@ Reproduzierbares Benutzer-Deployment für Hermes Agent auf Arch Linux mit: - optionalem CUDA-Build von llama-server, llama-cli und llama-bench - Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg -Stand: 2026-10-23. Die Anleitung für das **aktuelle Setup auf einem neuen Rechner** +Stand: 2026-10-27. Die Anleitung für das **aktuelle Setup auf einem neuen Rechner** steht in [NEW_WORKSTATION.md](docs/NEW_WORKSTATION.md): feste Modellrevisionen und Prüfsummen, Vorbereitung der Profile und Hermes-Limits, Aktivierung und Rückweg. Für deine **bestehende Workstation** zuerst [`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für -eine neue Maschine. Das Routineprofil nutzt Qwen3.5-4B; der kurze lokale Texttest -vom 23.10.2026 erreichte rund 130–136 Tokens/s. Das 27B-Modell bleibt optional. +eine neue Maschine. Das Routineprofil erprobt Qwen3.5-9B IQ4_XS mit 64K Kontext. Das bisherige +4B-Modell mit 64K bleibt als `fallback` verfügbar. Details und Rückweg: +[QWEN9B_TRIAL.md](docs/QWEN9B_TRIAL.md). Das 27B-Modell bleibt optional. Frühere VRAM-Messungen stehen in [RTX3080.md](docs/RTX3080.md). Der Installer richtet keinen Gitea-Server ein und pusht keine Änderungen. @@ -134,13 +135,14 @@ Start des llama-Dienstes geladen, nicht bei der Konfigurationsprüfung. Die aktuellen Schutzmaßnahmen gegen endlose Modellantworten und die explizite Umschaltung zwischen Routine- und großem Modell stehen in [INFERENCE_LIMITS.md](docs/INFERENCE_LIMITS.md). -Hermes wird mit `model.context_length: 65536` auf das tatsächliche 64K-Fenster -beider Hauptmodellprofile eingestellt; die CPU-Kompression beginnt spätestens -bei 24.000 Tokens. +Hermes verwendet zum aktiven Modell passend 64K Kontext für `routine` (9B), +`fallback` (4B) und `large` (27B). Der Modellschalter passt auch die +Kompressionsschwelle an: 24.000 Tokens. ```bash hermes-stack-model status hermes-stack-model routine +hermes-stack-model fallback # bisheriges 4B-/64K-Setup hermes-stack-model large ``` diff --git a/config/models.lock.json b/config/models.lock.json index 892f0a2..de3da50 100644 --- a/config/models.lock.json +++ b/config/models.lock.json @@ -2,19 +2,19 @@ "llama_cpp_commit": "d81235049384534c167caea52b85a694f6103d14", "models": { "routine": { - "repo": "unsloth/Qwen3.5-4B-GGUF", - "revision": "e87f176479d0855a907a41277aca2f8ee7a09523", - "directory": "qwen35-4b/e87f176479d0855a907a41277aca2f8ee7a09523", + "repo": "unsloth/Qwen3.5-9B-GGUF", + "revision": "3885219b6810b007914f3a7950a8d1b469d598a5", + "directory": "qwen35-9b/3885219b6810b007914f3a7950a8d1b469d598a5", "files": [ { - "name": "Qwen3.5-4B-Q4_K_M.gguf", - "sha256": "00fe7986ff5f6b463e62455821146049db6f9313603938a70800d1fb69ef11a4", - "size": 2740937888 + "name": "Qwen3.5-9B-IQ4_XS.gguf", + "size": 5168653536, + "sha256": "7e918aeca06c52bcb528ea6b04b4ec957e75ee8c0a73138854c0dfcf371ea429" }, { "name": "mmproj-F16.gguf", - "sha256": "cd88edcf8d031894960bb0c9c5b9b7e1fea6ebee02b9f7ce925a00d12891f864", - "size": 672423616 + "sha256": "f70dc3509053962b0d0d3ee8a7eacebf5d60aa560cad78254ae8698516ae029f", + "size": 918166080 } ] }, @@ -46,6 +46,23 @@ "size": 1282439264 } ] + }, + "fallback": { + "repo": "unsloth/Qwen3.5-4B-GGUF", + "revision": "e87f176479d0855a907a41277aca2f8ee7a09523", + "directory": "qwen35-4b/e87f176479d0855a907a41277aca2f8ee7a09523", + "files": [ + { + "name": "Qwen3.5-4B-Q4_K_M.gguf", + "sha256": "00fe7986ff5f6b463e62455821146049db6f9313603938a70800d1fb69ef11a4", + "size": 2740937888 + }, + { + "name": "mmproj-F16.gguf", + "sha256": "cd88edcf8d031894960bb0c9c5b9b7e1fea6ebee02b9f7ce925a00d12891f864", + "size": 672423616 + } + ] } } } diff --git a/docs/INFERENCE_LIMITS.md b/docs/INFERENCE_LIMITS.md index 3529bd0..f5e932e 100644 --- a/docs/INFERENCE_LIMITS.md +++ b/docs/INFERENCE_LIMITS.md @@ -1,12 +1,12 @@ # Lokale Inferenz begrenzen Die Workstation verwendet `hermes-local` als stabilen API-Modellnamen. Das -Routineprofil lädt Qwen3.5-4B Q4_K_M (Unsloth-Revision -`e87f176479d0855a907a41277aca2f8ee7a09523`), das große Profil weiterhin das +Routineprofil lädt Qwen3.5-9B IQ4_XS (Unsloth-Revision +`3885219b6810b007914f3a7950a8d1b469d598a5`), das große Profil weiterhin das vorhandene Qwen3.8-27B. Die tatsächliche Auswahl steht in `~/.config/hermes-stack-maintenance/llama.env`. -`hermes-stack-model routine` bzw. `hermes-stack-model large` schaltet explizit um +`hermes-stack-model routine`, `fallback` bzw. `large` schaltet explizit um und beendet dabei laufende Gateway-Anfragen. Beide Dateien bleiben erhalten. Es wird nur ein Hauptmodell gleichzeitig geladen. Die lokalen profilspezifischen Konfigurationen `llama-routine.env` und `llama-large.env` enthalten absolute @@ -22,21 +22,16 @@ ebenfalls das aktiv ausgewählte Modell; die tatsächliche Modelldatei zeigt ## Kontextgröße und Kompression -Beide Hauptmodellprofile starten mit `--ctx-size 65536`. Hermes muss dieselbe -Grenze kennen: `model.context_length: 65536` in `~/.hermes/config.yaml`. -Der lokale Alias `hermes-local` wurde bei der Live-Prüfung vom 23.10.2026 nicht -zuverlässig erkannt; Hermes meldete einen Fallback auf 256.000 Tokens. -Die explizite Grenze verhindert eine zu große Annahme bei Kontextverwaltung und -Platzplanung. Sie vergrößert weder das Modellfenster noch die GPU-Belegung. +Das Routineprofil verwendet `--ctx-size 65536`, das bisherige 4B-Profil +(`fallback`) und das große Profil jeweils 65536. Der Modellschalter setzt +`model.context_length` und `compression.threshold_tokens` zusammen mit dem +Modell: 65536/24000 für alle drei Profile. Er sichert +beide Konfigurationen und stellt bei einem Wechsel-Fehler beide wieder her. +Legacy-Profile ohne `LLAMA_CONTEXT_TOKENS` werden als 64K behandelt; eigene +Profile müssen diese Metadaten passend zu `--ctx-size` setzen. -`scripts/prepare-workstation.py --configure-hermes` setzt die Grenze bei der -Einrichtung für Routine- und großes Profil. Bei einem späteren eigenen Profil -mit anderer `--ctx-size` muss `model.context_length` entsprechend mitgeändert -werden. - -Die bestehende CPU-Kompression bleibt bei `threshold: 0.5` und -`threshold_tokens: 24000`: Die absolute Obergrenze zieht die Kompression vor -die rechnerische 50-Prozent-Schwelle von 32.768 Tokens. `target_ratio: 0.2`, +Die CPU-Kompression bleibt bei `threshold: 0.5` und der absoluten Schwelle +von 24.000 Tokens. `target_ratio: 0.2`, Schutz der ersten drei und letzten zehn Nachrichten sowie die bestehenden Zeitbudgets bleiben erhalten. Die Schwelle ist ein Auslöser, keine harte Obergrenze für einzelne Nachrichten oder Werkzeugergebnisse. @@ -44,18 +39,13 @@ Obergrenze für einzelne Nachrichten oder Werkzeugergebnisse. Für eine bestehende Installation genügt nach Abschluss laufender Aufträge: ```bash -cp -p ~/.hermes/config.yaml ~/.hermes/config.yaml.before-context-64k -hermes config set model.context_length 65536 -hermes config get model.context_length -systemctl --user restart hermes-gateway.service +hermes-stack-model routine # 9B, 64K +hermes-stack-model fallback # 4B, 64K inklusive Kompressionsschwelle ``` -Den Sicherungsnamen bei wiederholter Ausführung ändern, damit die frühere -Sicherung erhalten bleibt. Der Hauptmodell- und CPU-Kompressionsdienst brauchen -für diese Hermes-Einstellung keinen Neustart. Neue Sitzungen verwenden die neue -Grenze; bereits im Agentzustand gehaltene Kontextwerte sind kein verlässlicher -Nachweis. Zur Abnahme eine neue Sitzung öffnen und die Modellantwort sowie das -Gateway-Protokoll auf erneute Kontext-Fallback-Warnungen prüfen. +Der Modellschalter startet Hauptmodell und Gateway kontrolliert neu. Bereits +laufende Anfragen werden beendet; CPU-Kompression läuft unverändert weiter. +Die aktuellen Messungen und der Rückweg stehen in [QWEN9B_TRIAL.md](QWEN9B_TRIAL.md). ## Drei Schutzebenen @@ -99,5 +89,5 @@ sowie Gateway neu starten. Cronjob-Modellbindungen sind über `hermes cron edit JOB_ID --model MODELL --provider custom` wiederherstellbar. Quellen: [llama.cpp Serverparameter](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md), -[Qwen3.5-4B](https://huggingface.co/Qwen/Qwen3.5-4B), -[verwendete GGUF-Dateien](https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/tree/e87f176479d0855a907a41277aca2f8ee7a09523). +[Qwen3.5-9B](https://huggingface.co/Qwen/Qwen3.5-9B), +[verwendete GGUF-Dateien](https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/tree/3885219b6810b007914f3a7950a8d1b469d598a5). diff --git a/docs/NEW_WORKSTATION.md b/docs/NEW_WORKSTATION.md index 1babb88..f5503fa 100644 --- a/docs/NEW_WORKSTATION.md +++ b/docs/NEW_WORKSTATION.md @@ -1,8 +1,9 @@ # Aktuelles Workstation-Setup neu installieren -Stand: 23.10.2026. Diese Anleitung stellt den aktuellen lokalen Modellbetrieb -wieder her: Qwen3.5-4B für den Alltag, Qwen3.8-27B als explizite Alternative, -Qwen3-1.7B für Kompression auf der CPU, 64K Kontext sowie Antwort- und Zeitlimits. +Stand: 27.10.2026. Diese Anleitung stellt den aktuellen lokalen Modellbetrieb +wieder her: Qwen3.5-9B mit 64K für den Alltag als Erprobung, Qwen3.5-4B mit 64K als +Rückfallprofil, Qwen3.8-27B als explizite Alternative und Qwen3-1.7B für +Kompression auf der CPU sowie Antwort- und Zeitlimits. Die Modelle werden nacheinander betrieben; der Kompressionsserver läuft separat. ## 1. Voraussetzungen und Deployment @@ -75,7 +76,7 @@ Während dieser Konfigurationsänderung muss der Gateway gestoppt sein: ```bash systemctl --user stop hermes-gateway.service python3 scripts/prepare-workstation.py \ - --download --models routine,large,compression \ + --download --models routine,large,compression,fallback \ --select routine --configure-hermes --unpin-local-cron ``` @@ -83,7 +84,7 @@ Bei noch nicht installierter Gateway-Unit ist deren Stop-Fehler unkritisch. Die Vorbereitung startet selbst keine Dienste. Sie prüft vor Änderungen, dass kein Gateway läuft, und teilt das Maintenance-Lock mit Updater und Watchdog. -Alle fünf Modelldateien zusammen benötigen rund **22,6 GiB**. Wer zunächst nur +Alle sieben Modelldateien zusammen benötigen rund **28,3 GiB**. Wer zunächst nur das Alltagsmodell möchte, verwendet `--models routine`. Für Routine plus CPU- Kompression `--models routine,compression`. Ohne `--download` müssen die Dateien bereits im erwarteten Modellverzeichnis liegen; das ermöglicht eine Offline- @@ -96,7 +97,7 @@ Prüfung umbenannt. Beschädigte vorhandene Dateien werden gemeldet und behalten Fehlgeschlagene Downloads können erneut gestartet werden; bei einem falschen Partial-Download muss die betroffene `.part` vor erneutem Download entfernt werden. -Der Helfer erzeugt fehlende Dateien `llama-routine.env`, `llama-large.env` und +Der Helfer erzeugt fehlende Dateien `llama-routine.env`, `llama-fallback.env`, `llama-large.env` und `compression.env` für den jeweiligen Benutzernamen. Bereits vorhandene Profile und deren Tuning bleiben erhalten. `--select routine` übernimmt das vorbereitete Routineprofil ausdrücklich als aktive `llama.env` und sichert deren alten Stand. @@ -106,9 +107,9 @@ verzeichnis verschieben. Das gilt auch für `LLAMA_SERVER_BIN`. `--configure-hermes` aktualisiert mit Backup die lokalen Provider-, Vision- und optional Kompressionseinstellungen, das 30-Minuten-Auftragsbudget und die -600-Sekunden-/2048-Token-Limits. Dabei setzt er `model.context_length: 65536` -passend zu beiden Hauptmodellprofilen. Bei CPU-Kompression bleibt die -Kompressionsschwelle auf 24.000 Tokens begrenzt. Fremde Provider, Terminal, Plugins und andere +600-Sekunden-/2048-Token-Limits. Dabei setzt er `model.context_length` +passend zur Auswahl: 65536 für alle drei Profile. Die +Kompressionsschwelle beträgt 24.000 Tokens. Fremde Provider, Terminal, Plugins und andere Einstellungen bleiben bestehen. `--unpin-local-cron` löst nur bekannte lokale Modellbindungen über die Hermes-CLI; eigene Cloud-Provider bleiben gebunden. Die Option legt keine Cronjobs an. Vorhandene Cronjobs kommen aus dem privaten @@ -146,8 +147,9 @@ systemctl --user status hermes-stack-llama-watchdog.timer ``` Danach eine deutsche Textantwort, ein Bild und einen Werkzeugaufruf in Hermes -prüfen. Der bisherige Live-Test erreichte bei kurzen Textanfragen etwa -130–136 Tokens/s; das ist kein Benchmark für lange Kontexte oder Bildaufgaben. +prüfen. Messungen zur aktuellen 9B-Erprobung stehen in +[QWEN9B_TRIAL.md](QWEN9B_TRIAL.md). Der historische 4B-Test erreichte bei +kurzen Textanfragen etwa 130–136 Tokens/s; das ist kein Benchmark für lange Kontexte oder Bildaufgaben. Eine erzwungene lange Antwort endete bei genau 2048 Tokens, und ein reiner Tool-Call wurde korrekt als JSON erzeugt. Ein zehnminütiger echter Hänger wurde nicht absichtlich erzeugt; dessen Watchdog-Recovery ist automatisiert simuliert. @@ -156,15 +158,17 @@ nicht absichtlich erzeugt; dessen Watchdog-Recovery ist automatisiert simuliert. ```bash hermes-stack-model # aktives Modell anzeigen -hermes-stack-model routine # schnelles Alltagsmodell +hermes-stack-model routine # 9B/64K-Erprobung +hermes-stack-model fallback # bisheriges 4B/64K-Profil hermes-stack-model large # größeres Modell für bewusste Einzelnutzung ``` Ein Wechsel stoppt laufende Gateway-Anfragen, prüft die Kandidatenkonfiguration, -sichert `llama.env`, wartet nach dem Modellstart auf `/health` und startet den +sichert `llama.env` und die Hermes-Konfiguration, setzt Kontext und +Kompressionsschwelle passend, wartet nach dem Modellstart auf `/health` und startet den Gateway wieder. Bei Fehlern wird die vorherige Konfiguration zurückkopiert und die Wiederaufnahme der Dienste versucht; Journal und Dienststatus kontrollieren. -Beide Modelle verwenden den stabilen API-Alias `hermes-local` und dieselben Limits. +Alle Hauptmodelle verwenden den stabilen API-Alias `hermes-local` und dieselben Limits. Automatischer Wechsel zwischen den Modellen findet nicht statt. Das große Modell bleibt auf der 10-GB-Karte langsam. Für Betrieb ohne Anmeldung optional diff --git a/docs/QWEN9B_TRIAL.md b/docs/QWEN9B_TRIAL.md new file mode 100644 index 0000000..d37bd37 --- /dev/null +++ b/docs/QWEN9B_TRIAL.md @@ -0,0 +1,114 @@ +# Qwen3.5-9B auf der RTX 3080 erproben + +Stand: 27.10.2026. Ziel ist bessere Alltagsqualität bei vollständigem GPU-Offload +des Sprachmodells und weiter nutzbarem Hyprland-/Browser-Desktop. Die Freigabe +ist eine Erprobung, keine Langzeit-Stabilitätszusage oder Qualitätsbewertung. + +## Profile und Schutzgrenzen + +- `routine`: Qwen3.5-9B IQ4_XS, 65536 Kontexttokens, Q4_0-K/V-Cache, + vollständiger GPU-Offload (`--n-gpu-layers 99 --fit off`), Batch 512, + Microbatch 128. Der Bildprojektor läuft mit `--no-mmproj-offload` auf der CPU. +- `fallback`: das bisherige Qwen3.5-4B Q4_K_M mit 65536 Kontexttokens und + Q8_0-K/V-Cache. Auf Archwork wurde die aktive Ausgangskonfiguration unverändert + als `llama-fallback.env` übernommen. Eine Neuinstallation erzeugt sie aus dem Manifest. +- `large`: bisheriges Qwen3.8-27B-Profil für bewusste Einzelnutzung. + +Die Gewichte und der Bildprojektor sind in `config/models.lock.json` mit fester +Hugging-Face-Revision, Dateigröße und SHA-256 fixiert. Der Projektor bleibt erhalten; +die Bildverarbeitung benötigt CPU-Zeit. IQ4_XS und der Q4_0-Kontextcache sparen +Speicher gegenüber Q4_K_M/Q8_0 und können die Antwortqualität beeinflussen. + +Hermes und Server bleiben bei 65536 Kontexttokens; die CPU-Kompression beginnt +spätestens bei 24000 Tokens. Antwortlimit 2048 Tokens, Request-Watchdog und +bestehende Zeitbudgets bleiben bestehen. Die jetzige Hermes-Version verlangt +mindestens 64000 Kontexttokens: Eine 16K-Konfiguration wird beim Agentstart +abgelehnt, auch wenn der Modellserver selbst antwortet. Das wurde lokal geprüft. +Der Modellschalter lehnt bei vorhandener Hermes-Konfiguration Metadaten unter +dieser Grenze ab und stellt bei Fehlern die vorherige Modell- und Hermes-Konfiguration +wieder her. + +## Messungen und verworfene Konfigurationen + +Alle Speicherangaben sind MiB laut `nvidia-smi`; die freie Menge hängt von den +gleichzeitig laufenden Desktop-Anwendungen ab und ist nicht reserviert. + +| Konfiguration | Beobachtung | +|---|---| +| 4B Q4_K_M, 64K, GPU-Projektor | Vergleichstest: mindestens 2288 MiB frei; kurze Textgenerierung ca. 136 Tokens/s | +| 9B Q4_K_M, 16K, GPU-Projektor, Standardpuffer | Nur ca. 900 MiB frei nach Start; verworfen | +| 9B Q4_K_M, 16K, CPU-Projektor, kleinere Puffer | Mindestens 1760 MiB frei; ca. 94 Tokens/s, Screenshot ca. 39 s; direktes API funktioniert, Hermes lehnt 16K ab | +| 9B Q4_K_M, 64K, CPU-Projektor, Q4_0-Cache | Nur ca. 1439 MiB frei nach Start; zugunsten kleinerer Gewichtsdatei verworfen | +| 9B IQ4_XS, 64K, CPU-Projektor, Q4_0-Cache, Batch 512/Microbatch 128 | Nach Start 1988 MiB frei; im Text-/Bild-/Werkzeug-/58315-Token-Test mindestens **1627 MiB frei**; ca. 73–101 Tokens/s | + +Die ersten API-Tests prüfen deutsche Textantwort, Screenshot-Erkennung, +strukturierten Werkzeugaufruf (`add`, Argumente 17/25) und knapp 11000 Tokens +Eingabekontext. Der Werkzeugaufruf wird nur als JSON geprüft und nicht ausgeführt. +Die kurzen Beispiele belegen Funktionsfähigkeit, keinen Qualitätsgewinn gegenüber +4B. Die ursprüngliche Modellbewertung beruhte auf Hersteller-Benchmarks und muss +sich in Johannes' tatsächlichen Aufgaben bewähren. + +Das endgültige IQ4_XS-Profil beantwortete die 58315-Token-Anfrage ohne +Abschneiden in ca. 22,4 Sekunden. Der strukturierte Werkzeugaufruf enthielt +korrekt `{"a":17,"b":25}`. Ein echter lokaler Hermes-Lauf mit 65536 Kontext, +20243 Zeichen Systemprompt und 19 Signal-Werkzeugbeschreibungen antwortete +in ca. 5,2 Sekunden mit „Modelltest erfolgreich“. Werkzeugausführung war für +diesen Lauf deaktiviert; es wurden keine Nachrichten an Kontakte versendet. + +**Bildgrenze:** Der Screenshot brauchte mit dem CPU-Projektor ca. 38,9 Sekunden. +CPU-Modell und die Angabe 6.0G/10G wurden erkannt, im freien Text wurde aber die +Leistungsangabe 106 W fälschlich als „106 GB“ wiedergegeben. Die Bildverarbeitung +funktioniert technisch; fehlerfreie OCR oder ein Qualitätsgewinn gegenüber 4B +sind damit nicht belegt. Eine gezielte JSON-Nachfrage nach belegtem und gesamtem +VRAM lieferte anschließend korrekt `6.0`/`10.0` in ca. 38,9 Sekunden. +Die freien 1627 MiB sind ein gemessenes Minimum während +dieses kurzen Tests, keine dauerhafte Zusicherung bei zusätzlichen Anwendungen. + +Repository-Prüfungen: 55 Python-Tests und Bash-Syntaxprüfungen bestanden; +Shellcheck ist auf der Workstation nicht installiert. Hauptmodell, Gateway und +CPU-Kompression waren abschließend `active/running`, jeweils ohne automatische +Neustarts. Im zugänglichen Kerneljournal seit Versuchsbeginn keine Treffer für +NVIDIA-/Xid-/Speicherfehler. Kein absichtlich erzeugter +UI-Absturz, keine Neustart- oder Langzeitabnahme. + +## Bestehende 4B-Installation übernehmen + +Die Vorbereitungshilfe überschreibt vorhandene Profil-Dateien absichtlich nicht. +Vor einer manuellen Migration deshalb die aktive `llama.env`, das bisherige +`llama-routine.env` und die Hermes-YAML separat sichern, die bisherige aktive +4B-Konfiguration als `llama-fallback.env` erhalten und das alte Routineprofil +in die Sicherung verschieben. Erst dann `prepare-workstation.py --download +--models routine` ausführen (gegebenenfalls `--server-bin /usr/bin/llama-server`). +Das erzeugt die neue Routinekonfiguration, ohne bereits umzuschalten. +Nach Installation des aktuellen Umschalters erfolgt die Aktivierung ausdrücklich +mit `hermes-stack-model routine`. Kein Modellwechsel durch `install.sh` allein. + +## Umschalten und zurückrollen + +```bash +hermes-stack-model status +hermes-stack-model fallback # ursprüngliches 4B-/64K-Setup wieder aktivieren +hermes-stack-model routine # 9B-Erprobung wieder aktivieren +``` + +Ein Wechsel beendet laufende Gateway-Anfragen. Der Umschalter sichert beide +Konfigurationen, übernimmt das Profil samt Kontext-/Kompressionsgrenze, wartet +auf den Modell-Healthcheck und startet den Gateway. Bei Startfehlern versucht er +die Rückkehr zum vorherigen Profil; danach Dienststatus prüfen. + +Der Rückweg auf Archwork wurde während der Erprobung tatsächlich ausgeführt: +`llama.env` war danach bytegleich zum Ausgangsstand, die gesamte Hermes-YAML +semantisch identisch; ein lokaler Hermes-Lauf mit 19 Signal-Werkzeugbeschreibungen +antwortete mit „Modelltest erfolgreich.“. Es wurde keine Nachricht an einen +Kontakt gesendet. + +Die vollständige lokale Ausgangssicherung liegt auf Archwork unter +`~/.local/state/hermes-stack-maintenance/qwen9b-20261027/` (private Hermes-YAML, +aktives Profil, altes Routineprofil, vorheriger Umschalter und Messberichte). +Diese privaten Dateien werden nicht eingecheckt. Der normale Rückweg braucht +keine Wiederherstellung der gesamten Sicherung, sondern nur den Fallback-Befehl. + +Bei erneutem UI-Stottern, Grafikfehlern oder zu geringer Reserve zuerst +`hermes-stack-model fallback` verwenden. Bei einem dauerhaft unbedienbaren +Desktop lässt sich derselbe Befehl aus einer Textkonsole ausführen. Ein +automatischer Wechsel wegen neuer UI-Probleme ist nicht eingerichtet. diff --git a/docs/RTX3080.md b/docs/RTX3080.md index 1994019..1dc39a4 100644 --- a/docs/RTX3080.md +++ b/docs/RTX3080.md @@ -1,5 +1,8 @@ # Qwen auf RTX 3080 10 GB / Hyprland +Aktuelle 9B-Erprobung mit 4B-Rückfallprofil: [QWEN9B_TRIAL.md](QWEN9B_TRIAL.md). +Die folgenden Fit-Profile und älteren Messungen betreffen den 27B-Betrieb. + Zielhardware: RTX 3080 mit 10 GiB, i9-13900KF, 64 GB RAM. Der Desktop und llama.cpp nutzen dieselbe GPU. Das 27B-Modell in Q4_K_M passt nicht vollständig in 10 GiB: CPU/RAM-Offload bleibt erforderlich. Mehr VRAM für Gewichte kann diff --git a/docs/WORKSTATION.md b/docs/WORKSTATION.md index 790673b..811a7e6 100644 --- a/docs/WORKSTATION.md +++ b/docs/WORKSTATION.md @@ -1,10 +1,12 @@ # Bestehende Workstation behutsam übernehmen -Aktueller Stand seit 23.10.2026: Qwen3.5-4B als Routineprofil und Qwen3.8-27B -als Alternative mit gemeinsamem Alias `hermes-local`, Token-/Zeitlimits und -Slot-Watchdog. Einrichtung und Migration: [NEW_WORKSTATION.md](NEW_WORKSTATION.md), -[INFERENCE_LIMITS.md](INFERENCE_LIMITS.md). Die nachfolgende Profilumstellung -beschreibt den früheren 27B-Betrieb und ersetzt diese aktuelle Auswahl nicht. +Aktueller Stand seit 27.10.2026: Erprobung von Qwen3.5-9B IQ4_XS mit 16K +Kontext als Routineprofil. Das bisherige Qwen3.5-4B-/64K-Profil bleibt mit +`hermes-stack-model fallback` erreichbar. Messungen und Rückweg: +[QWEN9B_TRIAL.md](QWEN9B_TRIAL.md). Einrichtung und Migration: +[NEW_WORKSTATION.md](NEW_WORKSTATION.md), [INFERENCE_LIMITS.md](INFERENCE_LIMITS.md). +Die nachfolgenden historischen Profilumstellungen beschreiben den früheren +27B-Betrieb und ersetzen diese aktuelle Auswahl nicht. Nicht blind den Schnellstart für eine neue Maschine ausführen. Vorhandene Hermes-/Signal-Konfiguration bleibt privat und wird nicht durch das Repo ersetzt. diff --git a/libexec/hermes-stack-model b/libexec/hermes-stack-model index b15824f..1591fef 100755 --- a/libexec/hermes-stack-model +++ b/libexec/hermes-stack-model @@ -4,7 +4,7 @@ set -Eeuo pipefail umask 077 config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance" usage() { - printf 'Aufruf: %s [status|routine|large] (Modellwechsel beendet laufende Anfragen)\n' "$0" + printf 'Aufruf: %s [status|routine|fallback|large] (Modellwechsel beendet laufende Anfragen)\n' "$0" } case "${1:-}" in ''|status) @@ -13,7 +13,7 @@ case "${1:-}" in printf 'Aktives Modell: %s\nKonfiguration: %s/llama.env\n' "$LLAMA_MODEL" "$config_dir" exit 0 ;; -h|--help) usage; exit 0 ;; - routine|large) selected="$1" ;; + routine|fallback|large) selected="$1" ;; *) usage >&2; exit 2 ;; esac [[ $# -eq 1 ]] || exit 2 @@ -24,12 +24,20 @@ candidate="$config_dir/llama-$selected.env" HERMES_LLAMA_CONFIG="$candidate" "$runner" --check backup="$(mktemp "$config_dir/llama.env.before-model-switch.XXXXXX")" cp -p -- "$config_dir/llama.env" "$backup" -systemctl --user stop hermes-gateway.service +hermes_config="${HERMES_HOME:-$HOME/.hermes}/config.yaml" +hermes_backup="" +if [[ -f "$hermes_config" ]]; then + hermes_backup="$(mktemp "$config_dir/hermes.yaml.before-model-switch.XXXXXX")" + cp -p -- "$hermes_config" "$hermes_backup" +fi rollback() { local rc=$? trap - EXIT if ((rc != 0)); then install -m 0600 "$backup" "$config_dir/llama.env" || true + if [[ -n "$hermes_backup" ]]; then + install -m 0600 "$hermes_backup" "$hermes_config" || true + fi systemctl --user restart llama-qwen.service || true systemctl --user start hermes-gateway.service || true printf 'Modellwechsel fehlgeschlagen; vorherige Konfiguration zurückkopiert. Dienststatus prüfen.\n' >&2 @@ -37,11 +45,38 @@ rollback() { exit "$rc" } trap rollback EXIT +systemctl --user stop hermes-gateway.service install -m 0600 "$candidate" "$config_dir/llama.env" +# Keep Hermes' actual context and compression trigger paired with the model. +# Legacy profiles without metadata use the original 64K window. +LLAMA_CONTEXT_TOKENS=65536 +# shellcheck disable=SC1090 +source "$candidate" +if [[ -n "$hermes_backup" ]]; then + python3 - "$hermes_config" "$LLAMA_CONTEXT_TOKENS" <<'PY' +import os +from pathlib import Path +import sys +import tempfile +import yaml +path = Path(sys.argv[1]) +context = int(sys.argv[2]) +if context < 64000: + raise ValueError("Hermes requires at least 64000 context tokens") +config = yaml.safe_load(path.read_text()) +config.setdefault("model", {})["context_length"] = context +config.setdefault("compression", {})["threshold_tokens"] = min(24000, context * 3 // 8) +with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as out: + yaml.safe_dump(config, out, allow_unicode=True, sort_keys=False, width=1000) + temporary = Path(out.name) +os.chmod(temporary, 0o600) +temporary.replace(path) +PY +fi ready=0 if systemctl --user restart llama-qwen.service; then for ((i=0; i<90; i++)); do - if curl -fsS --max-time 2 http://127.0.0.1:8080/health | jq -e '.status == "ok"' >/dev/null 2>&1; then + if curl -fsS --max-time 2 http://127.0.0.1:8080/health 2>/dev/null | jq -e '.status == "ok"' >/dev/null 2>&1; then ready=1; break fi sleep 2 diff --git a/scripts/prepare-workstation.py b/scripts/prepare-workstation.py index 801c9e4..a33454d 100755 --- a/scripts/prepare-workstation.py +++ b/scripts/prepare-workstation.py @@ -14,7 +14,7 @@ import tempfile import time ROOT = Path(__file__).resolve().parents[1] -ALIASES = ["hermes-local", "qwen35-4b-local", "qwen38-27b-local", "qwen36-27b-local"] +ALIASES = ["hermes-local", "qwen35-9b-local", "qwen35-4b-local", "qwen38-27b-local", "qwen36-27b-local"] LOCAL_URL = "http://127.0.0.1:8080/v1" @@ -68,7 +68,8 @@ def write_config(path, text, replace=False): def profile_config(name, paths, server_bin=""): - args = ["--ctx-size", "65536", "--parallel", "1", "--jinja"] + context = 65536 + args = ["--ctx-size", str(context), "--parallel", "1", "--jinja"] if name == "compression": args += ["--rope-scaling", "yarn", "--rope-scale", "2", "--yarn-orig-ctx", "32768", "--reasoning-budget", "0", "--no-reasoning-preserve", "--n-gpu-layers", "0", @@ -77,27 +78,32 @@ def profile_config(name, paths, server_bin=""): else: args += ["--reasoning", "auto", "--reasoning-budget", "128", "--no-reasoning-preserve", "--threads", "6", "--threads-batch", "16"] - if name == "routine": - # Fits on the verified 10-GiB workstation. Explicit layers disable fit. + if name in ("routine", "fallback"): + # Explicit full offload: fail instead of silently moving layers to CPU. args += ["--n-gpu-layers", "99", "--fit", "off"] else: args += ["--n-gpu-layers", "auto", "--fit", "on", "--fit-target", "4096"] + if name == "routine": + args += ["--no-mmproj-offload", "--batch-size", "512", "--ubatch-size", "128"] args += ["--sleep-idle-seconds", "300", "--image-min-tokens", "1024", "--cors-origins", "localhost"] - args += ["--flash-attn", "auto", "--cache-type-k", "q8_0", "--cache-type-v", "q8_0", "--no-ui"] + cache = "q4_0" if name == "routine" else "q8_0" + args += ["--flash-attn", "auto", "--cache-type-k", cache, "--cache-type-v", cache, "--no-ui"] settings = {"LLAMA_SERVER_BIN": server_bin, "LLAMA_MODEL": str(paths[0]), "LLAMA_MMPROJ": str(paths[1]) if len(paths) > 1 else "", "LLAMA_HF_REPO": "", "LLAMA_ALIAS": "qwen3-1.7b-compression" if name == "compression" else ",".join(ALIASES), "LLAMA_HOST": "127.0.0.1", "LLAMA_PORT": "8081" if name == "compression" else "8080", - "LLAMA_PROFILE": "", "LLAMA_MAX_OUTPUT_TOKENS": "2048"} + "LLAMA_PROFILE": "", "LLAMA_CONTEXT_TOKENS": str(context), "LLAMA_MAX_OUTPUT_TOKENS": "2048"} return ("# Generated from config/models.lock.json; no service started.\n" + "".join(f"{key}={shlex.quote(value)}\n" for key, value in settings.items()) + "LLAMA_ARGS=(\n " + shlex.join(args) + "\n)\n") -def configure_hermes(config, compression=False): +def configure_hermes(config, compression=False, context=65536): + if context < 64000: + raise ValueError("Hermes requires at least 64000 context tokens") config.setdefault("model", {}).update(default="hermes-local", provider="custom", - base_url=LOCAL_URL, context_length=65536) + base_url=LOCAL_URL, context_length=context) config.setdefault("agent", {}).update(run_budget_seconds=1800, budget_warning_ratio=0.8) config.setdefault("providers", {}).setdefault("custom", {}).update( request_timeout_seconds=600, stale_timeout_seconds=180) @@ -119,6 +125,7 @@ def configure_hermes(config, compression=False): config.setdefault("compression", {}).update(enabled=True, threshold=0.5, threshold_tokens=24000, target_ratio=0.2, protect_last_n=10, protect_first_n=3, context_timeout_seconds=120, context_total_ceiling_seconds=600) + config.setdefault("compression", {})["threshold_tokens"] = min(24000, context * 3 // 8) return config @@ -144,22 +151,22 @@ def unpin_local_jobs(path): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--download", action="store_true", help="Fehlende, fest versionierte GGUF-Dateien herunterladen") - parser.add_argument("--models", default="routine,large,compression", help="Kommagetrennt: routine,large,compression") + parser.add_argument("--models", default="routine,large,compression,fallback", help="Kommagetrennt: routine,large,compression,fallback") parser.add_argument("--server-bin", default="", help="Leer: verwalteter CUDA-Build oder llama-server im PATH") parser.add_argument("--model-dir", type=Path, default=Path.home() / ".local/share/hermes-stack/models") parser.add_argument("--config-dir", type=Path, default=Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "hermes-stack-maintenance") parser.add_argument("--hermes-config", type=Path, default=Path(os.environ.get("HERMES_HOME", str(Path.home() / ".hermes"))) / "config.yaml") - parser.add_argument("--select", choices=["routine", "large"], help="Aktive llama.env vorbereiten; kein Dienststart") + parser.add_argument("--select", choices=["routine", "large", "fallback"], help="Aktive llama.env vorbereiten; kein Dienststart") parser.add_argument("--configure-hermes", action="store_true", help="Gezielte Hermes-/Maintenance-Einstellungen mit Backup übernehmen") parser.add_argument("--unpin-local-cron", action="store_true", help="Lokale Cron-Modellbindungen über Hermes-CLI lösen") options = parser.parse_args() names = list(dict.fromkeys(options.models.split(","))) manifest = json.loads((ROOT / "config/models.lock.json").read_text())["models"] if not names or any(name not in manifest for name in names): - parser.error("--models erlaubt nur routine,large,compression") + parser.error("--models erlaubt nur routine,large,compression,fallback") if options.select and options.select not in names: parser.error("--select muss in --models enthalten sein") - if options.configure_hermes and "routine" not in names and "large" not in names: + if options.configure_hermes and not any(n in names for n in ("routine", "large", "fallback")): parser.error("--configure-hermes benötigt ein Hauptmodell") if options.select or options.configure_hermes or options.unpin_local_cron: state = subprocess.check_output(["systemctl", "--user", "show", "hermes-gateway.service", @@ -185,7 +192,7 @@ def main(): if options.configure_hermes: text = options.hermes_config.read_text() if options.hermes_config.exists() else "" config = yaml.safe_load(text) or {} - configure_hermes(config, "compression" in names) + configure_hermes(config, "compression" in names, 65536) write_config(options.hermes_config, yaml.safe_dump(config, allow_unicode=True, sort_keys=False, width=1000), replace=True) maintenance = options.config_dir / "config.env" text = maintenance.read_text() if maintenance.exists() else (ROOT / "config/config.env.example").read_text() diff --git a/tests/test_workstation.py b/tests/test_workstation.py index d4fc21a..d778d6b 100644 --- a/tests/test_workstation.py +++ b/tests/test_workstation.py @@ -58,22 +58,25 @@ class PreparationTests(unittest.TestCase): self.assertEqual(updated["custom_providers"][1]["extra_body"]["max_tokens"], 2048) self.assertEqual(updated["auxiliary"]["compression"]["base_url"], "http://127.0.0.1:8081/v1") - def test_context_matches_both_main_profiles_and_replaces_stale_window(self): - for previous in (None, 256000): - with self.subTest(previous=previous): - config = {"model": {"context_length": previous}, - "compression": {"threshold_tokens": 24000}} - prepare.configure_hermes(config) - window = config["model"]["context_length"] - self.assertEqual(window, 65536) - for name in ("routine", "large"): - text = prepare.profile_config(name, [self.root / "weights.gguf"]) - self.assertIn(f"--ctx-size {window}", text) - self.assertEqual(config["compression"]["threshold_tokens"], 24000) + def test_context_and_compression_follow_selected_model(self): + for name, window, trigger in (("routine", 65536, 24000), ("fallback", 65536, 24000), ("large", 65536, 24000)): + with self.subTest(name=name): + config = {"model": {"context_length": 256000}, "compression": {"threshold_tokens": 24000}} + prepare.configure_hermes(config, context=window) + self.assertEqual(config["model"]["context_length"], window) + self.assertEqual(config["compression"]["threshold_tokens"], trigger) + text = prepare.profile_config(name, [self.root / "weights.gguf"]) + self.assertIn(f"--ctx-size {window}", text) before = json.dumps(config, sort_keys=True) - prepare.configure_hermes(config) + prepare.configure_hermes(config, context=window) self.assertEqual(json.dumps(config, sort_keys=True), before) + def test_configuration_refuses_below_hermes_context_floor(self): + config = {"model": {"context_length": 65536}} + with self.assertRaisesRegex(ValueError, "at least 64000"): + prepare.configure_hermes(config, context=16384) + self.assertEqual(config, {"model": {"context_length": 65536}}) + def test_existing_profiles_preserved_and_explicit_selection_backed_up(self): path = self.root / "llama.env" path.write_text("custom tuning\n") @@ -141,7 +144,44 @@ class ModelSwitchTests(unittest.TestCase): p.write_text("#!/usr/bin/env bash\n" + body + "\n") p.chmod(0o755) self.env = dict(os.environ, PATH=str(self.bin) + ":" + os.environ["PATH"], - XDG_CONFIG_HOME=str(self.config.parent), XDG_RUNTIME_DIR=str(self.root), TEST_CALLS=str(self.root / "calls")) + HERMES_HOME=str(self.root / "hermes"), XDG_CONFIG_HOME=str(self.config.parent), XDG_RUNTIME_DIR=str(self.root), TEST_CALLS=str(self.root / "calls")) + + def test_switch_and_failure_keep_hermes_window_paired_with_model(self): + import yaml + directory = self.root / "hermes" + directory.mkdir() + path = directory / "config.yaml" + original = "model:\n context_length: 65536\ncompression:\n threshold_tokens: 24000\nterminal:\n backend: docker\n" + (self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\nLLAMA_CONTEXT_TOKENS=64000\n") + for fail in (False, True): + path.write_text(original) + (self.config / "llama.env").write_text("LLAMA_MODEL=old.gguf\n") + result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"], + env=dict(self.env, TEST_FAIL=str(int(fail))), capture_output=True, text=True) + if fail: + self.assertNotEqual(result.returncode, 0) + self.assertEqual(path.read_text(), original) + self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n") + else: + self.assertEqual(result.returncode, 0, result.stderr) + config = yaml.safe_load(path.read_text()) + self.assertEqual(config["model"]["context_length"], 64000) + self.assertEqual(config["compression"]["threshold_tokens"], 24000) + self.assertEqual(config["terminal"]["backend"], "docker") + + def test_hermes_rejects_too_small_context_and_restores_pair(self): + directory = self.root / "hermes" + directory.mkdir() + path = directory / "config.yaml" + original = "model:\n context_length: 65536\n" + path.write_text(original) + (self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\nLLAMA_CONTEXT_TOKENS=16384\n") + result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"], + env=self.env, capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn("at least 64000", result.stderr) + self.assertEqual(path.read_text(), original) + self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n") def test_successful_switch_through_path_entrypoint(self): (self.root / "entrypoint").symlink_to(self.bin / "hermes-stack-model")