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
+305 -91

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 - optionalem CUDA-Build von llama-server, llama-cli und llama-bench
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg - 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 steht in [NEW_WORKSTATION.md](docs/NEW_WORKSTATION.md): feste Modellrevisionen und
Prüfsummen, Vorbereitung der Profile und Hermes-Limits, Aktivierung und Rückweg. Prüfsummen, Vorbereitung der Profile und Hermes-Limits, Aktivierung und Rückweg.
Für deine **bestehende Workstation** zuerst Für deine **bestehende Workstation** zuerst
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für [`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 eine neue Maschine. Das Routineprofil erprobt Qwen3.5-9B IQ4_XS mit 64K Kontext. Das bisherige
vom 23.10.2026 erreichte rund 130–136 Tokens/s. Das 27B-Modell bleibt optional. 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 Frühere VRAM-Messungen stehen in [RTX3080.md](docs/RTX3080.md). Der Installer richtet keinen
Gitea-Server ein und pusht keine Änderungen. 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 Die aktuellen Schutzmaßnahmen gegen endlose Modellantworten und die explizite
Umschaltung zwischen Routine- und großem Modell stehen in Umschaltung zwischen Routine- und großem Modell stehen in
[INFERENCE_LIMITS.md](docs/INFERENCE_LIMITS.md). [INFERENCE_LIMITS.md](docs/INFERENCE_LIMITS.md).
Hermes wird mit `model.context_length: 65536` auf das tatsächliche 64K-Fenster Hermes verwendet zum aktiven Modell passend 64K Kontext für `routine` (9B),
beider Hauptmodellprofile eingestellt; die CPU-Kompression beginnt spätestens `fallback` (4B) und `large` (27B). Der Modellschalter passt auch die
bei 24.000 Tokens. Kompressionsschwelle an: 24.000 Tokens.
```bash ```bash
hermes-stack-model status hermes-stack-model status
hermes-stack-model routine hermes-stack-model routine
hermes-stack-model fallback # bisheriges 4B-/64K-Setup
hermes-stack-model large hermes-stack-model large
``` ```
+25 -8
View File
@@ -2,19 +2,19 @@
"llama_cpp_commit": "d81235049384534c167caea52b85a694f6103d14", "llama_cpp_commit": "d81235049384534c167caea52b85a694f6103d14",
"models": { "models": {
"routine": { "routine": {
"repo": "unsloth/Qwen3.5-4B-GGUF", "repo": "unsloth/Qwen3.5-9B-GGUF",
"revision": "e87f176479d0855a907a41277aca2f8ee7a09523", "revision": "3885219b6810b007914f3a7950a8d1b469d598a5",
"directory": "qwen35-4b/e87f176479d0855a907a41277aca2f8ee7a09523", "directory": "qwen35-9b/3885219b6810b007914f3a7950a8d1b469d598a5",
"files": [ "files": [
{ {
"name": "Qwen3.5-4B-Q4_K_M.gguf", "name": "Qwen3.5-9B-IQ4_XS.gguf",
"sha256": "00fe7986ff5f6b463e62455821146049db6f9313603938a70800d1fb69ef11a4", "size": 5168653536,
"size": 2740937888 "sha256": "7e918aeca06c52bcb528ea6b04b4ec957e75ee8c0a73138854c0dfcf371ea429"
}, },
{ {
"name": "mmproj-F16.gguf", "name": "mmproj-F16.gguf",
"sha256": "cd88edcf8d031894960bb0c9c5b9b7e1fea6ebee02b9f7ce925a00d12891f864", "sha256": "f70dc3509053962b0d0d3ee8a7eacebf5d60aa560cad78254ae8698516ae029f",
"size": 672423616 "size": 918166080
} }
] ]
}, },
@@ -46,6 +46,23 @@
"size": 1282439264 "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 # Lokale Inferenz begrenzen
Die Workstation verwendet `hermes-local` als stabilen API-Modellnamen. Das Die Workstation verwendet `hermes-local` als stabilen API-Modellnamen. Das
Routineprofil lädt Qwen3.5-4B Q4_K_M (Unsloth-Revision Routineprofil lädt Qwen3.5-9B IQ4_XS (Unsloth-Revision
`e87f176479d0855a907a41277aca2f8ee7a09523`), das große Profil weiterhin das `3885219b6810b007914f3a7950a8d1b469d598a5`), das große Profil weiterhin das
vorhandene Qwen3.8-27B. Die tatsächliche Auswahl steht in vorhandene Qwen3.8-27B. Die tatsächliche Auswahl steht in
`~/.config/hermes-stack-maintenance/llama.env`. `~/.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. und beendet dabei laufende Gateway-Anfragen. Beide Dateien bleiben erhalten.
Es wird nur ein Hauptmodell gleichzeitig geladen. Die lokalen profilspezifischen Es wird nur ein Hauptmodell gleichzeitig geladen. Die lokalen profilspezifischen
Konfigurationen `llama-routine.env` und `llama-large.env` enthalten absolute 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 ## Kontextgröße und Kompression
Beide Hauptmodellprofile starten mit `--ctx-size 65536`. Hermes muss dieselbe Das Routineprofil verwendet `--ctx-size 65536`, das bisherige 4B-Profil
Grenze kennen: `model.context_length: 65536` in `~/.hermes/config.yaml`. (`fallback`) und das große Profil jeweils 65536. Der Modellschalter setzt
Der lokale Alias `hermes-local` wurde bei der Live-Prüfung vom 23.10.2026 nicht `model.context_length` und `compression.threshold_tokens` zusammen mit dem
zuverlässig erkannt; Hermes meldete einen Fallback auf 256.000 Tokens. Modell: 65536/24000 für alle drei Profile. Er sichert
Die explizite Grenze verhindert eine zu große Annahme bei Kontextverwaltung und beide Konfigurationen und stellt bei einem Wechsel-Fehler beide wieder her.
Platzplanung. Sie vergrößert weder das Modellfenster noch die GPU-Belegung. 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 Die CPU-Kompression bleibt bei `threshold: 0.5` und der absoluten Schwelle
Einrichtung für Routine- und großes Profil. Bei einem späteren eigenen Profil von 24.000 Tokens. `target_ratio: 0.2`,
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`,
Schutz der ersten drei und letzten zehn Nachrichten sowie die bestehenden Schutz der ersten drei und letzten zehn Nachrichten sowie die bestehenden
Zeitbudgets bleiben erhalten. Die Schwelle ist ein Auslöser, keine harte Zeitbudgets bleiben erhalten. Die Schwelle ist ein Auslöser, keine harte
Obergrenze für einzelne Nachrichten oder Werkzeugergebnisse. 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: Für eine bestehende Installation genügt nach Abschluss laufender Aufträge:
```bash ```bash
cp -p ~/.hermes/config.yaml ~/.hermes/config.yaml.before-context-64k hermes-stack-model routine # 9B, 64K
hermes config set model.context_length 65536 hermes-stack-model fallback # 4B, 64K inklusive Kompressionsschwelle
hermes config get model.context_length
systemctl --user restart hermes-gateway.service
``` ```
Den Sicherungsnamen bei wiederholter Ausführung ändern, damit die frühere Der Modellschalter startet Hauptmodell und Gateway kontrolliert neu. Bereits
Sicherung erhalten bleibt. Der Hauptmodell- und CPU-Kompressionsdienst brauchen laufende Anfragen werden beendet; CPU-Kompression läuft unverändert weiter.
für diese Hermes-Einstellung keinen Neustart. Neue Sitzungen verwenden die neue Die aktuellen Messungen und der Rückweg stehen in [QWEN9B_TRIAL.md](QWEN9B_TRIAL.md).
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.
## Drei Schutzebenen ## Drei Schutzebenen
@@ -99,5 +89,5 @@ sowie Gateway neu starten. Cronjob-Modellbindungen sind über `hermes cron edit
JOB_ID --model MODELL --provider custom` wiederherstellbar. JOB_ID --model MODELL --provider custom` wiederherstellbar.
Quellen: [llama.cpp Serverparameter](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md), 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), [Qwen3.5-9B](https://huggingface.co/Qwen/Qwen3.5-9B),
[verwendete GGUF-Dateien](https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/tree/e87f176479d0855a907a41277aca2f8ee7a09523). [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 # Aktuelles Workstation-Setup neu installieren
Stand: 23.10.2026. Diese Anleitung stellt den aktuellen lokalen Modellbetrieb Stand: 27.10.2026. Diese Anleitung stellt den aktuellen lokalen Modellbetrieb
wieder her: Qwen3.5-4B für den Alltag, Qwen3.8-27B als explizite Alternative, wieder her: Qwen3.5-9B mit 64K für den Alltag als Erprobung, Qwen3.5-4B mit 64K als
Qwen3-1.7B für Kompression auf der CPU, 64K Kontext sowie Antwort- und Zeitlimits. 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. Die Modelle werden nacheinander betrieben; der Kompressionsserver läuft separat.
## 1. Voraussetzungen und Deployment ## 1. Voraussetzungen und Deployment
@@ -75,7 +76,7 @@ Während dieser Konfigurationsänderung muss der Gateway gestoppt sein:
```bash ```bash
systemctl --user stop hermes-gateway.service systemctl --user stop hermes-gateway.service
python3 scripts/prepare-workstation.py \ python3 scripts/prepare-workstation.py \
--download --models routine,large,compression \ --download --models routine,large,compression,fallback \
--select routine --configure-hermes --unpin-local-cron --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 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. 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- das Alltagsmodell möchte, verwendet `--models routine`. Für Routine plus CPU-
Kompression `--models routine,compression`. Ohne `--download` müssen die Dateien Kompression `--models routine,compression`. Ohne `--download` müssen die Dateien
bereits im erwarteten Modellverzeichnis liegen; das ermöglicht eine Offline- 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 Fehlgeschlagene Downloads können erneut gestartet werden; bei einem falschen
Partial-Download muss die betroffene `.part` vor erneutem Download entfernt werden. 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 `compression.env` für den jeweiligen Benutzernamen. Bereits vorhandene Profile
und deren Tuning bleiben erhalten. `--select routine` übernimmt das vorbereitete und deren Tuning bleiben erhalten. `--select routine` übernimmt das vorbereitete
Routineprofil ausdrücklich als aktive `llama.env` und sichert deren alten Stand. 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 `--configure-hermes` aktualisiert mit Backup die lokalen Provider-, Vision- und
optional Kompressionseinstellungen, das 30-Minuten-Auftragsbudget und die optional Kompressionseinstellungen, das 30-Minuten-Auftragsbudget und die
600-Sekunden-/2048-Token-Limits. Dabei setzt er `model.context_length: 65536` 600-Sekunden-/2048-Token-Limits. Dabei setzt er `model.context_length`
passend zu beiden Hauptmodellprofilen. Bei CPU-Kompression bleibt die passend zur Auswahl: 65536 für alle drei Profile. Die
Kompressionsschwelle auf 24.000 Tokens begrenzt. Fremde Provider, Terminal, Plugins und andere Kompressionsschwelle beträgt 24.000 Tokens. Fremde Provider, Terminal, Plugins und andere
Einstellungen bleiben bestehen. `--unpin-local-cron` löst nur bekannte lokale Einstellungen bleiben bestehen. `--unpin-local-cron` löst nur bekannte lokale
Modellbindungen über die Hermes-CLI; eigene Cloud-Provider bleiben gebunden. Modellbindungen über die Hermes-CLI; eigene Cloud-Provider bleiben gebunden.
Die Option legt keine Cronjobs an. Vorhandene Cronjobs kommen aus dem privaten 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 Danach eine deutsche Textantwort, ein Bild und einen Werkzeugaufruf in Hermes
prüfen. Der bisherige Live-Test erreichte bei kurzen Textanfragen etwa prüfen. Messungen zur aktuellen 9B-Erprobung stehen in
130–136 Tokens/s; das ist kein Benchmark für lange Kontexte oder Bildaufgaben. [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 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 Tool-Call wurde korrekt als JSON erzeugt. Ein zehnminütiger echter Hänger wurde
nicht absichtlich erzeugt; dessen Watchdog-Recovery ist automatisiert simuliert. nicht absichtlich erzeugt; dessen Watchdog-Recovery ist automatisiert simuliert.
@@ -156,15 +158,17 @@ nicht absichtlich erzeugt; dessen Watchdog-Recovery ist automatisiert simuliert.
```bash ```bash
hermes-stack-model # aktives Modell anzeigen 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 hermes-stack-model large # größeres Modell für bewusste Einzelnutzung
``` ```
Ein Wechsel stoppt laufende Gateway-Anfragen, prüft die Kandidatenkonfiguration, 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 Gateway wieder. Bei Fehlern wird die vorherige Konfiguration zurückkopiert und
die Wiederaufnahme der Dienste versucht; Journal und Dienststatus kontrollieren. 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 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 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 # 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 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 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 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 # Bestehende Workstation behutsam übernehmen
Aktueller Stand seit 23.10.2026: Qwen3.5-4B als Routineprofil und Qwen3.8-27B Aktueller Stand seit 27.10.2026: Erprobung von Qwen3.5-9B IQ4_XS mit 16K
als Alternative mit gemeinsamem Alias `hermes-local`, Token-/Zeitlimits und Kontext als Routineprofil. Das bisherige Qwen3.5-4B-/64K-Profil bleibt mit
Slot-Watchdog. Einrichtung und Migration: [NEW_WORKSTATION.md](NEW_WORKSTATION.md), `hermes-stack-model fallback` erreichbar. Messungen und Rückweg:
[INFERENCE_LIMITS.md](INFERENCE_LIMITS.md). Die nachfolgende Profilumstellung [QWEN9B_TRIAL.md](QWEN9B_TRIAL.md). Einrichtung und Migration:
beschreibt den früheren 27B-Betrieb und ersetzt diese aktuelle Auswahl nicht. [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 Nicht blind den Schnellstart für eine neue Maschine ausführen. Vorhandene
Hermes-/Signal-Konfiguration bleibt privat und wird nicht durch das Repo ersetzt. 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 umask 077
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance" config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
usage() { 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 case "${1:-}" in
''|status) ''|status)
@@ -13,7 +13,7 @@ case "${1:-}" in
printf 'Aktives Modell: %s\nKonfiguration: %s/llama.env\n' "$LLAMA_MODEL" "$config_dir" printf 'Aktives Modell: %s\nKonfiguration: %s/llama.env\n' "$LLAMA_MODEL" "$config_dir"
exit 0 ;; exit 0 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
routine|large) selected="$1" ;; routine|fallback|large) selected="$1" ;;
*) usage >&2; exit 2 ;; *) usage >&2; exit 2 ;;
esac esac
[[ $# -eq 1 ]] || exit 2 [[ $# -eq 1 ]] || exit 2
@@ -24,12 +24,20 @@ candidate="$config_dir/llama-$selected.env"
HERMES_LLAMA_CONFIG="$candidate" "$runner" --check HERMES_LLAMA_CONFIG="$candidate" "$runner" --check
backup="$(mktemp "$config_dir/llama.env.before-model-switch.XXXXXX")" backup="$(mktemp "$config_dir/llama.env.before-model-switch.XXXXXX")"
cp -p -- "$config_dir/llama.env" "$backup" 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() { rollback() {
local rc=$? local rc=$?
trap - EXIT trap - EXIT
if ((rc != 0)); then if ((rc != 0)); then
install -m 0600 "$backup" "$config_dir/llama.env" || true 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 restart llama-qwen.service || true
systemctl --user start hermes-gateway.service || true systemctl --user start hermes-gateway.service || true
printf 'Modellwechsel fehlgeschlagen; vorherige Konfiguration zurückkopiert. Dienststatus prüfen.\n' >&2 printf 'Modellwechsel fehlgeschlagen; vorherige Konfiguration zurückkopiert. Dienststatus prüfen.\n' >&2
@@ -37,11 +45,38 @@ rollback() {
exit "$rc" exit "$rc"
} }
trap rollback EXIT trap rollback EXIT
systemctl --user stop hermes-gateway.service
install -m 0600 "$candidate" "$config_dir/llama.env" 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 ready=0
if systemctl --user restart llama-qwen.service; then if systemctl --user restart llama-qwen.service; then
for ((i=0; i<90; i++)); do 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 ready=1; break
fi fi
sleep 2 sleep 2
+20 -13
View File
@@ -14,7 +14,7 @@ import tempfile
import time import time
ROOT = Path(__file__).resolve().parents[1] 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" 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=""): 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": if name == "compression":
args += ["--rope-scaling", "yarn", "--rope-scale", "2", "--yarn-orig-ctx", "32768", args += ["--rope-scaling", "yarn", "--rope-scale", "2", "--yarn-orig-ctx", "32768",
"--reasoning-budget", "0", "--no-reasoning-preserve", "--n-gpu-layers", "0", "--reasoning-budget", "0", "--no-reasoning-preserve", "--n-gpu-layers", "0",
@@ -77,27 +78,32 @@ def profile_config(name, paths, server_bin=""):
else: else:
args += ["--reasoning", "auto", "--reasoning-budget", "128", "--no-reasoning-preserve", args += ["--reasoning", "auto", "--reasoning-budget", "128", "--no-reasoning-preserve",
"--threads", "6", "--threads-batch", "16"] "--threads", "6", "--threads-batch", "16"]
if name == "routine": if name in ("routine", "fallback"):
# Fits on the verified 10-GiB workstation. Explicit layers disable fit. # Explicit full offload: fail instead of silently moving layers to CPU.
args += ["--n-gpu-layers", "99", "--fit", "off"] args += ["--n-gpu-layers", "99", "--fit", "off"]
else: else:
args += ["--n-gpu-layers", "auto", "--fit", "on", "--fit-target", "4096"] 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", args += ["--sleep-idle-seconds", "300", "--image-min-tokens", "1024",
"--cors-origins", "localhost"] "--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]), 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_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_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_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" return ("# Generated from config/models.lock.json; no service started.\n"
+ "".join(f"{key}={shlex.quote(value)}\n" for key, value in settings.items()) + "".join(f"{key}={shlex.quote(value)}\n" for key, value in settings.items())
+ "LLAMA_ARGS=(\n " + shlex.join(args) + "\n)\n") + "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", 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("agent", {}).update(run_budget_seconds=1800, budget_warning_ratio=0.8)
config.setdefault("providers", {}).setdefault("custom", {}).update( config.setdefault("providers", {}).setdefault("custom", {}).update(
request_timeout_seconds=600, stale_timeout_seconds=180) 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, config.setdefault("compression", {}).update(enabled=True, threshold=0.5,
threshold_tokens=24000, target_ratio=0.2, protect_last_n=10, protect_first_n=3, threshold_tokens=24000, target_ratio=0.2, protect_last_n=10, protect_first_n=3,
context_timeout_seconds=120, context_total_ceiling_seconds=600) context_timeout_seconds=120, context_total_ceiling_seconds=600)
config.setdefault("compression", {})["threshold_tokens"] = min(24000, context * 3 // 8)
return config return config
@@ -144,22 +151,22 @@ def unpin_local_jobs(path):
def main(): def main():
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--download", action="store_true", help="Fehlende, fest versionierte GGUF-Dateien herunterladen") 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("--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("--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("--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("--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("--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") parser.add_argument("--unpin-local-cron", action="store_true", help="Lokale Cron-Modellbindungen über Hermes-CLI lösen")
options = parser.parse_args() options = parser.parse_args()
names = list(dict.fromkeys(options.models.split(","))) names = list(dict.fromkeys(options.models.split(",")))
manifest = json.loads((ROOT / "config/models.lock.json").read_text())["models"] manifest = json.loads((ROOT / "config/models.lock.json").read_text())["models"]
if not names or any(name not in manifest for name in names): 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: if options.select and options.select not in names:
parser.error("--select muss in --models enthalten sein") 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") parser.error("--configure-hermes benötigt ein Hauptmodell")
if options.select or options.configure_hermes or options.unpin_local_cron: if options.select or options.configure_hermes or options.unpin_local_cron:
state = subprocess.check_output(["systemctl", "--user", "show", "hermes-gateway.service", state = subprocess.check_output(["systemctl", "--user", "show", "hermes-gateway.service",
@@ -185,7 +192,7 @@ def main():
if options.configure_hermes: if options.configure_hermes:
text = options.hermes_config.read_text() if options.hermes_config.exists() else "" text = options.hermes_config.read_text() if options.hermes_config.exists() else ""
config = yaml.safe_load(text) or {} 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) 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" maintenance = options.config_dir / "config.env"
text = maintenance.read_text() if maintenance.exists() else (ROOT / "config/config.env.example").read_text() text = maintenance.read_text() if maintenance.exists() else (ROOT / "config/config.env.example").read_text()
+52 -12
View File
@@ -58,22 +58,25 @@ class PreparationTests(unittest.TestCase):
self.assertEqual(updated["custom_providers"][1]["extra_body"]["max_tokens"], 2048) 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") 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): def test_context_and_compression_follow_selected_model(self):
for previous in (None, 256000): for name, window, trigger in (("routine", 65536, 24000), ("fallback", 65536, 24000), ("large", 65536, 24000)):
with self.subTest(previous=previous): with self.subTest(name=name):
config = {"model": {"context_length": previous}, config = {"model": {"context_length": 256000}, "compression": {"threshold_tokens": 24000}}
"compression": {"threshold_tokens": 24000}} prepare.configure_hermes(config, context=window)
prepare.configure_hermes(config) self.assertEqual(config["model"]["context_length"], window)
window = config["model"]["context_length"] self.assertEqual(config["compression"]["threshold_tokens"], trigger)
self.assertEqual(window, 65536)
for name in ("routine", "large"):
text = prepare.profile_config(name, [self.root / "weights.gguf"]) text = prepare.profile_config(name, [self.root / "weights.gguf"])
self.assertIn(f"--ctx-size {window}", text) self.assertIn(f"--ctx-size {window}", text)
self.assertEqual(config["compression"]["threshold_tokens"], 24000)
before = json.dumps(config, sort_keys=True) 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) 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): def test_existing_profiles_preserved_and_explicit_selection_backed_up(self):
path = self.root / "llama.env" path = self.root / "llama.env"
path.write_text("custom tuning\n") 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.write_text("#!/usr/bin/env bash\n" + body + "\n")
p.chmod(0o755) p.chmod(0o755)
self.env = dict(os.environ, PATH=str(self.bin) + ":" + os.environ["PATH"], 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): def test_successful_switch_through_path_entrypoint(self):
(self.root / "entrypoint").symlink_to(self.bin / "hermes-stack-model") (self.root / "entrypoint").symlink_to(self.bin / "hermes-stack-model")