Compare commits
7
Commits
e8512b56f5
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dec534cc88 | ||
|
|
0929cdb555 | ||
|
|
8544abd2d4 | ||
|
|
7006991843 | ||
|
|
8a4a01c42f | ||
|
|
c6dba0b9ec | ||
|
|
eb456b37ea |
No files matched your search
@@ -12,6 +12,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install test dependencies
|
||||
run: pacman -Syu --noconfirm bash shellcheck ripgrep python jq
|
||||
run: pacman -Syu --noconfirm bash shellcheck ripgrep python python-yaml jq
|
||||
- name: Run smoke tests
|
||||
run: ./tests/smoke.sh
|
||||
@@ -1,3 +1,41 @@
|
||||
# 2026-10-23 — Explizites 64K-Kontextfenster für Hermes
|
||||
|
||||
- `model.context_length: 65536` passend zu beiden Hauptmodellprofilen gesetzt;
|
||||
verhindert den beobachteten Alias-Fallback auf 256.000 Tokens.
|
||||
- Bestehende CPU-Kompressionsschwelle von 24.000 Tokens beibehalten.
|
||||
- Bereitstellung, Migration und Betriebsdokumentation aktualisiert; Regression
|
||||
für alte Kontextwerte, Profilabgleich und wiederholte Konfiguration ergänzt.
|
||||
|
||||
# 2026-10-23 — Routineprofil und Begrenzung lokaler Inferenz
|
||||
|
||||
- Qwen3.5-4B als Alltagsmodell, 27B als explizite Alternative und bestehendes
|
||||
CPU-Kompressionsmodell mit festen Revisionen, Größen und SHA-256 erfasst.
|
||||
- Offline-/Download-Vorbereitung, portable Profile und gezielte Hermes-
|
||||
Konfigurationsübernahme mit Backups und optionalem Lösen lokaler Cronbindungen.
|
||||
- 2048-Tokens-Serverstandard, Hermes-Requestlimits und unabhängiger 15-Sekunden-
|
||||
Slot-Watchdog gegen zu lange oder endlose Modellantworten ergänzt.
|
||||
- `hermes-stack-model` ohne Pfad, mit Status, Vorprüfung und Fehler-Rückweg.
|
||||
- Neuinstallation, Migration, Modellwahl, Aktivierung und Grenzen dokumentiert;
|
||||
Tests für Prüfsummen, Konfigurationsschutz, Modellwechsel und Recovery.
|
||||
|
||||
# 2026-10-21 — Erfolgreiche Hermes-Prüfung ohne Versionswechsel
|
||||
|
||||
- Ein unveränderter Commit nach erfolgreichem `hermes update` gilt als Erfolg:
|
||||
Der konfigurierte Stable-Kanal kann aktuell sein, obwohl `origin/main` weiter ist.
|
||||
- Abschließende Meldungen erscheinen direkt im Journal des Update-Dienstes.
|
||||
- Isolierte Regressionstests prüfen unveränderte/aktualisierte Commits,
|
||||
Updater-Fehler und Gateway-Rollback ohne Netzwerk oder echte Dienstaufrufe.
|
||||
|
||||
# 2026-10-12 — VRAM-Reserve und portable CPU-Kompression
|
||||
|
||||
- `desktop-64k` mit 4096 MiB Fit-Zielmarge und 64K Kontext als Standard neuer
|
||||
Konfigurationen ergänzt; frühere Profile bleiben verfügbar.
|
||||
- Separaten CPU-Kompressionsdienst mit eigener portabler Konfiguration,
|
||||
Geräte-/Op-/KV-Offload-Ausschluss und Installation ohne Aktivierung ergänzt.
|
||||
- Migrationsschritte, tatsächliche Workstation-Abnahme und Prüfgrenzen dokumentiert.
|
||||
- Bestehende lokale Konfigurationen und laufende Dienste bleiben beim Deployment
|
||||
erhalten; Optionen werden vor dem Modellstart auf Unterstützung geprüft.
|
||||
|
||||
# 2026-09-10 — Cleanup-Reparatur und dauerhafte Modelle
|
||||
|
||||
- Korrigierte Cleanup-Skripte unter maintenance/ aufgenommen: kein globales
|
||||
|
||||
@@ -5,6 +5,7 @@ Reproduzierbares Benutzer-Deployment für Hermes Agent auf Arch Linux mit:
|
||||
- offizieller Hermes-Git-Installation unter `~/.hermes/hermes-agent`
|
||||
- von Hermes erzeugtem systemd-User-Service für den Messaging Gateway
|
||||
- optionalem lokalen llama.cpp/OpenAI-kompatiblen Modellendpunkt
|
||||
- optionalem CPU-Kompressionsserver für Hermes
|
||||
- optionalem signal-cli HTTP-Daemon
|
||||
- robustem Hermes-Update mit Vorprüfung, Retry, Backup und Rollback
|
||||
- regelmäßigen Healthchecks und Selbstheilungsversuchen
|
||||
@@ -13,10 +14,16 @@ 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-09-05. Für deine **bestehende Workstation** zuerst
|
||||
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. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU
|
||||
gemessen. Es wird kein Gitea-Server angelegt und nichts automatisch gepusht.
|
||||
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.
|
||||
|
||||
Das Repository enthält **keine Zugangsdaten, Signal-Sitzung oder
|
||||
Modellgewichte**. Diese Daten werden lokal angelegt und sind durch `.gitignore`
|
||||
@@ -43,6 +50,8 @@ Danach die beim ersten Lauf erzeugten lokalen Konfigurationen bearbeiten:
|
||||
```bash
|
||||
$EDITOR ~/.config/hermes-stack-maintenance/config.env
|
||||
$EDITOR ~/.config/hermes-stack-maintenance/llama.env
|
||||
# Bei Nutzung der lokalen Kompression:
|
||||
$EDITOR ~/.config/hermes-stack-maintenance/compression.env
|
||||
$EDITOR ~/.config/hermes-stack-maintenance/signal-cli.env
|
||||
$EDITOR ~/.hermes/.env
|
||||
```
|
||||
@@ -56,7 +65,9 @@ hermes gateway install --force
|
||||
```
|
||||
|
||||
In Hermes als lokalen OpenAI-kompatiblen Endpunkt `http://127.0.0.1:8080/v1`
|
||||
und als Modell-ID `qwen38-27b-local` wählen. Bestehende Hermes-Konfigurationen,
|
||||
und als Modell-ID `hermes-local` wählen, wenn die aktuellen Routine-/Large-Profile
|
||||
nach [NEW_WORKSTATION.md](docs/NEW_WORKSTATION.md) vorbereitet wurden. Der ältere
|
||||
HF-Schnellstart verwendet weiterhin `qwen38-27b-local`. Bestehende Hermes-Konfigurationen,
|
||||
Profile, Skills und Memories werden nicht durch generische Vorgaben ersetzt.
|
||||
Für Neuinstallationen diese Einstellungen interaktiv vornehmen oder die eigenen
|
||||
Daten gemäß Migration sicher übernehmen. Der Alias allein setzt kein Kontextlimit.
|
||||
@@ -78,6 +89,7 @@ Maintenance aktivieren:
|
||||
systemctl --user enable --now \
|
||||
hermes-stack-hermes-update.timer \
|
||||
hermes-stack-health.timer \
|
||||
hermes-stack-llama-watchdog.timer \
|
||||
hermes-stack-signal-health.timer \
|
||||
hermes-stack-signal-update-check.timer
|
||||
```
|
||||
@@ -111,23 +123,46 @@ Abschließend:
|
||||
Ohne Optionen installiert das Skript nur die versionierten Dateien und legt
|
||||
fehlende Beispielkonfigurationen an. Bestehende lokale Konfigurationen werden
|
||||
nicht überschrieben.
|
||||
`hermes-stack-model` wird über `~/.local/bin` ohne Pfadangabe aufrufbar. Der
|
||||
separate Helfer `scripts/prepare-workstation.py` stellt auf Wunsch die fixierten
|
||||
Modelldateien bereit und übernimmt gezielt die aktuellen Hermes-Einstellungen.
|
||||
Ersetzte Wrapper und Units werden vorher gesichert. Eine vorhandene
|
||||
hermes-gateway.service wird nicht neu erzeugt. Ein Modell wird erst beim echten
|
||||
Start des llama-Dienstes geladen, nicht bei der Konfigurationsprüfung.
|
||||
|
||||
## RTX 3080 und Qwen
|
||||
|
||||
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 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
|
||||
```
|
||||
|
||||
```bash
|
||||
~/.local/libexec/hermes-stack-llama-run --check
|
||||
~/.local/libexec/hermes-stack-llama-profile list
|
||||
~/.local/libexec/hermes-stack-llama-report
|
||||
```
|
||||
|
||||
`baseline` behält deine bisherigen 64K Kontext / 1536 MiB Fit-Zielmarge.
|
||||
`balanced` reduziert nur den Kontext auf 48K; `vram-plus` danach optional die
|
||||
Marge auf 1280 MiB. `compact` bietet 32K bei 1536 MiB. Das sind Vergleichsprofile,
|
||||
keine Garantie gegen Desktop-Speicherdruck. Details, Messprotokoll und Rücknahme:
|
||||
[`docs/RTX3080.md`](docs/RTX3080.md).
|
||||
Für neue Konfigurationen ist `desktop-64k` der Standard: 64K Kontext,
|
||||
4096 MiB Fit-Zielmarge, Reasoning-Budget 128 ohne Reasoning-Preserve.
|
||||
Das priorisiert Spielraum für Browser und Desktop auf einer gemeinsam genutzten
|
||||
GPU. Die früheren Profile bleiben für bewusste Vergleiche verfügbar.
|
||||
Bestehende Konfigurationen werden nicht umgestellt. Bei expliziten `LLAMA_ARGS`
|
||||
und leerem `LLAMA_PROFILE` wirken die Profile nicht.
|
||||
|
||||
Der optionale Dienst `llama-compression.service` verwendet eine eigene
|
||||
`compression.env`, Port 8081 und CPU-Ausführung einschließlich KV-/Op-Offload.
|
||||
Für beide Server werden Modellgewichte separat bereitgestellt. Einrichtung,
|
||||
Messgrenzen und Migration: [`docs/RTX3080.md`](docs/RTX3080.md).
|
||||
|
||||
Profilwechsel ändern zunächst nur eine separate Auswahl-Datei. Ein Neustart
|
||||
benötigt `hermes-stack-llama-restart --jobs-finished`. Der Healthcheck teilt das
|
||||
@@ -139,7 +174,7 @@ Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr au
|
||||
|
||||
1. exklusives Lock und Prüfung auf lokale Änderungen
|
||||
2. `origin/main`-Fetch mit begrenzten Retries und letztem HTTP/1.1-Fallback
|
||||
3. schneller Abschluss ohne Backup, wenn kein Update vorliegt
|
||||
3. schneller Abschluss ohne Backup, wenn Checkout und gefetchter Branch identisch sind
|
||||
4. komprimiertes Backup vor jeder Änderung
|
||||
5. Stoppen des Gateways und Aufruf von `hermes update`
|
||||
6. einmaliger Retry bei eindeutig transientem Fetch-Fehler
|
||||
@@ -148,9 +183,17 @@ Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr au
|
||||
9. separater llama.cpp-Healthcheck ohne unsinnigen Hermes-Rollback
|
||||
10. Aufbewahrung der drei neuesten Hermes-Backups
|
||||
|
||||
Den Zielstand wählt `hermes update` anhand des konfigurierten Update-Kanals.
|
||||
Bei Stable kann der Checkout bereits aktuell sein, obwohl `origin/main` weiter
|
||||
ist. Ein unveränderter Commit bei Updater-Exitcode 0 und gesundem Gateway gilt
|
||||
deshalb als Erfolg. Dabei können Backup und Gateway-Neustart trotzdem stattfinden.
|
||||
Ein fehlgeschlagener Updater bei unverändertem Commit bleibt ein Fehler.
|
||||
|
||||
Ein Fehler von Telegram oder Signal kann den Updateprozess niemals fehlschlagen
|
||||
lassen. Detaillierte Zwischenstände landen im Journal; nur zusammengefasste
|
||||
Ergebnisse werden versendet.
|
||||
Ergebnisse werden versendet. Abschlussmeldungen stehen auch direkt im Journal
|
||||
von `hermes-stack-hermes-update.service`. Diagnose, gezielte Installation und
|
||||
Prüfung sind in [HERMES_UPDATE.md](docs/HERMES_UPDATE.md) beschrieben.
|
||||
|
||||
## Bedienung
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Optionaler CPU-Kompressionsserver. Modellgewichte separat bereitstellen.
|
||||
# Gleiche Binary-Auswahl wie beim Hauptmodell; bei Bedarf explizit setzen.
|
||||
LLAMA_SERVER_BIN=""
|
||||
LLAMA_MODEL="$HOME/.local/share/hermes-stack/models/qwen3-1.7b-compression/Qwen3-1.7B-Q4_K_M.gguf"
|
||||
LLAMA_HF_REPO=""
|
||||
LLAMA_MMPROJ=""
|
||||
LLAMA_ALIAS="qwen3-1.7b-compression"
|
||||
LLAMA_HOST="127.0.0.1"
|
||||
LLAMA_PORT="8081"
|
||||
LLAMA_PROFILE=""
|
||||
LLAMA_ARGS=(
|
||||
--ctx-size 65536 --rope-scaling yarn --rope-scale 2 --yarn-orig-ctx 32768
|
||||
--parallel 1 --jinja --reasoning-budget 0 --no-reasoning-preserve
|
||||
--n-gpu-layers 0 --device none --no-op-offload --no-kv-offload
|
||||
--threads 8 --threads-batch 8 --flash-attn auto
|
||||
--cache-type-k q8_0 --cache-type-v q8_0 --no-ui
|
||||
)
|
||||
# 64K-Kontext ist die übernommene Workstation-Konfiguration mit YaRN.
|
||||
# Die Binary meldet Kontext über dem Trainingslimit; Qualität bei langen
|
||||
# Kompressionsaufgaben auf dem Zielmodell gesondert prüfen.
|
||||
@@ -7,6 +7,9 @@ HERMES_BRANCH="main"
|
||||
GATEWAY_SERVICE="hermes-gateway.service"
|
||||
LLAMA_SERVICE="llama-qwen.service"
|
||||
LLAMA_URL="http://127.0.0.1:8080"
|
||||
# Independent slot watchdog. Active requests only; updates hold the same lock.
|
||||
LLAMA_MAX_REQUEST_SECONDS=600
|
||||
LLAMA_MAX_OUTPUT_TOKENS=2048
|
||||
# 1: inaktiven Dienst wieder starten. HTTP-Timeouts allein starten nie neu.
|
||||
# Standard 0 verhindert unerwünschte Modellstarts während Wartung/Tuning.
|
||||
LLAMA_HEALTH_AUTORESTART=0
|
||||
|
||||
@@ -9,5 +9,5 @@ LLAMA_HF_REPO=""
|
||||
LLAMA_ALIAS="qwen38-27b-local"
|
||||
LLAMA_HOST="127.0.0.1"
|
||||
LLAMA_PORT="8080"
|
||||
LLAMA_PROFILE="baseline"
|
||||
LLAMA_PROFILE="desktop-64k"
|
||||
LLAMA_ARGS=()
|
||||
@@ -8,10 +8,11 @@ LLAMA_ALIAS="qwen38-27b-local"
|
||||
LLAMA_MMPROJ=""
|
||||
LLAMA_HOST="127.0.0.1"
|
||||
LLAMA_PORT="8080"
|
||||
LLAMA_MAX_OUTPUT_TOKENS=2048
|
||||
|
||||
# Erster Start unverändert mit 64K / 1536 MiB Fit-Zielmarge.
|
||||
# baseline bleibt Standard; vram-64k ist ein optionales Testprofil.
|
||||
LLAMA_PROFILE="baseline"
|
||||
# Standard für eine gemeinsam mit dem Desktop genutzte GPU: 64K / 4096 MiB.
|
||||
# Fit ist ein Planungsziel; die freie Reserve auf der Zielmaschine messen.
|
||||
LLAMA_PROFILE="desktop-64k"
|
||||
LLAMA_ARGS=()
|
||||
# HF lädt beim ersten echten Start herunter. Für offline/reproduzierbar:
|
||||
# LLAMA_MODEL und optional LLAMA_MMPROJ setzen; LLAMA_HF_REPO leeren.
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"llama_cpp_commit": "d81235049384534c167caea52b85a694f6103d14",
|
||||
"models": {
|
||||
"routine": {
|
||||
"repo": "unsloth/Qwen3.5-9B-GGUF",
|
||||
"revision": "3885219b6810b007914f3a7950a8d1b469d598a5",
|
||||
"directory": "qwen35-9b/3885219b6810b007914f3a7950a8d1b469d598a5",
|
||||
"files": [
|
||||
{
|
||||
"name": "Qwen3.5-9B-IQ4_XS.gguf",
|
||||
"size": 5168653536,
|
||||
"sha256": "7e918aeca06c52bcb528ea6b04b4ec957e75ee8c0a73138854c0dfcf371ea429"
|
||||
},
|
||||
{
|
||||
"name": "mmproj-F16.gguf",
|
||||
"sha256": "f70dc3509053962b0d0d3ee8a7eacebf5d60aa560cad78254ae8698516ae029f",
|
||||
"size": 918166080
|
||||
}
|
||||
]
|
||||
},
|
||||
"large": {
|
||||
"repo": "ggml-org/Qwen3.8-27B-GGUF",
|
||||
"revision": "0669b98607d47046c7c2b3f801011d54a08cfccf",
|
||||
"directory": "qwen38-27b/0669b98607d47046c7c2b3f801011d54a08cfccf",
|
||||
"files": [
|
||||
{
|
||||
"name": "Qwen3.8-27B-Q4_K_M.gguf",
|
||||
"sha256": "31629f53165ab6a7dad8c9847dcfd1fdf55829dac1e6e748f4a68581b0033d34",
|
||||
"size": 18973870432
|
||||
},
|
||||
{
|
||||
"name": "mmproj-Qwen3.8-27B-Q8_0.gguf",
|
||||
"sha256": "2e968a6af97ce35d8971890b257b9b7edabf20ad91450501fa53162a19ee33eb",
|
||||
"size": 629247008
|
||||
}
|
||||
]
|
||||
},
|
||||
"compression": {
|
||||
"repo": "ggml-org/Qwen3-1.7B-GGUF",
|
||||
"revision": "daeb8e2d528a760970442092f6bf1e55c3b659eb",
|
||||
"directory": "qwen3-1.7b-compression",
|
||||
"files": [
|
||||
{
|
||||
"name": "Qwen3-1.7B-Q4_K_M.gguf",
|
||||
"sha256": "d2387ca2dbfee2ffabce7120d3770dadca0b293052bc2f0e138fdc940d9bc7b5",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@ außerhalb des Caches abgelegt und mit cmp gegen die Quellen geprüft.
|
||||
|
||||
Nach Umstellung und kontrolliertem Neustart bestätigte das Serverlog beide
|
||||
neuen Pfade. Eine Textanfrage lieferte OK, der Healthcheck Result=success.
|
||||
Profil baseline bleibt unverändert (65536 Kontext, 1536 MiB Fit-Zielmarge).
|
||||
Historisch blieb damals baseline unverändert (65536 Kontext, 1536 MiB Fit-Zielmarge).
|
||||
Seit 12.10.2026 gilt die größere Desktop-Reserve; siehe
|
||||
[aktuellen Workstation-Abgleich](WORKSTATION.md#workstation-abgleich-vom-2026-10-12).
|
||||
Damit sind Modellstart, kurze Textinferenz und leichte Gesundheitsprüfung
|
||||
belegt; ein kompletter Bildjob und der Docker-Netzwerk-Canary wurden bei
|
||||
dieser Wiederherstellung noch nicht erneut getestet.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Hermes-Update betreiben und prüfen
|
||||
|
||||
## Bereits aktuelle Stable-Version
|
||||
|
||||
Der Updater prüft zunächst den konfigurierten Remote-Branch (standardmäßig
|
||||
`origin/main`). Den tatsächlichen Zielstand wählt anschließend `hermes update`
|
||||
anhand des Hermes-Update-Kanals. Bei Stable kann der installierte Release-Commit
|
||||
aktuell sein, obwohl auf `main` neuere Entwicklungscommits liegen.
|
||||
|
||||
Bis zur Korrektur vom 21.10.2026 endete dieser Fall trotz `Already up to date!`
|
||||
und Updater-Exitcode 0 mit einem fehlgeschlagenen Wartungsdienst. Das Skript
|
||||
forderte fälschlich einen Commitwechsel. Jetzt endet ein erfolgreicher Updater
|
||||
mit unverändertem Commit nach den Konfigurations- und Gateway-Prüfungen
|
||||
erfolgreich. Der Kanal wird durch diese Korrektur nicht geändert.
|
||||
|
||||
Ein fehlgeschlagener Updater bei unverändertem Commit liefert weiterhin Exitcode
|
||||
1. Ein ungesunder Gateway löst weiterhin den vorhandenen Rollback aus. Der
|
||||
separate llama-Endpunkt wird unabhängig geprüft; dessen Ausfall führt nicht zu
|
||||
einem Hermes-Rollback.
|
||||
|
||||
## Nur den korrigierten Wrapper installieren
|
||||
|
||||
Im Repository-Verzeichnis ausführen, wenn gerade kein Update läuft. Vorher
|
||||
laufende Hermes-Aufgaben beenden lassen. Diese gezielte Installation ersetzt nur
|
||||
den Wrapper; `./install.sh` installiert dagegen alle versionierten Wrapper/Units.
|
||||
|
||||
```bash
|
||||
make test
|
||||
backup_dir="$(mktemp -d "$HOME/.local/state/hermes-update-wrapper.XXXXXXXX")"
|
||||
cp -p ~/.local/libexec/hermes-stack-hermes-update "$backup_dir/"
|
||||
printf 'Wrapper-Sicherung: %s\n' "$backup_dir"
|
||||
install -m 0755 libexec/hermes-stack-hermes-update \
|
||||
~/.local/libexec/hermes-stack-hermes-update
|
||||
cmp libexec/hermes-stack-hermes-update \
|
||||
~/.local/libexec/hermes-stack-hermes-update
|
||||
```
|
||||
|
||||
Die Unit bleibt unverändert; dafür ist kein `daemon-reload` erforderlich.
|
||||
Die vorherige Wrapper-Version liegt im ausgegebenen bzw. in `$backup_dir`
|
||||
gespeicherten Sicherungsverzeichnis. Bei Bedarf dieselbe Datei mit `install
|
||||
-m 0755` zurückkopieren, während kein Update läuft.
|
||||
|
||||
## Manueller Lauf und Diagnose
|
||||
|
||||
Der Lauf kann den Gateway kurz stoppen und neu starten. Er erzeugt nach dem
|
||||
Remote-Vergleich gegebenenfalls auch dann ein Backup, wenn Stable bereits aktuell
|
||||
ist. Standardmäßig werden die drei neuesten Hermes-Backups behalten.
|
||||
|
||||
```bash
|
||||
systemctl --user start hermes-stack-hermes-update.service
|
||||
systemctl --user show hermes-stack-hermes-update.service \
|
||||
-p ActiveState -p SubState -p Result -p ExecMainStatus
|
||||
systemctl --user is-active hermes-gateway.service
|
||||
journalctl --user -u hermes-stack-hermes-update.service -n 100 --no-pager
|
||||
journalctl --user -t hermes-stack-maintenance -n 100 --no-pager
|
||||
```
|
||||
|
||||
Erwartet: `Result=success`, `ExecMainStatus=0` und ein aktiver Gateway. Als
|
||||
`Type=oneshot` ist der Update-Dienst nach erfolgreichem Abschluss selbst wieder
|
||||
`inactive/dead`. Die Meldung `Hermes ist im konfigurierten Update-Kanal bereits
|
||||
aktuell` bestätigt den erfolgreichen Lauf ohne Versionswechsel.
|
||||
|
||||
Updater-Ausgaben liegen zusätzlich unter
|
||||
`~/.local/state/hermes-stack-maintenance/hermes-update-*.log`.
|
||||
Wrapper-Abschlussmeldungen stehen im Dienstjournal; die Benachrichtigung schreibt
|
||||
zusätzlich unter dem Tag `hermes-stack-maintenance`.
|
||||
|
||||
Die Abnahme auf Archwork und ihre Prüfgrenzen stehen in
|
||||
[VALIDATION.md](VALIDATION.md).
|
||||
@@ -0,0 +1,93 @@
|
||||
# Lokale Inferenz begrenzen
|
||||
|
||||
Die Workstation verwendet `hermes-local` als stabilen API-Modellnamen. 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`, `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
|
||||
Modellpfade. `scripts/prepare-workstation.py` prüft bzw. lädt die fest versionierten
|
||||
Modelldateien und erzeugt fehlende Profile. Die vollständige Neuinstallation ist
|
||||
in [NEW_WORKSTATION.md](NEW_WORKSTATION.md) beschrieben. Der normale Installer
|
||||
allein lädt keine Gewichte und erzeugt diese Profile nicht.
|
||||
|
||||
Die alten Aliasse `qwen38-27b-local` und `qwen36-27b-local` bleiben als
|
||||
Kompatibilitätsnamen für bestehende Sitzungen erreichbar. Sie bezeichnen dann
|
||||
ebenfalls das aktiv ausgewählte Modell; die tatsächliche Modelldatei zeigt
|
||||
`/props.model_path`. Neue Sitzungen und entbundene Cronjobs verwenden `hermes-local`.
|
||||
|
||||
## Kontextgröße und Kompression
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Für eine bestehende Installation genügt nach Abschluss laufender Aufträge:
|
||||
|
||||
```bash
|
||||
hermes-stack-model routine # 9B, 64K
|
||||
hermes-stack-model fallback # 4B, 64K inklusive Kompressionsschwelle
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- Der Runner setzt `--n-predict 2048` als Serverstandard. Das ist kein harter
|
||||
Schutz gegen abweichende Clientparameter.
|
||||
- In Hermes bekommt der lokale Custom-Provider `extra_body.max_tokens: 2048`,
|
||||
`t_max_predict_ms: 540000` und ein Request-Timeout von 600 Sekunden. Der native
|
||||
Generierungszeitgeber von llama.cpp prüft nur bei Zeilenumbrüchen und ersetzt
|
||||
deshalb keine unabhängige Überwachung. `agent.run_budget_seconds: 1800` begrenzt
|
||||
einen ganzen Auftrag einschließlich mehrerer Modellaufrufe und Wiederholungen.
|
||||
- `hermes-stack-llama-watchdog.timer` prüft alle 15 Sekunden `/slots`. Bleibt
|
||||
dieselbe Anfrage 600 Sekunden beobachtet aktiv oder überschreitet sie 2048
|
||||
Tokens, startet die Überwachung den Hauptmodellserver neu. Dieses letzte
|
||||
Auffangnetz hat eine Verzögerung von bis zu einem Prüfintervall nach erstmaliger
|
||||
Beobachtung. Die Tokenzahl kann zwischen zwei Prüfungen überschritten werden.
|
||||
|
||||
Der Watchdog startet keine absichtlich gestoppten Dienste. Einzelne fehlgeschlagene
|
||||
Proben führen nicht zum Neustart; erst 600 Sekunden zusammenhängende Fehler bei
|
||||
demselben aktiven Server. Wartung, Modellwechsel und Updater verwenden dieselbe
|
||||
Sperre. Neustarts haben mindestens 600 Sekunden Abstand. Ein Bootwechsel setzt
|
||||
monotone Zeitstempel zurück. Der Status speichert keine Prompts oder Antworten;
|
||||
Diagnosen stehen ausschließlich im lokalen Journal, ohne Chatbenachrichtigung.
|
||||
|
||||
Die Grenzen stehen in `config.env` (Watchdog), `llama.env` (Serverstandard) und
|
||||
`~/.hermes/config.yaml` (Hermes). Bei Änderungen diese Werte gemeinsam pflegen.
|
||||
Grenzen gelten pro Hauptmodell; das CPU-Kompressionsmodell bleibt getrennt.
|
||||
|
||||
## Prüfung und Rücknahme
|
||||
|
||||
```
|
||||
systemctl --user status hermes-stack-llama-watchdog.timer
|
||||
journalctl --user -u hermes-stack-llama-watchdog.service
|
||||
cat ~/.local/state/hermes-stack-maintenance/llama-watchdog.json
|
||||
```
|
||||
|
||||
Die Bereitstellung sichert vorherige Konfigurationen unter
|
||||
`~/.local/state/hermes-stack-maintenance/inference-limits-*`. Ein Rückwechsel
|
||||
auf `large` behält die Schutzmaßnahmen bei. Zum vollständigen Rückbau Timer
|
||||
deaktivieren, die gesicherten Konfigurationen wiederherstellen und Modellserver
|
||||
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-9B](https://huggingface.co/Qwen/Qwen3.5-9B),
|
||||
[verwendete GGUF-Dateien](https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/tree/3885219b6810b007914f3a7950a8d1b469d598a5).
|
||||
+16
-1
@@ -1,5 +1,9 @@
|
||||
# Migration auf eine neue Arch-Linux-Maschine
|
||||
|
||||
Für den aktuellen Routine-/Large-Betrieb samt Limits zuerst
|
||||
[NEW_WORKSTATION.md](NEW_WORKSTATION.md) verwenden. Die folgenden Abschnitte
|
||||
ergänzen die Übernahme persönlicher Daten und die ältere manuelle Modellkonfiguration.
|
||||
|
||||
Das Gitea-Repository reproduziert Programme, Units und Wartungslogik. Persönliche
|
||||
Hermes-Daten, Secrets, Signal-Geräteidentität und Modellgewichte werden separat
|
||||
übertragen.
|
||||
@@ -56,7 +60,16 @@ betreiben. Für Parallelbetrieb die neue Maschine als separates Gerät verknüpf
|
||||
inklusive CLI und Benchmark) oder vorhandene Binary verwenden. Die GGUF-Datei
|
||||
und einen ggf. benötigten Vision-Projektor übertragen oder beim ersten Start
|
||||
über HF beziehen. Danach
|
||||
`~/.config/hermes-stack-maintenance/llama.env` anpassen. Modellgewichte gehören
|
||||
`~/.config/hermes-stack-maintenance/llama.env` anpassen.
|
||||
|
||||
Für eine Workstation mit Desktop auf derselben GPU `desktop-64k` (64K,
|
||||
4096 MiB Fit-Zielmarge) als Ausgangspunkt verwenden. Den optionalen lokalen
|
||||
Kompressionsserver über `compression.env` auf CPU konfigurieren und seine
|
||||
GGUF-Datei separat übertragen. Die vollständige Einrichtung und Abnahme steht
|
||||
unter [Desktop-Reserve und CPU-Kompression](RTX3080.md#desktop-reserve-und-cpu-kompression-seit-12102026).
|
||||
Auch die eigenen `~/.config/hermes-stack-maintenance/`-Dateien sichern und
|
||||
gezielt übertragen; lokale Pfade, Binary-Auswahl und Profil-Override auf der
|
||||
neuen Maschine prüfen. Keine vollständige systemd-Konfiguration blind kopieren. Modellgewichte gehören
|
||||
wegen Größe und Lizenz nicht in dieses Repository.
|
||||
|
||||
Die RTX-3080-Profile sind für 10 GB VRAM und die bisherige CPU ausgelegt; auf
|
||||
@@ -70,6 +83,8 @@ NVIDIA-/Kernelpakete. Keine Binärkopie des alten CUDA-Builds erzwingen.
|
||||
hermes gateway install --force
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now signal-cli.service llama-qwen.service
|
||||
# Nur nach Einrichtung der optionalen Kompression:
|
||||
systemctl --user enable --now llama-compression.service
|
||||
systemctl --user enable --now \
|
||||
hermes-stack-hermes-update.timer \
|
||||
hermes-stack-health.timer \
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
# Aktuelles Workstation-Setup neu installieren
|
||||
|
||||
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
|
||||
|
||||
Ziel ist Arch Linux mit systemd-User-Sitzung, einer funktionierenden NVIDIA-/CUDA-
|
||||
Installation und ausreichend RAM und VRAM. Die kurze Abnahme erfolgte auf einer
|
||||
RTX 3080 mit 10 GB VRAM und 64 GB RAM. Der Installer richtet keine Grafiktreiber
|
||||
ein. Für andere GPUs muss der CUDA-Build angepasst und die Speicherbelegung
|
||||
neu geprüft werden. Das Routineprofil erzwingt vollständigen GPU-Offload.
|
||||
|
||||
```bash
|
||||
git clone https://gitea.jr-it-services.de/johannes/hermes-arch-stack.git
|
||||
cd hermes-arch-stack
|
||||
./install.sh --install-packages --install-hermes
|
||||
```
|
||||
|
||||
Für einen CUDA-Quellbuild des hier geprüften llama.cpp-Standes:
|
||||
|
||||
```bash
|
||||
sudo pacman -Syu --needed base-devel cmake ninja cuda openssl
|
||||
./scripts/install-llama-cpp.sh \
|
||||
--ref d81235049384534c167caea52b85a694f6103d14 --cuda-arch 86
|
||||
```
|
||||
|
||||
`86` ist für die RTX 3080, auf anderer Hardware die passende Architektur setzen.
|
||||
Alternativ eine vorhandene kompatible `llama-server`-Binary verwenden; später
|
||||
beim Vorbereiten `--server-bin /usr/bin/llama-server` ergänzen. Der Quellbuild
|
||||
setzt einen neuen Compiler/CUDA-Stand voraus und wurde hier nicht erneut gebaut.
|
||||
|
||||
Das Deployment installiert `hermes-stack-model` nach `~/.local/bin`. Falls dieses
|
||||
Verzeichnis in einer neuen Shell noch nicht im PATH steht, einmal in die eigene
|
||||
Shell-Konfiguration aufnehmen und eine neue Shell starten:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
## 2. Eigene Hermes-Daten und Plattformen
|
||||
|
||||
Bei einer neuen Hermes-Installation zuerst `hermes setup` ausführen. Für die
|
||||
Übernahme der vorhandenen Chats, Skills, Plugins, Cronjobs und Zugangsdaten
|
||||
stattdessen die privaten Daten gemäß [MIGRATION.md](MIGRATION.md) wiederherstellen.
|
||||
Das Repository enthält diese Daten nicht. Die gesamte persönliche Konfiguration
|
||||
kann aus dem Quellcode allein nicht wiederhergestellt werden.
|
||||
|
||||
Vorbereitung auf der alten Maschine: Gateway und Maintenance-Timer kontrolliert
|
||||
anhalten und die privaten Hermes-Daten, `~/.config/hermes-stack-maintenance/`,
|
||||
gegebenenfalls `~/HermesWorkspace/` und die Signal-Geräteidentität verschlüsselt
|
||||
sichern. Auch private lokale Plugins und eigene Skills berücksichtigen.
|
||||
Signal auf dem neuen Rechner separat verknüpfen oder die Geräteidentität nur
|
||||
auf einem Rechner gleichzeitig betreiben. Secrets gehören nicht nach Gitea.
|
||||
|
||||
Nach dem Import absolute Pfade auf alten Benutzernamen prüfen. Besonders
|
||||
`terminal.docker_volumes` und `HERMES_REPO`/`HERMES_ENV` können noch auf das alte
|
||||
Home zeigen. Private Konfigurationsdateien gezielt bearbeiten und nicht blind
|
||||
alte systemd-Units samt Drop-ins über die neue Installation kopieren.
|
||||
|
||||
Wenn das bisherige Docker-Terminal übernommen wird, Docker installieren und
|
||||
starten, dem Benutzer die erforderlichen Docker-Rechte geben und neu anmelden.
|
||||
Das eigene Arbeitsverzeichnis anlegen, das Netzwerk `hermes-net` bereitstellen
|
||||
und das in der persönlichen Hermes-Konfiguration angegebene Container-Image
|
||||
laden. Die alten DNS-Adressen sind nur im ursprünglichen Netzwerk gültig.
|
||||
Der Modellhelfer verändert Terminal-, Netzwerk-, Plugin- und Auth-Einstellungen
|
||||
nicht.
|
||||
|
||||
## 3. Modelle und Schutzmechanismen vorbereiten
|
||||
|
||||
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,fallback \
|
||||
--select routine --configure-hermes --unpin-local-cron
|
||||
```
|
||||
|
||||
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 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-
|
||||
Übernahme. `--model-dir /anderer/absoluter/pfad` unterstützt eine andere Ablage.
|
||||
|
||||
`config/models.lock.json` hält für jede Datei Repository, vollständige Revision,
|
||||
Dateigröße und SHA-256 fest. Downloads erfolgen ausschließlich von der fixierten
|
||||
Revision, werden zunächst als `.part` gespeichert und erst nach erfolgreicher
|
||||
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-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.
|
||||
Soll nach einer Migration ein Modellpfad geändert werden, vorhandene Profile
|
||||
vorher gezielt sichern und anpassen oder zur Neuerzeugung aus dem Konfigurations-
|
||||
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`
|
||||
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
|
||||
Hermes-Backup. Details und Grenzen des Watchdogs: [INFERENCE_LIMITS.md](INFERENCE_LIMITS.md).
|
||||
|
||||
## 4. Prüfen und aktivieren
|
||||
|
||||
```bash
|
||||
~/.local/libexec/hermes-stack-llama-run --check
|
||||
HERMES_LLAMA_CONFIG="$HOME/.config/hermes-stack-maintenance/compression.env" \
|
||||
~/.local/libexec/hermes-stack-llama-run --check
|
||||
hermes config check
|
||||
hermes gateway install --force
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now llama-qwen.service
|
||||
systemctl --user enable --now llama-compression.service
|
||||
# Nach Einrichtung von signal-cli und dessen Konfiguration:
|
||||
# systemctl --user enable --now signal-cli.service
|
||||
systemctl --user enable --now hermes-gateway.service
|
||||
systemctl --user enable --now \
|
||||
hermes-stack-llama-watchdog.timer \
|
||||
hermes-stack-health.timer \
|
||||
hermes-stack-hermes-update.timer
|
||||
```
|
||||
|
||||
Bei einer Vorbereitung ohne Kompressionsmodell dessen Prüfung und Dienst
|
||||
weglassen. Signal-Wartungstimer erst nach der Signal-Einrichtung aktivieren.
|
||||
Ein lokaler Modellserver kann beim Laden kurz HTTP 503 liefern.
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:8080/health
|
||||
hermes-stack-model status
|
||||
systemctl --user status hermes-stack-llama-watchdog.timer
|
||||
~/.local/libexec/hermes-stack-doctor
|
||||
```
|
||||
|
||||
Danach eine deutsche Textantwort, ein Bild und einen Werkzeugaufruf in Hermes
|
||||
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.
|
||||
|
||||
## Alltag und Rückweg
|
||||
|
||||
```bash
|
||||
hermes-stack-model # aktives Modell anzeigen
|
||||
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` 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.
|
||||
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
|
||||
`sudo loginctl enable-linger "$USER"` setzen. Vollständige Rücknahme und Sicherungs-
|
||||
orte stehen in [INFERENCE_LIMITS.md](INFERENCE_LIMITS.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.
|
||||
+105
-7
@@ -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
|
||||
@@ -9,17 +12,22 @@ helfen; weder Voll-Offload noch eine bestimmte Beschleunigung werden versprochen
|
||||
|
||||
| Profil | Kontextlimit | Fit-Zielmarge | Einsatz |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `baseline` | 65536 | 1536 MiB | bisherige Startparameter, Referenz und Rückweg |
|
||||
| `desktop-64k` | 65536 | 4096 MiB | Standard für Neuinstallationen mit Desktop auf derselben GPU |
|
||||
| `baseline` | 65536 | 1536 MiB | historische Vergleichsparameter |
|
||||
| `vram-64k` | 65536 | 1280 MiB | optionaler Vergleich, kein Standard |
|
||||
| `balanced` | 49152 | 1536 MiB | erster Vergleich, nur Kontext reduzieren |
|
||||
| `vram-plus` | 49152 | 1280 MiB | zweiter Vergleich, kleinere Zielmarge |
|
||||
| `compact` | 32768 | 1536 MiB | Alternative für kürzere Sitzungen |
|
||||
|
||||
Alle Profile: ein Slot, K/V-Cache q8_0, Flash Attention auto, GPU-Layer auto,
|
||||
Fit an, 6 Generierungs-/16 Batch-Threads, Jinja, Reasoning auto/512/preserve,
|
||||
Gemeinsam: ein Slot, K/V-Cache q8_0, Flash Attention auto, GPU-Layer auto,
|
||||
Fit an, 6 Generierungs-/16 Batch-Threads, Jinja, Reasoning auto,
|
||||
Image-Minimum 1024 Tokens und Idle-Sleep nach 300 Sekunden. Modellalias bleibt
|
||||
`qwen38-27b-local`. Es werden keine Sampling-Werte oder Hermes-Memories geändert.
|
||||
|
||||
`desktop-64k` verwendet Reasoning-Budget 128 ohne Preserve wie die am
|
||||
12.10.2026 geprüfte Workstation. Die älteren Vergleichsprofile behalten 512 mit
|
||||
Preserve; sie ändern neben der Speicherplanung auch diesen Parameter.
|
||||
|
||||
Die Definitionen stehen zentral in `libexec/hermes-stack-llama-run`.
|
||||
Die Konfiguration und Optionsnamen werden anhand der installierten Binary
|
||||
geprüft. Nicht unterstützte Optionen führen zum Abbruch, nicht zum stillen
|
||||
@@ -35,14 +43,15 @@ hier bewusst nicht als fertiges Profil vorgesehen.
|
||||
|
||||
## Messung statt Auslastungsziel
|
||||
|
||||
1. Zuerst `baseline` mit deiner normalen Browser-/Hyprland-Nutzung messen.
|
||||
1. Zuerst `desktop-64k` mit deiner normalen Browser-/Hyprland-Nutzung messen.
|
||||
2. Nur bei erneutem Tuning: Jobs beenden, ein Vergleichsprofil wählen
|
||||
und kontrolliert neu starten. Für 64K Kontext ist dies `vram-64k`.
|
||||
3. Dieselben Aufgaben erneut ausführen: kurze Textantwort, längerer Agent-Task
|
||||
mit Tools und eine repräsentative Bildaufgabe. Qualität mitbewerten.
|
||||
4. Kalten Start nach Idle-Sleep getrennt von warmem Prompt-Cache messen.
|
||||
5. Erst bei genügend Spielraum `vram-plus` probieren. Bei Rucklern, OOM,
|
||||
Grafikfehlern oder deutlich schlechterer Latenz zurück auf `baseline`.
|
||||
Grafikfehlern wieder die größere Reserve von `desktop-64k` verwenden.
|
||||
Latenz und Qualität gegen die eigenen Anforderungen prüfen.
|
||||
|
||||
```bash
|
||||
~/.local/libexec/hermes-stack-llama-profile list
|
||||
@@ -80,10 +89,10 @@ zwingend automatisch. Lange alte Sitzungen nicht ungeprüft fortsetzen; eine
|
||||
frische Sitzung für den Vergleich verwenden. Das Deployment überschreibt deine
|
||||
`config.yaml` bewusst nicht.
|
||||
|
||||
## Rückkehr
|
||||
## Zur größeren Desktop-Reserve zurückkehren
|
||||
|
||||
```bash
|
||||
~/.local/libexec/hermes-stack-llama-profile baseline
|
||||
~/.local/libexec/hermes-stack-llama-profile desktop-64k
|
||||
~/.local/libexec/hermes-stack-llama-restart --jobs-finished
|
||||
```
|
||||
|
||||
@@ -98,3 +107,92 @@ für Fit-Marge, Kontext, Cache und Idle-Sleep; geprüft am 2026-09-05. Maßgebli
|
||||
für deinen Rechner bleibt `llama-server --help` seiner tatsächlich installierten
|
||||
Version. Die Profile übernehmen die von dir gezeigten Modellparameter; sie
|
||||
sind noch kein auf deiner GPU gemessener Optimalwert.
|
||||
|
||||
## Desktop-Reserve und CPU-Kompression seit 12.10.2026
|
||||
|
||||
Auf der RTX 3080 mit NVIDIA Open 615.78.08 standen vor den GUI-Abstürzen
|
||||
NVKMS-Allokationsfehler und `NV_ERR_NO_MEMORY` im Kerneljournal; danach folgten
|
||||
anhaltende GSP-Zustandsfehler und Abstürze neuer GPU-Anwendungen. VRAM-Druck ist
|
||||
belegt, die vollständige Kausalkette zum Treiberfehler nicht. Eine spätere
|
||||
Momentaufnahme mit freiem Speicher schließt frühere Lastspitzen nicht aus.
|
||||
|
||||
Die aktive Workstation-Konfiguration wurde auf 4096 MiB Fit-Zielmarge geändert.
|
||||
Das Kompressionsmodell bekam zusätzlich zu `--n-gpu-layers 0` die Optionen
|
||||
`--device none --no-op-offload --no-kv-offload`. Nur null GPU-Layer hatte zuvor
|
||||
noch etwa 700 MiB VRAM belegt. `MemoryHigh`/`MemoryMax` begrenzen RAM, nicht VRAM.
|
||||
|
||||
Kurze produktive Abnahme am 12.10.2026: CPU-Kompression antwortet, Hauptmodell
|
||||
verarbeitet einen Text mit 984 Eingabetokens und erkennt ein synthetisches
|
||||
96×96-Bild. Mindestens 4096 MiB VRAM frei während dieser Tests; keine neuen
|
||||
NVIDIA-/CUDA-OOM-Fehler im Prüfzeitraum. Kein Nachweis langfristiger
|
||||
Treiberstabilität, maximaler Kontext-/Bildlast oder einer bestimmten Leistung.
|
||||
Das Hauptmodell behält 64K Kontext und GPU-Visionprojektor. Bei weiterem
|
||||
Speicherdruck zuerst Reserve messen; optional `--no-mmproj-offload` prüfen,
|
||||
Kontextkürzungen mit Hermes abstimmen. Mehr CPU-Offload kann langsamer sein.
|
||||
|
||||
### Auf einer neuen Workstation einrichten
|
||||
|
||||
Nach `./install.sh` und Bereitstellung der Modellgewichte:
|
||||
|
||||
1. In `llama.env` Binary/Modell/Projektor setzen; `LLAMA_PROFILE="desktop-64k"`
|
||||
und `LLAMA_ARGS=()` verwenden. Ein eventuell aus dem alten Rechner kopiertes
|
||||
`llama-profile.env` kann die Profilwahl überschreiben. Über den Profilhelfer
|
||||
bewusst `desktop-64k` wählen. Die lokale Modellvorlage verwendet `$HOME`.
|
||||
2. In `compression.env` CPU-Binary bzw. die gemeinsame CUDA-Binary, Modellpfad
|
||||
und Port prüfen. Die CUDA-Binary kann mit `--device none` auf CPU rechnen.
|
||||
3. Beide Konfigurationen ohne Modellstart prüfen:
|
||||
|
||||
```bash
|
||||
~/.local/libexec/hermes-stack-llama-run --check
|
||||
HERMES_LLAMA_CONFIG="$HOME/.config/hermes-stack-maintenance/compression.env" \
|
||||
~/.local/libexec/hermes-stack-llama-run --check
|
||||
```
|
||||
|
||||
4. Hermes' Hauptmodell auf `http://127.0.0.1:8080/v1`, Alias
|
||||
`qwen38-27b-local`, und den separaten Kompressionsanbieter auf
|
||||
`http://127.0.0.1:8081/v1`, Alias `qwen3-1.7b-compression`, konfigurieren.
|
||||
Bestehende `config.yaml` übernehmen und mit der installierten Hermes-Version
|
||||
abgleichen; keine Zugangsdaten oder allgemeine Modellantworten überschreiben.
|
||||
5. Erst ohne laufende Backend-Aufträge aktivieren:
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now llama-compression.service llama-qwen.service
|
||||
```
|
||||
|
||||
Bei bereits laufenden Diensten aktiviert `enable --now` keine neuen
|
||||
Einstellungen: nach Abschluss aller Anfragen gezielt neu starten.
|
||||
|
||||
6. Mit normalem Browser-/Desktopbetrieb Text- und Bildanfragen prüfen und
|
||||
`nvidia-smi` im Sekundentakt beobachten. Für diese 10-GB-Klasse mindestens
|
||||
etwa 3 GiB tatsächlich freie Reserve unter den eigenen Aufgaben anstreben;
|
||||
4096 MiB Fit-Zielmarge ist nur der Ausgangspunkt. CPU-Kompression sollte
|
||||
nicht als NVIDIA-Verbraucher erscheinen. Journal auf neue CUDA-OOM-, NVKMS-
|
||||
und GSP-Fehler prüfen. Aufwachen nach Idle-Sleep separat testen.
|
||||
|
||||
Die Unit-Vorlage verwendet den üblichen Konfigurationspfad unter `~/.config`.
|
||||
Bei abweichendem `XDG_CONFIG_HOME` den Environment-Pfad in der Unit ebenfalls
|
||||
anpassen. Auch vorhandene `ExecStart`-Drop-ins vor der Übernahme prüfen.
|
||||
|
||||
Der Installer installiert den Kompressionsdienst, aktiviert ihn aber nicht und
|
||||
lädt keine Kompressionsgewichte herunter. Fehlende `compression.env` wird
|
||||
angelegt, eine bestehende Datei bleibt erhalten. Die alte Workstation nutzt
|
||||
aktuell explizite Argumente im Hauptmodell und eine direkte Kompressions-Unit;
|
||||
diese portable Repo-Variante bildet die wirksamen Optionen ab. Ein erneuter
|
||||
Installerlauf ersetzt Units/Wrapper nach Sicherung, startet aber keine laufenden
|
||||
Modellserver neu. Für bestehende Setups deshalb zuerst sichern und den geplanten
|
||||
Dienstwechsel ohne laufende Anfragen durchführen.
|
||||
|
||||
### Rücknahme auf einer Zielmaschine
|
||||
|
||||
Vor Änderungen `llama.env`, `llama-profile.env` (falls vorhanden),
|
||||
`compression.env` und bestehende Units/Drop-ins sichern. Die Installer-Sicherung
|
||||
enthält ersetzte Units/Wrapper; unveränderte Konfigurationen separat sichern.
|
||||
Bei Rücknahme die betroffenen Dateien gezielt wiederherstellen, `daemon-reload`
|
||||
ausführen und die beiden Server nach Abschluss der Anfragen neu starten.
|
||||
Die früheren Profile sind Vergleichsmöglichkeiten, kein bewiesener stabiler
|
||||
Rückfallweg bei Speicherdruck.
|
||||
|
||||
Quellen: [llama.cpp: Serveroptionen](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md),
|
||||
[ähnlicher NVIDIA-GSP-Bericht](https://github.com/NVIDIA/open-gpu-kernel-modules/issues/1408).
|
||||
Der dort genannte Display-Wakeup-Auslöser ist lokal nicht bewiesen.
|
||||
@@ -1,3 +1,55 @@
|
||||
# Validierung vom 2026-10-23 — Kontextabgleich
|
||||
|
||||
- `make test`: Syntax-/Secret-Musterprüfung und **52 Funktionstests bestanden**
|
||||
(26 llama-, zehn Watchdog-, elf Workstation- und fünf Updater-Tests).
|
||||
ShellCheck fehlt lokal und wurde nicht ausgeführt.
|
||||
- Regression prüft Ersatz eines veralteten 256.000-Token-Werts, Abgleich mit
|
||||
beiden 65.536-Token-Hauptmodellprofilen, Erhalt der 24.000-Token-
|
||||
Kompressionsschwelle und unveränderten zweiten Konfigurationslauf.
|
||||
- `git diff --check`: erfolgreich.
|
||||
- Auf Archwork ausschließlich `model.context_length: 65536` ergänzt;
|
||||
semantischer YAML-Vergleich bestätigt, dass keine andere Einstellung geändert
|
||||
wurde. Private Sicherung unter
|
||||
`~/.local/state/hermes-stack-maintenance/context-64k-20261023-144440/config.yaml`.
|
||||
- Gateway kontrolliert neu gestartet; Hauptmodell und CPU-Kompression ohne
|
||||
Neustart. `hermes config get model.context_length` liefert `65536`.
|
||||
- Neue lokale Hermes-Sitzung über den Gateway antwortet korrekt mit
|
||||
`HERMES_OK 323`. Seit der Aktivierung keine erneute Kontext-Fallback-Warnung
|
||||
im Hermes-Fehlerprotokoll. Bestehende Plugin-/Docker-Warnungen bleiben sichtbar.
|
||||
- Kein langer 64K-Dialog, keine erzwungene Kompression und kein Test des großen
|
||||
Modells für diese Änderung; der Live-Nachweis gilt für die neue kurze Sitzung
|
||||
mit dem aktiven Routineprofil.
|
||||
|
||||
# Validierung vom 2026-10-21
|
||||
|
||||
- `make test`: Syntax- und Secret-Musterprüfung sowie **31 Funktionstests
|
||||
bestanden** (26 vorhandene Tests und fünf neue Updater-Regressionstests).
|
||||
- Neue isolierte Tests: unveränderter Stable-Commit bei neuerem `main`,
|
||||
fehlgeschlagener Updater ohne Commitwechsel, erfolgreicher Commitwechsel,
|
||||
Commitwechsel mit Updater-Restartwarnung und Gateway-Rollback.
|
||||
Externe Programme werden simuliert; kein echtes Netzwerk oder systemd darin.
|
||||
- `git diff --check`: erfolgreich. ShellCheck ist lokal nicht installiert.
|
||||
- Korrigierter Wrapper auf Archwork installiert und bytegleich geprüft.
|
||||
- Echter Update-Dienstlauf am 21.10.2026, 16:26 MESZ: Stable v0.22.3 blieb auf
|
||||
Commit `5b147ab036d6d19131bff8d20701b5001eab844a`; Abschluss nach rund 47 Sekunden
|
||||
mit `Result=success` und `ExecMainStatus=0`. Gateway danach `active/running`.
|
||||
Die neue Erfolgsmeldung erschien im Journal des Update-Dienstes.
|
||||
- Ein echter Versionswechsel und ein echter Rollback wurden bei dieser Abnahme
|
||||
nicht ausgelöst. Ein vollständiger Installer-Lauf wurde nicht durchgeführt.
|
||||
|
||||
# Validierung vom 2026-10-12
|
||||
|
||||
- `make test`: Shell-Syntaxprüfung, Secret-Mustercheck und **26 isolierte
|
||||
Funktionstests bestanden**, einschließlich separater CPU-Kompressionskonfiguration.
|
||||
- `git diff --check`: erfolgreich.
|
||||
- Neue Profil- und Kompressionsvorlagen mit der installierten llama.cpp-Binary
|
||||
auf unterstützte Optionsnamen geprüft, ohne Modellstart.
|
||||
- ShellCheck ist lokal nicht installiert; weiterhin über Gitea-CI vorgesehen.
|
||||
- Die aktive Workstation-Konfiguration wurde vor der Repo-Änderung mit echten
|
||||
Text-/Bildanfragen und mindestens 4096 MiB freiem VRAM geprüft. Die portablen
|
||||
Installer-/Wrapperdateien wurden nicht erneut auf den laufenden Rechner
|
||||
installiert. Keine neue Workstation eingerichtet und kein Langzeitnachweis.
|
||||
|
||||
# Validierung vom 2026-09-05
|
||||
|
||||
Ausgeführt in einer isolierten Arbeitsumgebung, nicht auf der Workstation:
|
||||
|
||||
+45
-3
@@ -1,5 +1,13 @@
|
||||
# Bestehende Workstation behutsam übernehmen
|
||||
|
||||
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.
|
||||
Das aktuelle Deployment enthält deine gezeigten Anpassungen; weitere lokale,
|
||||
@@ -60,7 +68,7 @@ LLAMA_ALIAS="qwen38-27b-local"
|
||||
LLAMA_MMPROJ=""
|
||||
LLAMA_HOST="127.0.0.1"
|
||||
LLAMA_PORT="8080"
|
||||
LLAMA_PROFILE="baseline"
|
||||
LLAMA_PROFILE="desktop-64k"
|
||||
LLAMA_ARGS=()
|
||||
```
|
||||
|
||||
@@ -90,7 +98,8 @@ der neue Standard: HTTP-Probleme werden gemeldet, nicht durch Neustarts behandel
|
||||
## 3. Optimieren und zurücksetzen
|
||||
|
||||
Nun gemäß [RTX3080.md](RTX3080.md) jeweils nur ein Profil vergleichen. Zurück:
|
||||
Profil `baseline` wählen und kontrolliert neustarten. Für vollständige Rückkehr
|
||||
Bei Speicherdruck die größere Reserve von `desktop-64k` verwenden. Die
|
||||
früheren Vergleichsprofile bieten keine Stabilitätsgarantie. Für vollständige Rückkehr
|
||||
alte llama.env und ggf. alte Unit/Wrapper aus der Deployment-Sicherung einzeln
|
||||
wiederherstellen, `daemon-reload` und Neustart nach Jobende. Nicht pauschal ein
|
||||
Archiv über dein gesamtes Home extrahieren. Die Datei `llama-profile.env` hat nur
|
||||
@@ -98,7 +107,7 @@ bei aktivem Profilbetrieb Vorrang; ihre vorherigen Stände liegen daneben.
|
||||
|
||||
## Workstation-Abgleich vom 2026-09-09
|
||||
|
||||
Alltagsprofil bleibt baseline: 65536 Kontext, Fit-Zielmarge 1536 MiB.
|
||||
Historischer Stand (durch den Abgleich vom 12.10.2026 abgelöst): baseline, 65536 Kontext, Fit-Zielmarge 1536 MiB.
|
||||
Die zwei kurzen Texttests vom 5. September ergaben im Mittel rund 2,58 tok/s
|
||||
für baseline und 2,62 tok/s für vram-64k. Das belegt keinen belastbaren
|
||||
Geschwindigkeitsvorteil. Im fünfminütigen Desktop-/Bildtest mit vram-64k
|
||||
@@ -126,3 +135,36 @@ Ein echter Update-/Rollback-Lauf der neuen Fassung war damit noch nicht geprüft
|
||||
Modell und Projektor liegen jetzt außerhalb von ~/.cache. Die korrigierten
|
||||
Cleanup-Skripte sind unter ~/Scripts installiert. Details und Prüfgrenzen:
|
||||
[CLEANUP_RECOVERY.md](CLEANUP_RECOVERY.md).
|
||||
|
||||
|
||||
## Workstation-Abgleich vom 2026-10-12
|
||||
|
||||
Nach NVIDIA-Speicherallokationsfehlern und GUI-Abstürzen wurde der aktive
|
||||
Hauptserver auf 4096 MiB Fit-Zielmarge bei unverändert 65536 Kontext umgestellt.
|
||||
Die aktive `llama.env` enthält dafür explizite `LLAMA_ARGS` bei leerem
|
||||
`LLAMA_PROFILE`; Profilwechsel wirken dort erst nach bewusster Umstellung auf
|
||||
Profilbetrieb. Reasoning: auto, Budget 128, kein Preserve.
|
||||
|
||||
Die bestehende Kompressions-Unit wurde um `--device none --no-op-offload
|
||||
--no-kv-offload` ergänzt. Der vorher mit null GPU-Layern weiterhin belegte
|
||||
VRAM wurde freigegeben. Kurze Text-/Bildtests bestanden mit mindestens 4096 MiB
|
||||
freiem VRAM und ohne neue NVIDIA-/CUDA-OOM-Fehler. Langzeitstabilität bleibt offen.
|
||||
|
||||
Diese aktiven Änderungen wurden separat installiert. Die neuen portablen
|
||||
Repo-Dateien wurden danach aufgenommen und getestet, aber nicht erneut über
|
||||
`install.sh` auf die Workstation ausgerollt. Der aktuelle Betrieb bleibt bei den
|
||||
bereits geprüften Einstellungen. Details zur Übertragung und zu bestehenden
|
||||
Units/Drop-ins stehen in [RTX3080.md](RTX3080.md).
|
||||
|
||||
## Hermes-Update-Korrektur vom 2026-10-21
|
||||
|
||||
Der korrigierte `hermes-stack-hermes-update` ist gezielt auf der Workstation
|
||||
installiert und mit der Repository-Datei bytegleich geprüft. Der echte
|
||||
Dienstlauf mit bereits aktuellem Stable v0.22.3 endete erfolgreich
|
||||
(`Result=success`, `ExecMainStatus=0`); der Gateway war danach `active/running`.
|
||||
Abschlussmeldungen erscheinen jetzt direkt im Dienstjournal.
|
||||
|
||||
Vorherige Wrapper-Version:
|
||||
`~/.local/libexec/hermes-stack-hermes-update.before-noop-fix-20261021`.
|
||||
Ein vollständiger Installer-Lauf war für diese Korrektur nicht erforderlich.
|
||||
Betrieb und Fehlerdiagnose: [HERMES_UPDATE.md](HERMES_UPDATE.md).
|
||||
+20
-2
@@ -9,6 +9,8 @@ INSTALL_LLAMA=0
|
||||
LLAMA_REF=""
|
||||
ENABLE_TIMERS=0
|
||||
SKIP_BROWSER=1
|
||||
USER_BIN_ON_PATH=0
|
||||
[[ ":$PATH:" != *":$HOME/.local/bin:"* ]] || USER_BIN_ON_PATH=1
|
||||
|
||||
usage() {
|
||||
sed -n '/^## Installationsoptionen/,/^## /p' "$ROOT_DIR/README.md" |
|
||||
@@ -43,7 +45,7 @@ export PATH="$HOME/.local/bin:$PATH"
|
||||
if ((INSTALL_PACKAGES)); then
|
||||
# Vollständige Aktualisierung statt partieller Arch-Aktualisierung.
|
||||
packages=(
|
||||
bash git curl xz jq zstd tar ripgrep ffmpeg coreutils util-linux \
|
||||
bash git curl xz jq zstd tar ripgrep ffmpeg coreutils util-linux python python-yaml \
|
||||
jre-openjdk-headless
|
||||
)
|
||||
if ((INSTALL_LLAMA)); then
|
||||
@@ -83,9 +85,19 @@ for file in "$ROOT_DIR"/systemd/user/*; do
|
||||
done
|
||||
printf 'Vorherige Deployment-Dateien: %s\n' "$deploy_backup"
|
||||
|
||||
# Public command entry point; never depend on adding libexec to PATH.
|
||||
mkdir -p "$HOME/.local/bin" "$deploy_backup/bin"
|
||||
command_target="$HOME/.local/bin/hermes-stack-model"
|
||||
if [[ -e "$command_target" || -L "$command_target" ]]; then
|
||||
cp -a -- "$command_target" "$deploy_backup/bin/"
|
||||
fi
|
||||
ln -sfn -- ../libexec/hermes-stack-model "$command_target"
|
||||
|
||||
for file in "$ROOT_DIR"/libexec/*; do
|
||||
install -m 0755 "$file" "$libexec_dir/$(basename "$file")"
|
||||
done
|
||||
install -d -m 0755 "$HOME/.local/lib/hermes-stack"
|
||||
install -m 0644 "$ROOT_DIR/scripts/llama_watchdog.py" "$HOME/.local/lib/hermes-stack/llama_watchdog.py"
|
||||
|
||||
for file in "$ROOT_DIR"/systemd/user/*; do
|
||||
install -m 0644 "$file" "$unit_dir/$(basename "$file")"
|
||||
@@ -105,6 +117,8 @@ install_config_if_missing \
|
||||
"$ROOT_DIR/config/config.env.example" "$config_dir/config.env"
|
||||
install_config_if_missing \
|
||||
"$ROOT_DIR/config/llama.env.example" "$config_dir/llama.env"
|
||||
install_config_if_missing \
|
||||
"$ROOT_DIR/config/compression.env.example" "$config_dir/compression.env"
|
||||
install_config_if_missing \
|
||||
"$ROOT_DIR/config/signal-cli.env.example" "$config_dir/signal-cli.env"
|
||||
|
||||
@@ -119,7 +133,7 @@ systemctl --user daemon-reload
|
||||
if command -v hermes >/dev/null 2>&1 && [[ ! -e "$unit_dir/hermes-gateway.service" ]]; then
|
||||
if [[ -d "$HOME/.hermes/hermes-agent/.git" ]]; then
|
||||
hermes gateway install ||
|
||||
printf 'Gateway-Unit konnte noch nicht erzeugt werden; nach `hermes setup` erneut ausführen.\n' >&2
|
||||
printf 'Gateway-Unit konnte noch nicht erzeugt werden; nach hermes setup erneut ausführen.\n' >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -127,9 +141,13 @@ if ((ENABLE_TIMERS)); then
|
||||
systemctl --user enable --now \
|
||||
hermes-stack-hermes-update.timer \
|
||||
hermes-stack-health.timer \
|
||||
hermes-stack-llama-watchdog.timer \
|
||||
hermes-stack-signal-health.timer \
|
||||
hermes-stack-signal-update-check.timer
|
||||
fi
|
||||
|
||||
printf '\nDeployment installiert. Nächster Schritt:\n %s\n' \
|
||||
"$libexec_dir/hermes-stack-doctor"
|
||||
if ((USER_BIN_ON_PATH == 0)); then
|
||||
printf 'Für Befehle ohne Pfad ~/.local/bin in den Shell-PATH aufnehmen.\n'
|
||||
fi
|
||||
@@ -15,25 +15,25 @@ bad() { printf '✗ %s\n' "$*"; failures=$((failures + 1)); }
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
for command in git curl jq tar zstd systemctl hermes; do
|
||||
command -v "$command" >/dev/null 2>&1 && ok "$command vorhanden" || bad "$command fehlt"
|
||||
if command -v "$command" >/dev/null 2>&1; then ok "$command vorhanden"; else bad "$command fehlt"; fi
|
||||
done
|
||||
|
||||
for script in "$HOME/.local/libexec"/hermes-stack-*; do
|
||||
[[ -f "$script" ]] || continue
|
||||
bash -n "$script" && ok "Syntax: ${script##*/}" || bad "Syntaxfehler: ${script##*/}"
|
||||
if bash -n "$script"; then ok "Syntax: ${script##*/}"; else bad "Syntaxfehler: ${script##*/}"; fi
|
||||
done
|
||||
|
||||
if [[ -d "$HERMES_REPO/.git" ]]; then
|
||||
ok "Hermes-Git-Checkout vorhanden"
|
||||
dirty="$(git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
|
||||
grep -vxF '?? .install_method' || true)"
|
||||
[[ -z "$dirty" ]] && ok "Hermes-Checkout sauber" || bad "Hermes-Checkout enthält lokale Änderungen"
|
||||
if [[ -z "$dirty" ]]; then ok "Hermes-Checkout sauber"; else bad "Hermes-Checkout enthält lokale Änderungen"; fi
|
||||
else
|
||||
bad "Hermes-Git-Checkout fehlt"
|
||||
fi
|
||||
|
||||
systemctl --user is-active --quiet "$GATEWAY_SERVICE" && ok "Gateway aktiv" || bad "Gateway inaktiv"
|
||||
hermes gateway status >/dev/null 2>&1 && ok "Gateway gesund" || bad "Gateway-Status fehlgeschlagen"
|
||||
if systemctl --user is-active --quiet "$GATEWAY_SERVICE"; then ok "Gateway aktiv"; else bad "Gateway inaktiv"; fi
|
||||
if hermes gateway status >/dev/null 2>&1; then ok "Gateway gesund"; else bad "Gateway-Status fehlgeschlagen"; fi
|
||||
|
||||
if curl --fail --silent --max-time 5 "$LLAMA_URL/health" |
|
||||
jq -e '.status == "ok"' >/dev/null 2>&1; then
|
||||
@@ -51,7 +51,7 @@ set -u
|
||||
if [[ -n "${SIGNAL_HTTP_URL:-}" ]]; then
|
||||
status="$(curl --silent --output /dev/null --max-time 5 --write-out '%{http_code}' \
|
||||
"${SIGNAL_HTTP_URL%/}/api/v1/check" || true)"
|
||||
[[ "$status" =~ ^2[0-9][0-9]$ ]] && ok "signal-cli HTTP erreichbar" || bad "signal-cli HTTP nicht erreichbar"
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then ok "signal-cli HTTP erreichbar"; else bad "signal-cli HTTP nicht erreichbar"; fi
|
||||
fi
|
||||
|
||||
printf '\nFehler: %d\n' "$failures"
|
||||
|
||||
@@ -15,6 +15,8 @@ log() {
|
||||
|
||||
notify_user() {
|
||||
local text="$1"
|
||||
# Keep the outcome in this service's journal as well as the notifier's journal.
|
||||
printf '%s\n' "$text"
|
||||
if [[ -x "$NOTIFIER" ]]; then
|
||||
"$NOTIFIER" "$text" || true
|
||||
else
|
||||
@@ -226,7 +228,12 @@ mapfile -t old_backups < <(
|
||||
((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}"
|
||||
|
||||
new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
|
||||
if [[ "$new_commit" == "$old_commit" ]]; then
|
||||
# The updater selects the configured channel (e.g. stable), whereas the fetch
|
||||
# above only checks remote connectivity. Stable may legitimately stay on the
|
||||
# same commit even when origin/main has advanced.
|
||||
if [[ "$new_commit" == "$old_commit" ]] && ((update_exit == 0)); then
|
||||
notify_user "Hermes ist im konfigurierten Update-Kanal bereits aktuell: $new_commit. Konfiguration und frisch gestarteter Gateway sind gesund."
|
||||
elif [[ "$new_commit" == "$old_commit" ]]; then
|
||||
notify_user "Hermes-Update wurde nicht installiert (Updater-Exitcode $update_exit); der bisherige Commit $new_commit und der frisch gestartete Gateway sind gesund."
|
||||
exit 1
|
||||
elif ((update_exit != 0)); then
|
||||
|
||||
@@ -8,10 +8,10 @@ runner="$(dirname -- "${BASH_SOURCE[0]}")/hermes-stack-llama-run"
|
||||
case "${1:-list}" in
|
||||
list)
|
||||
printf '%-12s %8s %10s\n' Profil Kontext Fit-MiB \
|
||||
baseline 65536 1536 vram-64k 65536 1280 \
|
||||
desktop-64k 65536 4096 baseline 65536 1536 vram-64k 65536 1280 \
|
||||
balanced 49152 1536 vram-plus 49152 1280 compact 32768 1536
|
||||
exit 0 ;;
|
||||
baseline|vram-64k|balanced|vram-plus|compact) selected="$1" ;;
|
||||
desktop-64k|baseline|vram-64k|balanced|vram-plus|compact) selected="$1" ;;
|
||||
*)
|
||||
printf 'Unbekanntes Profil. Verfügbare Profile: %s list\n' "$0" >&2
|
||||
exit 2 ;;
|
||||
@@ -24,7 +24,7 @@ flock -n 9 || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$config_dir/llama.env"
|
||||
[[ -n "${LLAMA_PROFILE:-}" ]] || {
|
||||
printf 'Profilbetrieb ist nicht konfiguriert.\n' >&2
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -Euo pipefail
|
||||
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||
if [[ -r "$config_dir/config.env" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$config_dir/config.env"
|
||||
fi
|
||||
printf '\nGPU (Gesamtspeicher, belegt, frei in MiB):\n'
|
||||
|
||||
@@ -8,7 +8,7 @@ if [[ "${1:-}" != --jobs-finished || $# -ne 1 ]]; then
|
||||
fi
|
||||
|
||||
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||
# shellcheck disable=SC1090
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$config_dir/config.env"
|
||||
|
||||
exec 8>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||
|
||||
@@ -27,7 +27,7 @@ while (($#)); do
|
||||
done
|
||||
|
||||
if [[ -n "${LLAMA_PROFILE:-}" && -r "$config_dir/llama-profile.env" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$config_dir/llama-profile.env"
|
||||
fi
|
||||
[[ -z "$profile_override" ]] || LLAMA_PROFILE="$profile_override"
|
||||
@@ -70,9 +70,15 @@ if [[ -n "${LLAMA_MMPROJ:-}" ]]; then
|
||||
fi
|
||||
[[ -z "${LLAMA_ALIAS:-}" ]] || model_args+=(--alias "$LLAMA_ALIAS")
|
||||
|
||||
# Server defaults are not a wall-clock limit; the independent slot watchdog is.
|
||||
[[ "${LLAMA_MAX_OUTPUT_TOKENS:-2048}" =~ ^[1-9][0-9]*$ ]] || exit 2
|
||||
|
||||
profile_args=()
|
||||
if [[ -n "${LLAMA_PROFILE:-}" ]]; then
|
||||
reasoning_budget=512
|
||||
reasoning_preserve=--reasoning-preserve
|
||||
case "$LLAMA_PROFILE" in
|
||||
desktop-64k) ctx=65536; margin=4096; reasoning_budget=128; reasoning_preserve=--no-reasoning-preserve ;;
|
||||
baseline) ctx=65536; margin=1536 ;;
|
||||
vram-64k) ctx=65536; margin=1280 ;;
|
||||
balanced) ctx=49152; margin=1536 ;;
|
||||
@@ -86,7 +92,7 @@ if [[ -n "${LLAMA_PROFILE:-}" ]]; then
|
||||
|
||||
profile_args=(
|
||||
--ctx-size "$ctx" --parallel 1 --jinja
|
||||
--reasoning auto --reasoning-budget 512 --reasoning-preserve
|
||||
--reasoning auto --reasoning-budget "$reasoning_budget" "$reasoning_preserve"
|
||||
--flash-attn auto --threads 6 --threads-batch 16
|
||||
--n-gpu-layers auto --fit on --fit-target "$margin"
|
||||
--cache-type-k q8_0 --cache-type-v q8_0
|
||||
@@ -104,6 +110,7 @@ args=(
|
||||
"${model_args[@]}"
|
||||
--host "${LLAMA_HOST:-127.0.0.1}"
|
||||
--port "${LLAMA_PORT:-8080}"
|
||||
--n-predict "${LLAMA_MAX_OUTPUT_TOKENS:-2048}"
|
||||
"${profile_args[@]}"
|
||||
"${LLAMA_ARGS[@]}"
|
||||
)
|
||||
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$config_dir/config.env"
|
||||
set +a
|
||||
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||
flock -n 9 || exit 0
|
||||
exec python3 "$HOME/.local/lib/hermes-stack/llama_watchdog.py"
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Explicit model selection. Stops current gateway requests; keeps both model files.
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||
usage() {
|
||||
printf 'Aufruf: %s [status|routine|fallback|large] (Modellwechsel beendet laufende Anfragen)\n' "$0"
|
||||
}
|
||||
case "${1:-}" in
|
||||
''|status)
|
||||
# shellcheck disable=SC1091
|
||||
source "$config_dir/llama.env"
|
||||
printf 'Aktives Modell: %s\nKonfiguration: %s/llama.env\n' "$LLAMA_MODEL" "$config_dir"
|
||||
exit 0 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
routine|fallback|large) selected="$1" ;;
|
||||
*) usage >&2; exit 2 ;;
|
||||
esac
|
||||
[[ $# -eq 1 ]] || exit 2
|
||||
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||
flock -n 9 || { printf 'Maintenance läuft.\n' >&2; exit 1; }
|
||||
runner="$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")/hermes-stack-llama-run"
|
||||
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"
|
||||
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
|
||||
fi
|
||||
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 2>/dev/null | jq -e '.status == "ok"' >/dev/null 2>&1; then
|
||||
ready=1; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
if ((ready == 0)); then
|
||||
exit 1
|
||||
fi
|
||||
systemctl --user start hermes-gateway.service
|
||||
trap - EXIT
|
||||
printf 'Modellprofil %s aktiv. Vorherige Konfiguration: %s\n' "$selected" "$backup"
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bound a busy llama.cpp slot even if the client loses its cancellation path.
|
||||
|
||||
Only task IDs, monotonic timestamps and service invocation IDs are persisted.
|
||||
The wrapper holds the same maintenance lock as the updater and restart helper.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
from urllib.request import urlopen
|
||||
|
||||
|
||||
def observe(previous, slots, invocation, now, seconds, tokens):
|
||||
old = previous if previous.get("invocation") == invocation else {}
|
||||
active = {}
|
||||
reasons = []
|
||||
for slot in slots:
|
||||
if not slot.get("is_processing"):
|
||||
continue
|
||||
if "id_task" not in slot:
|
||||
raise ValueError("busy slot without task ID")
|
||||
key = f'{slot["id"]}:{slot["id_task"]}'
|
||||
started = old.get("active", {}).get(key, now)
|
||||
if started > now:
|
||||
started = now
|
||||
active[key] = started
|
||||
decoded = max((item.get("n_decoded", 0) for item in slot.get("next_token", [])), default=0)
|
||||
if now - started >= seconds:
|
||||
reasons.append(f"slot {slot['id']}: request exceeded {seconds}s")
|
||||
if decoded > tokens:
|
||||
reasons.append(f"slot {slot['id']}: output exceeded {tokens} tokens")
|
||||
return {"invocation": invocation, "active": active}, reasons
|
||||
|
||||
|
||||
def systemctl(*args):
|
||||
return subprocess.run(["systemctl", "--user", *args], check=True,
|
||||
capture_output=True, text=True, timeout=35).stdout.strip()
|
||||
|
||||
|
||||
def main():
|
||||
service = os.environ.get("LLAMA_SERVICE", "llama-qwen.service")
|
||||
url = os.environ.get("LLAMA_URL", "http://127.0.0.1:8080").rstrip("/")
|
||||
seconds = int(os.environ.get("LLAMA_MAX_REQUEST_SECONDS", "600"))
|
||||
tokens = int(os.environ.get("LLAMA_MAX_OUTPUT_TOKENS", "2048"))
|
||||
if seconds <= 0 or tokens <= 0:
|
||||
raise ValueError("watchdog limits must be positive")
|
||||
state_dir = Path(os.environ.get("XDG_STATE_HOME", str(Path.home() / ".local/state"))) / "hermes-stack-maintenance"
|
||||
state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
path = state_dir / "llama-watchdog.json"
|
||||
try:
|
||||
previous = json.loads(path.read_text())
|
||||
except (OSError, ValueError):
|
||||
previous = {}
|
||||
boot = Path("/proc/sys/kernel/random/boot_id").read_text().strip()
|
||||
if previous.get("boot") != boot:
|
||||
previous = {}
|
||||
status = systemctl("show", service, "--property=ActiveState", "--property=InvocationID")
|
||||
info = dict(line.split("=", 1) for line in status.splitlines())
|
||||
if info.get("ActiveState") != "active":
|
||||
return # Respect explicit stops and maintenance; never start an inactive server.
|
||||
invocation = info["InvocationID"]
|
||||
now = time.monotonic()
|
||||
try:
|
||||
with urlopen(url + "/slots", timeout=5) as response:
|
||||
slots = json.load(response)
|
||||
if not isinstance(slots, list):
|
||||
raise ValueError("invalid slot response")
|
||||
state, reasons = observe(previous, slots, invocation, now, seconds, tokens)
|
||||
except (OSError, ValueError, KeyError, TypeError):
|
||||
# A single failed probe is not evidence of a hung model. Require a full
|
||||
# request budget of consecutive failures within the same server process.
|
||||
since = previous.get("unreachable_since", now) if previous.get("invocation") == invocation else now
|
||||
state = {"invocation": invocation, "active": previous.get("active", {}), "unreachable_since": since}
|
||||
reasons = [f"slot endpoint unreachable for {seconds}s"] if now - since >= seconds else []
|
||||
last_restart = previous.get("last_restart", -1e12)
|
||||
state["last_restart"] = last_restart
|
||||
if reasons and now - last_restart >= max(300, seconds):
|
||||
print("llama watchdog: " + "; ".join(reasons) + "; restarting " + service, flush=True)
|
||||
systemctl("restart", "--no-block", service)
|
||||
state = {"invocation": invocation, "active": {}, "last_restart": now}
|
||||
state["boot"] = boot
|
||||
pending = path.with_suffix(".pending")
|
||||
pending.write_text(json.dumps(state) + "\n")
|
||||
pending.chmod(0o600)
|
||||
pending.replace(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare the documented local model setup; never start services or send messages."""
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ALIASES = ["hermes-local", "qwen35-9b-local", "qwen35-4b-local", "qwen38-27b-local", "qwen36-27b-local"]
|
||||
LOCAL_URL = "http://127.0.0.1:8080/v1"
|
||||
|
||||
|
||||
def verify(path, item):
|
||||
if not path.is_file() or path.stat().st_size != item["size"]:
|
||||
return False
|
||||
with path.open("rb") as model:
|
||||
return hashlib.file_digest(model, "sha256").hexdigest() == item["sha256"]
|
||||
|
||||
|
||||
def ensure_model(group, model_dir, download=False):
|
||||
directory = model_dir / group["directory"]
|
||||
for item in group["files"]:
|
||||
path = directory / item["name"]
|
||||
if path.exists():
|
||||
if not verify(path, item):
|
||||
raise ValueError(f"Prüfsumme falsch: {path}; vorhandene Datei bleibt unverändert")
|
||||
else:
|
||||
if not download:
|
||||
raise FileNotFoundError(f"Modell fehlt: {path}; mit --download bereitstellen")
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
partial = path.with_name(path.name + ".part")
|
||||
url = f'https://huggingface.co/{group["repo"]}/resolve/{group["revision"]}/{item["name"]}'
|
||||
subprocess.run(["curl", "-fL", "--retry", "3", "--connect-timeout", "15",
|
||||
"--max-time", "14400", "--continue-at", "-", url,
|
||||
"--output", str(partial)], check=True)
|
||||
if not verify(partial, item):
|
||||
raise ValueError(f"Download-Prüfsumme falsch: {partial}; nicht aktiviert")
|
||||
partial.replace(path)
|
||||
print(f"SHA256 OK: {path.name}", flush=True)
|
||||
return [directory / item["name"] for item in group["files"]]
|
||||
|
||||
|
||||
def write_config(path, text, replace=False):
|
||||
if path.exists():
|
||||
if path.read_text() == text:
|
||||
return
|
||||
if not replace:
|
||||
print(f"Vorhandene Konfiguration beibehalten: {path}")
|
||||
return
|
||||
backup = path.with_name(path.name + f".before-setup-{time.time_ns()}")
|
||||
shutil.copy2(path, backup)
|
||||
backup.chmod(0o600)
|
||||
print(f"Sicherung: {backup}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as pending:
|
||||
pending.write(text)
|
||||
staging = Path(pending.name)
|
||||
staging.chmod(0o600)
|
||||
staging.replace(path)
|
||||
|
||||
|
||||
def profile_config(name, paths, server_bin=""):
|
||||
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",
|
||||
"--device", "none", "--no-op-offload", "--no-kv-offload",
|
||||
"--threads", "8", "--threads-batch", "8"]
|
||||
else:
|
||||
args += ["--reasoning", "auto", "--reasoning-budget", "128", "--no-reasoning-preserve",
|
||||
"--threads", "6", "--threads-batch", "16"]
|
||||
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"]
|
||||
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_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, 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=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)
|
||||
entries = config.setdefault("custom_providers", [])
|
||||
matches = [entry for entry in entries if entry.get("base_url", "").rstrip("/") == LOCAL_URL]
|
||||
if not matches:
|
||||
matches = [{"name": "Local (127.0.0.1:8080)", "base_url": LOCAL_URL}]
|
||||
entries.extend(matches)
|
||||
for entry in matches:
|
||||
entry["model"] = "hermes-local"
|
||||
entry.setdefault("models", {}).update({name: {} for name in ALIASES})
|
||||
entry.setdefault("extra_body", {}).update(max_tokens=2048, t_max_predict_ms=540000)
|
||||
auxiliary = config.setdefault("auxiliary", {})
|
||||
auxiliary.setdefault("vision", {}).update(provider="main", model="hermes-local",
|
||||
base_url=LOCAL_URL, api_key="local-no-key", timeout=300, reasoning_effort="none", max_concurrency=1)
|
||||
if compression:
|
||||
auxiliary.setdefault("compression", {}).update(provider="custom", model="qwen3-1.7b-compression",
|
||||
base_url="http://127.0.0.1:8081/v1", api_key="local-no-key", timeout=300, reasoning_effort="none")
|
||||
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
|
||||
|
||||
|
||||
def maintenance_config(text):
|
||||
for key, value in {"LLAMA_SERVICE": "llama-qwen.service", "LLAMA_URL": "http://127.0.0.1:8080",
|
||||
"MODEL_NAME": "hermes-local", "LLAMA_MAX_REQUEST_SECONDS": "600",
|
||||
"LLAMA_MAX_OUTPUT_TOKENS": "2048"}.items():
|
||||
setting = f"{key}={shlex.quote(value)}"
|
||||
pattern = rf"^{key}=.*$"
|
||||
text = re.sub(pattern, lambda _: setting, text, flags=re.MULTILINE) if re.search(pattern, text, re.MULTILINE) else text + "\n" + setting + "\n"
|
||||
return text
|
||||
|
||||
|
||||
def unpin_local_jobs(path):
|
||||
if not path.exists():
|
||||
return
|
||||
for job in json.loads(path.read_text()).get("jobs", []):
|
||||
if (job.get("model") in ALIASES and job.get("provider") == "custom"
|
||||
and job.get("base_url") in (None, "", LOCAL_URL)):
|
||||
subprocess.run(["hermes", "cron", "edit", job["id"], "--unpin"], check=True)
|
||||
|
||||
|
||||
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,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", "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,fallback")
|
||||
if options.select and options.select not in names:
|
||||
parser.error("--select muss in --models enthalten sein")
|
||||
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",
|
||||
"--property=ActiveState", "--value"], text=True).strip()
|
||||
if state not in ("inactive", "failed", ""):
|
||||
parser.error("Gateway zuerst mit systemctl --user stop hermes-gateway.service anhalten")
|
||||
# Import before downloads, so a missing dependency does not waste bandwidth.
|
||||
if options.configure_hermes:
|
||||
import yaml
|
||||
lock_path = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "hermes-stack-hermes-update.lock"
|
||||
with lock_path.open("w") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
profiles = {}
|
||||
for name in names:
|
||||
paths = ensure_model(manifest[name], options.model_dir, options.download)
|
||||
profiles[name] = profile_config(name, paths, options.server_bin)
|
||||
for name, text in profiles.items():
|
||||
filename = "compression.env" if name == "compression" else f"llama-{name}.env"
|
||||
write_config(options.config_dir / filename, text)
|
||||
if options.select:
|
||||
source = options.config_dir / f"llama-{options.select}.env"
|
||||
write_config(options.config_dir / "llama.env", source.read_text(), replace=True)
|
||||
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, 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()
|
||||
write_config(maintenance, maintenance_config(text), replace=True)
|
||||
if options.unpin_local_cron:
|
||||
unpin_local_jobs(options.hermes_config.parent / "cron/jobs.json")
|
||||
print("Vorbereitung abgeschlossen. Keine Dienste gestartet. Siehe docs/NEW_WORKSTATION.md.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Enforce local model request duration and output limits
|
||||
After=llama-qwen.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=%h/.local/libexec/hermes-stack-llama-watchdog
|
||||
TimeoutStartSec=45
|
||||
UMask=0077
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Check local model request limits every 15 seconds
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=15s
|
||||
AccuracySec=1s
|
||||
Unit=hermes-stack-llama-watchdog.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Hermes auxiliary compression LLM (CPU)
|
||||
After=default.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=HERMES_LLAMA_CONFIG=%h/.config/hermes-stack-maintenance/compression.env
|
||||
ExecStart=%h/.local/libexec/hermes-stack-llama-run
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
Nice=10
|
||||
MemoryHigh=8G
|
||||
MemoryMax=12G
|
||||
OOMPolicy=stop
|
||||
TimeoutStopSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Local llama.cpp Qwen model server
|
||||
Description=Hermes local main model (routine/large)
|
||||
After=network-online.target graphical-session.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=3
|
||||
|
||||
@@ -21,6 +21,9 @@ else
|
||||
fi
|
||||
|
||||
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
|
||||
python3 "$ROOT_DIR/tests/test_llama_watchdog.py" || failures=$((failures + 1))
|
||||
python3 "$ROOT_DIR/tests/test_workstation.py" || failures=$((failures + 1))
|
||||
python3 "$ROOT_DIR/tests/test_hermes_update.py" || failures=$((failures + 1))
|
||||
|
||||
if (cd -- "$ROOT_DIR" && rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \
|
||||
'(TELEGRAM_BOT_TOKEN|SIGNAL_ACCOUNT|API_KEY)=("|'"'"')?[A-Za-z0-9+]' .); then
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the actual maintenance script with isolated state and fake external tools."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNNER = ROOT / "libexec/hermes-stack-hermes-update"
|
||||
|
||||
|
||||
class HermesUpdateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix="hermes-update-", dir=ROOT.parent)
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.bin = self.root / "bin"
|
||||
self.bin.mkdir()
|
||||
config = self.root / "config/hermes-stack-maintenance"
|
||||
config.mkdir(parents=True)
|
||||
(self.root / "hermes-agent/.git").mkdir(parents=True)
|
||||
self.env = dict(os.environ, XDG_CONFIG_HOME=str(config.parent),
|
||||
XDG_STATE_HOME=str(self.root / "state"),
|
||||
XDG_RUNTIME_DIR=str(self.root), TEST_ROOT=str(self.root),
|
||||
PATH=str(self.bin) + ":" + os.environ["PATH"],
|
||||
TEST_UPDATE_EXIT="0", TEST_CHANGED="0", TEST_HEALTHY="1")
|
||||
(config / "config.env").write_text(
|
||||
f'HERMES_REPO={shlex.quote(str(self.root / "hermes-agent"))}\n'
|
||||
f'NOTIFIER={shlex.quote(str(self.bin / "notify-test"))}\n'
|
||||
'HERMES_REMOTE=origin\nHERMES_BRANCH=main\n'
|
||||
'GATEWAY_SERVICE=test-gateway.service\n'
|
||||
'GATEWAY_HEALTH_ATTEMPTS=1\nGATEWAY_HEALTH_DELAY=0\n'
|
||||
'LLAMA_URL=http://unused.invalid\n')
|
||||
self.program("git", '''case "$*" in
|
||||
*"status --porcelain"*|*"fetch --no-tags"*) exit 0 ;;
|
||||
*"rev-parse HEAD"*)
|
||||
if [[ -f "$TEST_ROOT/changed" ]]; then printf 'c%.0s' {1..40};
|
||||
else printf 'a%.0s' {1..40}; fi ;;
|
||||
*"rev-parse refs/remotes/"*) printf 'b%.0s' {1..40} ;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
''')
|
||||
self.program("hermes", '''case "$*" in
|
||||
update)
|
||||
[[ "$TEST_CHANGED" == 0 ]] || touch "$TEST_ROOT/changed"
|
||||
echo "Simulated updater completion"
|
||||
exit "$TEST_UPDATE_EXIT" ;;
|
||||
"gateway status") [[ "$TEST_HEALTHY" == 1 ]] ;;
|
||||
"config check"|"config migrate") exit 0 ;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
''')
|
||||
self.program("systemctl", 'printf "%s\\n" "$*" >>"$TEST_ROOT/systemctl-calls"\n')
|
||||
self.program("tar", 'printf "%s\\n" "$*" >>"$TEST_ROOT/tar-calls"\n')
|
||||
self.program("curl", "echo '{\"status\":\"ok\"}'\n")
|
||||
self.program("notify-test", 'printf "%s\\n" "$*" >>"$TEST_ROOT/notices"\n')
|
||||
self.program("systemd-cat", "cat >/dev/null\n")
|
||||
self.program("sleep", "exit 0\n")
|
||||
|
||||
def program(self, name, body):
|
||||
path = self.bin / name
|
||||
path.write_text("#!/usr/bin/env bash\nset -eu\n" + body)
|
||||
path.chmod(0o755)
|
||||
|
||||
def run_update(self, **settings):
|
||||
return subprocess.run(["bash", str(RUNNER)], env=dict(self.env, **settings),
|
||||
capture_output=True, text=True, timeout=15)
|
||||
|
||||
def test_stable_unchanged_with_newer_main_succeeds(self):
|
||||
result = self.run_update()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("bereits aktuell", result.stdout)
|
||||
self.assertIn("restart test-gateway.service", (self.root / "systemctl-calls").read_text())
|
||||
self.assertNotIn("zurückgerollt", (self.root / "notices").read_text())
|
||||
|
||||
def test_unchanged_failed_updater_still_fails(self):
|
||||
result = self.run_update(TEST_UPDATE_EXIT="7")
|
||||
self.assertEqual(result.returncode, 1, result.stderr)
|
||||
self.assertIn("Updater-Exitcode 7", result.stdout)
|
||||
|
||||
def test_changed_commit_succeeds(self):
|
||||
result = self.run_update(TEST_CHANGED="1")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("erfolgreich aktualisiert", result.stdout)
|
||||
|
||||
def test_changed_commit_with_restart_warning(self):
|
||||
result = self.run_update(TEST_CHANGED="1", TEST_UPDATE_EXIT="7")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("Updater-Restartwarnung", result.stdout)
|
||||
|
||||
def test_unhealthy_gateway_still_rolls_back(self):
|
||||
result = self.run_update(TEST_HEALTHY="0")
|
||||
self.assertEqual(result.returncode, 1, result.stderr)
|
||||
self.assertIn("zurückgerollt", result.stdout)
|
||||
self.assertIn("-xf", (self.root / "tar-calls").read_text())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+27
-3
@@ -13,9 +13,10 @@ RUNNER = ROOT / "libexec/hermes-stack-llama-run"
|
||||
PROFILE = ROOT / "libexec/hermes-stack-llama-profile"
|
||||
RESTART = ROOT / "libexec/hermes-stack-llama-restart"
|
||||
OPTIONS = """--model --hf-repo --mmproj --alias --host --port --ctx-size --parallel
|
||||
--jinja --reasoning --reasoning-budget --reasoning-preserve --flash-attn --threads
|
||||
--jinja --reasoning --reasoning-budget --reasoning-preserve --no-reasoning-preserve --flash-attn --threads
|
||||
--threads-batch --n-gpu-layers --fit --fit-target --cache-type-k --cache-type-v
|
||||
--sleep-idle-seconds --image-min-tokens --cors-origins --no-ui"""
|
||||
--sleep-idle-seconds --image-min-tokens --cors-origins --no-ui --rope-scaling --rope-scale --yarn-orig-ctx
|
||||
--device --no-op-offload --no-kv-offload --n-predict"""
|
||||
|
||||
|
||||
class LlamaTests(unittest.TestCase):
|
||||
@@ -72,7 +73,8 @@ class LlamaTests(unittest.TestCase):
|
||||
return result
|
||||
|
||||
def test_profile_values(self):
|
||||
for name, ctx, margin in [("baseline", "65536", "1536"),
|
||||
for name, ctx, margin in [("desktop-64k", "65536", "4096"),
|
||||
("baseline", "65536", "1536"),
|
||||
("vram-64k", "65536", "1280"),
|
||||
("balanced", "49152", "1536"),
|
||||
("vram-plus", "49152", "1280"),
|
||||
@@ -85,6 +87,28 @@ class LlamaTests(unittest.TestCase):
|
||||
self.assertEqual(args.count("--ctx-size"), 1)
|
||||
self.assertIn("qwen38-27b-local", args)
|
||||
|
||||
def test_cpu_compression_template_uses_separate_config(self):
|
||||
model = self.root / "compression model.gguf"
|
||||
model.touch()
|
||||
template = (ROOT / "config/compression.env.example").read_text()
|
||||
template = template.replace('LLAMA_SERVER_BIN=""',
|
||||
f'LLAMA_SERVER_BIN={shlex.quote(str(self.server))}')
|
||||
template = template.replace(
|
||||
'LLAMA_MODEL="$HOME/.local/share/hermes-stack/models/qwen3-1.7b-compression/Qwen3-1.7B-Q4_K_M.gguf"',
|
||||
f'LLAMA_MODEL={shlex.quote(str(model))}')
|
||||
config = self.config / "compression.env"
|
||||
config.write_text(template)
|
||||
self.env["HERMES_LLAMA_CONFIG"] = str(config)
|
||||
self.run_tool(RUNNER)
|
||||
args = self.record.read_text().splitlines()
|
||||
self.assertEqual(args[args.index("--port") + 1], "8081")
|
||||
self.assertEqual(args[args.index("--device") + 1], "none")
|
||||
self.assertIn("--no-op-offload", args)
|
||||
self.assertIn("--no-kv-offload", args)
|
||||
self.assertIn(str(model), args)
|
||||
self.assertNotIn("qwen38-27b-local", args)
|
||||
self.assertNotIn("--fit-target", args)
|
||||
|
||||
def test_check_does_not_run_inference(self):
|
||||
self.run_tool(RUNNER, "--check")
|
||||
self.assertFalse(self.record.exists())
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location("watchdog", Path(__file__).resolve().parents[1] / "scripts/llama_watchdog.py")
|
||||
watchdog = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(watchdog)
|
||||
|
||||
|
||||
def busy(task=1, decoded=20):
|
||||
return [{"id": 0, "id_task": task, "is_processing": True,
|
||||
"next_token": [{"n_decoded": decoded}]}]
|
||||
|
||||
|
||||
class WatchdogTests(unittest.TestCase):
|
||||
def test_busy_request_is_allowed_then_expires_despite_progress(self):
|
||||
state, reasons = watchdog.observe({}, busy(), "server1", 100, 600, 2048)
|
||||
self.assertFalse(reasons)
|
||||
state, reasons = watchdog.observe(state, busy(decoded=1000), "server1", 699, 600, 2048)
|
||||
self.assertFalse(reasons)
|
||||
_, reasons = watchdog.observe(state, busy(decoded=1100), "server1", 700, 600, 2048)
|
||||
self.assertIn("600s", reasons[0])
|
||||
|
||||
def test_new_request_and_restarted_server_do_not_inherit_deadline(self):
|
||||
state, _ = watchdog.observe({}, busy(), "server1", 100, 600, 2048)
|
||||
for slots, invocation in [(busy(2), "server1"), (busy(), "server2")]:
|
||||
_, reasons = watchdog.observe(state, slots, invocation, 900, 600, 2048)
|
||||
self.assertFalse(reasons)
|
||||
|
||||
def test_idle_slot_drops_old_deadline(self):
|
||||
state, _ = watchdog.observe({}, busy(), "server1", 100, 600, 2048)
|
||||
state, reasons = watchdog.observe(state, [{"id": 0, "is_processing": False}], "server1", 900, 600, 2048)
|
||||
self.assertEqual(state["active"], {})
|
||||
self.assertFalse(reasons)
|
||||
|
||||
def test_excess_output_is_stopped_even_when_client_overrides_default(self):
|
||||
_, reasons = watchdog.observe({}, busy(decoded=2049), "server1", 100, 600, 2048)
|
||||
self.assertIn("2048 tokens", reasons[0])
|
||||
|
||||
def test_prompt_and_generated_text_are_not_persisted(self):
|
||||
slots = busy()
|
||||
slots[0].update(prompt="private prompt", generated="private output")
|
||||
state, _ = watchdog.observe({}, slots, "server1", 100, 600, 2048)
|
||||
self.assertNotIn("private", str(state))
|
||||
|
||||
|
||||
class WatchdogIntegrationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.environment = patch.dict(watchdog.os.environ, {"XDG_STATE_HOME": self.temp.name,
|
||||
"LLAMA_MAX_REQUEST_SECONDS": "600", "LLAMA_MAX_OUTPUT_TOKENS": "2048"})
|
||||
self.environment.start()
|
||||
self.addCleanup(self.environment.stop)
|
||||
self.path = Path(self.temp.name) / "hermes-stack-maintenance/llama-watchdog.json"
|
||||
self.boot = Path("/proc/sys/kernel/random/boot_id").read_text().strip()
|
||||
self.calls = []
|
||||
|
||||
def control(self, *args):
|
||||
self.calls.append(args)
|
||||
return "ActiveState=active\nInvocationID=test-server" if args[0] == "show" else ""
|
||||
|
||||
def tick(self, now, slots=None, error=False):
|
||||
response = io.BytesIO(json.dumps(slots or []).encode())
|
||||
with patch.object(watchdog, "systemctl", self.control), \
|
||||
patch.object(watchdog.time, "monotonic", return_value=now), \
|
||||
patch.object(watchdog, "urlopen", side_effect=OSError() if error else None, return_value=response):
|
||||
watchdog.main()
|
||||
|
||||
def restarts(self):
|
||||
return [call for call in self.calls if call[0] == "restart"]
|
||||
|
||||
def test_deadline_restarts_actual_configured_service_once(self):
|
||||
self.tick(100, busy())
|
||||
self.tick(699, busy())
|
||||
self.assertFalse(self.restarts())
|
||||
self.tick(700, busy())
|
||||
self.assertEqual(self.restarts(), [("restart", "--no-block", "llama-qwen.service")])
|
||||
self.tick(701, busy(decoded=3000))
|
||||
self.assertEqual(len(self.restarts()), 1)
|
||||
self.assertEqual(self.path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_only_persistent_probe_failure_triggers_recovery(self):
|
||||
self.tick(100, error=True)
|
||||
self.tick(699, error=True)
|
||||
self.assertFalse(self.restarts())
|
||||
self.tick(700, error=True)
|
||||
self.assertEqual(len(self.restarts()), 1)
|
||||
|
||||
def test_successful_probe_resets_failure_deadline(self):
|
||||
self.tick(100, error=True)
|
||||
self.tick(500)
|
||||
self.tick(700, error=True)
|
||||
self.assertFalse(self.restarts())
|
||||
|
||||
def test_inactive_service_never_started_or_probed(self):
|
||||
with patch.object(watchdog, "systemctl", return_value="ActiveState=inactive\nInvocationID=") as control, \
|
||||
patch.object(watchdog, "urlopen") as probe:
|
||||
watchdog.main()
|
||||
probe.assert_not_called()
|
||||
self.assertEqual(control.call_count, 1)
|
||||
|
||||
def test_reboot_clears_old_monotonic_deadlines_and_cooldown(self):
|
||||
self.path.parent.mkdir()
|
||||
self.path.write_text(json.dumps({"boot": "previous-boot", "invocation": "test-server",
|
||||
"active": {"0:1": 100}, "last_restart": 10000, "unreachable_since": 50}))
|
||||
self.tick(100, busy())
|
||||
self.tick(700, busy())
|
||||
self.assertEqual(len(self.restarts()), 1)
|
||||
self.assertEqual(json.loads(self.path.read_text())["boot"], self.boot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,202 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location("prepare", ROOT / "scripts/prepare-workstation.py")
|
||||
prepare = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(prepare)
|
||||
|
||||
|
||||
class PreparationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.data = b"test model weights"
|
||||
self.item = {"name": "weights.gguf", "sha256": hashlib.sha256(self.data).hexdigest(), "size": len(self.data)}
|
||||
self.group = {"repo": "owner/model", "revision": "a" * 40, "directory": "model/revision", "files": [self.item]}
|
||||
|
||||
def test_download_uses_fixed_revision_and_publishes_only_verified_file(self):
|
||||
def download(args, **kwargs):
|
||||
self.assertIn("/resolve/" + "a" * 40 + "/weights.gguf", args[-3])
|
||||
Path(args[-1]).write_bytes(self.data)
|
||||
with patch.object(prepare.subprocess, "run", side_effect=download):
|
||||
paths = prepare.ensure_model(self.group, self.root, True)
|
||||
self.assertEqual(paths[0].read_bytes(), self.data)
|
||||
self.assertFalse(paths[0].with_name("weights.gguf.part").exists())
|
||||
|
||||
def test_corrupt_download_cannot_become_live_model(self):
|
||||
with patch.object(prepare.subprocess, "run", side_effect=lambda args, **_: Path(args[-1]).write_bytes(b"broken")):
|
||||
with self.assertRaisesRegex(ValueError, "nicht aktiviert"):
|
||||
prepare.ensure_model(self.group, self.root, True)
|
||||
self.assertFalse((self.root / "model/revision/weights.gguf").exists())
|
||||
|
||||
def test_existing_wrong_file_is_preserved_without_download(self):
|
||||
path = self.root / "model/revision/weights.gguf"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(b"user file")
|
||||
with patch.object(prepare.subprocess, "run") as download, self.assertRaises(ValueError):
|
||||
prepare.ensure_model(self.group, self.root, True)
|
||||
download.assert_not_called()
|
||||
self.assertEqual(path.read_bytes(), b"user file")
|
||||
|
||||
def test_config_merge_preserves_unrelated_settings_and_external_provider(self):
|
||||
config = {"terminal": {"backend": "docker"}, "plugins": {"enabled": ["private"]},
|
||||
"custom_providers": [{"name": "Other", "base_url": "https://other.invalid/v1", "model": "other"}]}
|
||||
updated = prepare.configure_hermes(config, True)
|
||||
self.assertEqual(updated["terminal"]["backend"], "docker")
|
||||
self.assertEqual(updated["plugins"]["enabled"], ["private"])
|
||||
self.assertEqual(updated["custom_providers"][0]["model"], "other")
|
||||
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_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, 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")
|
||||
prepare.write_config(path, "generated\n")
|
||||
self.assertEqual(path.read_text(), "custom tuning\n")
|
||||
prepare.write_config(path, "generated\n", replace=True)
|
||||
prepare.write_config(path, "generated\n", replace=True)
|
||||
self.assertEqual(len(list(self.root.glob("llama.env.before-setup-*"))), 1)
|
||||
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_profiles_quote_other_user_paths_and_do_not_use_remote_hf_start(self):
|
||||
text = prepare.profile_config("routine", [self.root / "user space/model.gguf", self.root / "projector.gguf"])
|
||||
result = subprocess.run(["bash", "-c", text + '\nprintf "%s|%s|%s" "$LLAMA_MODEL" "$LLAMA_HF_REPO" "${LLAMA_ARGS[*]}"'], text=True, capture_output=True, check=True)
|
||||
self.assertTrue(result.stdout.startswith(str(self.root / "user space/model.gguf") + "||"))
|
||||
self.assertIn("--n-gpu-layers 99 --fit off", result.stdout)
|
||||
|
||||
def test_prepare_whole_new_workstation_offline_without_home_override(self):
|
||||
repository = self.root / "repository"
|
||||
(repository / "config").mkdir(parents=True)
|
||||
shutil.copy2(ROOT / "config/config.env.example", repository / "config")
|
||||
(repository / "config/models.lock.json").write_text(json.dumps({"models": {"routine": self.group}}))
|
||||
path = self.root / "models/model/revision/weights.gguf"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(self.data)
|
||||
config = self.root / "config/hermes-stack-maintenance"
|
||||
hermes = self.root / "hermes/config.yaml"
|
||||
args = ["prepare", "--models", "routine", "--model-dir", str(self.root / "models"),
|
||||
"--config-dir", str(config), "--hermes-config", str(hermes),
|
||||
"--configure-hermes", "--select", "routine"]
|
||||
with patch.object(prepare, "ROOT", repository), patch("sys.argv", args), \
|
||||
patch.dict(os.environ, {"XDG_RUNTIME_DIR": str(self.root)}), \
|
||||
patch.object(prepare.subprocess, "check_output", return_value="inactive\n"):
|
||||
prepare.main()
|
||||
self.assertEqual((config / "llama.env").read_text(), (config / "llama-routine.env").read_text())
|
||||
self.assertIn("LLAMA_MAX_REQUEST_SECONDS=600", (config / "config.env").read_text())
|
||||
self.assertIn("run_budget_seconds: 1800", hermes.read_text())
|
||||
|
||||
def test_local_cron_unpin_does_not_touch_other_provider_jobs(self):
|
||||
path = self.root / "jobs.json"
|
||||
path.write_text(json.dumps({"jobs": [
|
||||
{"id": "local", "model": "qwen38-27b-local", "provider": "custom"},
|
||||
{"id": "cloud", "model": "qwen38-27b-local", "provider": "other"},
|
||||
{"id": "remote", "model": "qwen38-27b-local", "provider": "custom", "base_url": "https://other.invalid/v1"}]}))
|
||||
with patch.object(prepare.subprocess, "run") as edit:
|
||||
prepare.unpin_local_jobs(path)
|
||||
edit.assert_called_once_with(["hermes", "cron", "edit", "local", "--unpin"], check=True)
|
||||
|
||||
|
||||
class ModelSwitchTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.bin = self.root / "bin"
|
||||
self.bin.mkdir()
|
||||
self.config = self.root / "config/hermes-stack-maintenance"
|
||||
self.config.mkdir(parents=True)
|
||||
(self.config / "llama.env").write_text("LLAMA_MODEL=old.gguf\n")
|
||||
(self.config / "llama-routine.env").write_text("LLAMA_MODEL=new.gguf\n")
|
||||
shutil.copy2(ROOT / "libexec/hermes-stack-model", self.bin)
|
||||
for name, body in {"hermes-stack-llama-run": "exit 0",
|
||||
"systemctl": 'echo "$*" >>"$TEST_CALLS"; if [[ "$*" == *"restart llama-qwen"* && "${TEST_FAIL:-0}" == 1 ]]; then exit 1; fi',
|
||||
"curl": "echo '{\"status\":\"ok\"}'"}.items():
|
||||
p = self.bin / name
|
||||
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"],
|
||||
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")
|
||||
result = subprocess.run(["bash", str(self.root / "entrypoint"), "routine"], env=self.env, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=new.gguf\n")
|
||||
self.assertIn("start hermes-gateway.service", (self.root / "calls").read_text())
|
||||
|
||||
def test_failed_switch_restores_configuration_and_still_attempts_gateway_start(self):
|
||||
result = subprocess.run(["bash", str(self.bin / "hermes-stack-model"), "routine"],
|
||||
env=dict(self.env, TEST_FAIL="1"), capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual((self.config / "llama.env").read_text(), "LLAMA_MODEL=old.gguf\n")
|
||||
self.assertIn("start hermes-gateway.service", (self.root / "calls").read_text())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+12
-2
@@ -6,14 +6,18 @@ set -Eeuo pipefail
|
||||
units=(
|
||||
hermes-stack-hermes-update.service hermes-stack-hermes-update.timer
|
||||
hermes-stack-health.service hermes-stack-health.timer
|
||||
hermes-stack-llama-watchdog.service hermes-stack-llama-watchdog.timer
|
||||
hermes-stack-signal-health.service hermes-stack-signal-health.timer
|
||||
hermes-stack-signal-update-check.service hermes-stack-signal-update-check.timer
|
||||
llama-qwen.service signal-cli.service
|
||||
llama-qwen.service llama-compression.service signal-cli.service
|
||||
)
|
||||
|
||||
systemctl --user disable --now llama-compression.service 2>/dev/null || true
|
||||
|
||||
systemctl --user disable --now \
|
||||
hermes-stack-hermes-update.timer \
|
||||
hermes-stack-health.timer \
|
||||
hermes-stack-llama-watchdog.timer \
|
||||
hermes-stack-signal-health.timer \
|
||||
hermes-stack-signal-update-check.timer 2>/dev/null || true
|
||||
|
||||
@@ -25,10 +29,16 @@ for file in \
|
||||
hermes-stack-notify hermes-stack-hermes-update hermes-stack-health \
|
||||
hermes-stack-signal-health hermes-stack-signal-update-check hermes-stack-signal-update \
|
||||
hermes-stack-llama-run hermes-stack-llama-profile hermes-stack-llama-restart \
|
||||
hermes-stack-llama-report \
|
||||
hermes-stack-llama-report hermes-stack-llama-watchdog hermes-stack-model \
|
||||
hermes-stack-signal-run hermes-stack-doctor; do
|
||||
rm -f -- "$HOME/.local/libexec/$file"
|
||||
done
|
||||
|
||||
rm -f -- "$HOME/.local/lib/hermes-stack/llama_watchdog.py"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
command_target="$HOME/.local/bin/hermes-stack-model"
|
||||
if [[ -L "$command_target" && "$(readlink -- "$command_target")" == ../libexec/hermes-stack-model ]]; then
|
||||
rm -f -- "$command_target"
|
||||
fi
|
||||
printf 'Deployment-Dateien entfernt; Benutzerdaten und Konfigurationen wurden beibehalten.\n'
|
||||
Reference in new issue
Block a user