Trial GPU-resident Qwen3.5-9B with verified 4B rollback

This commit is contained in:
admin committed 2026-10-27 17:14:38 +01:00
1 parent 0929cdb555
commit dec534cc88
10 files changed
+307 -93

No files matched your search

+8 -6
View File
@@ -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
```
+25 -8
View File
@@ -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
}
]
}
}
}
+19 -29
View File
@@ -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).
+18 -14
View File
@@ -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
+114
View File
@@ -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.
+3
View File
@@ -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
+7 -5
View File
@@ -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.
+39 -4
View File
@@ -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
+20 -13
View File
@@ -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()
+54 -14
View File
@@ -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")