StudioForge: Ein GPU-Only-LLM-Server, der LM Studio auf meinem Rig ersetzt hat – ferngesteuert von meinen Agenten

Veröffentlicht
23 August 2026
Von
Jacob Lloyd — mit KI-Unterstützung geschrieben, im Nachhinein
Lesezeit
52 Min. Lesezeit

Kurz gesagt: Dies ist ein Programm, das auf dem Rechner mit den Grafikkarten läuft. Apps bitten es um eine Antwort von einem KI-Modell, und es ermittelt, auf welche Karten dieses Modell passt, startet es, hält es warm, solange es genutzt wird, und fährt es wieder herunter, wenn es still wird. Die Regel, die es niemals bricht: Ein Modell passt entweder vollständig auf die Grafikkarten oder es wird abgelehnt – es wird nicht stillschweigend die Hälfte davon auf dem langsamen Prozessor ausführen und dich ratlos zurücklassen, warum alles kriecht. Der Haken: Es braucht NVIDIA-Karten, ist am besten auf Windows getestet, und ich habe noch keine Lizenz dafür gewählt.

StudioForge ist der lokale Modellserver, den ich geschrieben habe, um LM Studio auf meinem GPU-Rig zu ersetzen: ein llama.cpp-Supervisor mit einer OpenAI-kompatiblen API auf Port 1234 – bewusst LM Studios Port –, einem Browser-Bedienfeld, einem Recovery-Sidecar, das antwortet, wenn der Hauptprozess es nicht mehr tut, und einer über MCP veröffentlichten Verwaltungsebene, damit ein Agent auf einem anderen Rechner ihn ohne Shell steuern kann.

Es existiert wegen eines Incident-Protokolls, das irgendwann nicht mehr lustig war. Ein 200, das nichts bewies: LM Studio beantwortet nicht geroutete Pfade mit einem Erfolgsstatus und einem Fehler-Body – sein eigenes Log schreibt Returning 200 anyway. /v1/models listet alles Heruntergeladene statt alles Geladene, sodass „was ist tatsächlich resident?“ keine Antwort hatte. Reasoning-Modelle, die ein 8.192-Token-Fenster überlaufen, weil der Kontext, den ich in der Client-Konfiguration gesetzt hatte, nicht der Kontext war, den das Laden verwendete – in LM Studio ist das Fenster beim Laden festgelegt, und nichts sagte es mir. Und der teure: Ein Modell, das nicht passte, wurde verkleinert statt abgelehnt – das dokumentierte Verhalten ist, dass es „die GPU-Offload-Größe automatisch reduziert … und den Rest in den System-RAM legt“, was GPU-Geschwindigkeit gegen System-RAM-Geschwindigkeit tauscht und so oder so Erfolg meldet.

Wenn du den dsh-Artikel gelesen hast, kennst du diesen Server bereits: Der Provider-Block mit der Bezeichnung „StudioForge (GPU rig)“ ist genau das hier. Er ist es, worauf der Acht-Agenten-Stack über das Netzwerk für schwere Arbeit zugreift, womit DisPatch spricht und worum OpenClaw Email für ein Chat-Modell und ein Embedding-Modell bittet. Mein OpenClaw-Artikel benannte das Problem – „Minuten an Modell-Ladezeit, VRAM-Jonglieren zwischen Diensten“ – und was folgt, ist die Antwort, wobei jede Regel die Messung trägt, die sie erzwungen hat.

tl;dr

  • Was es ist: ein GPU-only, OpenAI-kompatibler LLM-Server auf dem llama-server von llama.cpp, gemessen an Build b10425 (CUDA 13.3). Ein Gateway auf 1234, ein Bedienfeld auf 8080, ein Watchdog auf 1235, ein Kindprozess pro geladenem Modell auf 18100–18200.
  • Was es tut: lädt ein Modell bei der ersten Nutzung, plant dabei Kontext, KV-Cache-Typ, GPU-Platzierung und Slot-Anzahl gegen den in diesem Moment freien VRAM; fährt es nach Leerlauf herunter; hält gepinnte Modelle resident; stellt auf Anfrage einem einzelnen Modell ganze Karten zur Verfügung; und veröffentlicht 29 Tools über MCP – 19 Verwaltungs-, 10 Recovery-Tools.
  • Was es niemals tut: auf die CPU auslagern (ein Modell passt vollständig in den VRAM oder wird mit den Zahlen abgelehnt), nach Hause telefonieren (die einzigen ausgehenden Verbindungen sind Hugging Face für Modelle, GitHub für den gepinnten llama-server-Build und seine Update-Prüfung, die optionale StudioForge-Release-Prüfung und Bild-URLs, die eine Anfrage nennt) oder Inferenz über MCP ausführen – die Steuerungsebene hat kein Completion-Tool und sagt das in Großbuchstaben.
  • Was du brauchst: NVIDIA-GPUs mit einem Treiber der 580er-Serie oder neuer, Python 3.12+, uv, einen Ordner mit GGUFs. Noch nie ein Modell lokal ausgeführt? Dann fang hier an.
  • Was du am Ende hast: eine Basis-URL, die jeder OpenAI-Client in deinem Netzwerk unverändert nutzt, ein Bedienfeld, das benennt, was jedes Gigabyte auf jeder Karte belegt, und einen Assistenten, der sagen kann „lade die 27B mit 128k auf die beiden 5090er“ – und es passiert.
  • Der ehrliche Teil: Windows ist die Referenzplattform, nur NVIDIA, und es gibt noch keine Lizenzdatei – lies zuerst Zum Download.

Was es macht, in einem Bild

Was es nicht ist:

  • Kein Modell. Es führt die GGUFs aus, die du hast, und lädt weitere in denselben Ordner und dieselbe Struktur, die LM Studio nutzt.
  • Keine Inferenz-Engine. llama.cpp rechnet; das hier entscheidet, welcher Prozess mit welchen Flags auf welchen Karten läuft.
  • Keine Chat-App. Es gibt einen Chat-Tab, und er existiert, um zu beweisen, dass der echte Anfragepfad funktioniert.
  • Kein Cluster. Ein Rechner, seine eigenen Karten. Das RPC-Backend von llama.cpp existiert und ist nicht angebunden.
  • Kein CPU-Inferenz-Server. Im gesamten Codebestand gibt es keinen anderen --n-gpu-layers-Wert als 999.
  • Keine zweite API-Oberfläche. Kein Ollama-/api/generate, keine KoboldCpp-API – die OpenAI-Oberfläche, ein LM-Studio-artiger /api/v0-Spiegel und das Verwaltungs-REST /api, und das war's.

Was wo läuft

TeilWoWozu es dient
Gatewayder GPU-Host, ein Prozess, 1234/v1, /mcp (19 Tools), /api. Enthält die Registry, den Planer und den Supervisor.
Bedienfeldderselbe Prozess, zweiter uvicorn, 8080Dashboard, Setup, Models, Download, Chat, Server, Logs.
Watchdogein separater Prozess, 1235Sein eigener MCP-Server, 10 Recovery-Tools. Er überlebt, was er überwacht.
llama-server-Kindprozesse18100–18200, nur LoopbackEiner pro geladenem Modell; ein Absturz reißt ein Modell mit, nie das Gateway.
sfctl-Begleiterder Rechner des AgentenEin reiner HTTP-Client – Python 3.11+, kein CUDA, keine Server-Abhängigkeit. Auch die Stdio-MCP-Bridge.
GGUF-Bibliothekmodels.dir, wo sie bereits istAn Ort und Stelle indiziert; nichts kopiert. LM Studio nutzt denselben Ordner weiter.

Was die Tabelle sagt:

  • Nur der GPU-Host installiert etwas. Ein Client braucht eine Basis-URL; ein Agent braucht ein kleines Python-Paket, und das nur für die Verwaltungs-Tools.
  • Die Kindprozesse sind von außen unsichtbar. Sie binden an 127.0.0.1 und sonst nichts, sodass das Gateway die einzige öffentliche Oberfläche ist – genau das macht einen API-Schlüssel zu einer echten Grenze.

Wie eine Anfrage fließt

Alles, was ein Client falsch machen kann, wird vor dem ersten Byte geprüft, denn eine fehlerhafte Anfrage soll ein echtes 4xx bekommen (eine 404 für eine Modell-ID, die es nicht gibt) mit einem JSON-Body, statt eines Fehler-Frames, der in einem 200-SSE-Stream vergraben ist – Ersteres beherrschen Clients, Letzteres behandeln sie regelmäßig falsch. Was GET /health auf meinem Rig meldet:

{"status": "ok", "version": "1.26-08-23", "uptime_s": 12690.6,
 "loaded_models": ["ggml-org/SmolVLM-256M-Instruct-GGUF/SmolVLM-256M-Instruct-Q8_0"],
 "busy": {"active_requests": 0, "busy_models": [], "loading": [], "testing": null},
 "draining": false, "instance": "primary",
 "boot": {"phase": "ready", "ready": true, "elapsed_s": 0.2, "error": null},
 "engine": {"ok": true, "tag": "b10425", "variant": "cuda", "smoke_tested": true},
 "gpu_count": 4, "models_indexed": 34, "can_serve": true}
  • can_serve ist die Antwort auf das 200-das-nichts-beweist-Problem. Es ist false, solange der erste Bibliotheks-Scan läuft, während status bei ok bleibt, weil der Prozess lebt und genau das ein Liveness-Poller fragt. GET /health?deep=true führt eine echte 8-Token-Completion (bei Embedding-Modellen einen Embeddings-Aufruf) gegen jedes geladene Modell aus – und ohne geladene Modelle antwortet es mit no_models_loaded statt zu bestehen, denn eine Prüfung, die nicht scheitern kann, ist schlimmer als keine Prüfung.
  • Der local-model-Alias wird aufgelöst. local-model, default, auto und current mappen alle auf models.default_model. LM-Studio-Clients fallen auf diesen Literal-String zurück, daher würde ein 404 für ihn sie grundlos kaputtmachen.
  • Ein kaltes Modell sieht nicht wie ein Hänger aus. Der offene Stream trägt alle fünf Sekunden : loading <model id> (5s), dann : prefilling <model id> (Ns) bis zum ersten echten Token – SSE-Kommentarzeilen, die jeder Parser ignoriert. Ein so lange stummer Socket löst ein Read-Timeout aus, und ein erneut versuchender Client stapelt noch mehr Prefill auf einen gesättigten Batch.
  • Ein Ladevorgang nach dem anderen, maschinenweit. Zwei kalte Modelle wurden einmal gleichzeitig auf dieselben Karten geplant; eines starb mit CUDA error: out of memory, sein Retry verdrängte das andere neue Modell, und die Anfrage dieses Clients traf einen toten Kindprozess.
  • Ein ttl auf Anfrageebene verschiebt nur den Leerlauf-Timer und sonst nichts. ttl: 0 ist hier überall die Wire-Form von „gepinnt“, daher wird es ignoriert statt befolgt – ein Client, der {"ttl": 60} sendete, hat früher entpinnt, was sein Besitzer gepinnt hatte.

Die Wahrheit über VRAM: der Planer

Das ist der Teil, den sonst niemand macht, deshalb hier im Detail statt in Adjektiven. core/planner.py umfasst 3.188 Zeilen, die eine Frage beantworten, bevor ein Modell startet: Angesichts des gerade wirklich freien VRAM auf jeder Karte – was ist das beste Fenster, die beste Cache-Qualität und die beste Slot-Anzahl, die dieses Modell haben kann – und wenn die Antwort „nichts“ ist, was soll ich dir dann sagen?

Die Leiter, und was sie nicht zu tauschen bereit ist

Die dort gezeichneten Stufen sind illustrativ, nicht die ausgelieferten Standardwerte: Der Mechanismus ist exakt, die Zahlen ein Beispiel. Was config.example.yaml ausliefert, ist target_ctx: 1048576 als Ziel und default_ctx: 8192 als Boden, wobei das Ziel zuerst auf das trainierte Fenster des Modells begrenzt wird – obwohl bei einem ersten Lauf tune_for_hardware default_ctx: 16384 schreibt, wenn die kleinste Karte 24 GiB oder mehr hat, und 8192 ab 12 GiB; mein Rig fährt mit einem Boden von 128000. Über ein trainiertes Fenster hinauszugehen braucht RoPE-Skalierung und verschlechtert die Qualität, daher wird eine Stufe darüber nie angeboten. Ein explizites ctx_size ist eine einstufige Leiter.

Zwei Durchgänge, der zweite nur, wenn der erste überall scheiterte. Der Grund ist ein Ladevorgang um 12:03 an einem Nachmittag: 79,832 MB frei, weitere 19,423 MB von einem inaktiven Modell zurückgewinnbar. Bei diesem Budget passte 262144 mit einem q4_0-Cache in 96,004 MB, und 65536 mit vollem f16 in 95,236 MB. Geladen wurde 8192/f16 bei 89,860 MB – das Modell zahlte den vollen Preis der Verdrängung und bekam das kleinste Fenster der Leiter.

KV ist nicht eine Zahl pro Modell

Fast jeder VRAM-Rechner im Netz berechnet KV als Layer × Heads × Head-Dimension × 2 × Bytes × Kontext. Für eine Llama richtig, für zwei Familien, die tatsächlich genutzt werden, kräftig falsch: Gemma 3 und 4 verschränken fünf Sliding-Window-Layer pro vollem Layer bei halber Head-Dimension, und Qwen3.5, 3.6 und 3.8 deklarieren full_attention_interval = 4, sodass ein KV-Cache nur auf jedem vierten Layer existiert und der Rest Gated-DeltaNet-rekurrente Layer mit einem festen Zustand pro Sequenz sind.

Die Kosten des Irrtums: Eine Gemma-4-31B, die 262,144 Tokens anforderte, wurde auf 480 GiB KV geschätzt und auf 65,536 begrenzt, während das Kalibrierungs-Log seit Wochen predicted_mb=95615 actual_mb=40037 verzeichnete. Nach dem Geometrie-Fix landen Prognose und Realität beide bei 38 GiB über zwei 5090ern bei n_ctx=262144 – eine 4×-Freischaltung, die die ganze Gemma-4-Flotte mitnahm. KV auf jeden Layer einer Qwen3.5 anzurechnen war derselbe Bug mit anderem Hut: eine glatte 4×-Überberechnung.

Zwei Details, die es wert sind, geklaut zu werden. Die Sliding-Window-Zellenanzahl muss llama.cpp exakt spiegeln; ein pauschaler 1.25×-Multiplikator war in die gefährliche Richtung falsch, bei vier Slots um das 3.6-Fache zu niedrig. Und attention_kind wird aus der Layer-Geometrie abgeleitet statt aus general.architecture – wo es nicht abgeleitet werden kann, meldet es unknown, was „misstraue hier jeder KV-Zahl“ bedeutet, nie „nimm den günstigen Fall an“.

Die Cache-Qualität wird innerhalb jeder Stufe gewählt statt gegen ein breiteres Fenster getauscht: f16/f16 → q8_0/q8_0 → q8_0 K + q4_0 V, wobei symmetrisches q4_0 aus jedem automatischen Pfad entfernt wurde. Kein Geschmack – mit einem q4_0-K-Cache reproduziert Qwen2.5-7B nur 11.7% der Tokens, die sein f16-Ich erzeugt, während ein passendes q8_0/q8_0-Paar bei einer KL-Divergenz von 0.0018 liegt.

Vier Karten, zwei Generationen

Das Rig sind zwei RTX 5090 und zwei RTX 3090 – nominell 32-GB- und 24-GB-Karten, die das Panel als 31.84 GiB und 24.0 GiB zählt, 111.7 GiB insgesamt – auf Treiber 610.88, CUDA-Treiber 13.3. Immer zuerst eine einzelne Karte – ein gesplittetes Modell auf PCIe ohne NVLink ist spürbar langsamer – und der Planer überschreibt das nur, wenn die Einzelkarten-Platzierung bei einem Slot ausgehungert ist, der Split sie mindestens verdoppelt, jede hinzugefügte Karte mindestens so leistungsfähig ist und du die Slot-Anzahl auf auto gelassen hast. Diese letzte Bedingung ist keine Höflichkeit: Ein Split läuft im Tempo seines langsamsten Mitglieds.

Platzierung (1.5B Q4_K_M, zwei 3090er, 8k Kontext)GenerierungPrompt-Verarbeitung
Eine 3090352.5 tok/s2803.6 tok/s
Zwei, -sm layer344.4 tok/s2722.5 tok/s
Zwei, -sm tensor294.3 tok/s1182.0 tok/s
Zwei, -sm rowschlägt fehl: error loading model: device CUDA2 does not support split buffers

Was die Tabelle sagt:

  • Eine Karte schlug zwei auf beiden Achsen. Layer-Split kostet etwa 2% Generierung; Tensor-Split kostet 17% Generierung und 58% Prompt-Verarbeitung – daher ist der Tensor-Modus opt-in, und nur eine Messung darf ihn wählen.
  • -sm row ist unter CUDA tot. Der Parser akzeptiert es, und der Ladevorgang scheitert dann – daher wird es vor dem Spawnen des Kindprozesses abgefangen statt danach.

Zwei Platzierungsdetails zeigen sich erst, wenn man pro Karte misst. Der Output-Layer wird dem letzten Device angerechnet, weil Quantizer Embedding- und Output-Tensoren selbst in einer Q4-Datei bei Q6_K oder Q8_0 halten: Eine 27B, geplant als --device CUDA1,CUDA0 --tensor-split 0.5079,0.4921, landete mit 15.52 GiB auf CUDA0 – dem letzten Device, dem der Split weniger gab – gegenüber 14.48 auf CUDA1. Und llama.cpp öffnet auf jedem sichtbaren Device einen CUDA-Kontext – ~0.22 GiB auf einer 3090, 0.43 GiB auf einer 5090 – weshalb die Platzierungsspalte einen Boden von 512 MiB hat.

Wie viele Gespräche eine Platzierung wert ist

Das --ctx-size von llama.cpp ist das gesamte KV-Budget, das über die Slots geteilt wird, nicht das Fenster pro Slot – ein weithin missverstandenes Flag, und die README von Upstream erklärt es nicht. Ein Ladevorgang mit --ctx-size 4096 und ohne --parallel meldet total_slots: 4: 1,024 Tokens pro Gespräch. StudioForge startet mit ctx_per_slot × parallel. Dann: Wie viele Slots sind es wert – und genau da habe ich aufgehört, meiner eigenen Arithmetik zu vertrauen.

GleichzeitigPro StreamGesamtp50p95Erreichter Batch
1302.8 tok/s302.8 tok/s0.41 s0.41 s1.00
2225.3 tok/s425.3 tok/s0.46 s0.49 s1.84
4134.5 tok/s436.0 tok/s0.83 s1.00 s3.46
883.3 tok/s576.9 tok/s1.57 s1.77 s6.03

Qwen2.5-1.5B-Instruct-Q4_K_M, eine RTX 3090, 8,192 Tokens pro Slot, f16-KV, acht Slots gestartet, 512-Token-Prompts, je 192 generierte Tokens.

Was die Tabelle sagt:

  • Der Schätzer sagte 8. Die Messung sagte 2. Bei vier Slots fällt jeder Stream auf 44% der Solo-Geschwindigkeit, unter eine 65%-Untergrenze; die Regel nimmt die größte aus 1/2/4/8, die diese Untergrenze überschreitet und dabei noch 15% Gesamtdurchsatz gegenüber der Stufe darunter gewinnt.
  • Der Gesamtdurchsatz steigt unaufhörlich – acht Slots bewegen das 1.9-Fache der Tokens eines Slots – während ein einzelnes Gespräch auf 27% einbricht. Eine Regel, die den Gesamtdurchsatz maximiert, würde 8 wählen, und jeder Nutzer würde ein Modell erleben, das dreimal langsamer ist, als die Karte es ausführen kann.
  • Das Batching ist echt, kein Queuing. Der erreichte Batch, der von 1.00 → 1.84 → 3.46 → 6.03 steigt, beweist geteilte Decode-Schritte; über drei Läufe innerhalb von 2% reproduziert.

Ein zweiter Lauf auf zwei 3090ern mit 32,768 pro Slot ergab 301.7 → 230.5 pro Stream und antwortete wieder 2. Übrigens hatte der Katalog 308.0 tok/s für diese Platzierung vorhergesagt, und der Lauf maß 301.7 – 2% daneben, besser als erwartet. Zeilen tragen jetzt max_parallel (wie viele passen) neben recommended_parallel (wie viele es wert sind, ausgeführt zu werden).

Zwei Flags, die ich gemessen habe, bevor ich ihnen vertraute

Spekulatives Decoding ist ein Einzel-Stream-Gewinn. Qwen3.8-27B Q5_K_S mit MTP-Head, eine 3090, vier verschiedene 256-Token-Prompts, Prompt-Cache aus: ohne Spekulation 37.75 tok/s; draft-mtp in Tiefe 3 ergab 50.70 tok/s, +34.3%, bei 0.528 Akzeptanz. Tiefe 4 fällt ab auf 47.48, weil die Akzeptanz auf 0.446 sinkt und jeder zusätzlich abgelehnte Token umsonst verifiziert wurde. ngram-mod schaffte +0.4% und erzeugte überhaupt keine Drafts.

Was zur Falle führt: Derselbe Prompt dreimal gemessen ergab +751% auf dieser 27B. Wiederhole einen Prompt, und du misst den Prompt-Cache und nennst es Drafting. Oberhalb von vier Slots gibt auto jetzt none zurück und sagt, warum – „Spekulation ist ein Einzel-Stream-Gewinn und schadet einem gesättigten Batch“ – nachdem ein Lauf eine 27B mit --parallel 8 lud und auto immer noch draft-mtp wählte, weil es den MTP-Head sah und nicht die Slot-Anzahl.

Micro-Batch kauft Prefill für VRAM. Dieselbe 1.5B, ein 5,166-Token-Prompt: -ub 512 (der Engine-Standard) ergab 15,232 tok/s bei 1492 MiB; -ub 1024, 17,307 tok/s (+13.6%) bei 1562 MiB; -ub 2048, 18,061 tok/s (+18.6%) bei 1702 MiB. Es war lange aus, weil der Compute-Puffer mit -ub wächst, der Planer ihn nicht modellierte und ein nicht modellierter Puffer aus einem Passen ein Out-of-Memory macht. Der Planer berechnet ihn jetzt, aufgerundet, sodass er eher zur Ablehnung neigt, und erhöht den Micro-Batch nur oberhalb von vier Slots automatisch.

Die Ablehnung, mit den Zahlen

Jeder Start übergibt --fit off und --n-gpu-layers 999, und das Zweite ist eine Konstante, keine Einstellung. Das zählt heute mehr als früher: Der gepinnte Build b10425 liefert -fit, --fit [on|off]„ob nicht gesetzte Argumente angepasst werden sollen, um in den Gerätespeicher zu passen“standardmäßig auf on, neben --n-gpu-layers auto; beides landete im Dezember 2025 in PR #16653 upstream. Genau dieses Paar ist ein stiller Teil-Offload-Pfad: ein vernünftiger Standard für einen Allzweck-Server und exakt das Verhalten, das zu verweigern dieses Projekt existiert. Wenn nichts passt, ist die Antwort HTTP 507 mit der Rechnung im Body, hier gekürzt:

HTTP 507  {"error": {"code": "insufficient_vram", "type": "server_error", "message":
 "Cannot load 'lmstudio-community/gemma-4-31B-it-QAT-GGUF/gemma-4-31B-it-QAT-Q4_0'
  entirely in VRAM: needs 29.09 GiB, 20.90 GiB usable. largest single GPU offers
  20.90 GiB usable (headroom 10% reserved). Suggestions: set KV cache type to q8_0
  (roughly halves KV cache VRAM for a small quality cost); VRAM is held by other
  processes: 5.87 GiB held by python.exe (pid 45072) on CUDA1; 0.83 GiB held by
  dwm.exe (pid 2468) on CUDA0; …; clear the per-model device override so the
  planner can use other GPUs",
 "studioforge": {
   "required_bytes": 31235974510, "available_bytes": 22438368871,
   "per_gpu_free": {"3": 22438368871},
   "max_ctx_that_fits": null, "max_parallel_that_fits": null,
   "suggestions": ["set KV cache type to q8_0 (roughly halves KV cache VRAM
                   for a small quality cost)",
                   "VRAM is held by other processes: …",
                   "clear the per-model device override so the planner can use
                   other GPUs"],
   "notes": ["wanted up to 262144 tokens of context but not even the 128000 floor
             fits in the VRAM available right now",
             "device placement forced by per-model device_override"],
   "estimate_mb": {"weights_bytes": 16818.2, "kv_bytes": 8575.0,
                   "compute_bytes": 2438.6, "mmproj_bytes": 1145.1,
                   "mmproj_compute_bytes": 512.0, "cuda_context_bytes": 300.0,
                   …, "total": 29788.9},
   "vram_holders": [ … one entry per process, per card … ],
   "busy_models": [], "retry_after_s": null }}}

Das ist eine echte Ablehnung, festgehalten beim Schreiben dieses Artikels: Die 31B wollte auf eine einzelne RTX 3090 geladen werden. Der Fließtext benennt die Lücke und die Inhaber; error.studioforge trägt denselben Fehler als Daten – die benötigten und verfügbaren Bytes, freier VRAM pro Karte, die Schätzung aufgeschlüsselt in Gewichte, KV, Compute-Puffer, Projektionsmodell und CUDA-Kontext, jeder Prozess, der VRAM hält, sowie notes, die sagen, auf welcher Stufe der Leiter sie stand, als sie aufgab. Wenn ein kleineres Fenster passen würde, benennt max_ctx_that_fits es, berechnet auf der Pro-Layer-Geometrie, sodass das Angebot eines ist, das der nächste Ladevorgang akzeptiert; hier passte nicht einmal der Boden, daher ist es null statt einer Zahl, die scheitern würde. Eine Ablehnung, die nicht auf ein ausgelastetes Modell zurückgeht, trägt kein retry_after_s, denn „später nochmal versuchen“ ist ein schlechter Rat, wenn sich nichts ändern wird.

Pins, TTLs, Leases und der Rebalancer

Ein Pin ist ein gewünschter Zustand, keine Ausnahme. Früher bedeutete er TTL null, Ausschluss aus jeder Verdrängungs-Leiter und ein Warm-up beim Start – und nicht das Vierte: Ein gepinntes Modell wurde nie erneut geladen, sodass ein Kindprozess, der über sein modellspezifisches max_restarts hinaus in eine Crash-Schleife geriet, mit state="failed" herumsaß und nichts hielt. Ein Reconciler läuft jetzt im 15-Sekunden-Sweep mit, mit Backoff von 60 s bis zu einer 900-s-Obergrenze. Das Einzige, das einen Pin schlägt, ist ein Mensch: Ein explizites Entladen markiert die ID als unterdrückt, und sie bleibt unten.

Ein Lease ist eine Karte, die einem Modell gehört. Eine geleaste Karte ist aus der GPU-Sicht jedes anderen Modells verschwunden – nicht zuletzt gereiht, keine Option – und der Besitzer wird auf genau diese Karten gezwungen, vom Schätzer dimensioniert, im Split-Modus, den sein eigener Benchmark dort als schnellsten maß. Eine bereits geleaste Karte ist ein 409-Konflikt, nie eine Übernahme. Ein Lease ohne Modell hält Karten für etwas außerhalb des Servers: reserve_gpus(devices=[3], reason="ComfyUI render") ist der Grund, warum meine Bildgenerierung und meine Sprachmodelle aufgehört haben zu streiten.

Der Rebalancer korrigiert die gute Entscheidung von gestern. Um 13:42 wurde eine 27B auf die Karten [1, 3] geplant – ein Stufen-übergreifender Split mit geteilter GPU1 – weil eine 31B [1, 0, 2] hielt und eine Bildgenerierungs-App 7.5 GiB von GPU2 belegte. Um 13:53 schrumpfte die 31B auf [0, 1]; von da an lagen [2, 3] frei und strikt besser, und die 27B blieb. Also wird ein inaktives Modell jetzt verschoben, wenn ein Plan ohne Verdrängung bei seinen exakt aktuellen Einstellungen es von jeder geteilten Karte herunterbekommt. Geschätzte Tokens pro Sekunde rechtfertigen eine Verschiebung nie.

Er schaut einmal pro Minute, und nur, wenn sich die Welt geändert hat: nur auf einem ruhigen Rechner, nur bei einem fünf Minuten inaktiven Modell, eine Verschiebung pro Modell pro 30 Minuten – denn eine Verlagerung ist ein Neuladen, und ein Neuladen verwirft den Prompt-Cache, und bei der Langgespräch-Last dieses Rigs machte dieser Cache 93% eines 98k-Token-Prompts aus. Verdrängung hat außerdem drei harte Regeln – nie ein gepinntes Modell, nie eines mitten in einer Anfrage, nie eine ladende Instanz – und eine geleaste Karte ist von vornherein nicht im Blickfeld des Planers. Ein Just-in-time-Laden kann force nie setzen.

Wenn Dinge kaputtgehen

VRAM stirbt mit dem Prozess, der ihn genommen hat. Unter Windows leben die Kindprozesse in einem anonymen Jobobjekt, das mit JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE erstellt wurde, sodass der Kernel jedes Mitglied beendet, wenn das letzte Handle schließt – egal, wie der Elternprozess endet. Anonym, weil ein benanntes Jobobjekt mit allem geteilt würde, das den Namen errät. Linux bekommt einen PR_SET_PDEATHSIG-Shim, der ein kill -9 des Gateways abdeckt; das ist Best Effort statt Kernel-Garantie, daher fangen der Startup-Sweep und reclaim_orphan_engines ab, was durchrutscht.

Deshalb gibt es einen Startup-Sweep. Am 18. August 2026 waren ~10 GiB auf GPU0 und ~15.6 GiB auf GPU1 nicht verfügbar, obwohl „alles gestoppt“ war. Die Inhaber waren drei llama-server.exe-Kindprozesse eines python -m pytest tests -q-Laufs, den ein Coding-Agent gestartet hatte und der inzwischen beendet war. Jeder Inhaber wird jetzt klassifiziert – ours, child-of-live-process, orphan, other-instance, foreign – und nur orphan wird je beendet, sicher per Konstruktion, weil nichts anderes Binärdateien aus unserem Engines-Baum startet.

Zu benennen, wer was hält, brauchte zwei Anläufe. NVML meldet unter Windows null genutzten Speicher pro Prozess, daher stammen die Größen aus dem Zähler, den die Task-Manager-Spalte „Dedizierter GPU-Speicher“ liest – aber das ist eine prozessweite Summe über alle Adapter, daher war die Device-Spalte falsch: Ein Prozess, der auf CUDA0,1,2,3 gemeldet wurde, hielt tatsächlich 15.52 GiB auf CUDA0 und nichts auf den 3090ern. Der Fix verbindet die Adapter-LUID mit einer PCI-Busadresse, und eine Falle verdient den Absatz: Die Busnummer in NVMLs busId ist hexadezimal. "00000000:42:00.0" ist Bus 66, nicht 42 – eine selbstbewusst falsche Antwort, ununterscheidbar von einer richtigen.

Ein Entladen wird verifiziert, nicht angekündigt. Der Supervisor prüft die PID erneut, mit einem Erstellungszeit-Schutz gegen PID-Wiederverwendung, eskaliert einen Überlebenden zu einem erzwungenen Tree-Kill und zeichnet den VRAM davor und danach auf; ein Überlebender wirft eine 500 mit der Aufforderung, ihn manuell zu beenden. Ein Entladen, das Erfolg meldet, während der Prozess resident bleibt, ist die teuerste Lüge, die dieses System erzählen kann, denn jeder spätere Ladevorgang wird dann gegen VRAM geplant, der nicht frei ist.

Exit-Codes sind Vokabular. 2 ist ein Konfigurationsfehler, der den Schlüssel benennt. 3 ist ein Portkonflikt, und der Tray startet nie auf den konfliktierenden Port neu – er wartet, bis der Inhaber /health als StudioForge-Server beantwortet, und hängt sich stattdessen an. 75 ist „Neustart angefordert“: Der Server drainiert, setzt den Code und fährt sauber herunter – 1.0 s von Anfrage bis Exit, gemessen – und der Tray startet neu, ohne einen Absturzversuch zu verbrauchen. Diese Unterscheidung existiert, weil ein GUI-Neustart einmal zwei Server hervorbrachte, die um 1234 rasten, und drei gezählte Abstürze später saß der Tray auf Abgestürzt – siehe den Logs-Ordner neben einem gesunden Server, den er nicht mehr stoppen konnte.

Wenn das Gateway festhängt statt tot ist, sprichst du mit dem Watchdog: ein separater Always-on-Prozess auf 1235, gebaut aus argparse und stdlib-Logging, sodass er selbst dann startet, wenn config.yaml das Kaputte ist. Seine zehn Tools sind health, get_config, set_config, restart_server, kill_model, nuke_all_models, reclaim_orphan_engines, tail_logs, gpu_status und rollback_update. Er reimplementiert die Orphan-Regel lokal, statt das Modul zu importieren, das sie besitzt – der Recovery-Prozess darf den Stack, den er repariert, nicht importieren.

Das Bedienfeld

Das Panel auf 8080 ist ein zweiter uvicorn-Server im selben Prozess, der den Objektgraphen des Gateways per Referenz teilt – es enthält überhaupt keine absoluten URLs, was es über simples HTTP im Mesh-VPN und hinter einem HTTPS-Frontend identisch funktionieren lässt.

Das StudioForge-Dashboard während einer Anfrage: eine Kopfzeile mit 2 geladen, 4 GPU, 45.5 von 111.7 GiB frei; vier GPU-Karten – zwei RTX 5090, zwei RTX 3090 – mit belegten, freien und Auslastungs-Werten; eine VRAM-Inhaber-Liste mit zwei als ours markierten llama-server.exe-Prozessen und fünf fremden; ein leeres GPU-Leases-Panel; und zwei Karten geladener Modelle, eine beim Prefill, eine gepinnt und inaktiv
Das Dashboard während einer Anfrage: eine 31B, über beide 5090er aufgeteilt, beim Prefill eines 2,700-Token-Prompts, eine auf einer 3090 gepinnte 26B und jeder Prozess, der auf irgendeiner Karte VRAM hält, mit seinem Anteil benannt. Das Inhaber-Panel existiert, weil einmal 25 GiB verschwanden.
Eine Nahaufnahme der beiden Karten geladener Modelle: die 31B zeigt Port 18100, tatsächlicher ctx 262144, 1 busy / 0 idle, GPU0, GPU1 (layer), TTL 15m 00s, aktive Anfragen 1, gesamt 3, zuletzt 58.77 tok/s, Build b10425, und eine Slot-Zeile mit Processing prompt 2202/2742 (80%), cache hit 7/2742; die 26B zeigt Port 18101, GPU2, TTL pinned, zuletzt 46.67 tok/s, Slot 0 Idle
Dieselben beiden Karten, aus dem Bild oben zugeschnitten: der Port, den jedes Modell tatsächlich bekam, der Kontext, den es tatsächlich erhielt, die Karten, auf denen es tatsächlich landete, der TTL-Countdown – dann eine Zeile pro Engine-Slot. „Tatsächlich“ wird aus dem Kindprozess zurückgelesen, nicht aus dem, was wir angefordert haben.
Der Setup-Tab: eine Checkliste mit der Überschrift Ready to serve, but fix before exposing it: Network exposure, mit grünen Zeilen für das Datenverzeichnis, 50 GGUF-Dateien unter E:\LLM\Models, 34 indizierte Modelle, 4 GPUs auf Treiber 610.88 und CUDA 13.3, Engine b10425 mit Smoke-Test bestanden und das Gateway auf 0.0.0.0:1234, eine gelbe Network-exposure-Zeile mit einem Set-API-key-Button; dann der Abschnitt Model library und die Felder Defaults for every load – Kontext-Boden 128000, Ziel 1048576, Thinking-Modell-Boden 32768
Eine frische Installation öffnet hier, und „frisch“ wird gemessen statt erinnert – kein Modellverzeichnis, nichts indiziert oder keine Engine. Jede nicht erfüllte Zeile trägt den Button, der sie behebt; auf meinem Rig ist die eine gelbe Zeile das Gateway, das auf jeder Schnittstelle ohne API-Schlüssel lauscht – eine Entscheidung, kein Unfall.
Die Bibliothekstabelle des Models-Tabs: Spalten für Name, Downloaded, Size, Quant, Arch, Features, Last used, Status und Actions; elf Zeilen von zwei Qwen3.8-27B-Builds mit 21.29 und 17.95 GiB bis hinunter zu Qwen2.5-0.5B mit 0.49 GiB, mit Feature-Icons für Chat, Vision und Embedding; die gemma-4-26B-Zeile trägt ein Pinned-Badge und einen grünen Loaded-Chip; sechs kleine Aktions-Buttons beenden jede Zeile
Eine Tabelle statt Karten, denn bei ein paar Dutzend Modellen sind die Fragen vergleichend – welches kam zuletzt an, welches ist das große, welches kann Bilder sehen. Die Standardsortierung ist neuester Download zuerst, und die Status-Chips rechts sind die Live-Wahrheit darüber, welche davon resident sind.
Der Einstellungsdialog pro Modell für gemma-4-31B-it-QAT-Q4_0 (16.44 GiB, trainierter Kontext 262144): ein Optimal-settings-Block mit einer Zeile pro GPU-Satz – zwei RTX 5090 bei 262144 Kontext, q8_0, 3 Slots, etwa 62 tok/s; zwei RTX 3090 bei 1 Slot, etwa 34 tok/s; alle vier GPUs bei 3 Slots, etwa 40 tok/s, als fits now markiert; eine RTX 5090 bei 131072 Kontext, etwa 66 tok/s – jeweils mit Load-here- und Measure-parallel-Buttons; eine Load-at-exactly-Zeile mit 64K, 128K, 256K und 512K; ein grünes Fit-Urteil mit Fits across GPU1, GPU0, GPU3 (split: layer), KV-Cache q8_0, flash-attn an und prognostizierten 32.55 GiB VRAM pro Karte aufgeschlüsselt; und die Basic-Felder – Kontextlänge, KV-Typ K und V, TTL, eine Pinned-Checkbox, Draft-Modell und Device-Override
Dieser Dialog ist das ganze Argument auf einem Bildschirm: was das Modell auf jedem Kartensatz tun könnte, wenn sie frei wären, ein Fit-Urteil, das bei jeder Änderung mit dem prognostizierten VRAM pro Karte neu berechnet wird, und dann jeder Regler, um es zu übersteuern (die Stufen Advanced und Expert gehen unterhalb des Ausschnitts weiter). Unerreichbare Kontextgrößen werden ausgegraut mit dem Grund statt versteckt – „warum kann dieses Modell kein 512k“ ist die Frage, zu deren Beantwortung der Button existiert.
Der Download-Tab nach einer Hugging-Face-Suche nach gemma-4, sortiert nach Downloads (30d): zwölf Repositories von unsloth, lmstudio-community und google, jede Zeile zeigt den Herausgeber, eine Download-Zahl von 1,187,348 bis hinunter zu 458,712, Likes, die Anzahl der Quantisierungen und wann aktualisiert, mit einem Model-Card-Link und einem Quants-Button
Hugging-Face-Suche und Download-Warteschlange in einem Tab. Das Sortier-Label sagt absichtlich „Downloads (30d)“ – diese Zahl ist ein gleitender 30-Tage-Zähler, keine Gesamtsumme aller Zeiten – und der Quants-Button ist der Ort, an dem die Fit-Mathematik beginnt.
Der obere Teil des Quant-Auswahlers für unsloth/Qwen3.8-27B-GGUF: ein Kopfzeilen-Hinweis, dass Fit nur eine Gewichts-Schätzung ist, bis der Modell-Header gelesen wurde, eine Geometrie-Zeile mit attention hybrid, 65 Layer, KV 65 KB pro Token, dann sechzehn Zeilen von BF16 mit 51.77 GiB und einem gelben needs-multiple-GPUs-Chip bis hinunter zu Q5_K_M mit 19.28 GiB, jeweils mit einem grünen fits-one-GPU-Chip, einer Kontext-Zeile pro Platzierung für eine 5090, zwei 5090er und alle vier Karten sowie einem Download-Button
Bevor irgendetwas heruntergeladen wird, wird jede Zeile gegen den gerade tatsächlich freien VRAM geprüft: ein Fit-Chip pro Quant und darunter der Kontext, den jede Platzierung erhalten würde – eine 5090, zwei oder alle vier Karten. Der Auswahldialog füllt sich zweimal: Dateigröße gegen freien VRAM, damit er sofort auf dem Bildschirm steht, dann ein Remote-Lesen des GGUF-Headers, damit das Badge zur Antwort des Planers selbst wird. So sauber war es nicht immer: Bis zu einem Fix während des Schreibens listete der Auswahldialog auch das MTP-Draft-Modul des Repos und dessen Imatrix-Datei, als wären sie Quants des Modells. Der Dialog geht unterhalb des Ausschnitts weiter – insgesamt 25 Zeilen.
Der Chat-Tab: ein Use-the-loaded-model-Schalter, der auf gemma-4-31B-it-QAT-Q4_0 auflöst, zuletzt verwendet von 2 geladenen; eine Modell-Auswahl, Temperatur 0.7, top_p 0.95, max_tokens 1024 und ein Raten-Label mit 9.7 tok/s; ein System-Prompt-Feld; und ein Transkript mit dem Nutzer-Prompt, der eine dreisätzige Erklärung verlangt, was ein KV-Cache ist und warum seine Größe vom Kontextfenster abhängt, gefolgt von der fertigen Antwort des Modells
Ein Test-Harness, keine Chat-App: Es ruft denselben Ladepfad auf und streamt von demselben Kindprozess-Port, den auch die OpenAI-Endpunkte nutzen. Das 9.7 tok/s-Label ist die eigene Rechnung dieses Tabs – gestreamte Chunks über die Wanduhr ab dem Moment, in dem die Anfrage rausging, Prefill inbegriffen – nicht das Generierungs-Timing der Engine, das sind die 58.77 tok/s auf der Modell-Karte. Ein erfolgreicher Chat hier ist der Beleg, dass ein Client funktionieren wird, kein Mock-Pfad, der abdriften kann.
Der obere Teil des Server-Tabs: ein Ready-to-serve-Streifen mit Modellverzeichnis, 34 indizierte Modelle, 4 GPUs und Engine b10425; das aufgeklappte Panel Point OpenClaw at this server mit dem generierten OPENAI_BASE_URL- und OPENAI_API_KEY-Paar, dem MCP-Server-JSON mit openclaw_cli, openclaw_config unter mcp.servers und einem generischen mcpServers-Block sowie der Companion-Konfiguration; dann ein Health-Block mit Version 1.26-08-23, Uptime, dem Konfigurationspfad und beiden geladenen Modellen
Der Server-Tab schreibt die Client-Konfiguration für dich – den exakten Block, den ich in OpenClaw eingefügt habe, in beiden Formen – und zeigt darunter die Health-Zeilen; weiter unten im selben Tab sitzen die Engine-Liste und ein Bericht, wie viel der Bibliothek der Rechner tatsächlich ausführen kann. Weil der MCP-Eintrag an sfctl weitergibt, muss die Pairing-PIN überhaupt nie in einer Konfigurationsdatei auftauchen.
Der Logs-Tab: Quelle StudioForge server, Level INFO, Follow angehakt, tailt einen 84-Zeilen-In-Memory-Ringpuffer – wiederholte load-planned-Zeilen von studioforge.core.planner mit chosen ctx=262144 kv=q8_0 parallel=1, devices=[1, 0, 3] und estimate_mb=33331, dann unloading idle model at ttl_s=900 und model_unload_verified mit vram_reclaimed_mb=37086
Der Logs-Tab tailt den In-Memory-Ringpuffer: Hier schreibt der Planer jede Entscheidung nieder, bevor er handelt – den Kontext, Cache-Typ, die Slot-Anzahl und die gewählten Devices sowie die erwarteten Kosten des Ladevorgangs – dann ein Leerlauf-Entladen und den VRAM, den er zurückverifiziert hat. Nichts hier wird hinterher rekonstruiert; es ist, was der Planer zu dem Zeitpunkt dachte.

Vier Zeilen weiter unten im selben Puffer sind die, die ich lese, wenn mich ein Ladevorgang überrascht – die von ihm gebaute Befehlszeile, der Prozess, der antwortete, und der Planer, der seine eigenen Hausaufgaben korrigiert (Zeitstempel und Logger-Namen gekürzt):

model_spawn  argv='C:\Users\<you>\…\engines\b10425\llama-server.exe
  --model E:\LLM\Models\…\gemma-4-31B-it-QAT-Q4_0.gguf --host 127.0.0.1 --port 18100
  --n-gpu-layers 999 --ctx-size 262144 --parallel 1 --device CUDA1,CUDA0
  --tensor-split 0.5368,0.4632 --split-mode layer --cache-type-k q8_0 --cache-type-v q8_0
  --flash-attn on --fit off --cache-reuse 256 --cache-ram 32603 --reasoning-format deepseek
  --mmproj …'  port=18100 source=jit:/v1/chat/completions
model_ready  pid=34732 port=18100 source=jit:/v1/chat/completions
load observation  actual_mb=37046 predicted_mb=33031 ratio=1.122 ctx=262144
  devices=[1, 0] per_device_mb={'0': 18908, '1': 18138}
[warning] a device holds more than its planned share  devices=[1, 0]
  overruns={'CUDA0': {'planned_mb': 15886, 'actual_mb': 18908}}
  detail='llama.cpp places the output layer on the last device of the list; the planner
  now charges it there, so a persistent overrun means the charge is too small for this model'

Das ist der Planer, der sich selbst um 3 GB auf einer Karte ertappt, 4 GB über das Paar, und sagt, welche Karte und warum – der Output-Layer landete auf dem letzten Device, genau dort, wo er ihn anrechnet, und die Anrechnung war immer noch zu klein.

Das Windows-Infobereich-Menü für StudioForge: eine Kopfzeile mit Running — 1 model loaded, 23.4 GiB free, dann Open control panel, Open API docs, Open logs folder, Open models folder, Unload all models (free VRAM), Restart engines, Start server ausgegraut, Stop server, Restart server, Copy MCP URL, Copy MCP PIN, ein angehaktes Start at login und Quit
Im Alltag ist das Ganze ein Tray-Icon. Die Kopfzeile ist der gesamte Status – was geladen ist und wie viel VRAM übrig ist – und die PIN ist die einzige Zugangsdaten, die der Tray je in die Zwischenablage legt.

Als Harness-Backend nutzen

Sechs Clients in meinem Netzwerk sprechen mit diesem Ding, und nur einer von ihnen weiß, dass es StudioForge ist. Das ist der Punkt. Fünf davon sind unten gezeichnet; OpenClaw Email ist der sechste.

Jeder OpenAI-Client

Zwei Umgebungsvariablen. server.api_key ist ab Werk null, daher funktioniert jeder nicht-leere String – die meisten OpenAI-Clients weigern sich, mit einem leeren zu starten.

export OPENAI_BASE_URL=http://my-gpu-rig:1234/v1
export OPENAI_API_KEY=not-required        # any non-empty string while server.api_key is unset

curl http://my-gpu-rig:1234/v1/chat/completions -H "Content-Type: application/json" \
  -d '{"model": "<id from /v1/models>", "messages": [{"role": "user", "content": "hello"}]}'
from openai import OpenAI
client = OpenAI(base_url="http://my-gpu-rig:1234/v1", api_key="none")   # any non-empty string, until you set one
print(client.models.list())   # every downloaded model; naming an unloaded one loads it on demand

GET /v1/models listet alles Heruntergeladene auf, im LM-Studio-Stil, und fügt state sowie – wenn resident – ctx_per_slot, max_parallel und parallel_limited_by hinzu, denn eine Kontextlänge allein ist mehrdeutig, sobald ein Modell mehr als einen Slot betreibt. IDs machen Round-Trip: die volle publisher/repo/file-ID, ein bloßer Dateiname oder publisher/name, ohne Beachtung der Groß-/Kleinschreibung. DisPatch brauchte nichts als eine neue Basis-URL; OpenClaw Email ist der anspruchsvollere Client, der ein Chat-Modell und ein Embedding-Modell will und beim Start /v1/models aufruft, um zu fragen, was wirklich bedient wird.

OpenClaw, auf einem anderen Rechner

Der Agenten-Rechner installiert ein kleines Wheel: sfctl, das auf Python 3.11 zielt statt auf die 3.12 des Servers, weil der Rechner, der den Agenten ausführt, dem Rig häufig hinterherhinkt, und das bewusst nicht vom Server-Paket abhängt – kein CUDA, kein Planer, keine Registry.

sfctl servers add rig http://my-gpu-rig:1234 --api-key <PIN> --use
openclaw mcp add studioforge --command sfctl --arg mcp

Oder von Hand – das Detail, das Leute einen Nachmittag kostet. OpenClaws Schlüssel ist mcp.servers, verschachtelt unter mcp: Die flache mcpServers-Map ist richtig für Claude Code, Cline und LibreChat und kein Schlüssel, den OpenClaws Schema kennt. Inferenz ist ein separater Pfad, unter models.providers – beachte baseUrl, mit kleinem rl:

// ~/.openclaw/openclaw.json
{ "mcp": { "servers": {
      "studioforge": { "command": "sfctl", "args": ["mcp"] } } },
  "models": { "providers": {
      "studioforge": {
        "baseUrl": "http://my-gpu-rig:1234/v1",
        "apiKey": "not-required",
        "api": "openai-completions",
        "models": [ { "id": "<id from /v1/models>", "name": "Rig 27B", "contextWindow": 131072 } ] } } } }

Was der Agent bekommt, ist eine zusammengeführte Tool-Liste von 29: die 19 des Gateways plus die 10 des Watchdogs, drei davon umbenannt in recovery_*get_config und set_config, weil sie mit Gateway-Tools kollidieren, health aus Symmetrie. restart_server behält seinen bloßen Namen, denn es ist der Name, den die Fehlermeldung eines toten Verwaltungs-Tools dem Agenten zu rufen sagt. Wenn der Hauptserver unten ist, bewirbt die Bridge weiterhin alle 19 Verwaltungs-Tools mit angehängtem Hinweis – ein Agent, der load_model nicht sehen kann, weiß nicht, dass die Fähigkeit existiert. Die Schleife, die er ausführt:

  1. list_models(limit=N) – der Katalog, neuester Download zuerst. Lies die empfohlene Zeile.
  2. load_model(**row["load_args"]) – unverändert durchreichen; ein Agent, der eine Zeile gewählt hat, ist mit dem Wählen fertig.
  3. load_recommended(model_id, ctx_size=N), wenn das, was du kennst, der Kontext ist, den du brauchst – der eine Ladepfad, der ablehnt statt zu schrumpfen.
  4. Inferenz über HTTP, nicht MCP. Das Benennen eines ungeladenen Modells lädt es, mit Planer-Standards statt der Zeile, die du gerade lasest.
  5. model_options(model_id), wenn die empfohlene Zeile nicht reicht: jede Kontext-Stufe, mit Geschwindigkeiten.
  6. search_modelsrepo_detailsdownload_model, um etwas Neues zu holen.
  7. pin_model für das Modell, das immer antworten muss; reserve_gpus / release_gpus für eigene Karten.
  8. server_status und connection_info – was resident ist, wer VRAM hält, jede Adresse, auf der er antwortet.

Und der Absatz, auf den ich am stolzesten bin, serviert an jeden Client beim Verbinden:

INFERENCE IS NOT HERE. This server exposes no chat/completion/generation tool
by design. To actually run a prompt, use the OpenAI-compatible HTTP API on the
gateway port (POST /v1/chat/completions, /v1/embeddings; GET /v1/models).
Naming an unloaded model in a request just-in-time loads it, so you usually do
not need load_model at all -- reach for it only to pre-warm a model or to load
one with non-default context/quantization settings.

dsh, Claude Code und fünf Einzeiler

dsh (DeepSeek Harness) wechselt Modelle durch Bearbeiten einer YAML-Datei, die für die nächste Anfrage per Hot-Reload neu geladen wird. Der Provider-Block ist camelCase – baseURL, apiKeyEnv:

# ~/.dsh/settings.yaml
llm-pi-ai:
  providers:
    gpu-rig:
      displayName: StudioForge (GPU rig)
      apiKeyEnv: STUDIOFORGE_PLACEHOLDER_KEY   # a reference, not a value
      api: openai-completions
      baseURL: http://my-gpu-rig:1234/v1
      defaultContextWindow: 131072
      compat:
        supportsDeveloperRole: false   # many local servers reject role: "developer"
        maxTokensField: max_tokens
      models:
        - id: <id from /v1/models>

apiKeyEnv ist eine Referenz, nie ein Literal – und ein schlüsselloser lokaler Server braucht trotzdem eine referenzierte Zugangsdaten, weil der OpenAI-kompatible Client auf einem Bearer-Token besteht. Claude Code kann seine eigene Inferenz dagegen überhaupt nicht hierher leiten: Seine Gateway-Protokollreferenz listet Anthropic Messages, Bedrock und Vertex, und keines davon ist /v1/chat/completions. Also ist StudioForge sein Tool, nicht sein Gehirn – claude mcp add studioforge -- sfctl mcp (das -- ist zwingend) gibt ihm alle 29.

bench-llm erzeugte die Rig-Zahlen, die Leute mir gegenüber zitieren – Gemma 4 26B-A4B (QAT, Q4) mit 229.0 tok/s, 110 ms bis zum ersten Token, damals durch LM Studio gemessen – und es ist ein simpler OpenAI-Client, daher braucht es nur eine Basis-URL; aber es führt zwischen Benchmarks pkill -f llama-server aus, was jedes StudioForge-Backend auf dem Rechner beendet. Der Rest ist je eine Zeile: Open WebUI, OPENAI_API_BASE_URL; LibreChat, ein custom-Endpoint mit baseURL und models.fetch: true; aider, OPENAI_API_BASE dann --model openai/<id>; Continue, provider: openai plus apiBase. Der Basis-URL-Schlüssel wird in jedem einzelnen anders geschrieben, was die zuverlässigste Quelle vergeudeter Abende in diesem Ökosystem ist.

Woraus ein Agent wählt

Der Katalog macht aus einer Modellwahl eine Nachschlageoperation statt eines Rätselratens: sortiert neuester Download zuerst, eine Zeile pro Kontext-Stufe, jede mit fits gegen den aktuell freien VRAM, devices, KV-Typen, max_parallel, recommended_parallel, einem confidence, einer if_gpus_idle-Spalte und load_args. Eine Zeile ist ein echter Plan-Aufruf, daher kann sie nichts versprechen, was ein Ladevorgang ablehnen würde – und if_gpus_idle ist der Unterschied zwischen einem Agenten, der aufgibt, und einem, der unload_model ruft.

repo_details ist das, was es wert ist, benannt zu werden: Es liest einen GGUF-Header remote, über HTTP-Range-Anfragen – 2–15 MB, meist die längenpräfixierten String-Arrays des Tokenizers, auf der Platte gecacht – statt 20 GB herunterzuladen, um herauszufinden, ob er passt, und ein CDN, das eine Range-Anfrage mit einem 200 und dem ganzen Body beantwortet, wird erkannt und abgelehnt. Zurück kommt eine context_fit-Matrix von demselben Planer, den ein echter Ladevorgang nutzt:

Quant1× RTX 50902× RTX 5090Alle vier Karten
BF16 (51.8 GiB)— Gewichte allein passen nicht32k bei q8_0256k
Q8_0 (27.9 GiB)256k256k
Q5_K_M (19.3 GiB)128k bei q8_0256k256k
IQ2_M (10.5 GiB)256k256k256k

Aus dem OpenClaw-Guide des Repos, auf diesem Rig berechnet, für unsloth/Qwen3.8-27B-GGUF. max_ctx ist das größte Fenster bei einem f16-Cache voller Qualität; ein q8_0-Wert erscheint nur dort, wo er weiter reicht.

Was die Tabelle sagt: Die Quantisierung entscheidet das Fenster, nicht die Kartenzahl – BF16 zu Q5_K_M verwandelt „passt überhaupt nicht“ in 128k auf einer Karte – und es ist die Antwort des Planers selbst, daher schreibt die Matrix, wo eine Stufe nur durch Quantisieren des Caches erreichbar ist, „bei q8_0“, statt es als Gewinn zu zählen. Für echte Zahlen ist das Playbook dreischrittig: ein Platzierungs-Benchmark (jeder GPU-Modus unter eigenem Lease, Durchsatz aus den eigenen Timings von llama-server), dann benchmark_parallel auf dem Gewinner, dann reserve_gpus zum Festschreiben. Benchmarke nie ein Modell, mit dem gerade jemand mitten im Gespräch ist.

Drop-in für LM Studio

Kompatibilität war die Design-Vorgabe, und das Repo listet auf, was übernommen wurde, damit niemand raten muss: Port 1234; /v1/models, das Heruntergeladenes statt Geladenes listet; Just-in-time-Laden; Leerlauf-TTL; das ttl pro Anfrage; das an Ort und Stelle genutzte publisher/repo/-Layout, sodass es keinen Import-Schritt gibt und beide Programme eine Bibliothek teilen; der /api/v0/models-Spiegel; und der lmstudio://open_from_hf-Deep-Link. Einen Client zu migrieren ist eine Host-Änderung, keine Host-und-Port-Änderung.

LM Studio 0.4.21StudioForge 1.26-08-23
Engineeigene llama.cpp-Builds plus MLX auf AppleUpstream-llama-server, ein gepinnter Build (b10425, CUDA 13.3), vor der Aktivierung einem Smoke-Test unterzogen
Nicht geroutete Pfade200 mit einem Fehler-Body – sein Log sagt Returning 200 anyway404 mit JSON-Hülle, und JSON bei jedem Status
Fehlerunstrukturierte Prosa, die Clients per Regex durchsuchenein stabiler error.code, Diagnosen unter error.studioforge
Lade-Konfigurationcontext_length auf einem von zwei Ladepfaden ignoriert; repetition_penalty stillschweigend ignoriertein Ladepfad, jedes Feld beachtet, effektive Werte zurückgemeldet; Sampler-Aliasse akzeptiert
Wenn es nicht passt„reduziert automatisch die GPU-Offload-Größe … und den Rest in den System-RAM“ – eine stille CPU-Auslagerung, per Design507 insufficient_vram mit benötigten und verfügbaren Bytes, freiem VRAM pro GPU, dem größten passenden Kontext, geordneten Vorschlägen
Multi-GPUPrioritäts- oder gleichmäßiger Split, Kippschalter pro GPU, Tensor-Parallelität seit 0.4.15ein Planer, der Kontext, KV-Typ und Slots pro Platzierung dimensioniert und Split-Anteile für den Output-Layer kippt
Leerlauf-TTL60 Minuten; Auto-Evict hält höchstens 1 JIT-geladenes Modell1,800 s ausgeliefert (15 min auf meinem Rig), alle 15 s gesweept; so viele wie passen, wobei Pins und Leases entscheiden, wer bleibt
Fernverwaltung/api/v1 Laden/Entladen/Download; LM Link in der Vorschau, soll monetarisiert werden, Discovery über LM Studios Hub/api-REST, sfctl, 29 MCP-Tools; die Reichweite ist dein LAN oder dein eigenes Mesh-VPN
MCPnur Host – es konsumiert MCP-Serverein MCP-Server für seine eigene Verwaltung, plus einer auf dem Watchdog
Quellcodegeschlossen; kostenlos für private und interne geschäftliche NutzungQuellcode im Zip, und noch keine Lizenz gewählt

Geprüft gegen LM Studios eigenes Changelog, die Doku und den Bug-Tracker am 2026-08-23, Version 0.4.21 (veröffentlicht am 12. August 2026). Zeile 2 steht in dessen öffentlichem Bug-Tracker; die Zeilen 3–4 sind das, was mein eigener Client an der 0.3.x-API umgehen musste. Ich habe keinen davon erneut gegen 0.4.21 getestet, also lies sie als „irgendwann dokumentiert“, nicht „heute kaputt“.

Was die Tabelle sagt: Es ist eine bessere Desktop-App, als dieses hier je sein wird – polierte GUI, MLX auf Apple Silicon, ein Anthropic-kompatibler Endpoint, ein Mobile-Begleiter, ein Team, das alle zwei Wochen liefert – und die Trennung ist philosophisch, nicht funktional. LM Studios Standardhaltung ist Best-Effort: Lass es irgendwie laufen. Meine ist ablehnen-und-erklären. Wo ein Agent entscheidet, was geladen wird, ist Best-Effort der falsche Standard, denn nichts nachgelagert kann schnell von langsam unterscheiden, ohne zu messen.

Wie es im Vergleich dasteht

Server · Engine · LizenzHot-Swap / Leerlauf-TTLMulti-GPU-PlatzierungLehnt CPU-Auslagerung abFernverwaltung / MCP
StudioForge 1.26-08-23
llama.cpp, ein gepinnter Build · Lizenz: noch keine
✅ JIT · 1,800 s TTL, 15 s Sweeppro Modell geplant, gemischte Karten, Pins + Leases-ngl 999 + --fit off, 507 mit Zahlen✅ REST + 29 MCP-Tools
LM Studio 0.4.21
eigene llama.cpp + MLX · geschlossen
✅ JIT · 60 min, Auto-Evict auf 1Prioritäts-/gleichmäßiger Split, Tensor-Parallelität❌ reduziert Offload, Rest in den RAMREST; MCP nur als Host
Ollama 0.32.15
llama.cpp/GGML; MLX auf Apple · MIT
keep_alive 5 min · 3 pro GPU residentautomatische Verteilung über Karten❌ lagert aus, zeigt CPU % in ollama psreichhaltige /api/*; kein MCP-Server
llama-server-Router (b105xx, August 2026)
ist llama.cpp · MIT
✅ Kindprozess pro Modell · --sleep-idle-seconds, LRU nach Anzahlmanuell -sm / -ts / -dev--fit on schrumpft deinen Plan/models/load|unload
llama-swap v251
ein Proxy, der andere spawnt · MIT
✅ das ganze Produkt · ttl pro Modell/Gruppe❌ was immer dein cmd sagtn/a – nur Proxy/ui + Upstream-Routen
vLLM 0.27.1
eigene (PagedAttention) · Apache-2.0
❌ ein Modell pro ProzessTensor- / Pipeline- / Expert-Parallelitätteilweise – kein Layer-Offload-Pfadnur LoRA, „Local Dev“
KoboldCpp 1.119
llama.cpp-Fork + Bild/Audio · AGPL-3.0
--admin + --routermodemanuell --tensor_split❌ lagert aus/api/admin/*; nur MCP-Client
TextGen (ex-oobabooga) 4.9
5 Loader inkl. ExLlamaV3, TRT-LLM · AGPL-3.0
✅ Wechsel ohne Neustart · TTL ?manuell --tensor-split❌ lagert aus/v1/internal/model/*; MCP-Client
TabbyAPI (rolling)
nur ExLlamaV3 – kein GGUF · AGPL-3.0
✅ Admin + Inline-Laden · TTL ?gpu_split_auto standardmäßig an✅ faktisch – ExLlama hat keinen CPU-PfadAdmin-Key /v1/model/load
Jan 0.8.4
llama.cpp-Router-Modus · Apache-2.0
✅ über den Router · TTL ?von llama.cpp geerbt❌ lagert aus/v1/orchestrations; MCP-Client
LocalAI 4.9.0
60+ Backends als Container-Images · MIT
✅ On-Demand · WATCHDOG_IDLE_TIMEOUT„automatische GPU-Modellanpassung“❌ „Keine GPU erforderlich“REST + UI; nur MCP-Client
GPUStack 2.2.3
vLLM, SGLang, MindIE, VoxBox · Apache-2.0
teilweise – Cluster-Deploymentsautomatisch Spread/Binpack, Multi-Node?vollständige Cluster-Verwaltungs-API

Aus Primärquellen geprüft am 2026-08-23. Ein Fragezeichen bedeutet unbekannt, nicht „nein“. Die Worker von GPUStack sind nur für Linux; vLLM hat keine native Windows-Unterstützung (nur WSL oder Forks).

Was die Tabelle sagt:

  • JIT-Laden und Leerlauf-TTL sind nicht neu, und ich beanspruche sie nicht. LM Studio, Ollama, llama-swap und LocalAI machen beides, und llama-swaps swap-/exclusive-/persistent-Flags pro Gruppe sind eine wirklich elegante Policy-Engine.
  • Drei Dinge dort sind selten: das Verweigern statt der CPU-Auslagerung (nur TabbyAPI kommt nahe, und nur, weil ExLlama keinen CPU-Pfad hat); ein Planer, der Kontext und Slots auf die tatsächlich gefundenen Karten dimensioniert; und Verwaltung, die als MCP-Tools veröffentlicht wird – was sonst nichts in der Kategorie tut, soweit ich es finde.
  • Das Nächste upstream ist llama.cpps eigener Router, ein fairer Kampf: Multi-Modell mit Prozess-Isolation, kostenlos, in der Binärdatei, die du hast. Was ihm fehlt, ist speicherbasierte statt zählbasierte Verdrängung, Pins, Leases und eine Ablehnung mit Zahlen; einen Leerlauf-Schlaf (--sleep-idle-seconds) hat er, obwohl ein /metrics-Poll ihn weckt.

Mehrere gute Ideen sind übernommen, und das Repo sagt, welche. Ollamas Modelfile wurde zu virtuellen Modellen, sodass zwei Personas über einer Basis einen einzigen llama-server teilen, und sein keep_alive wurde zum ttl pro Anfrage. TextGens dreistufige Einstellungsoberfläche wurde direkt übernommen, rohe „Extra-Flags“ inbegriffen, wobei Flags beim Speichern gegen das --help der gepinnten Engine validiert werden. KoboldCpps Einzel-Artefakt-Philosophie ist der Grund, warum Engines in versionierten Verzeichnissen leben.

Installation

Windows, die Referenzplattform, in vier Schritten: Installiere Git, Python 3.12+, uv und einen aktuellen NVIDIA-Treiber; klone das Repo oder entpacke den Download; doppelklicke auf launchers\Update StudioForge.bat, das trotz seines Namens der Erstlauf-Schritt ist – es baut die Virtualenv, installiert die neueste llama.cpp-Version, die einen Build für deinen Treiber hat, unterzieht sie einem Smoke-Test und pinnt sie (b10425 ist der Build, an dem dieser Artikel gemessen wurde, nicht der, den du bekommen wirst); dann launchers\Start StudioForge.bat oder launchers\StudioForge Tray.bat, wenn du es im Infobereich willst, und das Panel öffnet sich auf http://127.0.0.1:8080 in seinem Setup-Tab.

Linux, vier Zeilen – plus cmake und ein CUDA-Toolkit, dessen nvcc zu deinem Treiber passt, weil Upstream bei keinem Tag ein Linux-CUDA-Archiv veröffentlicht und die Engine einmal pro Version aus dem Quellcode gebaut wird:

git clone https://github.com/LaserLloyd/StudioForge.git && cd StudioForge
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e ".[dev]"
.venv/bin/studioforge serve --open     # first run builds the engine

Für einen headless Rechner enthält deploy/ zwei systemd-User-Units – bewusst User-Units, weil der Prozess als der Login-Benutzer laufen muss, dem die Modell-Bibliothek, das venv und die GPU-Geräteknoten gehören. Der Watchdog ist bewusst nicht mit BindsTo= an das Gateway gebunden und nutzt Restart=always: Er existiert, um oben zu sein, wenn das Gateway es nicht ist. Dann sudo loginctl enable-linger "$USER", dasselbe Muster wie im headless-ComfyUI-Artikel. Der erste Start öffnet auf Setup, wo Detect LM Studio library zuerst den downloadsFolder in ~/.lmstudio/settings.json prüft.

DienstStandard-PortKonfigurationsschlüssel
Gateway – /v1, /api, /mcp1234server.port
Web-Bedienfeld8080gui.port
Recovery-Watchdog1235watchdog.port
llama-server-Kindprozesse (nur Loopback)18100–18200gateway.child_port_start / _end

Was die Tabelle sagt: Port 1234 bedeutet auf beiden meiner Rechner „der lokale Modellserver“, und sie sind nicht dasselbe – der Agenten-Rechner betreibt seinen eigenen auf 127.0.0.1:1234, Loopback, während das Rig über das Mesh-VPN my-gpu-rig:1234 bedient – und nur drei Ports sind je erreichbar, wobei die Konfigurationsvalidierung beim Laden eine Kollision zwischen einem Dienst-Port und dem Kindprozess-Bereich ablehnt.

Die Datenverzeichnis-Regel ist SF_DATA_DIR zuerst, dann der Ordner einer --config-Datei, dann <repo>/data in einem Checkout – die vollständige Reihenfolge und warum data_dir nie in config.yaml zurückgeschrieben wird, steht in docs/SETUP.md. Eine Instanz besitzt ein Datenverzeichnis, durchgesetzt mit einem exklusiven OS-Lock; eine zweite Instanz ist schreibgeschützt.

Sicherheit, ehrlich

Eine Regel unter allem: Lesen, Inferenz und Residency bleiben offen; den Rechner zu ändern nicht. Mit ungesetztem server.api_key wird eine mutierende Anfrage an eine Box-ändernde Route nur von einem Aufrufer auf diesem Rechner akzeptiert oder mit der als X-MCP-Pin oder Bearer-Token gesendeten MCP-PIN – alles andere bekommt 403 remote_admin_requires_credential. Das gesperrte Set sind Konfiguration, Neustarts, Engines, Updates, VRAM-Reclaim, Downloads, Leases, Löschungen und die zwei pro-Modell-Schreibvorgänge, die die Instanz überleben. Das Problem, das es behob, war meins: Jeder im LAN konnte PATCH /api/config ausführen, selbst server.api_key setzen und mich aussperren – während das MCP-Tool set_config, dieselbe Fähigkeit im selben Prozess, die PIN verlangte.

  • Die PIN schützt nur MCP. Sie ist ein Pairing-Code, den du vom Startup-Banner abliest, beschränkt auf die Verwaltungs-Tools. Sie ist kein API-Schlüssel.
  • server.api_key ist die echte Zugangsdaten und standardmäßig null. Setze sie, und sie deckt /v1, /api, /mcp und den Watchdog ab; die PIN funktioniert daneben auf den beiden MCP-Endpunkten weiter.
  • Der ausgelieferte Bind ist 0.0.0.0 auf allen drei Listenern. Die Network-exposure-Zeile des Setup-Tabs wird gelb und „erforderlich“, sobald ein Listener ohne Schlüssel exponiert ist – alle drei werden geprüft, denn server.host auf Loopback mit gui.host auf 0.0.0.0 las sich früher grün, während das Panel sperrangelweit offen war.
  • Eine Cross-Origin-Browser-Anfrage ist nicht „dieser Rechner“, nicht einmal auf Loopback. Mit cors_origins: ["*"] könnte jede besuchte Seite PATCH /api/config bei 127.0.0.1:1234 preflichten und lokal aussehend ankommen – daher umfasst der Origin-Vergleich den Port, und Origin: null zählt als fremd. CORS regelt, was eine Seite lesen darf, nie, wem der Server vertraut. Das Websocket des Panels hat eine Host-only-Variante derselben Sperre, weil das Panel über den Port erreicht wird, von dem es ausgeliefert wurde.
  • Ein Remote-Browser auf einer schlüssellosen Installation bekommt Lesen und Inferenz, 403 bei Box-Änderungen und die PIN vorenthalten – sonst könnte alles im LAN die PIN von einem offenen Endpoint ablesen und nutzen. Die PIN war genau dann Theater, wenn es darauf ankam.
  • Bilder werden unter einem SSRF-Schutz geholt, der Loopback, Link-Local, private, ULA- und CGNAT-Raum blockiert – den 100.64/10-Bereich, wo jeder Mesh-VPN-Peer lebt – und nur einmal auflöst, wobei die Verbindung zur geprüften Adresse mit dem ursprünglichen Host und SNI hergestellt wird.
  • Nichts verlässt den Rechner ungefragt. Die einzigen ausgehenden Verbindungen sind Hugging Face für Modelle, GitHub für den gepinnten llama-server-Build und seine Update-Prüfung, die optionale StudioForge-Release-Prüfung und Bild-URLs, die eine Anfrage nennt; Self-Update meldet „nicht konfiguriert“ ohne einen Netzwerkaufruf, bis du update.repo setzt, und ein Unit-Test pinnt das fest.

Zwei Grenzen werden benannt statt versteckt: Eine Peer-Adress-Prüfung vertraut allem auf Loopback, was hinter einem Reverse-Proxy der Proxy ist, also stelle den Proxy hinter server.api_key; und es gibt keine Auth über einen gemeinsamen Schlüssel hinaus – keine Konten, kein Rate-Limiting. Die Hausregel aus dem OpenClaw-Artikel gilt weiterhin: Loopback plus ein authentifizierender Proxy oder ein Mesh-VPN, nie das rohe Internet.

Der ehrliche Teil

Klar aufgeschrieben, damit du entscheiden kannst, bevor du installierst:

  • Windows ist die Referenzplattform – daran wurden der Tray, der Jobobjekt-VRAM-Schutz und die GPU-Zähler pro Prozess gebaut. Linux wird unterstützt und CI läuft auf beiden, ist aber weniger kampferprobt; der Source-Build-Pfad hat seine Befehls-Konstruktion getestet, wurde hier aber nie durchgängig ausgeführt. macOS wird nicht unterstützt: kein CUDA.
  • Nur NVIDIA. Der Planer liest NVML, die Engine ist ein CUDA-Build, und die Quant-Affinität wird in Compute-Capabilities ausgedrückt.
  • Die VRAM-Schätzung ist eine Schätzung. Gewichte landen innerhalb von 2% der Dateigröße und KV ist aus der Pro-Layer-Geometrie exakt, aber der Compute-Puffer ist ein kalibrierter Anteil, einmal beim Start getunt, auf 0.03–0.15 begrenzt und nur im Speicher gehalten – daher macht ein Neustart eine schlechte Kalibrierung ungeschehen. Zwei Kalibrierungs-Historien auf diesem Rechner waren verunreinigt und werden jetzt vollständig ignoriert.
  • Multi-GPU-Splitting ist proportional, nicht gemessen. Es modelliert keine Interconnect-Bandbreite, und gemischte Generationen laufen im Tempo der langsameren Karte.
  • Gleichzeitigkeits-Schätzungen sind Arithmetik. Der Schätzer nimmt an, Slots seien halb voll, und stuft MoE-Modelle pauschal um die Hälfte herab, und --ctx-checkpoints wird überhaupt nicht modelliert. Beide Fehler weisen in Richtung weniger Slots, was die sichere Richtung ist – aber führe den Parallel-Benchmark aus, bevor du 8 vertraust.
  • Die Geschwindigkeits-Schätzungen nutzen nominelle Hersteller-Werte – 5090 mit 1792 GB/s und 209 fp16-TFLOPS, 3090 mit 936 und 71, nichts davon hier gemessen. Es gibt genau zwei Kalibrierungs-Anker, beide auf diesem Rig, beide bei einem Slot: eine dichte 31B, gemessen mit 39.4 tok/s gegen eine Schätzung von 36.1, und eine 122B-MoE, gemessen mit 37.3 gegen 47.4. Nichts ist bei vier oder acht Slots validiert oder auf einem dichten Modell über 31B.
  • Eine GPU für ein anderes Programm zu reservieren beschränkt nur meinen Planer. Nichts setzt es gegen das andere Programm durch, und nichts hindert es daran, den Speicher zuerst zu nehmen.
  • Eine Instanz pro Datenverzeichnis, und der Lock deckt das Datenverzeichnis ab statt der Modell-Bibliothek – zwei Instanzen mit verschiedenen Datenverzeichnissen über einer Bibliothek sind immer noch zwei Schreiber.
  • Es wurde keine Lizenz gewählt. Es gibt bewusst keine LICENSE-Datei, und pyproject.toml sagt es in einem Kommentar – Zum Download erklärt, was das in der Praxis bedeutet.
  • Es gibt kein Sicherheits-Audit durch Dritte. Die obigen Aussagen beschreiben, was der Code tut; ich habe beides geschrieben. Lies den Quellcode – deshalb ist es ein Download und kein Dienst.

2,503 Unit-Tests bestehen und 17 werden übersprungen, in 327 Sekunden auf diesem Rechner; CI führt dieselbe Suite auf Windows und Ubuntu aus, mit der GPU-Probe auf ein Null-Backend gezwungen. Eine zweite Suite lädt echte Gewichte auf echte GPUs und ist standardmäßig abgewählt und hinter eine Umgebungsvariable gesperrt – doppelt und dreifach, nach dem Orphan-Vorfall oben. Nicht fertig oder nicht eingeschaltet: das 8-Slot-Micro-Batch-A/B, benannte Sampler-Presets und mypy in CI; das App-Self-Update ist geschrieben, bleibt aber aus, bis du update.repo selbst setzt.

Stolperfallen

Die ehrliche Liste – Dinge, die wirklich zugebissen haben, grob geordnet nach der Zeit, die sie gekostet haben:

  • LM Studio auf 1234 muss zuerst beendet werden. Beide können den Port nicht halten; der Preflight benennt den Inhaber, statt einen Bind-Traceback auszugeben, und server.port verschiebt ihn. Die Bibliothek darf man bedenkenlos teilen.
  • Zwei llama.cpp-Flags bedeuten nicht, wonach sie aussehen. --ctx-size ist das Budget über alle Slots, nicht pro Slot; --fit steht upstream standardmäßig auf on, neben --n-gpu-layers auto – beides oben, unter dem Planer.
  • Reasoning-Modelle geben unter --reasoning-format auto eine leere Antwort zurück. Derselbe Prompt, nur das Flag geändert: content 0 Zeichen und reasoning_content 316 unter auto; content 323 unter none. reasoning_content steht nicht im OpenAI-Schema, daher liest ein Standard-Client einen leeren String und schließt, das Modell habe nichts gesagt. Ich fahre die 31B auf deepseek, weil meine Clients dieses Feld lesen; alles andere bekommt none.
  • llama.cpp-vX.Y.Z-Prereleases tragen kein Windows-CUDA-Asset. Ein getaggtes v0.1.2 saß über zwei gewöhnlichen bNNNN-Builds ganz ohne vorgefertigte Archive, daher bot der Server-Tab ein Update hinter einem Button an, der nur scheitern konnte. Tags werden jetzt auf ^b\d+$ gefiltert.
  • Tree-kille nie die Tray-Wurzel, während ein Browser-Tab auf dem Panel offen ist. Unter einem venv-Launcher-Stub ist der Watchdog das Enkelkind des Servers, und ein Neustart endete einmal mit totem Server, totem Watchdog und nichts Gespawntem. Nutze das Tray-Menü, das Panel oder sfctl recover --restart.
  • pkill -f llama-server aus einem anderen Tool beendet deine Backends. bench-llm tut genau das zwischen Läufen – oben, unter den Harness-Clients.
  • Die MCP-PIN ist kein API-Schlüssel, und ein schlüsselloser Server braucht in manchen Clients trotzdem eine Platzhalter-Zugangsdaten – oben, unter dsh.
  • OpenClaws MCP-Schlüssel ist mcp.servers, verschachtelt unter mcp. Die flache mcpServers-Map ist kein Schlüssel, den sein Schema kennt – oben, unter OpenClaw.
  • Zwei Mess-Abkürzungen werden dich anlügen. Einen Prompt in einem Spekulations-Benchmark zu wiederholen misst den Prompt-Cache (+751% gegen +0.4%), und eine Layer × Heads × Kontext-KV-Formel liegt bei einer Qwen3.5 um das 4-Fache daneben – beides oben, unter dem Planer. /props meldet außerdem speculative.types: "none", während gedraftet wird, also lies timings.draft_n aus einer echten Completion ab.
  • Vision-Modelle bekommen keinen Prompt-Cache-Nutzen. llama.cpp deaktiviert die Cache-Wiederverwendung für multimodale Modelle selbst, und jedes Bild wird mit 1,024 Tokens veranschlagt, sofern die mmproj-Metadaten nichts anderes sagen – genug, dass ein 8k-Fenster größtenteils Bilder sind.

Zum Download

Das Zip unten ist das Ganze: der getaggte Quellcode-Baum, die Tests, die Doku, die Launcher, die systemd-Units, plus ein dist/-Ordner mit beiden Wheels und dem sdist, damit du ohne Build-Schritt installieren kannst. studioforge-2026-08.zip – Release v1.26-08-23, 4,238,728 Bytes (4.04 MiB), SHA-256:

84f4f828b5c75206236890f28e8651c96146a7bb39c14e21b13a0922a13f7d3f  studioforge-2026-08.zip

226 Einträge unter einem obersten Verzeichnis, gebaut mit git archive aus dem annotierten Tag v1.26-08-23 bei Commit 0610446, daher kann es nur getrackte Dateien enthalten – kein config.yaml, kein data/, keine lokalen Overrides. Der Quellcode liegt auch unter github.com/LaserLloyd/StudioForge. Er braucht Python 3.12+, uv, einen NVIDIA-Treiber ab der 580er-Serie und einen Ordner mit GGUFs. Lizenz: noch nicht gewählt, daher sind alle Rechte formell vorbehalten – in der Praxis behandle es wie die übrigen Downloads dieser Seite: kostenlos für den privaten Gebrauch, und wenn du es kommerziell willst, frag mich.

Wenn etwas kaputtgeht, schreib mir eine E-Mail – die Adresse steht auf der About-Seite – und schick die Gestalt des Fehlers statt deiner Konfiguration: den error.code, die Zahlen aus einer 507, die letzten zwanzig Zeilen von logs/models/<model>.log. Nie die PIN oder den Schlüssel. Und wenn du den gemeinsamen Bin-Packing-Planer, die benannten Sampler-Presets oder einen AMD-Pfad baust, der wirklich funktioniert, bevor ich es tue, würde ich lieber deinen mergen als meinen schreiben.

Wo mich das zurücklässt

Was ich nicht erwartet hatte, war, wie viel davon sich als Messung herausstellte statt als Code. Der Planer ist Arithmetik, die jeder schreiben könnte; was ihn vertrauenswürdig machte, war, llama.cpps eigene KV-Geometrie zu lesen statt einer Formel, und dann den Slot-Knick bei 2 zu messen, als der Schätzer 8 sagte. Fast jede Entscheidung in diesem Log begann als eine Zahl, die einem Glauben widersprach. Wenn du LM Studio bereits auf 1234 betreibst, ist das ganze Experiment ein Klon, eine Batch-Datei und das Zeigen von models.dir auf den Ordner, den du schon hast – und wenn es seinen Platz nicht an einem Nachmittag verdient, bleibt dein altes Setup unberührt.

Verwandtes: DeepSeek Harness (dsh) (wo dieser Server zuerst auftaucht, als unerklärter Provider-Block), bench-llm (woher die Tokens-pro-Sekunde-Zahlen des Rigs stammen, und das Tool, das deine Backends beenden wird), Mein OpenClaw-Setup (der Artikel, der das Problem benannte, das dies löst) und DisPatch (die Chat-App davor).

Downloads

Kostenlos für die private Nutzung. Wenn es dir einen Nachmittag erspart, ist der Kaffee-Button gleich in der Nähe.


← Mehr KI & lokale LLMs