StudioForge : un serveur LLM uniquement GPU qui a remplacé LM Studio sur mon rig, piloté à distance par mes agents

Publié
23 août 2026
Par
Jacob Lloyd — rédigé avec l'aide de l'IA, une fois le projet terminé
Temps de lecture
63 min de lecture

En clair : C'est un programme qui tourne sur l'ordinateur équipé des cartes graphiques. Les applications lui demandent une réponse d'un modèle d'IA, et il détermine sur quelles cartes ce modèle tient, le démarre, le garde au chaud tant qu'il est utilisé, puis l'éteint quand il ne sert plus. La règle qu'il ne transgresse jamais : un modèle tient entièrement sur les cartes graphiques ou il est refusé — il ne va pas exécuter silencieusement la moitié sur le processeur lent et vous laisser vous demander pourquoi tout rampe. Le hic : il faut des cartes NVIDIA, il est surtout testé sous Windows, et je n'ai pas encore choisi de licence.

StudioForge est le serveur de modèles local que j'ai écrit pour remplacer LM Studio sur mon rig GPU : un superviseur llama.cpp avec une API compatible OpenAI sur le port 1234 — le port de LM Studio, exprès — un panneau de contrôle navigateur, un sidecar de récupération qui répond quand le processus principal ne le peut pas, et un plan de gestion publié via MCP pour qu'un agent sur une autre machine puisse le piloter sans shell.

Il existe à cause d'un journal d'incidents qui a cessé d'être drôle. Un 200 qui ne prouvait rien : LM Studio répond aux chemins non routés avec un statut de succès et un corps d'erreur, son propre journal affichant Returning 200 anyway. /v1/models listant tout ce qui est téléchargé plutôt que chargé, si bien que « qu'est-ce qui est réellement résident ? » n'avait pas de réponse. Des modèles de raisonnement débordant d'une fenêtre de 8 192 tokens, parce que le contexte que je réglais dans la config du client n'était pas le contexte utilisé par le chargement — dans LM Studio, la fenêtre est figée au moment du chargement, et rien ne me le disait. Et le plus coûteux : un modèle qui ne tenait pas était tronqué au lieu d'être refusé — le comportement documenté est qu'il « réduira automatiquement la taille de l'offload GPU … et le reste dans la RAM système », ce qui troque la vitesse GPU contre la vitesse de la RAM système et signale un succès dans les deux cas.

Si vous avez lu l'article sur dsh, vous avez déjà rencontré ce serveur : le bloc fournisseur intitulé « StudioForge (GPU rig) » est celui-ci. C'est ce que la pile à huit agents va chercher à travers le réseau pour le gros travail, ce à quoi DisPatch parle, et ce à quoi OpenClaw Email demande un modèle de chat et un modèle d'embedding. Mon article sur OpenClaw nommait le problème — « des minutes de chargement de modèle, de la VRAM jonglée entre les services » — et ce qui suit en est la réponse, chaque règle portant la mesure qui l'a imposée.

En bref

  • Ce que c'est : un serveur LLM uniquement GPU et compatible OpenAI, construit sur llama-server de llama.cpp, mesuré sur le build b10425 (CUDA 13.3). Une passerelle sur 1234, un panneau de contrôle sur 8080, un chien de garde sur 1235, un enfant par modèle chargé sur 18100–18200.
  • Ce qu'il fait : charge un modèle à la première utilisation, en planifiant son contexte, son type de cache KV, son placement GPU et son nombre de slots en fonction de la VRAM libre à cet instant ; le met en veille ; garde les modèles épinglés résidents ; attribue des cartes entières à un modèle sur demande ; et publie 29 outils via MCP — 19 de gestion, 10 de récupération.
  • Ce qu'il ne fait jamais : déverser sur le CPU (un modèle tient entièrement en VRAM ou est refusé avec les chiffres), téléphoner à la maison (les seuls appels sortants sont Hugging Face pour les modèles, GitHub pour le build épinglé de llama-server et sa vérification de mise à jour, la vérification optionnelle de version de StudioForge, et les URL d'images nommées par une requête), ni exécuter l'inférence via MCP — le plan de contrôle n'a aucun outil de complétion et le dit en majuscules.
  • Ce qu'il vous faut : des GPU NVIDIA avec un pilote série 580 ou plus récent, Python 3.12+, uv, et un dossier de GGUF. Vous n'avez jamais fait tourner de modèle en local ? Commencez plutôt ici.
  • Ce que vous obtenez au final : une URL de base que chaque client OpenAI de votre réseau utilise inchangée, un panneau qui nomme ce qui détient chaque gigaoctet sur chaque carte, et un assistant capable de dire « charge le 27B à 128k sur les deux 5090 » et de le voir se réaliser.
  • Le point honnête : Windows est la plateforme de référence, NVIDIA uniquement, et il n'y a pas encore de fichier de licence — lisez d'abord Se le procurer.

Ce qu'il fait, en une image

Ce qu'il n'est pas :

  • Pas un modèle. Il exécute les GGUF que vous avez, et en télécharge d'autres dans le même dossier et la même disposition que LM Studio.
  • Pas un moteur d'inférence. llama.cpp fait le calcul ; lui décide quel processus tourne avec quels drapeaux sur quelles cartes.
  • Pas une application de chat. Il y a un onglet Chat, et il existe pour prouver que le vrai chemin de requête fonctionne.
  • Pas un cluster. Une machine, ses propres cartes. Le backend RPC de llama.cpp existe et n'est pas câblé.
  • Pas un serveur d'inférence CPU. Il n'existe aucune valeur de --n-gpu-layers autre que 999 dans tout le code.
  • Pas une seconde surface d'API. Pas de /api/generate à la Ollama, pas d'API KoboldCpp — la surface OpenAI, un miroir /api/v0 façon LM Studio et le REST de gestion /api, et c'est tout.

Qu'est-ce qui tourne où

ÉlémentÀ quoi il sert
Passerellel'hôte GPU, un processus, 1234/v1, /mcp (19 outils), /api. Contient le registre, le planificateur et le superviseur.
Panneau de contrôlemême processus, second uvicorn, 8080Tableau de bord, Configuration, Modèles, Téléchargement, Chat, Serveur, Journaux.
Chien de gardeun processus séparé, 1235Son propre serveur MCP, 10 outils de récupération. Il survit à ce qu'il supervise.
Enfants llama-server18100–18200, loopback uniquementUn par modèle chargé ; un plantage fait tomber un modèle, jamais la passerelle.
Compagnon sfctlla machine de l'agentUn pur client HTTP — Python 3.11+, pas de CUDA, pas de dépendance serveur. Aussi le pont stdio MCP.
Bibliothèque GGUFmodels.dir, là où elle est déjàIndexée sur place ; rien n'est copié. LM Studio continue d'utiliser le même dossier.

Ce que dit le tableau :

  • Seul l'hôte GPU installe quoi que ce soit. Un client n'a besoin que d'une URL de base ; un agent a besoin d'un petit paquet Python, et uniquement pour les outils de gestion.
  • Les enfants sont invisibles de l'extérieur. Ils se lient à 127.0.0.1 et à rien d'autre, si bien que la passerelle est la seule surface publique — c'est ce qui fait d'une clé API une vraie frontière.

Comment circule une requête

Tout ce qu'un client peut mal faire est vérifié avant le premier octet, car une mauvaise requête doit recevoir un vrai 4xx (un 404 pour un id de modèle inexistant) avec un corps JSON plutôt qu'une trame d'erreur enfouie dans un flux SSE 200 — les clients gèrent le premier et ratent régulièrement le second. Ce que dit GET /health sur mon rig :

{"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 est la réponse au problème du 200-qui-ne-prouve-rien. Il est faux pendant que la première analyse de bibliothèque s'exécute, alors que status reste ok parce que le processus est vivant et que c'est ce que demande un sondeur de vivacité. GET /health?deep=true exécute une vraie complétion de 8 tokens (un appel d'embeddings pour les modèles d'embeddings) sur chaque modèle chargé — et sans rien de chargé, il répond no_models_loaded plutôt que de réussir, car une sonde qui ne peut pas échouer est pire que pas de sonde du tout.
  • L'alias local-model se résout. local-model, default, auto et current pointent tous vers models.default_model. Les clients LM Studio retombent sur cette chaîne littérale, donc lui renvoyer un 404 les casse pour rien.
  • Un modèle froid ne ressemble pas à un blocage. Le flux ouvert transporte : loading <model id> (5s) toutes les cinq secondes, puis : prefilling <model id> (Ns) jusqu'au premier vrai token — des lignes de commentaire SSE que tout analyseur ignore. Une socket silencieuse aussi longtemps déclenche un délai de lecture, et un client qui réessaie empile plus de prefill sur un batch saturé.
  • Un chargement à la fois, sur toute la machine. Deux modèles froids ont un jour été planifiés simultanément sur les mêmes cartes ; l'un est mort avec CUDA error: out of memory, sa nouvelle tentative a évincé l'autre nouveau modèle, et la requête de ce client a frappé un enfant mort.
  • Un ttl au niveau de la requête ne fait que déplacer la minuterie d'inactivité, rien d'autre. ttl: 0 est la forme filaire d'« épinglé » partout ici, donc il est ignoré plutôt qu'honoré — un client envoyant {"ttl": 60} désépinglait autrefois ce que son propriétaire avait épinglé.

La vérité sur la VRAM : le planificateur

C'est la partie que personne d'autre ne fait, alors la voici en détail plutôt qu'en adjectifs. core/planner.py fait 3 188 lignes et répond à une question avant qu'un modèle ne démarre : étant donnée la VRAM réellement libre sur chaque carte à cet instant, quelle est la meilleure fenêtre, la meilleure qualité de cache et le meilleur nombre de slots que ce modèle peut avoir — et si la réponse est « aucun », que dois-je vous dire ?

L'échelle, et ce qu'il refuse de marchander

Les barreaux dessinés là sont illustratifs, pas les valeurs par défaut livrées : le mécanisme est exact, les chiffres sont un exemple. Ce que config.example.yaml livre est target_ctx: 1048576 comme objectif et default_ctx: 8192 comme plancher, l'objectif étant d'abord plafonné à la fenêtre d'entraînement du modèle — bien qu'au premier lancement tune_for_hardware écrive default_ctx: 16384 quand la plus petite carte fait 24 Gio ou plus, et 8192 à partir de 12 Gio ; mon rig tourne avec un plancher de 128000. Dépasser une fenêtre d'entraînement exige un scaling RoPE et dégrade la qualité, donc un palier au-dessus n'est jamais proposé. Un ctx_size explicite est une échelle à un seul barreau.

Deux passes, la seconde seulement quand la première a échoué partout. La raison est un chargement à 12 h 03 un après-midi : 79 832 Mo libres, plus 19 423 Mo récupérables d'un modèle inactif. Avec ce budget, 262144 sur un cache q4_0 tenait à 96 004 Mo, et 65536 en f16 complet tenait à 95 236 Mo. Ce qui s'est chargé était 8192/f16 à 89 860 Mo — le modèle a payé le prix complet de l'éviction et obtenu la plus petite fenêtre de l'échelle.

Le KV n'est pas un seul chiffre par modèle

Presque tous les calculateurs de VRAM en ligne calculent le KV comme couches × têtes × dim-tête × 2 × octets × contexte. Exact pour un Llama, franchement faux pour deux familles que les gens exécutent vraiment : Gemma 3 et 4 entrelacent cinq couches à fenêtre glissante par couche complète à la moitié de la dimension de tête, et Qwen3.5, 3.6 et 3.8 déclarent full_attention_interval = 4, donc un cache KV existe sur chaque quatrième couche et le reste sont des couches récurrentes Gated-DeltaNet avec un état fixe par séquence.

Le coût de se tromper là-dessus : un Gemma-4 31B demandé pour 262 144 tokens était estimé à 480 Gio de KV et plafonné à 65 536, alors que le journal de calibration enregistrait predicted_mb=95615 actual_mb=40037 depuis des semaines. Après la correction de géométrie, prédit et réel tombent tous deux à 38 Gio sur deux 5090 à n_ctx=262144 — un déblocage de 4× qui a emporté toute la flotte Gemma-4 avec lui. Imputer le KV à chaque couche d'un Qwen3.5 était le même bug sous un autre chapeau : une surfacturation nette de 4×.

Deux détails qui valent la peine d'être empruntés. Le compte de cellules à fenêtre glissante doit refléter exactement llama.cpp ; un multiplicateur plat de 1,25× était faux dans la direction dangereuse, 3,6× en dessous à quatre slots. Et attention_kind est dérivé de la géométrie des couches plutôt que de general.architecture — là où il ne peut pas être dérivé, il rapporte unknown, ce qui signifie « méfiez-vous de tout chiffre KV ici », jamais « supposez le cas bon marché ».

La qualité de cache est choisie à l'intérieur de chaque barreau plutôt qu'échangée contre une fenêtre plus large : f16/f16 → q8_0/q8_0 → q8_0 K + q4_0 V, le q4_0 symétrique ayant disparu de tout chemin automatique. Pas une question de goût — avec un cache K en q4_0, Qwen2.5-7B ne reproduit que 11,7 % des tokens que son homologue f16 produit, tandis qu'une paire assortie q8_0/q8_0 se tient à une divergence KL de 0,0018.

Quatre cartes, deux générations

Le rig est fait de deux RTX 5090 et de deux RTX 3090 — des cartes nominalement de 32 Go et 24 Go, que le panneau compte comme 31,84 Gio et 24,0 Gio, soit 111,7 Gio au total — sous pilote 610.88, pilote CUDA 13.3. Une seule carte d'abord, toujours — un modèle réparti sur PCIe sans NVLink est nettement plus lent — et le planificateur ne passe outre que lorsque le placement sur une seule carte est affamé à un slot, que la répartition au moins le double, que chaque carte ajoutée est au moins aussi capable, et que vous avez laissé le nombre de slots sur auto. Cette dernière condition n'est pas de la politesse : une répartition tourne au rythme de son membre le plus lent.

Placement (1.5B Q4_K_M, deux 3090, ctx 8k)GénérationTraitement du prompt
Une 3090352.5 tok/s2803.6 tok/s
Deux, -sm layer344.4 tok/s2722.5 tok/s
Deux, -sm tensor294.3 tok/s1182.0 tok/s
Deux, -sm rowéchoue : error loading model: device CUDA2 does not support split buffers

Ce que dit le tableau :

  • Une carte a battu deux cartes sur les deux axes. La répartition par couches coûte environ 2 % de génération ; la répartition par tenseurs coûte 17 % de génération et 58 % de traitement du prompt — donc le mode tenseur est en opt-in, et seule une mesure peut le choisir.
  • -sm row est mort sur CUDA. L'analyseur l'accepte puis le chargement échoue, donc il est bloqué avant le lancement de l'enfant plutôt qu'après.

Deux détails de placement n'apparaissent que lorsqu'on mesure carte par carte. La couche de sortie est imputée au dernier périphérique, parce que les quantifieurs gardent les tenseurs d'embedding et de sortie en Q6_K ou Q8_0 même dans un fichier Q4 : un 27B planifié en --device CUDA1,CUDA0 --tensor-split 0.5079,0.4921 a atterri à 15,52 Gio sur CUDA0 — le dernier périphérique, celui auquel la répartition donnait le moins — contre 14,48 sur CUDA1. Et llama.cpp ouvre un contexte CUDA sur chaque périphérique visible — environ 0,22 Gio sur une 3090, 0,43 Gio sur une 5090 — c'est pourquoi la colonne de placement a un plancher de 512 Mio.

Combien de conversations vaut un placement

Le --ctx-size de llama.cpp est le budget KV total partagé entre les slots, pas la fenêtre par slot — un drapeau largement mal lu, que le README en amont n'explique pas. Un chargement avec --ctx-size 4096 et sans --parallel rapporte total_slots: 4 : 1 024 tokens par conversation. StudioForge lance avec ctx_per_slot × parallel. Ensuite : combien de slots valent la peine d'être ouverts, et c'est là que j'ai cessé de faire confiance à ma propre arithmétique.

ConcurrentPar fluxAgrégép50p95Batch atteint
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, une RTX 3090, 8 192 tokens par slot, KV f16, huit slots lancés, prompts de 512 tokens, 192 tokens générés chacun.

Ce que dit le tableau :

  • L'estimateur disait 8. La mesure disait 2. À quatre slots, chaque flux tombe à 44 % de sa vitesse en solo, sous un plancher de 65 % ; la règle prend le plus grand de 1/2/4/8 qui franchit ce plancher tout en gagnant encore 15 % d'agrégat par rapport au niveau inférieur.
  • L'agrégat ne cesse de grimper — huit slots déplacent 1,9× les tokens d'un seul — tandis qu'une seule conversation s'effondre à 27 %. Une règle maximisant l'agrégat choisirait 8, et chaque utilisateur subirait un modèle trois fois plus lent que ce que la carte peut faire.
  • Le batching est réel, pas de la mise en file. Le batch atteint grimpant de 1,00 → 1,84 → 3,46 → 6,03 prouve des étapes de décodage partagées ; reproduit sur trois exécutions à 2 % près.

Une seconde exécution sur deux 3090 à 32 768 par slot a donné 301,7 → 230,5 par flux et a répondu 2 à nouveau. Au passage, le catalogue avait prédit 308,0 tok/s pour ce placement et l'exécution a mesuré 301,7 — 2 % d'écart, mieux que ce que j'espérais. Les lignes portent désormais max_parallel (combien tiennent) à côté de recommended_parallel (combien valent la peine d'être exécutés).

Deux drapeaux que j'ai mesurés avant de leur faire confiance

Le décodage spéculatif est une victoire en flux unique. Qwen3.8-27B Q5_K_S avec une tête MTP, une 3090, quatre prompts distincts de 256 tokens, cache de prompt désactivé : sans spéculation 37,75 tok/s ; draft-mtp à profondeur 3 a donné 50,70 tok/s, +34,3 %, à 0,528 d'acceptation. La profondeur 4 redescend à 47,48, parce que l'acceptation tombe à 0,446 et que chaque token rejeté supplémentaire a été vérifié pour rien. ngram-mod a obtenu +0,4 % et n'a émis aucun draft du tout.

Ce qui mène au piège : le même prompt trois fois a mesuré +751 % sur ce 27B. Répétez un prompt et vous mesurez le cache de prompt en l'appelant du drafting. Au-dessus de quatre slots, auto renvoie désormais none et dit pourquoi — « la spéculation est une victoire en flux unique et nuit à un batch saturé » — après qu'une exécution a chargé un 27B avec --parallel 8 et que auto a quand même choisi draft-mtp, voyant la tête MTP mais pas le nombre de slots.

Le micro-batch achète du prefill contre de la VRAM. Même 1.5B, un prompt de 5 166 tokens : -ub 512 (le défaut du moteur) a donné 15 232 tok/s à 1 492 Mio ; -ub 1024, 17 307 tok/s (+13,6 %) à 1 562 Mio ; -ub 2048, 18 061 tok/s (+18,6 %) à 1 702 Mio. Il est resté désactivé longtemps parce que le tampon de calcul grandit avec -ub, que le planificateur ne le modélisait pas, et qu'un tampon non modélisé transforme un « ça tient » en mémoire épuisée. Le planificateur le facture désormais, arrondi vers le haut pour pencher du côté du refus, et n'augmente le micro-batch automatiquement qu'au-dessus de quatre slots.

Le refus, avec les chiffres

Chaque lancement passe --fit off et --n-gpu-layers 999, et le second est une constante, pas un réglage. Cela compte plus qu'avant : le build épinglé b10425 livre -fit, --fit [on|off]« s'il faut ajuster les arguments non définis pour tenir dans la mémoire du périphérique »activé par défaut, aux côtés de --n-gpu-layers auto ; les deux sont arrivés en amont dans la PR #16653 en décembre 2025. Cette paire est précisément un chemin silencieux d'offload partiel : un défaut raisonnable pour un serveur généraliste, et exactement le comportement que ce projet existe pour refuser. Quand rien ne tient, la réponse est HTTP 507 avec l'arithmétique dans le corps, tronquée ici :

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 }}}

C'est un vrai refus, capturé pendant que j'écrivais ceci : le 31B a demandé à se charger sur une seule RTX 3090. La prose nomme le manque et les détenteurs ; error.studioforge transporte le même échec sous forme de données — les octets requis et disponibles, la VRAM libre par carte, l'estimation décomposée en poids, KV, tampon de calcul, modèle de projection et contexte CUDA, chaque processus détenant de la VRAM, et des notes indiquant sur quel barreau de l'échelle il se tenait quand il a abandonné. Quand une fenêtre plus petite tiendrait, max_ctx_that_fits la nomme, calculée sur la géométrie par couches pour que l'offre soit une que le prochain chargement accepte ; ici même le plancher ne tenait pas, donc c'est null plutôt qu'un chiffre qui échouerait. Un refus qui ne concerne pas un modèle occupé ne porte pas de retry_after_s, car « réessayez plus tard » est un mauvais conseil quand rien ne changera.

Épinglages, TTL, baux et rééquilibreur

Un épinglage est un état souhaité, pas une exemption. Autrefois, cela signifiait TTL zéro, exclusion de toute échelle d'éviction et un préchauffage au démarrage — et pas la quatrième chose : un modèle épinglé n'était jamais rechargé, donc un enfant en boucle de plantage dépassant son max_restarts par modèle restait à state="failed" sans rien détenir. Un réconciliateur chevauche désormais le balayage de 15 secondes, avec un backoff de 60 s jusqu'à un plafond de 900 s. La seule chose qui bat un épinglage est une personne : un déchargement explicite marque l'id comme supprimé et il reste éteint.

Un bail est une carte appartenant à un modèle. Une carte louée est absente de la vue GPU de tout autre modèle — pas classée dernière, pas une option — et le propriétaire est forcé sur exactement ces cartes, dimensionnées par l'estimateur, dans le mode de répartition que son propre benchmark a mesuré le plus rapide là. Une carte déjà louée est un conflit 409, jamais une prise de contrôle. Un bail sans modèle retient des cartes pour quelque chose hors du serveur : reserve_gpus(devices=[3], reason="ComfyUI render") est la raison pour laquelle ma génération d'images et mes modèles de langage ont cessé de se battre.

Le rééquilibreur corrige la bonne décision d'hier. À 13 h 42, un 27B a été planifié sur les cartes [1, 3] — une répartition inter-niveaux partageant le GPU1 — parce qu'un 31B détenait [1, 0, 2] et qu'une application de génération d'images détenait 7,5 Gio du GPU2. À 13 h 53, le 31B s'est réduit à [0, 1] ; dès lors [2, 3] restait libre et strictement meilleur, et le 27B n'a pas bougé. Un modèle inactif est donc désormais déplacé quand un plan sans éviction à ses réglages actuels exacts le fait atterrir hors de toute carte partagée. Les tokens par seconde estimés ne justifient jamais un déplacement.

Il regarde une fois par minute, et seulement quand le monde a changé : seulement sur une machine calme, seulement pour un modèle inactif depuis cinq minutes, un déplacement par modèle toutes les 30 minutes — car une relocalisation est un rechargement et un rechargement fait tomber le cache de prompt, et sur la charge de travail à longues conversations de ce rig, ce cache représentait 93 % d'un prompt de 98k tokens. L'éviction a en outre trois règles dures — jamais un modèle épinglé, jamais un en pleine requête, jamais une instance en cours de chargement — et une carte louée n'est déjà pas dans la vue du planificateur. Un chargement juste-à-temps ne peut jamais fixer force.

Quand ça casse

La VRAM meurt avec le processus qui l'a prise. Sous Windows, les enfants vivent dans un objet job anonyme créé avec JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, donc le noyau tue chaque membre quand le dernier handle se ferme — quelle que soit la façon dont le parent se termine. Anonyme, parce qu'un job nommé serait partagé avec tout ce qui devine le nom. Linux reçoit un shim PR_SET_PDEATHSIG, qui couvre un kill -9 de la passerelle ; c'est un effort au mieux plutôt qu'une garantie du noyau, donc le balayage de démarrage et reclaim_orphan_engines rattrapent ce qui passe à travers.

C'est pourquoi il existe un balayage de démarrage. Le 18 août 2026, environ 10 Gio sur le GPU0 et environ 15,6 Gio sur le GPU1 étaient indisponibles avec « tout arrêté ». Les détenteurs étaient trois enfants llama-server.exe d'une exécution python -m pytest tests -q qu'un agent de codage avait lancée et qui s'était terminée depuis. Chaque détenteur est désormais classifié — ours, child-of-live-process, orphan, other-instance, foreign — et seul orphan est jamais tué, sûr par construction car rien d'autre ne lance des binaires depuis notre arbre engines.

Nommer qui détient quoi a demandé deux essais. NVML rapporte zéro de mémoire utilisée par processus sous Windows, donc les tailles viennent du compteur que lit la colonne « Mémoire GPU dédiée » du Gestionnaire des tâches — mais c'est un total par processus à travers les adaptateurs, donc la colonne périphérique était fausse : un processus signalé sur CUDA0,1,2,3 détenait en réalité 15,52 Gio sur CUDA0 et rien sur les 3090. Le correctif joint le LUID de l'adaptateur à une adresse de bus PCI, et un piège mérite le paragraphe : le numéro de bus dans le busId de NVML est hexadécimal. "00000000:42:00.0" est le bus 66, pas 42 — une mauvaise réponse assurée, indiscernable d'une bonne.

Un déchargement est vérifié, pas annoncé. Le superviseur revérifie le pid avec une garde de temps de création contre la réutilisation du pid, fait monter un survivant vers un tree-kill forcé, et enregistre la VRAM avant et après ; un survivant lève un 500 vous disant de le tuer manuellement. Un déchargement qui rapporte un succès alors que le processus reste résident est le mensonge le plus coûteux que ce système puisse raconter, car chaque chargement ultérieur est alors planifié contre une VRAM qui n'est pas libre.

Les codes de sortie sont un vocabulaire. 2 est une erreur de config nommant la clé. 3 est un conflit de port, et la zone de notification ne relance jamais sur le port en conflit — elle attend que le détenteur réponde à /health comme un serveur StudioForge et s'y attache à la place. 75 est « redémarrage demandé » : le serveur se draine, fixe le code et s'arrête proprement — 1,0 s de la demande à la sortie, mesuré — et la zone de notification relance sans dépenser de tentative de plantage. Cette distinction existe parce qu'un redémarrage depuis l'interface a un jour produit deux serveurs se disputant le 1234, et trois plantages comptés plus tard la zone de notification restait sur Crashé — voir le dossier logs à côté d'un serveur sain qu'elle ne pouvait plus arrêter.

Quand la passerelle est figée plutôt que morte, vous parlez au chien de garde : un processus séparé toujours actif sur 1235, construit à partir d'argparse et de la journalisation stdlib, donc il démarre même quand config.yaml est la chose cassée. Ses dix outils sont health, get_config, set_config, restart_server, kill_model, nuke_all_models, reclaim_orphan_engines, tail_logs, gpu_status et rollback_update. Il réimplémente la règle des orphelins localement plutôt que d'importer le module qui la détient — le processus de récupération ne doit pas importer la pile qu'il répare.

Le panneau de contrôle

Le panneau sur 8080 est un second serveur uvicorn dans le même processus, partageant le graphe d'objets de la passerelle par référence — il n'y a aucune URL absolue dedans, ce qui le fait fonctionner à l'identique en HTTP simple sur un VPN maillé et derrière un frontal HTTPS.

Le tableau de bord StudioForge en pleine requête : un en-tête indiquant 2 modèles chargés, 4 GPU, 45,5 Gio libres sur 111,7 ; quatre cartes GPU — deux RTX 5090, deux RTX 3090 — avec les chiffres d'utilisation, de libre et d'occupation ; une liste des détenteurs de VRAM avec deux processus llama-server.exe marqués comme nôtres et cinq étrangers ; un panneau de baux GPU vide ; et deux cartes de modèles chargés, l'un en préremplissage et l'autre épinglé et inactif
Le tableau de bord en pleine requête : un 31B réparti sur les deux 5090 préremplit un prompt de 2 700 tokens, un 26B est épinglé sur une 3090, et chaque processus détenant de la VRAM sur n'importe quelle carte est nommé avec sa part. Le panneau des détenteurs existe parce qu'un jour 25 Gio ont disparu.
Gros plan sur les deux cartes de modèles chargés : le 31B indique le port 18100, ctx réel 262144, 1 occupé / 0 inactif, GPU0, GPU1 (couche), TTL 15 min 00 s, requêtes actives 1, total 3, dernière 58,77 tok/s, build b10425, et une ligne de slot indiquant Traitement du prompt 2202/2742 (80 %), cache touché 7/2742 ; le 26B indique le port 18101, GPU2, TTL épinglé, dernière 46,67 tok/s, slot 0 inactif
Les deux mêmes cartes, recadrées depuis l'image ci-dessus : le port réellement attribué à chaque modèle, le contexte réellement obtenu, les cartes sur lesquelles il a réellement atterri, le compte à rebours TTL — puis une ligne par slot du moteur. « Réel » est relu depuis l'enfant, pas depuis ce que nous avons demandé.
L'onglet Configuration : une liste de vérification intitulée Prêt à servir, mais à corriger avant exposition : Exposition réseau, avec des lignes vertes pour le dossier de données, 50 fichiers GGUF sous E:\LLM\Models, 34 modèles indexés, 4 GPU sous pilote 610.88 et CUDA 13.3, moteur b10425 testé et la passerelle sur 0.0.0.0:1234, une ligne ambre Exposition réseau avec un bouton Définir une clé API ; puis la section Bibliothèque de modèles et les champs Défauts pour chaque chargement — plancher de contexte 128000, objectif 1048576, plancher modèle de réflexion 32768
Une installation fraîche s'ouvre ici, et « fraîche » est mesurée plutôt que mémorisée — pas de dossier de modèles, rien d'indexé, ou pas de moteur. Chaque ligne non satisfaite porte le bouton qui la corrige ; sur mon rig, la seule ligne ambre est la passerelle écoutant sur toutes les interfaces sans clé API, ce qui est un choix, pas un accident.
Le tableau de bibliothèque de l'onglet Modèles : colonnes Nom, Téléchargé, Taille, Quant, Arch, Fonctionnalités, Dernière utilisation, Statut et Actions ; onze lignes allant de deux builds Qwen3.8-27B à 21,29 et 17,95 Gio jusqu'à Qwen2.5-0.5B à 0,49 Gio, avec des icônes de fonctionnalité pour chat, vision et embedding ; la ligne gemma-4-26B porte un badge épinglé et une puce verte chargée ; six petits boutons d'action terminent chaque ligne
Un tableau plutôt que des cartes, car avec quelques dizaines de modèles les questions sont comparatives — lequel est arrivé en dernier, lequel est le gros, lequel peut voir des images. Le tri par défaut est le téléchargement le plus récent d'abord, et les puces de statut à droite sont la vérité en direct sur lesquels sont résidents.
La boîte de dialogue de réglages par modèle pour gemma-4-31B-it-QAT-Q4_0 (16,44 Gio, ctx d'entraînement 262144) : un bloc Réglages optimaux avec une ligne par jeu de GPU — deux RTX 5090 à ctx 262144, q8_0, 3 slots, environ 62 tok/s ; deux RTX 3090 à 1 slot, environ 34 tok/s ; les quatre GPU à 3 slots, environ 40 tok/s, marqué tient maintenant ; une RTX 5090 à ctx 131072, environ 66 tok/s — chacune avec des boutons Charger ici et Mesurer le parallèle ; une ligne Charger exactement à avec 64K, 128K, 256K et 512K ; un verdict vert tient indiquant Tient sur GPU1, GPU0, GPU3 (répartition : couche) avec cache KV q8_0, flash-attn activé et VRAM projetée de 32,55 Gio détaillée par carte ; et les champs Basique — longueur de contexte, type KV K et V, TTL, une case Épinglé, modèle de draft et dérogation de périphérique
Cette boîte de dialogue est tout l'argument en un seul écran : ce que le modèle pourrait faire sur chaque jeu de cartes si elles étaient libres, un verdict d'ajustement recalculé à chaque changement avec la VRAM projetée par carte, puis chaque bouton pour le contredire (les niveaux Avancé et Expert continuent sous le recadrage). Les tailles de contexte inaccessibles sont grisées avec la raison plutôt que masquées — « pourquoi ce modèle ne peut-il pas faire 512k » est la question à laquelle ce bouton existe pour répondre.
L'onglet Téléchargement après recherche de gemma-4 sur Hugging Face, trié par Téléchargements (30 j) : douze dépôts de unsloth, lmstudio-community et google, chaque ligne montrant l'éditeur, un nombre de téléchargements allant de 1 187 348 à 458 712, les mentions j'aime, le nombre de quantifications et la date de mise à jour, avec un lien vers la carte du modèle et un bouton Quants
La recherche Hugging Face et la file de téléchargement dans un seul onglet. Le libellé de tri dit « Téléchargements (30 j) » exprès — ce chiffre est un compte glissant sur trente jours, pas un total cumulé — et le bouton Quants est l'endroit où le calcul d'ajustement commence.
Le haut du sélecteur de quant pour unsloth/Qwen3.8-27B-GGUF : une note d'en-tête indiquant que l'ajustement est une estimation portant uniquement sur les poids tant que l'en-tête du modèle n'a pas été lu, une ligne de géométrie indiquant attention hybride, 65 couches, KV 65 Ko par token, puis seize lignes allant de BF16 à 51,77 Gio avec une puce ambre nécessite plusieurs GPU jusqu'à Q5_K_M à 19,28 Gio, chacune avec une puce verte tient sur un GPU, une ligne de contexte par placement pour une 5090, deux 5090 et les quatre cartes, et un bouton Télécharger
Avant que quoi que ce soit ne soit téléchargé, chaque ligne est vérifiée contre la VRAM réellement libre à cet instant : une puce d'ajustement par quant, et en dessous le contexte que chaque placement obtiendrait — une 5090, deux, ou les quatre cartes. Le sélecteur se remplit deux fois : la taille de fichier contre la VRAM libre pour l'afficher immédiatement, puis une lecture distante de l'en-tête GGUF pour que le badge devienne la propre réponse du planificateur. Cela n'a pas toujours été aussi propre : jusqu'à un correctif apporté pendant l'écriture de ceci, le sélecteur listait aussi le module de draft MTP du dépôt et son fichier imatrix comme s'ils étaient des quants du modèle. La boîte de dialogue continue sous le recadrage — 25 lignes au total.
L'onglet Chat : un interrupteur Utiliser le modèle chargé se résolvant en gemma-4-31B-it-QAT-Q4_0, le plus récemment utilisé des 2 chargés ; un sélecteur de modèle, température 0,7, top_p 0,95, max_tokens 1024 et un libellé de débit indiquant 9,7 tok/s ; une zone de prompt système ; et une transcription avec le prompt utilisateur demandant une explication en trois phrases de ce qu'est un cache KV et pourquoi sa taille dépend de la fenêtre de contexte, suivie de la réponse complète du modèle
Un harnais de test, pas une application de chat : il appelle le même chemin de chargement et streame depuis le même port enfant que les points de terminaison OpenAI. Le libellé 9,7 tok/s est la propre arithmétique de cet onglet — des morceaux streamés rapportés à l'horloge murale depuis l'instant où la requête est partie, prefill inclus — pas le temps de génération du moteur, qui est le 58,77 tok/s sur la carte du modèle. Un chat réussi ici est la preuve qu'un client fonctionnera, pas un chemin factice qui peut dériver.
Le haut de l'onglet Serveur : un bandeau Prêt à servir listant le dossier de modèles, 34 modèles indexés, 4 GPU et le moteur b10425 ; le panneau Pointer OpenClaw vers ce serveur déplié pour montrer la paire générée OPENAI_BASE_URL et OPENAI_API_KEY, le JSON du serveur MCP avec openclaw_cli, openclaw_config sous mcp.servers et un bloc mcpServers générique, et la config du compagnon ; puis un bloc Santé indiquant version 1.26-08-23, uptime, le chemin de config et les deux modèles chargés
L'onglet Serveur écrit la configuration client pour vous — le bloc exact que j'ai collé dans OpenClaw, dans les deux formes — et affiche les lignes de santé en dessous ; plus bas dans le même onglet se trouvent la liste des moteurs et un rapport sur la part de la bibliothèque que la machine peut réellement exécuter. Comme l'entrée MCP appelle sfctl en sous-processus, le PIN d'appairage n'a jamais à apparaître dans un fichier de config.
L'onglet Journaux : source serveur StudioForge, niveau INFO, Suivre coché, suivant un tampon circulaire en mémoire de 84 lignes — des lignes load planned répétées de studioforge.core.planner indiquant chosen ctx=262144 kv=q8_0 parallel=1, devices=[1, 0, 3] et estimate_mb=33331, puis unloading idle model at ttl_s=900 et model_unload_verified avec vram_reclaimed_mb=37086
L'onglet Journaux suit le tampon circulaire en mémoire : ici le planificateur écrit chaque décision avant d'agir — le contexte, le type de cache, le nombre de slots et les périphériques qu'il a choisis, et ce que le chargement devait coûter — puis un déchargement par inactivité et la VRAM qu'il a vérifiée en retour. Rien ici n'est reconstruit après coup ; c'est ce que le planificateur pensait sur le moment.

Quatre lignes plus bas dans le même tampon se trouvent celles que je lis quand un chargement me surprend — la ligne de commande qu'il a construite, le processus qui a répondu, et le planificateur qui corrige son propre devoir (horodatages et noms de journaux tronqués) :

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'

C'est le planificateur se prenant en flagrant délit de 3 Go d'écart sur une carte, 4 Go sur la paire, et disant quelle carte et pourquoi — la couche de sortie a atterri sur le dernier périphérique, exactement là où il la facture, et la facture était encore trop courte.

Le menu de la zone de notification Windows pour StudioForge : une ligne d'en-tête indiquant En cours — 1 modèle chargé, 23,4 Gio libres, puis Ouvrir le panneau de contrôle, Ouvrir la doc API, Ouvrir le dossier logs, Ouvrir le dossier modèles, Décharger tous les modèles (libérer la VRAM), Redémarrer les moteurs, Démarrer le serveur grisé, Arrêter le serveur, Redémarrer le serveur, Copier l'URL MCP, Copier le PIN MCP, un Démarrer à la connexion coché, et Quitter
Au quotidien, tout tient dans une icône de la zone de notification. La ligne d'en-tête est tout le statut — ce qui est chargé et combien de VRAM il reste — et le PIN est la seule donnée d'identification que la zone de notification met jamais dans le presse-papiers.

L'utiliser comme backend de harnais

Six clients sur mon réseau parlent à cette chose et un seul d'entre eux sait que c'est StudioForge. C'est tout l'intérêt. Cinq d'entre eux sont dessinés ci-dessous ; OpenClaw Email est le sixième.

N'importe quel client OpenAI

Deux variables d'environnement. server.api_key est null par défaut, donc n'importe quelle chaîne non vide fonctionne — la plupart des clients OpenAI refusent de démarrer avec une clé vide.

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 liste tout ce qui est téléchargé, à la manière de LM Studio, en ajoutant state et — quand il est résident — ctx_per_slot, max_parallel et parallel_limited_by, car une longueur de contexte seule est ambiguë dès qu'un modèle tourne sur plus d'un slot. Les ids font l'aller-retour : l'id complet publisher/repo/file, un simple nom de fichier, ou publisher/name, sans distinction de casse. DisPatch n'a eu besoin que d'une nouvelle URL de base ; OpenClaw Email est le client le plus exigeant, voulant un modèle de chat et un modèle d'embedding et appelant /v1/models au démarrage pour demander ce qui est réellement servi.

OpenClaw, sur une autre machine

La machine de l'agent installe un petit wheel : sfctl, qui cible Python 3.11 plutôt que le 3.12 du serveur parce que la machine exécutant l'agent est souvent en retard sur le rig, et qui ne dépend délibérément pas du paquet serveur — pas de CUDA, pas de planificateur, pas de registre.

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

Ou à la main — le détail qui coûte un après-midi aux gens. La clé d'OpenClaw est mcp.servers, imbriquée sous mcp : la map plate mcpServers est correcte pour Claude Code, Cline et LibreChat, et n'est pas une clé que le schéma d'OpenClaw connaît. L'inférence est un chemin séparé, sous models.providers — notez baseUrl, avec un rl en minuscules :

// ~/.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 } ] } } } }

Ce que l'agent obtient est une liste d'outils fusionnée de 29 : les 19 de la passerelle plus les 10 du chien de garde, trois renommés en recovery_*get_config et set_config parce qu'ils entrent en collision avec les outils de la passerelle, health pour la symétrie. restart_server garde son nom nu, car c'est le nom que le message d'erreur d'un outil de gestion mort dit à l'agent d'appeler. Quand le serveur principal est tombé, le pont annonce toujours les 19 outils de gestion avec une note ajoutée — un agent qui ne voit pas load_model ne sait pas que la capacité existe. La boucle qu'il exécute :

  1. list_models(limit=N) — le catalogue, téléchargement le plus récent d'abord. Lisez la ligne recommandée.
  2. load_model(**row["load_args"]) — transmettez-le inchangé ; un agent qui a choisi une ligne a fini de choisir.
  3. load_recommended(model_id, ctx_size=N) quand ce que vous connaissez est le contexte dont vous avez besoin — le seul chemin de chargement qui refuse plutôt que de rétrécir.
  4. Inférence via HTTP, pas via MCP. Nommer un modèle non chargé le charge, avec les défauts du planificateur plutôt que la ligne que vous lisiez.
  5. model_options(model_id) quand la ligne recommandée ne suffit pas : chaque palier de contexte, avec les vitesses.
  6. search_modelsrepo_detailsdownload_model pour obtenir quelque chose de nouveau.
  7. pin_model pour le modèle qui doit toujours répondre ; reserve_gpus / release_gpus pour des cartes à lui.
  8. server_status et connection_info — ce qui est résident, qui détient la VRAM, chaque adresse sur laquelle il répond.

Et le paragraphe dont je suis le plus fier, servi à chaque client à la connexion :

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 et cinq commandes d'une ligne

dsh (DeepSeek Harness) change de modèle en modifiant un seul fichier YAML, rechargé à chaud pour la requête suivante. Le bloc fournisseur est en 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 est une référence, jamais une valeur littérale — et un serveur local sans clé a quand même besoin qu'une donnée d'identification soit référencée, car le client compatible OpenAI insiste sur un jeton bearer. Claude Code, en revanche, ne peut pas du tout router sa propre inférence ici : sa référence de protocole de passerelle liste Anthropic Messages, Bedrock et Vertex, et aucun n'est /v1/chat/completions. StudioForge est donc son outil, pas son cerveau — claude mcp add studioforge -- sfctl mcp (le -- est obligatoire) lui donne les 29.

bench-llm a produit les chiffres du rig que les gens me citent en retour — Gemma 4 26B-A4B (QAT, Q4) à 229,0 tok/s, 110 ms jusqu'au premier token, mesurés via LM Studio à l'époque — et c'est un simple client OpenAI, donc une URL de base lui suffit ; mais il exécute pkill -f llama-server entre les benchmarks, ce qui tue chaque backend StudioForge sur la machine. Le reste tient en une ligne chacun : Open WebUI, OPENAI_API_BASE_URL ; LibreChat, un point de terminaison custom avec baseURL et models.fetch: true ; aider, OPENAI_API_BASE puis --model openai/<id> ; Continue, provider: openai plus apiBase. La clé d'URL de base s'écrit différemment dans chacun d'eux, ce qui est la source la plus fiable de soirées perdues dans cet écosystème.

Ce dans quoi un agent choisit

Le catalogue fait du choix de modèle une consultation plutôt qu'une devinette : trié du téléchargement le plus récent au plus ancien, une ligne par palier de contexte, chacune portant fits par rapport à la VRAM libre en direct, devices, les types KV, max_parallel, recommended_parallel, une confidence, une colonne if_gpus_idle et load_args. Une ligne est un vrai appel de plan, donc elle ne peut pas promettre ce qu'un chargement refuserait — et if_gpus_idle est la différence entre un agent qui abandonne et un agent qui appelle unload_model.

repo_details est celle qui vaut la peine d'être nommée : elle lit un en-tête GGUF à distance, via des requêtes HTTP Range — 2 à 15 Mo, surtout les tableaux de chaînes à préfixe de longueur du tokenizer, mis en cache sur disque — au lieu de télécharger 20 Go pour savoir si ça tient, et un CDN qui répond à une requête en plage par un 200 et le corps entier est détecté et refusé. Ce qui revient est une matrice context_fit du même planificateur qu'un vrai chargement utilise :

Quant1× RTX 50902× RTX 5090Les quatre cartes
BF16 (51,8 Gio)— les poids seuls ne tiennent pas32k en q8_0256k
Q8_0 (27,9 Gio)256k256k
Q5_K_M (19,3 Gio)128k en q8_0256k256k
IQ2_M (10,5 Gio)256k256k256k

Tiré du guide OpenClaw du dépôt, calculé sur ce rig, pour unsloth/Qwen3.8-27B-GGUF. max_ctx est la plus grande fenêtre avec un cache f16 pleine qualité ; un chiffre q8_0 n'apparaît que là où il va plus loin.

Ce que dit le tableau : c'est la quantification qui décide de la fenêtre, pas le nombre de cartes — de BF16 à Q5_K_M, « ne tient pas du tout » devient 128k sur une carte — et c'est la propre réponse du planificateur, donc là où un palier n'est atteignable qu'en quantifiant le cache, la matrice dit « en q8_0 » plutôt que de le compter comme une victoire. Pour de vrais chiffres, le playbook tient en trois étapes : un benchmark de placement (chaque mode GPU sous son propre bail, débit issu des propres chronométrages de llama-server), puis benchmark_parallel sur le gagnant, puis reserve_gpus pour le verrouiller. Ne benchmarkez jamais un modèle avec lequel quelqu'un est en pleine conversation.

Remplaçant direct de LM Studio

La compatibilité était la contrainte de conception, et le dépôt liste ce qui a été emprunté pour que personne n'ait à deviner : le port 1234 ; /v1/models listant le téléchargé plutôt que le chargé ; le chargement juste-à-temps ; le TTL d'inactivité ; le ttl par requête ; la disposition publisher/repo/ utilisée sur place, donc pas d'étape d'import et les deux programmes partagent une seule bibliothèque ; le miroir /api/v0/models ; et le lien profond lmstudio://open_from_hf. Migrer un client est un changement d'hôte, pas un changement d'hôte-et-de-port.

LM Studio 0.4.21StudioForge 1.26-08-23
Moteurses propres builds llama.cpp plus MLX sur Applellama-server en amont, un seul build épinglé (b10425, CUDA 13.3), testé avant activation
Chemins non routés200 avec un corps d'erreur — son journal dit Returning 200 anyway404 avec une enveloppe JSON, et du JSON sur chaque statut
Erreursde la prose non structurée que les clients matchent par regexun error.code stable, diagnostics sous error.studioforge
Config de chargementcontext_length ignoré sur l'un des deux chemins de chargement ; repetition_penalty silencieusement ignoréun seul chemin de chargement, chaque champ honoré, valeurs effectives renvoyées ; alias d'échantillonneur acceptés
Quand ça ne tient pas« réduira automatiquement la taille de l'offload GPU … et le reste dans la RAM système » — un déversement CPU silencieux, par conception507 insufficient_vram avec octets requis et disponibles, libre par GPU, le plus grand contexte qui tiendrait, suggestions ordonnées
Multi-GPUrépartition prioritaire ou égale, bascules par GPU, parallélisme tensoriel depuis la 0.4.15un planificateur dimensionnant contexte, type KV et slots par placement, inclinant les fractions de répartition pour la couche de sortie
TTL d'inactivité60 minutes ; l'auto-éviction garde au plus 1 modèle chargé en JIT1 800 s livrées (15 min sur mon rig), balayées toutes les 15 s ; autant qu'il en tient, pins et baux décidant qui reste
Gestion à distance/api/v1 load/unload/download ; LM Link en aperçu, à monétiser, découverte via le hub de LM Studio/api REST, sfctl, 29 outils MCP ; la portée est votre LAN ou votre propre VPN maillé
MCPhôte uniquement — il consomme des serveurs MCPun serveur MCP pour sa propre gestion, plus un sur le chien de garde
Sourcefermée ; gratuite pour usage personnel et professionnel internesource dans le zip, et aucune licence choisie à ce jour

Vérifié contre le changelog, la doc et le suivi de bugs de LM Studio le 2026-08-23, version 0.4.21 (publiée le 12 août 2026). La ligne 2 figure dans son suivi de bugs public ; les lignes 3–4 sont ce que mon propre client a dû contourner sur l'API 0.3.x. Je n'ai retesté aucun d'eux contre la 0.4.21, alors lisez-les comme « documenté à un moment donné », pas « cassé aujourd'hui ».

Ce que dit le tableau : c'est une meilleure application de bureau que ceci ne le sera jamais — GUI soignée, MLX sur Apple Silicon, un point de terminaison compatible Anthropic, un compagnon mobile, une équipe qui livre toutes les deux semaines — et la divergence est philosophique plutôt que fonctionnelle. La posture par défaut de LM Studio est le meilleur effort : le faire tourner coûte que coûte. La mienne est refuser-et-expliquer. Là où un agent décide de ce qui se charge, le meilleur effort est le mauvais défaut, car rien en aval ne peut distinguer le rapide du lent sans mesurer.

Comment il se compare

Serveur · moteur · licenceHot-swap / TTL d'inactivitéPlacement multi-GPURefuse le déversement CPUGestion distante / MCP
StudioForge 1.26-08-23
llama.cpp, un build épinglé · licence : aucune pour l'instant
✅ JIT · TTL 1 800 s, balayage 15 splanifié par modèle, cartes mélangées, pins + baux-ngl 999 + --fit off, 507 avec les chiffres✅ REST + 29 outils MCP
LM Studio 0.4.21
llama.cpp maison + MLX · fermé
✅ JIT · 60 min, auto-éviction vers 1répartition prioritaire/égale, parallélisme tensoriel❌ réduit l'offload, reste en RAMREST ; MCP hôte uniquement
Ollama 0.32.15
llama.cpp/GGML ; MLX sur Apple · MIT
keep_alive 5 min · 3 résidents par GPUétalement automatique sur les cartes❌ déverse, montre le % CPU dans ollama ps/api/* riche ; pas de serveur MCP
llama-server router (b105xx, août 2026)
c'est llama.cpp · MIT
✅ un enfant par modèle · --sleep-idle-seconds, LRU par nombre-sm / -ts / -dev manuels--fit on rétrécit votre plan/models/load|unload
llama-swap v251
un proxy qui lance les autres · MIT
✅ tout le produit · ttl par modèle/groupe❌ ce que dit votre cmdn/a — proxy uniquement/ui + routes en amont
vLLM 0.27.1
propre (PagedAttention) · Apache-2.0
❌ un modèle par processusparallélisme tensoriel / pipeline / expertpartiel — pas de chemin d'offload par couchesLoRA uniquement, « dev local »
KoboldCpp 1.119
fork llama.cpp + image/audio · AGPL-3.0
--admin + --routermode--tensor_split manuel❌ déverse/api/admin/* ; client MCP uniquement
TextGen (ex-oobabooga) 4.9
5 chargeurs dont ExLlamaV3, TRT-LLM · AGPL-3.0
✅ bascule sans redémarrage · TTL ?--tensor-split manuel❌ déverse/v1/internal/model/* ; client MCP
TabbyAPI (rolling)
ExLlamaV3 uniquement — pas de GGUF · AGPL-3.0
✅ admin + chargement en ligne · TTL ?gpu_split_auto activé par défaut✅ de fait — ExLlama n'a pas de chemin CPUadmin-key /v1/model/load
Jan 0.8.4
mode router llama.cpp · Apache-2.0
✅ via le router · TTL ?hérité de llama.cpp❌ déverse/v1/orchestrations ; client MCP
LocalAI 4.9.0
60+ backends en images conteneur · MIT
✅ à la demande · WATCHDOG_IDLE_TIMEOUT« ajustement GPU automatique des modèles »❌ « Aucun GPU requis »REST + UI ; client MCP uniquement
GPUStack 2.2.3
vLLM, SGLang, MindIE, VoxBox · Apache-2.0
partiel — déploiements en clusterSpread/Binpack auto, multi-nœud?API complète de gestion de cluster

Vérifié depuis des sources primaires le 2026-08-23. Un point d'interrogation signifie inconnu, pas « non ». Les workers de GPUStack sont Linux uniquement ; vLLM n'a pas de support Windows natif (WSL ou forks uniquement).

Ce que dit le tableau :

  • Le chargement JIT et le TTL d'inactivité ne sont pas nouveaux et je ne les revendique pas. LM Studio, Ollama, llama-swap et LocalAI font les deux, et les drapeaux par groupe swap / exclusive / persistent de llama-swap sont un moteur de politique réellement élégant.
  • Trois choses y sont rares : refuser de tourner plutôt que déverser sur le CPU (seul TabbyAPI s'en approche, et uniquement parce qu'ExLlama n'a pas de chemin CPU) ; un planificateur qui dimensionne contexte et slots selon les cartes qu'il a réellement trouvées ; et une gestion publiée sous forme d'outils MCP, ce qu'aucun autre de la catégorie ne fait à ma connaissance.
  • La chose la plus proche en amont est le propre router de llama.cpp, un combat loyal : multi-modèle avec isolation des processus, gratuit, dans le binaire que vous avez. Ce qui lui manque est une éviction basée sur la mémoire plutôt que sur le compte, les pins, les baux, et un refus portant des chiffres ; il a bien une mise en veille (--sleep-idle-seconds), bien qu'un poll de /metrics le réveille.

Plusieurs bonnes idées sont empruntées, et le dépôt dit lesquelles. Le Modelfile d'Ollama est devenu les modèles virtuels, si bien que deux personas sur une même base partagent un seul llama-server, et son keep_alive est devenu le ttl par requête. La surface de réglages à trois niveaux de TextGen a été adoptée directement, « drapeaux supplémentaires » bruts inclus, avec des drapeaux validés contre le propre --help du moteur épinglé au moment de l'enregistrement. La philosophie d'artefact unique de KoboldCpp est la raison pour laquelle les moteurs vivent dans des répertoires versionnés.

Installation

Windows, la plateforme de référence, en quatre étapes : installez Git, Python 3.12+, uv et un pilote NVIDIA à jour ; clonez le dépôt ou décompressez le téléchargement ; double-cliquez sur launchers\Update StudioForge.bat, qui malgré son nom est l'étape de première exécution — il construit le virtualenv, installe la version llama.cpp la plus récente qui a un build pour votre pilote, la teste et s'y épingle (b10425 est le build sur lequel cet article a été mesuré, pas celui que vous obtiendrez) ; puis launchers\Start StudioForge.bat, ou launchers\StudioForge Tray.bat si vous le voulez dans la zone de notification, et le panneau s'ouvre sur http://127.0.0.1:8080 dans son onglet Configuration.

Linux, quatre lignes — plus cmake et un toolkit CUDA dont le nvcc correspond à votre pilote, car l'amont ne publie aucune archive CUDA Linux à quelque tag que ce soit et le moteur est construit depuis les sources une fois par version :

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

Pour une machine headless, deploy/ contient deux unités systemd utilisateur — des unités utilisateur exprès, car le processus doit tourner comme l'utilisateur connecté qui possède la bibliothèque de modèles, le venv et les nœuds de périphériques GPU. Le chien de garde n'est délibérément pas BindsTo= la passerelle et utilise Restart=always : il existe pour être debout quand la passerelle ne l'est pas. Puis sudo loginctl enable-linger "$USER", le même schéma que l'article sur ComfyUI headless. La première exécution s'ouvre sur Configuration, où Détecter la bibliothèque LM Studio sonde d'abord le downloadsFolder dans ~/.lmstudio/settings.json.

ServicePort par défautClé de config
Passerelle — /v1, /api, /mcp1234server.port
Panneau de contrôle web8080gui.port
Chien de garde de récupération1235watchdog.port
Enfants llama-server (loopback uniquement)18100–18200gateway.child_port_start / _end

Ce que dit le tableau : le port 1234 signifie « le serveur de modèles local » sur mes deux machines et ce n'est pas la même chose — la machine de l'agent exécute le sien sur 127.0.0.1:1234, en loopback, tandis que le rig sert my-gpu-rig:1234 à travers le VPN maillé — et seuls trois ports sont jamais joignables, avec une validation de config refusant toute collision entre un port de service et la plage des enfants au chargement.

La règle du répertoire de données est SF_DATA_DIR d'abord, puis le dossier d'un fichier --config, puis <repo>/data dans un checkout — l'ordre complet, et pourquoi data_dir n'est jamais réécrit dans config.yaml, se trouve dans docs/SETUP.md. Une instance possède un répertoire de données, garanti par un verrou OS exclusif ; une seconde instance est en lecture seule.

La sécurité, honnêtement

Une règle sous tout cela : les lectures, l'inférence et la résidence restent ouvertes ; changer la machine, non. Avec server.api_key non défini, une requête mutante vers une route qui change la machine n'est acceptée que d'un appelant sur cette machine, ou avec le PIN MCP envoyé comme X-MCP-Pin ou comme jeton bearer — tout le reste reçoit 403 remote_admin_requires_credential. L'ensemble verrouillé est la config, les redémarrages, les moteurs, les mises à jour, la récupération de VRAM, les téléchargements, les baux, les suppressions et les deux écritures par modèle qui survivent à l'instance. Le problème qu'il a corrigé était le mien : n'importe qui sur le LAN pouvait faire PATCH /api/config, définir lui-même server.api_key et m'enfermer dehors — alors que l'outil MCP set_config, même capacité dans le même processus, exigeait le PIN.

  • Le PIN ne protège que MCP. C'est un code d'appairage que vous lisez sur la bannière de démarrage, limité aux outils de gestion. Ce n'est pas une clé API.
  • server.api_key est la vraie donnée d'identification et est null par défaut. Définissez-la et elle couvre /v1, /api, /mcp et le chien de garde ; le PIN continue de fonctionner sur les deux points de terminaison MCP à côté.
  • Le bind livré est 0.0.0.0 sur les trois écouteurs. La ligne Exposition réseau de l'onglet Configuration passe à l'ambre et devient requise dès qu'un écouteur est exposé sans clé — en vérifiant les trois, car server.host en loopback avec gui.host sur 0.0.0.0 lisait autrefois vert alors que le panneau était grand ouvert.
  • Une requête navigateur cross-origin n'est pas « cette machine », même en loopback. Avec cors_origins: ["*"], n'importe quelle page que vous visitez pourrait prévoler PATCH /api/config sur 127.0.0.1:1234 et arriver en paraissant locale — donc la comparaison d'origine inclut le port, et Origin: null compte comme étranger. CORS régit ce qu'une page peut lire, jamais à qui le serveur fait confiance. Le websocket du panneau a une version réservée à l'hôte de la même porte, car le panneau est atteint via le port depuis lequel il a été servi.
  • Un navigateur distant sur une installation sans clé obtient les lectures et l'inférence, des 403 sur les changements de machine, et le PIN retenu — sinon n'importe quoi sur le LAN pourrait lire le PIN depuis un point de terminaison ouvert et l'utiliser. Le PIN était du théâtre exactement quand il comptait.
  • Les images sont récupérées sous une garde SSRF qui bloque le loopback, le link-local, le privé, l'ULA et l'espace CGNAT — la plage 100.64/10, où vit chaque pair du VPN maillé — et qui résout une seule fois, se connectant à l'adresse vérifiée avec le Host et le SNI d'origine.
  • Rien ne quitte la machine sans qu'on le demande. Les seuls appels sortants sont Hugging Face pour les modèles, GitHub pour le build épinglé de llama-server et sa vérification de mise à jour, la vérification optionnelle de version de StudioForge, et les URL d'images nommées par une requête ; l'auto-mise à jour rapporte « non configurée » sans appel réseau jusqu'à ce que vous définissiez update.repo, et un test unitaire le fige.

Deux limites énoncées plutôt que cachées : un contrôle d'adresse de pair fait confiance à tout ce qui est en loopback, ce qui derrière un reverse proxy est le proxy, donc mettez le proxy derrière server.api_key ; et il n'y a pas d'authentification au-delà d'une clé partagée unique — pas de comptes, pas de limitation de débit. La règle maison de l'article sur OpenClaw s'applique toujours : loopback plus un proxy authentifiant, ou un VPN maillé, jamais l'internet brut.

La partie honnête

Écrit simplement pour que vous puissiez décider avant d'installer :

  • Windows est la plateforme de référence — c'est ce contre quoi la zone de notification, la garde de VRAM par objet job et les compteurs GPU par processus ont été construits. Linux est supporté et la CI exécute les deux, et il est moins éprouvé au combat ; le chemin de construction depuis les sources a sa construction de commande testée mais n'a jamais été exercé de bout en bout ici. macOS n'est pas supporté : pas de CUDA.
  • NVIDIA uniquement. Le planificateur lit NVML, le moteur est un build CUDA, et l'affinité des quants s'exprime en capacités de calcul.
  • L'estimation de VRAM est une estimation. Les poids atterrissent à 2 % près de la taille du fichier et le KV est exact d'après la géométrie par couches, mais le tampon de calcul est une fraction calibrée, réglée une fois au démarrage, bornée à 0,03–0,15 et conservée en mémoire uniquement — donc une mauvaise calibration est annulée par un redémarrage. Deux historiques de calibration sur cette machine ont été contaminés et sont désormais entièrement ignorés.
  • La répartition multi-GPU est proportionnelle, pas mesurée. Elle ne modélise pas la bande passante d'interconnexion, et mélanger les générations fait tourner au rythme de la carte la plus lente.
  • Les estimations de concurrence sont de l'arithmétique. L'estimateur suppose des slots à moitié pleins et dérate les modèles MoE d'une moitié forfaitaire, et --ctx-checkpoints n'est pas modélisé du tout. Les deux erreurs pointent vers moins de slots, ce qui est la direction sûre — mais exécutez le benchmark parallèle avant de faire confiance à 8.
  • Les estimations de vitesse utilisent les chiffres nominaux des fournisseurs — 5090 à 1 792 Go/s et 209 TFLOPS fp16, 3090 à 936 et 71, aucun mesuré ici. Il y a exactement deux ancres de calibration, toutes deux sur ce rig, toutes deux à un slot : un 31B dense mesuré à 39,4 tok/s contre une estimation de 36,1, et un 122B MoE mesuré à 37,3 contre 47,4. Rien n'est validé à quatre ou huit slots, ni sur un modèle dense au-dessus de 31B.
  • Réserver un GPU pour un autre programme ne contraint que mon planificateur. Rien ne l'impose à l'autre programme, et rien ne l'empêche de prendre la mémoire en premier.
  • Une instance par répertoire de données, et le verrou couvre le répertoire de données plutôt que la bibliothèque de modèles — deux instances avec des répertoires de données différents sur une même bibliothèque restent deux écrivains.
  • Aucune licence n'a été choisie. Il n'y a délibérément aucun fichier LICENSE, et pyproject.toml le dit dans un commentaire — Se le procurer explique ce que cela signifie en pratique.
  • Il n'y a pas d'audit de sécurité tiers. Les affirmations ci-dessus décrivent ce que fait le code ; j'ai écrit les deux. Lisez le code source — c'est pour cela que c'est un téléchargement et pas un service.

2 503 tests unitaires passent et 17 sont ignorés, en 327 secondes sur cette machine ; la CI exécute la même suite sur Windows et Ubuntu avec la sonde GPU forcée vers un backend nul. Une seconde suite charge de vrais poids sur de vrais GPU et est désélectionnée par défaut et verrouillée derrière une variable d'environnement — ceinture et bretelles, après l'incident d'orphelin ci-dessus. Pas fait, ou pas activé : l'A/B de micro-batch à 8 slots, les préréglages d'échantillonneur nommés et mypy en CI ; l'auto-mise à jour de l'app est écrite mais reste éteinte jusqu'à ce que vous définissiez vous-même update.repo.

Pièges

La liste honnête — des choses qui ont réellement mordu, dans l'ordre approximatif du temps qu'elles ont coûté :

  • LM Studio sur 1234 doit être quitté d'abord. Les deux ne peuvent pas détenir le port ; le prévol nomme le détenteur plutôt que d'imprimer une traceback de bind, et server.port le déplace. La bibliothèque peut être partagée sans problème.
  • Deux drapeaux llama.cpp ne veulent pas dire ce qu'ils semblent dire. --ctx-size est le budget à travers tous les slots, pas par slot ; --fit est activé par défaut en amont, aux côtés de --n-gpu-layers auto — les deux ci-dessus, sous le planificateur.
  • Les modèles de raisonnement renvoient une réponse vide sous --reasoning-format auto. Même prompt, seul le drapeau change : content 0 caractère et reasoning_content 316 sous auto ; content 323 sous none. reasoning_content n'est pas dans le schéma OpenAI, donc un client standard lit une chaîne vide et conclut que le modèle n'a rien dit. Je fais tourner le 31B sur deepseek parce que mes clients lisent ce champ ; tout le reste reçoit none.
  • Les préversions vX.Y.Z de llama.cpp ne portent aucun asset CUDA Windows. Un tag v0.1.2 siégeait au-dessus de deux builds bNNNN ordinaires sans aucune archive préconstruite, donc l'onglet Serveur offrait une mise à jour derrière un bouton qui ne pouvait qu'échouer. Les tags sont filtrés vers ^b\d+$ désormais.
  • Ne faites jamais de tree-kill sur la racine de la zone de notification avec un onglet navigateur ouvert sur le panneau. Sous un stub de lanceur venv, le chien de garde est le petit-enfant du serveur, et un redémarrage s'est un jour terminé avec le serveur mort, le chien de garde mort et rien de lancé. Utilisez le menu de la zone de notification, le panneau, ou sfctl recover --restart.
  • pkill -f llama-server depuis un autre outil tue vos backends. bench-llm fait exactement cela entre les exécutions — ci-dessus, sous les clients harnais.
  • Le PIN MCP n'est pas une clé API, et un serveur sans clé a quand même besoin d'une donnée d'identification de substitution dans certains clients — ci-dessus, sous dsh.
  • La clé MCP d'OpenClaw est mcp.servers, imbriquée sous mcp. La map plate mcpServers n'est pas une clé que son schéma connaît — ci-dessus, sous OpenClaw.
  • Deux raccourcis de mesure vous mentiront. Répéter un prompt dans un benchmark spéculatif chronomètre le cache de prompt (+751 % contre +0,4 %), et une formule KV couches × têtes × contexte est 4× à côté sur un Qwen3.5 — les deux ci-dessus, sous le planificateur. /props rapporte aussi speculative.types: "none" pendant le drafting, donc lisez timings.draft_n sur une vraie complétion.
  • Les modèles de vision ne tirent aucun bénéfice du cache de prompt. llama.cpp désactive lui-même la réutilisation du cache pour les modèles multimodaux, et chaque image est budgétée à 1 024 tokens sauf si les métadonnées mmproj disent le contraire — assez pour qu'une fenêtre de 8k soit surtout des images.

Se le procurer

Le zip ci-dessous est tout : l'arbre source tagué, les tests, la doc, les lanceurs, les unités systemd, plus un dossier dist/ contenant les deux wheels et le sdist pour installer sans étape de build. studioforge-2026-08.zip — version v1.26-08-23, 4 238 728 octets (4,04 Mio), SHA-256 :

84f4f828b5c75206236890f28e8651c96146a7bb39c14e21b13a0922a13f7d3f  studioforge-2026-08.zip

226 entrées sous un seul répertoire de premier niveau, construit avec git archive depuis le tag annoté v1.26-08-23 au commit 0610446, donc il ne peut contenir que des fichiers suivis — pas de config.yaml, pas de data/, pas de dérogations locales. Le source est aussi sur github.com/LaserLloyd/StudioForge. Il lui faut Python 3.12+, uv, un pilote NVIDIA de la série 580 et au-delà, et un dossier de GGUF. Licence : pas encore choisie, donc tous les droits sont formellement réservés — en pratique, traitez-le comme le reste des téléchargements de ce site : gratuit pour usage personnel, et si vous le voulez commercialement, demandez-moi.

Si quelque chose casse, écrivez-moi — l'adresse est sur la page À propos — et envoyez la forme de la panne plutôt que votre config : le error.code, les chiffres d'un 507, les vingt dernières lignes de logs/models/<model>.log. Jamais le PIN ni la clé. Et si vous construisez le planificateur de bin-packing conjoint, les préréglages d'échantillonneur nommés, ou un chemin AMD qui fonctionne vraiment avant moi, je préfère intégrer le vôtre plutôt que d'écrire le mien.

Où cela me laisse

Ce à quoi je ne m'attendais pas, c'est à quel point tout cela s'est révélé être de la mesure plutôt que du code. Le planificateur est de l'arithmétique que n'importe qui pourrait écrire ; ce qui l'a rendu digne de confiance, c'est de lire la propre géométrie KV de llama.cpp au lieu d'une formule, puis de mesurer le coude des slots à 2 alors que l'estimateur disait 8. Presque chaque décision de ce journal a commencé comme un chiffre qui contredisait une croyance. Si vous faites déjà tourner LM Studio sur 1234, toute l'expérience tient en un clone, un fichier batch, et pointer models.dir vers le dossier que vous avez déjà — et si elle ne gagne pas sa place en un après-midi, votre ancienne configuration est intacte.

À voir aussi : DeepSeek Harness (dsh) (où ce serveur apparaît pour la première fois, comme un bloc fournisseur inexpliqué), bench-llm (d'où viennent les chiffres de tokens par seconde du rig, et l'outil qui tuera vos backends), Mon installation OpenClaw (l'article qui a nommé le problème que ceci résout), et DisPatch (l'app de chat devant lui).

Téléchargements

Gratuit pour un usage personnel. Si ça vous fait gagner un après-midi, le bouton café n'est pas loin.


← Plus de IA et LLM locaux