StudioForge: Ein GPU-Only-LLM-Server, der LM Studio auf meinem Rig ersetzt hat – ferngesteuert von meinen Agenten
- Kategorie
- KI & lokale LLMs
- 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-servervon llama.cpp, gemessen an Buildb10425(CUDA 13.3). Ein Gateway auf1234, ein Bedienfeld auf8080, ein Watchdog auf1235, ein Kindprozess pro geladenem Modell auf18100–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 als999. - 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
| Teil | Wo | Wozu es dient |
|---|---|---|
| Gateway | der GPU-Host, ein Prozess, 1234 | /v1, /mcp (19 Tools), /api. Enthält die Registry, den Planer und den Supervisor. |
| Bedienfeld | derselbe Prozess, zweiter uvicorn, 8080 | Dashboard, Setup, Models, Download, Chat, Server, Logs. |
| Watchdog | ein separater Prozess, 1235 | Sein eigener MCP-Server, 10 Recovery-Tools. Er überlebt, was er überwacht. |
llama-server-Kindprozesse | 18100–18200, nur Loopback | Einer pro geladenem Modell; ein Absturz reißt ein Modell mit, nie das Gateway. |
sfctl-Begleiter | der Rechner des Agenten | Ein reiner HTTP-Client – Python 3.11+, kein CUDA, keine Server-Abhängigkeit. Auch die Stdio-MCP-Bridge. |
| GGUF-Bibliothek | models.dir, wo sie bereits ist | An 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.1und 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_serveist die Antwort auf das 200-das-nichts-beweist-Problem. Es istfalse, solange der erste Bibliotheks-Scan läuft, währendstatusbeiokbleibt, weil der Prozess lebt und genau das ein Liveness-Poller fragt.GET /health?deep=trueführt eine echte 8-Token-Completion (bei Embedding-Modellen einen Embeddings-Aufruf) gegen jedes geladene Modell aus – und ohne geladene Modelle antwortet es mitno_models_loadedstatt 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,autoundcurrentmappen alle aufmodels.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
ttlauf Anfrageebene verschiebt nur den Leerlauf-Timer und sonst nichts.ttl: 0ist 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) | Generierung | Prompt-Verarbeitung |
|---|---|---|
| Eine 3090 | 352.5 tok/s | 2803.6 tok/s |
Zwei, -sm layer | 344.4 tok/s | 2722.5 tok/s |
Zwei, -sm tensor | 294.3 tok/s | 1182.0 tok/s |
Zwei, -sm row | schlä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 rowist 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.
| Gleichzeitig | Pro Stream | Gesamt | p50 | p95 | Erreichter Batch |
|---|---|---|---|---|---|
| 1 | 302.8 tok/s | 302.8 tok/s | 0.41 s | 0.41 s | 1.00 |
| 2 | 225.3 tok/s | 425.3 tok/s | 0.46 s | 0.49 s | 1.84 |
| 4 | 134.5 tok/s | 436.0 tok/s | 0.83 s | 1.00 s | 3.46 |
| 8 | 83.3 tok/s | 576.9 tok/s | 1.57 s | 1.77 s | 6.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.
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.
sfctl weitergibt, muss die Pairing-PIN überhaupt nie in einer Konfigurationsdatei auftauchen.
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.
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:
list_models(limit=N)– der Katalog, neuester Download zuerst. Lies die empfohlene Zeile.load_model(**row["load_args"])– unverändert durchreichen; ein Agent, der eine Zeile gewählt hat, ist mit dem Wählen fertig.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.- Inferenz über HTTP, nicht MCP. Das Benennen eines ungeladenen Modells lädt es, mit Planer-Standards statt der Zeile, die du gerade lasest.
model_options(model_id), wenn die empfohlene Zeile nicht reicht: jede Kontext-Stufe, mit Geschwindigkeiten.search_models→repo_details→download_model, um etwas Neues zu holen.pin_modelfür das Modell, das immer antworten muss;reserve_gpus/release_gpusfür eigene Karten.server_statusundconnection_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:
| Quant | 1× RTX 5090 | 2× RTX 5090 | Alle vier Karten |
|---|---|---|---|
| BF16 (51.8 GiB) | — Gewichte allein passen nicht | 32k bei q8_0 | 256k |
| Q8_0 (27.9 GiB) | — | 256k | 256k |
| Q5_K_M (19.3 GiB) | 128k bei q8_0 | 256k | 256k |
| IQ2_M (10.5 GiB) | 256k | 256k | 256k |
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.21 | StudioForge 1.26-08-23 | |
|---|---|---|
| Engine | eigene llama.cpp-Builds plus MLX auf Apple | Upstream-llama-server, ein gepinnter Build (b10425, CUDA 13.3), vor der Aktivierung einem Smoke-Test unterzogen |
| Nicht geroutete Pfade | 200 mit einem Fehler-Body – sein Log sagt Returning 200 anyway | 404 mit JSON-Hülle, und JSON bei jedem Status |
| Fehler | unstrukturierte Prosa, die Clients per Regex durchsuchen | ein stabiler error.code, Diagnosen unter error.studioforge |
| Lade-Konfiguration | context_length auf einem von zwei Ladepfaden ignoriert; repetition_penalty stillschweigend ignoriert | ein 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 Design | 507 insufficient_vram mit benötigten und verfügbaren Bytes, freiem VRAM pro GPU, dem größten passenden Kontext, geordneten Vorschlägen |
| Multi-GPU | Prioritäts- oder gleichmäßiger Split, Kippschalter pro GPU, Tensor-Parallelität seit 0.4.15 | ein Planer, der Kontext, KV-Typ und Slots pro Platzierung dimensioniert und Split-Anteile für den Output-Layer kippt |
| Leerlauf-TTL | 60 Minuten; Auto-Evict hält höchstens 1 JIT-geladenes Modell | 1,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 |
| MCP | nur Host – es konsumiert MCP-Server | ein MCP-Server für seine eigene Verwaltung, plus einer auf dem Watchdog |
| Quellcode | geschlossen; kostenlos für private und interne geschäftliche Nutzung | Quellcode 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 · Lizenz | Hot-Swap / Leerlauf-TTL | Multi-GPU-Platzierung | Lehnt CPU-Auslagerung ab | Fernverwaltung / MCP |
|---|---|---|---|---|
| StudioForge 1.26-08-23 llama.cpp, ein gepinnter Build · Lizenz: noch keine | ✅ JIT · 1,800 s TTL, 15 s Sweep | pro 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 1 | Prioritäts-/gleichmäßiger Split, Tensor-Parallelität | ❌ reduziert Offload, Rest in den RAM | REST; MCP nur als Host |
| Ollama 0.32.15 llama.cpp/GGML; MLX auf Apple · MIT | ✅ keep_alive 5 min · 3 pro GPU resident | automatische Verteilung über Karten | ❌ lagert aus, zeigt CPU % in ollama ps | reichhaltige /api/*; kein MCP-Server |
| llama-server-Router (b105xx, August 2026) ist llama.cpp · MIT | ✅ Kindprozess pro Modell · --sleep-idle-seconds, LRU nach Anzahl | manuell -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 sagt | n/a – nur Proxy | /ui + Upstream-Routen |
| vLLM 0.27.1 eigene (PagedAttention) · Apache-2.0 | ❌ ein Modell pro Prozess | Tensor- / Pipeline- / Expert-Parallelität | teilweise – kein Layer-Offload-Pfad | nur LoRA, „Local Dev“ |
| KoboldCpp 1.119 llama.cpp-Fork + Bild/Audio · AGPL-3.0 | ✅ --admin + --routermode | manuell --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-Pfad | Admin-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-Deployments | automatisch 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.
| Dienst | Standard-Port | Konfigurationsschlüssel |
|---|---|---|
Gateway – /v1, /api, /mcp | 1234 | server.port |
| Web-Bedienfeld | 8080 | gui.port |
| Recovery-Watchdog | 1235 | watchdog.port |
llama-server-Kindprozesse (nur Loopback) | 18100–18200 | gateway.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_keyist die echte Zugangsdaten und standardmäßignull. Setze sie, und sie deckt/v1,/api,/mcpund den Watchdog ab; die PIN funktioniert daneben auf den beiden MCP-Endpunkten weiter.- Der ausgelieferte Bind ist
0.0.0.0auf 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, dennserver.hostauf Loopback mitgui.hostauf0.0.0.0las 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 SeitePATCH /api/configbei127.0.0.1:1234preflichten und lokal aussehend ankommen – daher umfasst der Origin-Vergleich den Port, undOrigin: nullzä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ünglichenHostund 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 duupdate.reposetzt, 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-checkpointswird ü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, undpyproject.tomlsagt 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.portverschiebt ihn. Die Bibliothek darf man bedenkenlos teilen. - Zwei llama.cpp-Flags bedeuten nicht, wonach sie aussehen.
--ctx-sizeist das Budget über alle Slots, nicht pro Slot;--fitsteht upstream standardmäßig auf on, neben--n-gpu-layers auto– beides oben, unter dem Planer. - Reasoning-Modelle geben unter
--reasoning-format autoeine leere Antwort zurück. Derselbe Prompt, nur das Flag geändert:content0 Zeichen undreasoning_content316 unterauto;content323 unternone.reasoning_contentsteht nicht im OpenAI-Schema, daher liest ein Standard-Client einen leeren String und schließt, das Modell habe nichts gesagt. Ich fahre die 31B aufdeepseek, weil meine Clients dieses Feld lesen; alles andere bekommtnone. - llama.cpp-
vX.Y.Z-Prereleases tragen kein Windows-CUDA-Asset. Ein getaggtesv0.1.2saß über zwei gewöhnlichenbNNNN-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-serveraus 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 untermcp. Die flachemcpServers-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.
/propsmeldet außerdemspeculative.types: "none", während gedraftet wird, also liestimings.draft_naus 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.