# Plan: NVIDIA-Windows-Rechner als flexibler Cluster hinter dem bestehenden Gateway ## Context Mehrere Windows-PCs mit NVIDIA-GPUs sollen zusätzliche Rechenleistung liefern. Der bestehende Linux-PC (AMD, Docker: Caddy + llama-swap) bleibt der **einzige Endpunkt** (:8800, gleicher API-Key). **Jeder Knoten kann jedes Cluster-Modell ausführen** und wechselt bei Bedarf (llama-swap). Linux ist auf den Knoten nicht nötig: llama-swap (`llama-swap_265_windows_amd64.zip`) und llama.cpp (`llama-bXXXXX-bin-win-cuda-{12.4|13.4}-x64.zip` + `cudart-…`) gibt es fertig für Windows. Erkenntnisse aus der Recherche: - llama-swap v265 leitet über `peers` pro Modell an **einen** Server weiter (URL-Pfad wird angehängt). Das Load-Balancing übernimmt deshalb Caddy. - llama-swap löst `${env.VAR}` in der Config auf, also kommen keine Secrets ins Git. - Ältere llama.cpp-Builds werden gelöscht (b11610 gibt es schon nicht mehr). Der Installer wählt den Build über die GitHub-API (`latest` oder fest). - Caddy auf dem Host-Netz ist für den Container wegen ufw schwer erreichbar. Deshalb läuft der Loadbalancer als **eigener Container im Compose-Netz**. ## Architektur ``` Client ─► Caddy :8800 (IP-Filter, API-Key) ─► llama-swap (AMD, lokal) │ lokale Modelle wie bisher └─ peers: cluster- ─► http://cluster-lb:8090/m/cluster- cluster-lb (Caddy-Container, generiert): /m/cluster-qwen3.5-35b-a3b/* ─► Knoten [A, B, C] (Stammknoten A) /m/cluster-glm-4.7-flash/* ─► Knoten [B, C, A] (Stammknoten B) /m/cluster-qwen3.5-9b/* ─► Knoten [C, A, B] (Stammknoten C) Win-Knoten: llama-swap.exe :8080 mit ALLEN cluster-Modellen, API-Key, Firewall nur Gateway-IP ``` **Routing ohne unnötige Modellwechsel:** Für jedes Modell enthält der Pool alle Knoten, aber in rotierter Reihenfolge. Dazu kommen `lb_policy first` und `unhealthy_request_count `: - Eine Anfrage geht an den Stammknoten des Modells. Dort ist das Modell meist schon geladen und der Prompt-Cache noch vorhanden. - Ist der Stammknoten belegt oder ausgefallen, geht sie an den nächsten Knoten. Der lädt das Modell dann, das ist der flexible Fall. - Sind alle Knoten belegt, wartet Caddy (`lb_try_duration 5m`, `lb_try_interval 1s`), bis einer frei wird. - Health-Checks auf `/health` alle 10 s, ausgefallene Knoten werden übersprungen. - Wenn es mehr Modelle als Knoten gibt, teilen sich Modelle einen Stammknoten (Zuordnung reihum). ## Änderungen auf dem Gateway (Linux) 1. **[docker-compose.yml](docker-compose.yml)**: neuer Service `cluster-lb` (`caddy:${CADDY_TAG}`), nur im Compose-Netz und ohne Port nach außen. Er mountet `caddy/cluster.Caddyfile`. `llama-swap` bekommt `CLUSTER_API_KEY` als Umgebungsvariable. 2. **Neu `scripts/cluster-gen.sh`**: liest `CLUSTER_NODES="192.168.178.50:8080 192.168.178.51:8080 …"` und `CLUSTER_SLOTS` (Standard 1) aus `.env`. Die Modellliste kommt aus `cluster/windows/node-config.yaml` (einzige Quelle, Schlüssel `cluster-*`). Daraus erzeugt das Skript `caddy/cluster.Caddyfile` (in `.gitignore`), mit einem `handle_path /m//*` und rotierter Upstream-Liste pro Modell. Ohne Knoten entsteht eine gültige Datei, die 503 liefert. [scripts/start.sh](scripts/start.sh) ruft das Skript vor `docker compose up` auf. 3. **[llama-swap/config.yaml](llama-swap/config.yaml)**: Block `peers:` mit einem Eintrag pro Cluster-Modell, `proxy: http://cluster-lb:8090/m/`, `apiKey: ${env.CLUSTER_API_KEY}`, dazu großzügige `timeouts.responseHeader` für Modellwechsel und Download. 4. **`.env.example` / `.env`**: `CLUSTER_API_KEY` (start.sh erzeugt ihn wie `LLM_API_KEY`, wenn er fehlt), `CLUSTER_NODES`, `CLUSTER_SLOTS`. 5. **Neu `scripts/cluster-status.sh`**: zeigt pro Knoten `/health`, das geladene Modell (`/running`) und die Antwortzeit. start.sh zeigt die Ausgabe im Statusblock, wenn Knoten gesetzt sind. ## Windows-Knoten-Skripte (`cluster/windows/`) - **`install-node.ps1`** (als Admin; wiederholbar, dient auch zum Update): Parameter `-GatewayIP` und `-ApiKey` (Pflicht), `-Port 8080`, `-InstallDir C:\localai-node`, `-LlamaBuild latest`, `-Cuda auto|12.4|13.4`, `-Preload ` (optional), `-HfToken`, `-KeepSleep`, `-Force`. 1. Prüft Admin-Rechte, `nvidia-smi` (GPU, VRAM, Compute Capability, Treiber) und freien Speicherplatz (Warnung unter 60 GB, weil alle Modelle auf jedem Knoten liegen können). 2. CUDA-Variante: 13.4, wenn Compute Capability ≥ 7.5 und Treiber ≥ 580, sonst 12.4. 3. Lädt llama-swap und llama.cpp CUDA + cudart herunter, entpackt sie nach `bin\` und testet `llama-server.exe --version`. 4. Schreibt `config.yaml` aus der Vorlage. Bestehende Dateien bleiben unangetastet, außer bei `-Force`. 5. Erzeugt `start-node.cmd`: setzt `LLAMA_CACHE`, `HF_TOKEN` und `NODE_API_KEY`, startet `llama-swap.exe -config … -listen 0.0.0.0:PORT` und schreibt das Log nach `logs\`. 6. Firewall-Regel: TCP-Port eingehend nur von `GatewayIP`. 7. Geplante Aufgabe `LocalAI-Node`: Start beim Booten, als SYSTEM, ohne Laufzeitlimit, Neustart bei Absturz. 8. Standby am Netzteil aus (`powercfg`), außer bei `-KeepSleep`. 9. Startet die Aufgabe, wartet auf `/health` und gibt die Zeile für `CLUSTER_NODES` aus. - **`download-models.ps1`** (optional): lädt alle Cluster-Modelle vorab herunter, indem es jedes Modell einmal kurz anfragt. So dauert die erste echte Anfrage nicht wegen eines 20-GB-Downloads. - **`node-config.yaml`** (Vorlage, Quelle der Modellliste): alle Cluster-Chat-Modelle (`cluster-qwen3.5-35b-a3b`, `cluster-glm-4.7-flash`, `cluster-qwen3.5-9b`) in **einer** Swap-Gruppe, Pfade mit `/`, `--fit` mit `--fit-target 1024` (passt sich automatisch an den VRAM des Knotens an), `-np `. Dazu `apiKeys: ["${env.NODE_API_KEY}"]`, optional Preload, `healthCheckTimeout: 3600`. - **`install-node.cmd`**: Doppelklick-Wrapper, fordert Admin-Rechte an, setzt `-ExecutionPolicy Bypass` und fragt Gateway-IP und Key ab. - **`status-node.ps1`**: Zustand der Aufgabe, `/health`, `/running`, `nvidia-smi`, letzte Logzeilen. - **`uninstall-node.ps1`**: entfernt Aufgabe und Firewall-Regel und löscht optional das Verzeichnis (`-KeepModels`). ## Doku - Neu **`docs/cluster.md`**: Architektur und Routing (Stammknoten, Überlauf), Ablauf (Key → Knoten installieren → IP in `CLUSTER_NODES` → `./scripts/start.sh`), Modell hinzufügen (nur in `node-config.yaml` + `peers`, dann Knoten mit `-Force` aktualisieren). Grenzen: Ein Modell, das größer als VRAM+RAM eines Knotens ist, wird nicht über mehrere PCs verteilt. Kleine GPUs lagern per `--fit` in den RAM aus und sind dann langsamer. - **[README.md](README.md)**: Abschnitt „Cluster“ mit Link, Dateiliste ergänzen. Die Client-Doku nennt die `cluster-*`-Modellnamen. ## Verifikation 1. `cluster-gen.sh` mit 0, 1 und 3 Test-Knoten ausführen und jeweils `caddy validate` (Docker, `caddy:2.10.2-alpine`) laufen lassen. Außerdem `docker compose config`. 2. Kette ohne Windows testen: Als Fake-Knoten dient eine zweite llama-swap-Linux-Binary bzw. ein Echo-Server im Scratchpad, auf zwei Ports. Prüfen: - `/v1/models` zeigt `cluster-*`. - Die Anfrage läuft über :8800 → peer → cluster-lb → Stammknoten. - Streaming funktioniert. - Bei belegtem Stammknoten geht die Anfrage an den nächsten Knoten. - Ein gestoppter Knoten wird übersprungen. 3. PowerShell-Syntax mit `pwsh` parsen, falls verfügbar. Den echten Lauf auf einem Windows-PC macht der Nutzer; der Installer endet mit einem Selbsttest (`/health`). 4. Der bestehende Betrieb bleibt unverändert: `./scripts/start.sh` startet weiter erfolgreich, auch ohne Knoten.