StudioForge: un servidor LLM solo-GPU que reemplazó a LM Studio en mi equipo y que mis agentes manejan a distancia

Categoría
IA y LLM Local
Publicado
23 agosto 2026
Por
Jacob Lloyd — escrito con ayuda de IA, después del proyecto
Tiempo de lectura
61 min de lectura

En palabras simples: Es un programa que se ejecuta en el ordenador que tiene las tarjetas gráficas. Las aplicaciones le piden una respuesta de un modelo de IA, y él calcula en qué tarjetas cabe ese modelo, lo inicia, lo mantiene caliente mientras está en uso y lo apaga cuando queda en silencio. La regla que nunca rompe es que un modelo cabe entero en las tarjetas gráficas o se rechaza: no va a ejecutar silenciosamente la mitad en el procesador lento y dejarte preguntándote por qué todo iba a paso de tortuga. La pega: necesita tarjetas NVIDIA, está mejor probado en Windows y todavía no le he elegido licencia.

StudioForge es el servidor de modelos local que escribí para reemplazar a LM Studio en mi equipo con GPU: un supervisor de llama.cpp con una API compatible con OpenAI en el puerto 1234 — el puerto de LM Studio, a propósito —, un panel de control en el navegador, un sidecar de recuperación que responde cuando el proceso principal no lo hace y un plano de gestión publicado por MCP para que un agente en otra máquina pueda manejarlo sin abrir una shell.

Existe por un registro de incidentes que dejó de tener gracia. Un 200 que no demostraba nada: LM Studio responde a rutas sin enrutar con un estado de éxito y un cuerpo de error, y su propio log dice Returning 200 anyway. /v1/models lista todo lo descargado en lugar de lo cargado, así que «¿qué está residente de verdad?» no tenía respuesta. Modelos de razonamiento que desbordaban una ventana de 8.192 tokens, porque el contexto que yo ponía en la configuración del cliente no era el contexto que usaba la carga: en LM Studio la ventana se fija en el momento de cargar, y nadie me lo decía. Y el caro: un modelo que no cabía se recortaba en lugar de rechazarse — el comportamiento documentado es que «reducirá automáticamente el tamaño de la descarga a GPU … y el resto en la RAM del sistema», lo que cambia velocidad de GPU por velocidad de RAM de sistema y, en cualquier caso, informa éxito.

Si leíste el artículo sobre dsh, ya conoces este servidor: el bloque de proveedor llamado «StudioForge (GPU rig)» es esto. Es a lo que recurre la pila de ocho agentes por la red para el trabajo pesado, con lo que habla DisPatch y lo que OpenClaw Email pide para un modelo de chat y un modelo de embeddings. Mi artículo sobre OpenClaw nombró el problema — «minutos de tiempo de carga de modelos, malabarismos de VRAM entre servicios» — y lo que sigue es la respuesta, con cada regla acompañada de la medición que la hizo necesaria.

tl;dr

  • Qué es: un servidor LLM solo-GPU compatible con OpenAI sobre llama-server de llama.cpp, medido en la build b10425 (CUDA 13.3). Una puerta de enlace en 1234, un panel de control en 8080, un vigilante en 1235 y un proceso hijo por cada modelo cargado en 18100–18200.
  • Qué hace: carga un modelo en su primer uso, planificando su contexto, tipo de caché KV, colocación en GPU y número de slots según la VRAM libre en ese instante; lo deja en reposo cuando no se usa; mantiene residentes los modelos fijados; cede tarjetas enteras a un modelo si se le pide; y publica 29 herramientas por MCP — 19 de gestión, 10 de recuperación.
  • Lo que nunca hace: volcar a la CPU (un modelo cabe entero en la VRAM o se rechaza con las cifras), llamar a casa (las únicas llamadas salientes son Hugging Face para los modelos, GitHub para la build fijada de llama-server y su comprobación de actualizaciones, la comprobación opcional de versiones de StudioForge y las URLs de imagen que nombre una petición) ni ejecutar inferencia por MCP: el plano de control no tiene herramienta de finalización y lo dice en mayúsculas.
  • Qué necesitas: GPU NVIDIA con un driver de la serie 580 o superior, Python 3.12+, uv y una carpeta de GGUF. ¿Nunca has ejecutado un modelo en local? Empieza aquí.
  • Con qué terminas: una URL base que todos los clientes OpenAI de tu red usan sin cambios, un panel que dice qué ocupa cada gigabyte de cada tarjeta y un asistente capaz de decir «carga el 27B a 128k en las dos 5090» y que ocurra.
  • La parte honesta: Windows es la plataforma de referencia, solo NVIDIA y todavía no hay archivo de licencia — lee primero Consíguelo.

Qué hace, en una imagen

Lo que no es:

  • No es un modelo. Ejecuta los GGUF que tienes y trae más a la misma carpeta y estructura que usa LM Studio.
  • No es un motor de inferencia. llama.cpp hace las cuentas; esto decide qué proceso se ejecuta con qué flags y en qué tarjetas.
  • No es una app de chat. Hay una pestaña Chat, y existe para demostrar que la ruta real de petición funciona.
  • No es un clúster. Una máquina, sus propias tarjetas. El backend RPC de llama.cpp existe y no está conectado.
  • No es un servidor de inferencia en CPU. No hay ningún valor de --n-gpu-layers distinto de 999 en todo el código.
  • No es una segunda superficie de API. No hay /api/generate de Ollama, no hay API de KoboldCpp — la superficie de OpenAI, un espejo /api/v0 al estilo de LM Studio y el REST de gestión /api, y eso es todo.

Qué corre dónde

PiezaDóndePara qué sirve
Puerta de enlaceel host GPU, un proceso, 1234/v1, /mcp (19 herramientas), /api. Contiene el registro, el planificador y el supervisor.
Panel de controlel mismo proceso, un segundo uvicorn, 8080Dashboard, Setup, Models, Download, Chat, Server, Logs.
Vigilanteun proceso aparte, 1235Su propio servidor MCP, 10 herramientas de recuperación. Sobrevive a lo que supervisa.
Hijos llama-server18100–18200, solo loopbackUno por modelo cargado; un fallo se lleva un modelo, nunca la puerta de enlace.
Acompañante sfctlla máquina del agenteUn cliente HTTP puro — Python 3.11+, sin CUDA, sin dependencia del servidor. También es el puente MCP por stdio.
Biblioteca GGUFmodels.dir, donde ya estáIndexada en su sitio; nada se copia. LM Studio sigue usando la misma carpeta.

Lo que dice la tabla:

  • Solo el host GPU instala algo. Un cliente necesita una URL base; un agente necesita un pequeño paquete de Python, y solo para las herramientas de gestión.
  • Los hijos son invisibles desde fuera. Se enlazan a 127.0.0.1 y a nada más, así que la puerta de enlace es la única superficie pública — que es lo que convierte una sola API key en una frontera real.

Cómo fluye una petición

Todo lo que un cliente puede equivocar se comprueba antes del primer byte, porque una petición incorrecta debería recibir un 4xx real (un 404 para un id de modelo que no existe) con un cuerpo JSON en lugar de un marco de error enterrado dentro de un stream SSE 200: los clientes manejan lo primero y suelen manejar mal lo segundo. Esto es lo que dice GET /health en mi equipo:

{"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 es la respuesta al problema del 200-que-no-demuestra-nada. Es false mientras se ejecuta el primer escaneo de la biblioteca, mientras status sigue en ok porque el proceso está vivo, que es lo que pregunta un sondeo de liveness. GET /health?deep=true ejecuta una finalización real de 8 tokens (una llamada de embeddings para los modelos de embeddings) contra cada modelo cargado — y sin nada cargado responde no_models_loaded en lugar de pasar, porque una sonda que no puede fallar es peor que no tener sonda.
  • El alias local-model se resuelve. local-model, default, auto y current apuntan todos a models.default_model. Los clientes de LM Studio recurren a esa cadena literal, así que devolverle un 404 los rompe sin motivo.
  • Un modelo frío no parece un cuelgue. El stream abierto lleva : loading <model id> (5s) cada cinco segundos, y luego : prefilling <model id> (Ns) hasta el primer token real — líneas de comentario SSE que todo parser ignora. Un socket en silencio durante tanto tiempo dispara un timeout de lectura, y un cliente que reintenta apila más prefill sobre un lote saturado.
  • Una carga cada vez, en toda la máquina. Una vez, dos modelos fríos se planificaron a la vez sobre las mismas tarjetas; uno murió con CUDA error: out of memory, su reintento desalojó al otro modelo nuevo, y la petición de ese cliente golpeó un hijo muerto.
  • Un ttl a nivel de petición mueve el temporizador de reposo y nada más. ttl: 0 es la forma en el cable de «fijado» en todo esto, así que se ignora en lugar de respetarse — un cliente que enviaba {"ttl": 60} solía desfijar lo que su dueño había fijado.

La verdad sobre la VRAM: el planificador

Esta es la parte que nadie más hace, así que aquí va en detalle y no con adjetivos. core/planner.py son 3.188 líneas que responden una pregunta antes de que arranque un modelo: dada la VRAM realmente libre en cada tarjeta ahora mismo, ¿cuál es la mejor ventana, calidad de caché y número de slots que este modelo puede tener — y si la respuesta es ninguna, qué debería decirte?

La escalera, y lo que se niega a ceder

Los peldaños dibujados ahí son ilustrativos, no los valores por defecto: el mecanismo es exacto, las cifras son un ejemplo. Lo que incluye config.example.yaml es target_ctx: 1048576 como objetivo y default_ctx: 8192 como suelo, con el objetivo limitado primero a la ventana entrenada del modelo — aunque en una primera ejecución tune_for_hardware escribe default_ctx: 16384 cuando la tarjeta más pequeña tiene 24 GiB o más, y 8192 a partir de 12 GiB; mi equipo usa un suelo de 128000. Pasar de una ventana entrenada requiere escalado RoPE y degrada la calidad, así que nunca se ofrece un peldaño por encima. Un ctx_size explícito es una escalera de un solo peldaño.

Dos pasadas, la segunda solo cuando la primera falló en todas partes. La razón es una carga a las 12:03 una tarde: 79.832 MB libres, otros 19.423 MB recuperables de un modelo en reposo. Con ese presupuesto, 262144 con caché q4_0 cabía en 96.004 MB, y 65536 con f16 completo cabía en 95.236 MB. Lo que cargó fue 8192/f16 en 89.860 MB — el modelo pagó el precio completo del desalojo y se quedó con la ventana más pequeña de la escalera.

La KV no es un solo número por modelo

Casi todas las calculadoras de VRAM online calculan la KV como capas × cabezas × dimensión de cabeza × 2 × bytes × contexto. Correcto para una Llama, y muy equivocado para dos familias que la gente sí ejecuta: Gemma 3 y 4 intercalan cinco capas de ventana deslizante por cada completa, a la mitad de la dimensión de cabeza, y Qwen3.5, 3.6 y 3.8 declaran full_attention_interval = 4, así que hay caché KV en cada cuarta capa y el resto son capas recurrentes Gated-DeltaNet con un estado fijo por secuencia.

El coste de equivocarse: una Gemma-4 31B a la que se le pedían 262.144 tokens se estimaba en 480 GiB de KV y se limitaba a 65.536, mientras el log de calibración llevaba semanas registrando predicted_mb=95615 actual_mb=40037. Tras el arreglo de la geometría, lo predicho y lo real aterrizan en 38 GiB en dos 5090 con n_ctx=262144 — un desbloqueo de 4× que se llevó consigo a toda la flota Gemma-4. Cargar la KV a cada capa de una Qwen3.5 era el mismo bug con otro sombrero: un sobrecargo directo de 4×.

Dos detalles que merecen copiarse. El recuento de celdas de ventana deslizante debe reflejar exactamente a llama.cpp; un multiplicador plano de 1,25× era erróneo en la dirección peligrosa, 3,6× por debajo a cuatro slots. Y attention_kind se deriva de la geometría de capas en lugar de general.architecture — donde no se puede derivar informa unknown, que significa «desconfía de cada cifra de KV aquí», nunca «supón el caso barato».

La calidad de la caché se elige dentro de cada peldaño en lugar de cambiarse por una ventana más ancha: f16/f16 → q8_0/q8_0 → q8_0 K + q4_0 V, con el q4_0 simétrico fuera de todas las rutas automáticas. No es cuestión de gusto: con una caché q4_0 de K, Qwen2.5-7B reproduce solo el 11,7% de los tokens que produce su yo en f16, mientras que un par q8_0/q8_0 emparejado queda en una divergencia KL de 0,0018.

Cuatro tarjetas, dos generaciones

El equipo son dos RTX 5090 y dos RTX 3090 — tarjetas nominalmente de 32 GB y 24 GB, que el panel cuenta como 31,84 GiB y 24,0 GiB, 111,7 GiB en total — con driver 610.88 y CUDA driver 13.3. Primero una sola tarjeta, siempre — un modelo dividido en PCIe sin NVLink es notablemente más lento — y el planificador lo cambia solo cuando la colocación en una tarjeta queda limitada a un slot, la división al menos lo duplica, cada tarjeta añadida es al menos igual de capaz y dejaste el número de slots en automático. Esa última condición no es cortesía: una división corre al ritmo de su miembro más lento.

Colocación (1,5B Q4_K_M, dos 3090, ctx 8k)GeneraciónProcesado de prompt
Una 3090352,5 tok/s2803,6 tok/s
Dos, -sm layer344,4 tok/s2722,5 tok/s
Dos, -sm tensor294,3 tok/s1182,0 tok/s
Dos, -sm rowfalla: error loading model: device CUDA2 does not support split buffers

Lo que dice la tabla:

  • Una tarjeta ganó a dos en ambos ejes. La división por capas cuesta un 2% de generación; la división por tensores cuesta un 17% de generación y el 58% del procesado de prompt — así que el modo tensor es opt-in, y solo una medición puede elegirlo.
  • -sm row está muerto en CUDA. El parser lo acepta y la carga falla después, así que se bloquea antes de lanzar el hijo y no después.

Dos detalles de colocación solo aparecen cuando mides por tarjeta. La capa de salida se carga al último dispositivo, porque los cuantizadores mantienen los tensores de embedding y salida en Q6_K u Q8_0 incluso dentro de un archivo Q4: un 27B planificado como --device CUDA1,CUDA0 --tensor-split 0.5079,0.4921 aterrizó con 15,52 GiB en CUDA0 — el último dispositivo, el que recibió menos en la división — frente a los 14,48 de CUDA1. Y llama.cpp abre un contexto CUDA en cada dispositivo visible — ~0,22 GiB en una 3090, 0,43 GiB en una 5090 — que es por lo que la columna de colocación tiene un suelo de 512 MiB.

Cuántas conversaciones vale una colocación

El --ctx-size de llama.cpp es el presupuesto total de KV compartido entre slots, no la ventana por slot — un flag muy malinterpretado, y el README de upstream no lo aclara. Una carga con --ctx-size 4096 y sin --parallel informa total_slots: 4: 1.024 tokens por conversación. StudioForge arranca con ctx_per_slot × parallel. Y luego: cuántos slots merece la pena tener, que es donde dejé de fiarme de mi propia aritmética.

ConcurrentesPor streamAgregadop50p95Lote logrado
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, una RTX 3090, 8.192 tokens por slot, KV f16, ocho slots lanzados, prompts de 512 tokens, 192 tokens generados cada uno.

Lo que dice la tabla:

  • El estimador dijo 8. La medición dijo 2. A cuatro slots cada stream cae al 44% de la velocidad en solitario, por debajo de un suelo del 65%; la regla toma el mayor de 1/2/4/8 que supere ese suelo ganando aún un 15% de agregado sobre el nivel inferior.
  • El agregado nunca deja de subir — ocho slots mueven 1,9× los tokens que mueve uno — mientras una sola conversación se desploma al 27%. Una regla que maximice el agregado elegiría 8, y cada usuario experimentaría un modelo tres veces más lento de lo que la tarjeta puede ejecutarlo.
  • El batching es real, no una cola. El lote logrado subiendo 1,00 → 1,84 → 3,46 → 6,03 demuestra pasos de decodificación compartidos; reproducido en tres ejecuciones con un margen del 2%.

Una segunda ejecución en dos 3090 a 32.768 por slot fue de 301,7 → 230,5 por stream y volvió a responder 2. De paso, el catálogo había predicho 308,0 tok/s para esa colocación y la ejecución midió 301,7 — un 2% de desviación, mejor de lo que esperaba. Las filas ahora llevan max_parallel (cuántos caben) junto a recommended_parallel (cuántos merece la pena ejecutar).

Dos flags que medí antes de fiarme

La decodificación especulativa es una victoria de un solo stream. Qwen3.8-27B Q5_K_S con cabeza MTP, una 3090, cuatro prompts distintos de 256 tokens, caché de prompt apagada: sin especulación 37,75 tok/s; draft-mtp a profundidad 3 dio 50,70 tok/s, +34,3%, con 0,528 de aceptación. La profundidad 4 baja a 47,48, porque la aceptación cae a 0,446 y cada token extra rechazado se verificó para nada. ngram-mod logró +0,4% y no emitió ningún borrador.

Y ahí viene la trampa: el mismo prompt tres veces midió +751% en ese 27B. Repite un prompt y estarás midiendo la caché de prompt llamándolo decodificación especulativa. Por encima de cuatro slots, auto ahora devuelve none y dice por qué — «la especulación es una victoria de un solo stream y perjudica a un lote saturado» — después de que una ejecución cargara un 27B con --parallel 8 y auto siguiera eligiendo draft-mtp, viendo la cabeza MTP y no el número de slots.

El micro-lote compra prefill a cambio de VRAM. El mismo 1,5B, un prompt de 5.166 tokens: -ub 512 (el valor por defecto del motor) dio 15.232 tok/s con 1492 MiB; -ub 1024, 17.307 tok/s (+13,6%) con 1562 MiB; -ub 2048, 18.061 tok/s (+18,6%) con 1702 MiB. Estuvo apagado mucho tiempo porque el buffer de cómputo crece con -ub, el planificador no lo modelaba, y un buffer no modelado convierte un ajuste en un out-of-memory. Ahora el planificador lo cobra, redondeado hacia arriba para que peque de rechazar, y sube el micro-lote automáticamente solo por encima de cuatro slots.

El rechazo, con las cifras

Cada lanzamiento pasa --fit off y --n-gpu-layers 999, y el segundo es una constante, no un ajuste. Eso importa más que antes: la build fijada b10425 incluye -fit, --fit [on|off]«si ajustar los argumentos sin definir para que quepan en la memoria del dispositivo»con on por defecto, junto a --n-gpu-layers auto; ambos llegaron a upstream en el PR #16653 en diciembre de 2025. Ese par es exactamente una ruta silenciosa de descarga parcial: un valor por defecto razonable para un servidor de propósito general, y el comportamiento exacto que este proyecto existe para rechazar. Cuando nada cabe, la respuesta es HTTP 507 con la aritmética en el cuerpo, recortada aquí:

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

Ese es un rechazo real, capturado mientras escribía esto: el 31B pidió cargar en una RTX 3090. La prosa nombra el déficit y los ocupantes; error.studioforge lleva el mismo fallo como datos — los bytes necesarios y disponibles, la VRAM libre por tarjeta, la estimación desglosada en pesos, KV, buffer de cómputo, modelo de proyección y contexto CUDA, cada proceso que retiene VRAM, y notes diciendo en qué peldaño de la escalera estaba parado cuando se rindió. Cuando una ventana más pequeña cabría, max_ctx_that_fits la nombra, calculada sobre la geometría por capas para que la oferta sea una que la siguiente carga acepte; aquí ni el suelo cabía, así que es null en lugar de un número que fallaría. Un rechazo que no es por un modelo ocupado no lleva retry_after_s, porque «inténtalo más tarde» es un mal consejo cuando nada va a cambiar.

Fijados, TTL, reservas y el reequilibrador

Un fijado es un estado deseado, no una exención. Antes significaba TTL cero, exclusión de toda escalera de desalojo y un calentamiento al arrancar — y no la cuarta cosa: un modelo fijado nunca se recargaba, así que un hijo que entrara en bucle de fallos más allá de su max_restarts por modelo se quedaba en state="failed" sin retener nada. Ahora un reconciliador cabalga el barrido de 15 segundos, retrocediendo de 60 s a un techo de 900 s. Lo único que supera a un fijado es una persona: una descarga explícita marca el id suprimido y se queda abajo.

Una reserva es una tarjeta que pertenece a un modelo. Una tarjeta reservada está ausente de la vista de GPU de cualquier otro modelo — no queda la última del ranking, no es una opción — y el dueño se ve forzado exactamente a esas tarjetas, dimensionadas por el estimador, en el modo de división que su propio benchmark midió más rápido allí. Una tarjeta ya reservada es un conflicto 409, nunca una toma de control. Una reserva sin modelo mantiene tarjetas para algo fuera del servidor: reserve_gpus(devices=[3], reason="ComfyUI render") es por lo que mi generación de imágenes y mis modelos de lenguaje dejaron de pelearse.

El reequilibrador arregla la buena decisión de ayer. A las 13:42 un 27B se planificó en las tarjetas [1, 3] — una división entre niveles que compartía la GPU1 — porque un 31B retenía [1, 0, 2] y una app de generación de imágenes retenía 7,5 GiB de la GPU2. A las 13:53 el 31B se encogió a [0, 1]; desde entonces [2, 3] quedó libre y estrictamente mejor, y el 27B se quedó donde estaba. Así que ahora un modelo en reposo se mueve cuando un plan sin desalojo con sus ajustes actuales exactos lo saca de toda tarjeta compartida. Los tokens por segundo estimados nunca justifican un movimiento.

Mira una vez por minuto, y solo cuando el mundo ha cambiado: solo en una máquina tranquila, solo para un modelo en reposo cinco minutos, un movimiento por modelo cada 30 minutos — porque una reubicación es una recarga y una recarga tira la caché de prompt, y en la carga de conversaciones largas de este equipo esa caché era el 93% de un prompt de 98k tokens. El desalojo tiene además tres reglas duras — nunca un modelo fijado, nunca uno a mitad de petición, nunca una instancia cargando — y una tarjeta reservada no está en la vista del planificador para empezar. Una carga just-in-time nunca puede poner force.

Cuando las cosas se rompen

La VRAM muere con el proceso que la tomó. En Windows los hijos viven en un job object anónimo creado con JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, así que el kernel mata a todos los miembros cuando se cierra el último handle — acabe como acabe el padre. Anónimo, porque un job con nombre se compartiría con cualquiera que adivinara el nombre. Linux recibe un shim PR_SET_PDEATHSIG, que cubre un kill -9 de la puerta de enlace; es un esfuerzo razonable más que una garantía del kernel, así que el barrido de arranque y reclaim_orphan_engines atrapan lo que se cuela.

Que es por lo que hay un barrido de arranque. El 18 de agosto de 2026, ~10 GiB en la GPU0 y ~15,6 GiB en la GPU1 estaban no disponibles con «todo parado». Los ocupantes eran tres hijos llama-server.exe de un python -m pytest tests -q que un agente de programación había lanzado y que desde entonces había terminado. Ahora cada ocupante se clasifica — ours, child-of-live-process, orphan, other-instance, foreign — y solo se mata orphan, seguro por construcción porque nada más lanza binarios de nuestro árbol de motores.

Nombrar quién retiene qué llevó dos intentos. NVML informa cero de memoria usada por proceso en Windows, así que los tamaños vienen del contador que lee la columna «Dedicated GPU memory» del Administrador de tareas — pero eso es un total por proceso entre adaptadores, así que la columna de dispositivo estaba mal: un proceso informado en CUDA0,1,2,3 en realidad retenía 15,52 GiB en la CUDA0 y nada en las 3090. El arreglo une el LUID del adaptador con una dirección de bus PCI, y una trampa se gana el párrafo: el número de bus en el busId de NVML es hexadecimal. "00000000:42:00.0" es el bus 66, no el 42 — una respuesta equivocada y segura de sí misma, indistinguible de una correcta.

Una descarga se verifica, no se anuncia. El supervisor vuelve a comprobar el pid con una protección de tiempo de creación contra la reutilización de pid, escala a un superviviente a un tree-kill forzado y registra la VRAM antes y después; un superviviente devuelve un 500 pidiéndote que lo mates manualmente. Una descarga que informa éxito mientras el proceso sigue residente es la mentira más cara que este sistema puede contar, porque cada carga posterior se planifica entonces contra una VRAM que no está libre.

Los códigos de salida son vocabulario. 2 es un error de configuración que nombra la clave. 3 es un conflicto de puerto, y la bandeja nunca relanza sobre el puerto en conflicto — espera a que el ocupante responda a /health como servidor StudioForge y se engancha a él en su lugar. 75 es «reinicio solicitado»: el servidor drena, fija el código y se apaga con elegancia — 1,0 s de la petición a la salida, medido — y la bandeja relanza sin gastar un intento de fallo. Esa distinción existe porque un reinicio por GUI produjo una vez dos servidores compitiendo por el 1234, y tres fallos contados después la bandeja se quedaba en Fallido — mira la carpeta de logs junto a un servidor sano que ya no podía parar.

Cuando la puerta de enlace está atascada en lugar de muerta, hablas con el vigilante: un proceso aparte siempre encendido en 1235, construido con argparse y logging de stdlib, así que arranca incluso cuando config.yaml es lo que está roto. Sus diez herramientas son health, get_config, set_config, restart_server, kill_model, nuke_all_models, reclaim_orphan_engines, tail_logs, gpu_status y rollback_update. Reimplementa la regla de huérfanos localmente en lugar de importar el módulo que la posee — el proceso de recuperación no debe importar la pila que repara.

El panel de control

El panel en 8080 es un segundo servidor uvicorn dentro del mismo proceso, que comparte el grafo de objetos de la puerta de enlace por referencia — no tiene ninguna URL absoluta, que es lo que lo hace funcionar igual por HTTP plano en una VPN mesh y detrás de un front end HTTPS.

El Dashboard de StudioForge a mitad de petición: una cabecera que dice 2 cargados, 4 GPU, 45,5 de 111,7 GiB libres; cuatro tarjetas GPU — dos RTX 5090, dos RTX 3090 — con cifras de usados, libres y utilización; una lista de ocupantes de VRAM con dos procesos llama-server.exe marcados como ours y cinco foreign; un panel de reservas de GPU vacío; y dos tarjetas de modelos cargados, una haciendo prefill y otra fijada e inactiva
El dashboard a mitad de petición: un 31B dividido entre las dos 5090 haciendo prefill de un prompt de 2.700 tokens, un 26B fijado en una 3090, y cada proceso que retiene VRAM en cualquier tarjeta nombrado con su parte. El panel de ocupantes existe porque una vez desaparecieron 25 GiB.
Un primer plano de las dos tarjetas de modelos cargados: el 31B dice puerto 18100, ctx real 262144, 1 ocupado / 0 inactivo, GPU0, GPU1 (layer), TTL 15m 00s, peticiones activas 1, total 3, último 58,77 tok/s, build b10425, y una línea de slot que dice Processing prompt 2202/2742 (80%), cache hit 7/2742; el 26B dice puerto 18101, GPU2, TTL pinned, último 46,67 tok/s, slot 0 Idle
Las mismas dos tarjetas, recortadas del marco anterior: el puerto que se le dio de verdad a cada modelo, el contexto que obtuvo de verdad, las tarjetas en las que aterrizó de verdad, la cuenta atrás del TTL — y luego una línea por slot del motor. Lo «real» se lee de vuelta del hijo, no de lo que pedimos.
La pestaña Setup: una lista de comprobación encabezada por Ready to serve, but fix before exposing it: Network exposure, con filas verdes para el directorio de datos, 50 archivos GGUF bajo E:\LLM\Models, 34 modelos indexados, 4 GPU con driver 610.88 y CUDA 13.3, motor b10425 smoke-tested y la puerta de enlace en 0.0.0.0:1234, una fila ámbar de Network exposure con un botón Set API key; luego la sección Model library y los campos Defaults for every load — suelo de contexto 128000, objetivo 1048576, suelo de modelos de pensamiento 32768
Una instalación limpia se abre aquí, y «limpia» se mide en lugar de recordarse — sin directorio de modelos, sin nada indexado, o sin motor. Cada fila sin cumplir lleva el botón que la arregla; en mi equipo la única fila ámbar es la puerta de enlace escuchando en todas las interfaces sin clave API, que es una elección, no un accidente.
La tabla de biblioteca de la pestaña Models: columnas para Name, Downloaded, Size, Quant, Arch, Features, Last used, Status y Actions; once filas desde dos builds de Qwen3.8-27B de 21,29 y 17,95 GiB hasta Qwen2.5-0.5B de 0,49 GiB, con iconos de funcionalidades para chat, visión y embedding; la fila gemma-4-26B lleva una insignia pinned y un chip verde loaded; seis pequeños botones de acción cierran cada fila
Una tabla en lugar de tarjetas, porque con unas pocas docenas de modelos las preguntas son comparativas — cuál llegó el último, cuál es el grande, cuál puede ver imágenes. La ordenación por defecto es la descarga más reciente primero, y los chips de estado a la derecha son la verdad en vivo sobre cuáles están residentes.
El diálogo de ajustes por modelo para gemma-4-31B-it-QAT-Q4_0 (16,44 GiB, ctx entrenado 262144): un bloque Optimal settings con una línea por cada conjunto de GPU — dos RTX 5090 con ctx 262144, q8_0, 3 slots, unos 62 tok/s; dos RTX 3090 con 1 slot, unos 34 tok/s; las cuatro GPU con 3 slots, unos 40 tok/s, marcado fits now; una RTX 5090 con ctx 131072, unos 66 tok/s — cada una con botones Load here y Measure parallel; una fila Load at exactly con 64K, 128K, 256K y 512K; un veredicto de ajuste verde que dice Fits across GPU1, GPU0, GPU3 (split: layer) con caché KV q8_0, flash-attn on y VRAM proyectada de 32,55 GiB desglosada por tarjeta; y los campos Basic — context length, KV type K y V, TTL, una casilla Pinned, draft model y device override
Este diálogo es todo el argumento en una pantalla: lo que el modelo podría hacer en cada conjunto de tarjetas si estuvieran libres, un veredicto de ajuste que se recalcula con cada cambio con la VRAM proyectada por tarjeta, y luego cada mando para anularlo (los niveles Advanced y Expert continúan debajo del recorte). Los tamaños de contexto inalcanzables se atenúan con el motivo en lugar de ocultarse — «¿por qué este modelo no puede con 512k?» es la pregunta que ese botón existe para responder.
La pestaña Download tras buscar gemma-4 en Hugging Face, ordenada por Downloads (30d): doce repositorios de unsloth, lmstudio-community y google, cada fila mostrando el editor, un recuento de descargas de 1.187.348 hasta 458.712, likes, el número de cuantizaciones y cuándo se actualizó, con un enlace a la ficha del modelo y un botón Quants
La búsqueda de Hugging Face y la cola de descargas en una pestaña. La etiqueta de ordenación dice «Downloads (30d)» a propósito — esa cifra es un recuento móvil de treinta días, no un total histórico — y el botón Quants es donde empiezan las cuentas del ajuste.
La parte superior del selector de cuantización para unsloth/Qwen3.8-27B-GGUF: una nota de cabecera que dice que el ajuste es una estimación solo de pesos hasta que se lee la cabecera del modelo, una línea de geometría que dice attention hybrid, 65 layers, KV 65 KB por token, y luego dieciséis filas desde BF16 de 51,77 GiB con un chip ámbar needs-multiple-GPUs hasta Q5_K_M de 19,28 GiB, cada una con un chip verde fits-one-GPU, una línea de contexto por colocación para una 5090, dos 5090 y las cuatro tarjetas, y un botón Download
Antes de descargar nada, cada fila se comprueba contra la VRAM que está libre de verdad ahora mismo: un chip de ajuste por cuantización, y debajo el contexto que obtendría cada colocación — una 5090, dos, o las cuatro tarjetas. El selector se rellena dos veces: el tamaño de archivo contra la VRAM libre para que esté en pantalla de inmediato, y luego una lectura remota de la cabecera del GGUF para que la insignia se convierta en la respuesta del propio planificador. No siempre ha sido tan limpio: hasta un arreglo hecho mientras escribía esto, el selector también listaba el módulo de borrador MTP del repo y su archivo imatrix como si fueran cuantizaciones del modelo. El diálogo continúa debajo del recorte — 25 filas en total.
La pestaña Chat: un interruptor Use the loaded model que se resuelve a gemma-4-31B-it-QAT-Q4_0, el más recientemente usado de 2 cargados; un selector de modelo, temperature 0.7, top_p 0.95, max_tokens 1024 y una etiqueta de velocidad que dice 9.7 tok/s; una caja de prompt de sistema; y una transcripción con el prompt del usuario pidiendo una explicación en tres frases de qué es una caché KV y por qué su tamaño depende de la ventana de contexto, seguida de la respuesta completada del modelo
Un arnés de pruebas, no una app de chat: llama a la misma ruta de carga y hace streaming desde el mismo puerto del hijo que usan los endpoints de OpenAI. La etiqueta 9.7 tok/s es la aritmética de esta pestaña — fragmentos en streaming sobre el reloj de pared desde el momento en que salió la petición, prefill incluido — no el tiempo de generación del motor, que son los 58,77 tok/s de la tarjeta del modelo. Un chat exitoso aquí es evidencia de que un cliente funcionará, no una ruta simulada que puede derivar.
La parte superior de la pestaña Server: una franja Ready to serve que lista el directorio de modelos, 34 modelos indexados, 4 GPU y el motor b10425; el panel Point OpenClaw at this server expandido para mostrar el par OPENAI_BASE_URL y OPENAI_API_KEY generado, el JSON de servidor MCP con openclaw_cli, openclaw_config bajo mcp.servers y un bloque genérico mcpServers, y la configuración del acompañante; luego un bloque Health que dice version 1.26-08-23, uptime, la ruta de configuración y ambos modelos cargados
La pestaña Server escribe la configuración del cliente por ti — el bloque exacto que pegué en OpenClaw, en ambas formas — y muestra las líneas de salud debajo; más abajo, en la misma pestaña, están la lista de motores y un informe de cuánto de la biblioteca puede ejecutar de verdad la máquina. Como la entrada MCP ejecuta sfctl por fuera, el PIN de emparejamiento nunca tiene que aparecer en un archivo de configuración.
La pestaña Logs: fuente StudioForge server, nivel INFO, Follow marcado, siguiendo un buffer en anillo en memoria de 84 líneas — líneas repetidas de load planned de studioforge.core.planner que dicen chosen ctx=262144 kv=q8_0 parallel=1, devices=[1, 0, 3] y estimate_mb=33331, y luego unloading idle model at ttl_s=900 y model_unload_verified con vram_reclaimed_mb=37086
La pestaña Logs sigue el buffer en anillo en memoria: aquí el planificador escribe cada decisión antes de actuar — el contexto, el tipo de caché, el número de slots y los dispositivos que eligió, y lo que esperaba que costara la carga — y luego una descarga por reposo y la VRAM que verificó de vuelta. Nada de esto se reconstruye después; es lo que pensaba el planificador en ese momento.

Cuatro líneas más abajo en el mismo buffer están las que leo cuando una carga me sorprende — la línea de comandos que construyó, el proceso que respondió, y el planificador corrigiendo su propia tarea (timestamps y nombres de logger recortados):

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'

Eso es el planificador pillándose a sí mismo por 3 GB en una tarjeta, 4 GB en el par, y diciendo qué tarjeta y por qué — la capa de salida aterrizó en el último dispositivo, exactamente donde la cobra, y el cobro aun así se quedó corto.

El menú del área de notificación de Windows para StudioForge: una línea de cabecera que dice En ejecución — 1 modelo cargado, 23,4 GiB libres, y luego Abrir panel de control, Abrir docs de la API, Abrir carpeta de logs, Abrir carpeta de modelos, Descargar todos los modelos (liberar VRAM), Reiniciar motores, Iniciar servidor atenuado, Detener servidor, Reiniciar servidor, Copiar URL MCP, Copiar PIN MCP, un Iniciar al iniciar sesión marcado y Salir
En el día a día, todo esto es un icono en la bandeja. La línea de cabecera es todo el estado — qué está cargado y cuánta VRAM queda — y el PIN es la única credencial que la bandeja pone jamás en el portapapeles.

Usarlo como backend de arnés

Seis clientes de mi red hablan con esto y solo uno sabe que es StudioForge. Esa es la gracia. Cinco están dibujados abajo; OpenClaw Email es el sexto.

Cualquier cliente OpenAI

Dos variables de entorno. server.api_key es null de fábrica, así que vale cualquier cadena no vacía — la mayoría de clientes OpenAI se niegan a arrancar con una vacía.

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 lista todo lo descargado, al estilo de LM Studio, añadiendo state y — cuando está residente — ctx_per_slot, max_parallel y parallel_limited_by, porque un solo tamaño de contexto es ambiguo cuando un modelo corre más de un slot. Los ids hacen round-trip: el id completo publisher/repo/file, un nombre de archivo a secas, o publisher/name, sin distinguir mayúsculas. DisPatch no necesitó más que una URL base nueva; OpenClaw Email es el cliente más exigente, que quiere un modelo de chat y un modelo de embeddings y llama a /v1/models al arrancar para preguntar qué se está sirviendo de verdad.

OpenClaw, en otra máquina

La máquina del agente instala un pequeño wheel: sfctl, que apunta a Python 3.11 en lugar del 3.12 del servidor porque la máquina que corre el agente suele ir por detrás del equipo, y que a propósito no depende del paquete del servidor — sin CUDA, sin planificador, sin registro.

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

O a mano — el detalle que le cuesta una tarde a la gente. La clave de OpenClaw es mcp.servers, anidada bajo mcp: el mapa plano mcpServers vale para Claude Code, Cline y LibreChat, y no es una clave que conozca el esquema de OpenClaw. La inferencia es un camino aparte, bajo models.providers — fíjate en baseUrl, con la rl en minúscula:

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

Lo que recibe el agente es una lista fusionada de 29 herramientas: las 19 de la puerta de enlace más las 10 del vigilante, tres renombradas como recovery_*get_config y set_config porque chocan con herramientas de la puerta de enlace, health por simetría. restart_server conserva su nombre a secas, porque es el nombre que el mensaje de error de una herramienta de gestión muerta le dice al agente que llame. Cuando el servidor principal está caído, el puente sigue anunciando las 19 herramientas de gestión con una nota añadida — un agente que no puede ver load_model no sabe que la capacidad existe. El bucle que ejecuta:

  1. list_models(limit=N) — el catálogo, la descarga más reciente primero. Lee la fila recomendada.
  2. load_model(**row["load_args"]) — pásalo tal cual; un agente que ha elegido una fila ya ha terminado de elegir.
  3. load_recommended(model_id, ctx_size=N) cuando lo que sabes es el contexto que necesitas — la única ruta de carga que rechaza en lugar de encogerse.
  4. Inferencia por HTTP, no por MCP. Nombrar un modelo no cargado lo carga, con los valores por defecto del planificador en lugar de la fila que estabas leyendo.
  5. model_options(model_id) cuando la fila recomendada no basta: cada nivel de contexto, con velocidades.
  6. search_modelsrepo_detailsdownload_model para conseguir algo nuevo.
  7. pin_model para el modelo que siempre debe responder; reserve_gpus / release_gpus para tarjetas propias.
  8. server_status y connection_info — qué está residente, quién retiene VRAM, cada dirección en la que responde.

Y el párrafo del que más orgulloso estoy, servido a cada cliente al conectar:

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 y cinco one-liners

dsh (DeepSeek Harness) cambia de modelo editando un archivo YAML, recargado en caliente para la siguiente petición. El bloque de proveedor es 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 es una referencia, nunca un literal — y un servidor local sin clave sigue necesitando que se referencie alguna credencial, porque el cliente compatible con OpenAI insiste en un bearer token. Claude Code, en cambio, no puede enrutar aquí su propia inferencia en absoluto: su referencia de protocolo de puerta de enlace lista Anthropic Messages, Bedrock y Vertex, y ninguno es /v1/chat/completions. Así que StudioForge es su herramienta, no su cerebro — claude mcp add studioforge -- sfctl mcp (el -- es obligatorio) le entrega las 29.

bench-llm produjo las cifras del equipo que la gente me cita — Gemma 4 26B-A4B (QAT, Q4) a 229,0 tok/s, 110 ms hasta el primer token, medidas a través de LM Studio en su momento — y es un cliente OpenAI normal, así que una URL base es todo lo que necesita; pero ejecuta pkill -f llama-server entre benchmarks, lo que mata a todos los backends de StudioForge de la máquina. El resto son una línea cada uno: Open WebUI, OPENAI_API_BASE_URL; LibreChat, un endpoint custom con baseURL y models.fetch: true; aider, OPENAI_API_BASE y luego --model openai/<id>; Continue, provider: openai más apiBase. La clave de la URL base se escribe distinta en cada uno de ellos, que es la fuente más fiable de tardes perdidas en este ecosistema.

De qué elige un agente

El catálogo convierte la elección de modelo en una consulta en lugar de una suposición: ordenado con la descarga más reciente primero, una fila por nivel de contexto, cada una con fits contra la VRAM libre en vivo, devices, tipos de KV, max_parallel, recommended_parallel, una confidence, una columna if_gpus_idle y load_args. Una fila es una llamada real al plan, así que no puede prometer lo que una carga rechazaría — y if_gpus_idle es la diferencia entre un agente que se rinde y un agente que llama a unload_model.

repo_details es la que merece nombrarse: lee una cabecera GGUF de forma remota, por peticiones HTTP Range — 2–15 MB, sobre todo los arrays de cadenas con prefijo de longitud del tokenizador, cacheados en disco — en lugar de descargar 20 GB para averiguar si cabe, y una CDN que responde a una petición con rango con un 200 y el cuerpo entero se detecta y se rechaza. Lo que vuelve es una matriz context_fit del mismo planificador que usa una carga real:

Quant1× RTX 50902× RTX 5090Las cuatro tarjetas
BF16 (51,8 GiB)— los pesos solos no caben32k en q8_0256k
Q8_0 (27,9 GiB)256k256k
Q5_K_M (19,3 GiB)128k en q8_0256k256k
IQ2_M (10,5 GiB)256k256k256k

De la guía de OpenClaw del repo, calculada en este equipo, para unsloth/Qwen3.8-27B-GGUF. max_ctx es la ventana más grande con una caché f16 de calidad completa; una cifra q8_0 aparece solo donde llega más lejos.

Lo que dice la tabla: la cuantización decide la ventana, no el número de tarjetas — de BF16 a Q5_K_M, «no cabe en absoluto» se convierte en 128k en una tarjeta — y es la respuesta del propio planificador, así que donde un nivel solo se alcanza cuantizando la caché la matriz dice «en q8_0» en lugar de contarlo como victoria. Para cifras reales el guion son tres pasos: un benchmark de colocación (cada modo de GPU bajo su propia reserva, throughput de los tiempos del propio llama-server), luego benchmark_parallel sobre el ganador, y luego reserve_gpus para fijarlo. Nunca hagas benchmark de un modelo con el que alguien está a mitad de conversación.

Drop-in para LM Studio

La compatibilidad era la restricción de diseño, y el repo lista lo que se tomó prestado para que nadie tenga que adivinarlo: el puerto 1234; /v1/models listando lo descargado en lugar de lo cargado; la carga just-in-time; el TTL de reposo; el ttl por petición; la estructura publisher/repo/ usada en su sitio, así que no hay paso de importación y ambos programas comparten una biblioteca; el espejo /api/v0/models; y el deep link lmstudio://open_from_hf. Migrar un cliente es un cambio de host, no un cambio de host y puerto.

LM Studio 0.4.21StudioForge 1.26-08-23
Motorbuilds propias de llama.cpp más MLX en Applellama-server de upstream, una build fijada (b10425, CUDA 13.3), probada antes de activarla
Rutas sin enrutar200 con un cuerpo de error — su log dice Returning 200 anyway404 con un sobre JSON, y JSON en cada estado
Erroresprosa sin estructurar que los clientes parsean con regexun error.code estable, diagnósticos bajo error.studioforge
Configuración de cargacontext_length ignorado en una de las dos rutas de carga; repetition_penalty ignorado en silenciouna sola ruta de carga, cada campo respetado, valores efectivos devueltos; alias de sampler aceptados
Cuando no cabe«reducirá automáticamente el tamaño de la descarga a GPU … y el resto en la RAM del sistema» — un volcado silencioso a CPU, por diseño507 insufficient_vram con bytes necesarios y disponibles, libre por GPU, el mayor contexto que cabría, sugerencias ordenadas
Multi-GPUprioridad o división uniforme, conmutadores por GPU, tensor parallel desde 0.4.15un planificador que dimensiona contexto, tipo de KV y slots por colocación, inclinando las fracciones de división para la capa de salida
TTL de reposo60 minutos; el auto-evict mantiene como mucho 1 modelo cargado JIT1.800 s de fábrica (15 min en mi equipo), barrido cada 15 s; tantos como quepan, con fijados y reservas decidiendo quién se queda
Gestión remota/api/v1 load/unload/download; LM Link en preview, que se monetizará, descubrimiento vía el hub de LM Studio/api REST, sfctl, 29 herramientas MCP; el alcance es tu LAN o tu propia VPN mesh
MCPsolo host — consume servidores MCPun servidor MCP para su propia gestión, más uno en el vigilante
Fuentecerrada; gratuita para uso personal y empresarial internofuente en el zip, y sin licencia elegida aún

Comprobado contra el changelog, los docs y el bug tracker del propio LM Studio el 2026-08-23, versión 0.4.21 (publicada el 12 de agosto de 2026). La fila 2 está en su bug tracker público; las filas 3–4 son lo que mi propio cliente tuvo que esquivar en la API 0.3.x. No he vuelto a probar ninguna contra la 0.4.21, así que léelas como «documentado en algún momento», no como «roto hoy».

Lo que dice la tabla: es una mejor app de escritorio de lo que esto será jamás — GUI pulida, MLX en Apple Silicon, un endpoint compatible con Anthropic, un compañero móvil, un equipo publicando cada dos semanas — y la división es filosófica más que de funciones. La postura por defecto de LM Studio es el mejor esfuerzo: haz que corra de alguna manera. La mía es rechazar-y-explicar. Donde un agente decide qué se carga, el mejor esfuerzo es el valor por defecto equivocado, porque nada aguas abajo puede distinguir lo rápido de lo lento sin medir.

Cómo se compara

Servidor · motor · licenciaHot-swap / TTL de reposoColocación multi-GPURechaza el volcado a CPUGestión remota / MCP
StudioForge 1.26-08-23
llama.cpp, una build fijada · licencia: ninguna aún
✅ JIT · TTL de 1.800 s, barrido de 15 splanificada por modelo, tarjetas mixtas, fijados + reservas-ngl 999 + --fit off, 507 con cifras✅ REST + 29 herramientas MCP
LM Studio 0.4.21
llama.cpp propio + MLX · cerrada
✅ JIT · 60 min, auto-evict a 1división por prioridad/uniforme, tensor parallel❌ reduce la descarga, el resto en RAMREST; MCP solo como host
Ollama 0.32.15
llama.cpp/GGML; MLX en Apple · MIT
keep_alive 5 min · 3 residentes por GPUreparto automático entre tarjetas❌ vuelca, muestra % de CPU en ollama ps/api/* rico; sin servidor MCP
llama-server router (b105xx, agosto de 2026)
es llama.cpp · MIT
✅ un hijo por modelo · --sleep-idle-seconds, LRU por recuento-sm / -ts / -dev manuales--fit on encoge tu plan/models/load|unload
llama-swap v251
un proxy que lanza a otros · MIT
✅ todo el producto · ttl por modelo/grupo❌ lo que diga tu cmdn/d — solo proxy/ui + rutas de upstream
vLLM 0.27.1
propio (PagedAttention) · Apache-2.0
❌ un modelo por procesotensor / pipeline / expert parallelparcial — sin ruta de descarga por capassolo LoRA, «local dev»
KoboldCpp 1.119
fork de llama.cpp + imagen/audio · AGPL-3.0
--admin + --routermode--tensor_split manual❌ vuelca/api/admin/*; solo cliente MCP
TextGen (ex-oobabooga) 4.9
5 loaders incl. ExLlamaV3, TRT-LLM · AGPL-3.0
✅ cambio sin reiniciar · ¿TTL?--tensor-split manual❌ vuelca/v1/internal/model/*; cliente MCP
TabbyAPI (rolling)
solo ExLlamaV3 — sin GGUF · AGPL-3.0
✅ admin + carga inline · ¿TTL?gpu_split_auto activado por defecto✅ en efecto — ExLlama no tiene ruta de CPU/v1/model/load con admin-key
Jan 0.8.4
modo router de llama.cpp · Apache-2.0
✅ vía el router · ¿TTL?heredada de llama.cpp❌ vuelca/v1/orchestrations; cliente MCP
LocalAI 4.9.0
más de 60 backends como imágenes de contenedor · MIT
✅ bajo demanda · WATCHDOG_IDLE_TIMEOUT«automatic GPU model fitting»❌ «No GPU required»REST + UI; solo cliente MCP
GPUStack 2.2.3
vLLM, SGLang, MindIE, VoxBox · Apache-2.0
parcial — despliegues de clústerSpread/Binpack automático, multi-nodo?API completa de gestión de clúster

Comprobado desde fuentes primarias el 2026-08-23. Un signo de interrogación significa desconocido, no «no». Los workers de GPUStack son solo Linux; vLLM no tiene soporte nativo de Windows (solo WSL o forks).

Lo que dice la tabla:

  • La carga JIT y el TTL de reposo no son novedosos y no lo estoy reivindicando. LM Studio, Ollama, llama-swap y LocalAI hacen ambas cosas, y los flags swap / exclusive / persistent por grupo de llama-swap son un motor de políticas genuinamente elegante.
  • Tres cosas de ahí son raras: negarse a correr antes que volcar a CPU (solo TabbyAPI se acerca, y solo porque ExLlama no tiene ruta de CPU); un planificador que dimensiona contexto y slots a las tarjetas que encontró de verdad; y la gestión publicada como herramientas MCP, que nada más de la categoría hace que yo sepa.
  • Lo más cercano en upstream es el router del propio llama.cpp, una pelea justa: multi-modelo con aislamiento de procesos, gratis, en el binario que ya tienes. Lo que le falta es desalojo por memoria en lugar de por recuento, fijados, reservas y un rechazo con cifras; sí tiene un sueño de reposo (--sleep-idle-seconds), aunque un sondeo de /metrics lo despierta.

Varias buenas ideas están tomadas prestadas, y el repo dice cuáles. El Modelfile de Ollama se convirtió en modelos virtuales, así que dos personas sobre una misma base comparten un solo llama-server, y su keep_alive se convirtió en el ttl por petición. La superficie de ajustes en tres niveles de TextGen se adoptó directamente, «extra flags» en bruto incluidos, con los flags validados contra el --help del propio motor fijado al guardar. La filosofía de un solo artefacto de KoboldCpp es por lo que los motores viven en directorios versionados.

Instalación

Windows, la plataforma de referencia, en cuatro pasos: instala Git, Python 3.12+, uv y un driver NVIDIA actual; clona el repo o descomprime la descarga; haz doble clic en launchers\Update StudioForge.bat, que pese a su nombre es el paso de primera ejecución — construye el virtualenv, instala la release de llama.cpp más nueva que tenga build para tu driver, la prueba y se fija a ella (b10425 es la build sobre la que se midió este artículo, no la que obtendrás tú); y luego launchers\Start StudioForge.bat, o launchers\StudioForge Tray.bat si lo quieres en el área de notificación, y el panel se abre en http://127.0.0.1:8080 en su pestaña Setup.

Linux, cuatro líneas — más cmake y un CUDA toolkit cuyo nvcc coincida con tu driver, porque upstream no publica ningún archivo CUDA de Linux en ningún tag y el motor se compila desde fuente una vez por versión:

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

Para una máquina headless, deploy/ contiene dos unidades systemd de usuario — unidades de usuario a propósito, porque el proceso debe correr como el usuario que inició sesión y que posee la biblioteca de modelos, el venv y los nodos de dispositivo GPU. El vigilante a propósito no está BindsTo= la puerta de enlace y usa Restart=always: existe para estar arriba cuando la puerta de enlace no lo está. Y luego sudo loginctl enable-linger "$USER", el mismo patrón que el artículo sobre ComfyUI headless. La primera ejecución se abre en Setup, donde Detect LM Studio library sondea primero el downloadsFolder en ~/.lmstudio/settings.json.

ServicioPuerto por defectoClave de configuración
Puerta de enlace — /v1, /api, /mcp1234server.port
Panel de control web8080gui.port
Vigilante de recuperación1235watchdog.port
Hijos llama-server (solo loopback)18100–18200gateway.child_port_start / _end

Lo que dice la tabla: el puerto 1234 significa «el servidor de modelos local» en mis dos máquinas y no son lo mismo — la máquina del agente corre el suyo en 127.0.0.1:1234, loopback, mientras el equipo sirve my-gpu-rig:1234 por la VPN mesh — y solo tres puertos son alcanzables jamás, con la validación de configuración rechazando una colisión entre cualquier puerto de servicio y el rango de hijos en el momento de cargar.

La regla del directorio de datos es SF_DATA_DIR primero, luego la carpeta de un archivo --config, y luego <repo>/data en un checkout — el orden completo, y por qué data_dir nunca se escribe de vuelta en config.yaml, está en docs/SETUP.md. Una instancia posee un directorio de datos, impuesto por un lock exclusivo del sistema operativo; una segunda instancia es de solo lectura.

Seguridad, con honestidad

Una regla debajo de todo: las lecturas, la inferencia y la residencia siguen abiertas; cambiar la máquina, no. Con server.api_key sin fijar, una petición mutante a una ruta que cambia la máquina se acepta solo desde un llamador en esta máquina, o con el PIN MCP enviado como X-MCP-Pin o como bearer token — cualquier otra cosa recibe 403 remote_admin_requires_credential. El conjunto protegido es configuración, reinicios, motores, actualizaciones, recuperación de VRAM, descargas, reservas, borrados y las dos escrituras por modelo que sobreviven a la instancia. El problema que arregló era mío: cualquiera en la LAN podía hacer PATCH /api/config, fijar server.api_key él mismo y dejarme fuera — mientras la herramienta MCP set_config, la misma capacidad en el mismo proceso, exigía el PIN.

  • El PIN protege solo MCP. Es un código de emparejamiento que lees del banner de arranque, limitado a las herramientas de gestión. No es una API key.
  • server.api_key es la credencial real y es null por defecto. Fíjala y cubre /v1, /api, /mcp y el vigilante; el PIN sigue funcionando en los dos endpoints MCP junto a ella.
  • El bind de fábrica es 0.0.0.0 en los tres listeners. La fila Network exposure de la pestaña Setup se pone ámbar y obligatoria en cuanto cualquier listener se expone sin clave — comprobando los tres, porque server.host en loopback con gui.host en 0.0.0.0 antes leía verde mientras el panel estaba de par en par.
  • Una petición de navegador cross-origin no es «esta máquina», ni en loopback. Con cors_origins: ["*"], cualquier página que visites podría preflight de PATCH /api/config en 127.0.0.1:1234 y llegar pareciendo local — así que la comparación de origen incluye el puerto, y Origin: null cuenta como foráneo. CORS gobierna lo que una página puede leer, nunca en quién confía el servidor. El websocket del panel tiene una versión solo-host de la misma puerta, porque el panel se alcanza por el puerto desde el que se sirvió.
  • Un navegador remoto en una instalación sin clave recibe lecturas e inferencia, 403 en los cambios de la máquina, y el PIN retenido — de lo contrario, cualquier cosa en la LAN podría leer el PIN de un endpoint abierto y usarlo. El PIN era teatro justo cuando importaba.
  • Las imágenes se obtienen bajo una protección SSRF que bloquea loopback, link-local, privado, ULA y espacio CGNAT — el rango 100.64/10, donde vive cada peer de VPN mesh — y resuelve una vez, conectando a la dirección verificada con el Host y el SNI originales.
  • Nada sale de la máquina sin pedirlo. Las únicas llamadas salientes son Hugging Face para los modelos, GitHub para la build fijada de llama-server y su comprobación de actualizaciones, la comprobación opcional de versiones de StudioForge, y las URLs de imagen que nombre una petición; el self-update informa «no configurado» sin una llamada de red hasta que pones update.repo, y un test unitario lo fija.

Dos límites declarados en lugar de ocultos: una comprobación de dirección de peer confía en lo que esté en loopback, que detrás de un reverse proxy es el proxy, así que pon el proxy detrás de server.api_key; y no hay autenticación más allá de una clave compartida — sin cuentas, sin limitación de velocidad. La regla de la casa de el artículo sobre OpenClaw sigue valiendo: loopback más un proxy con autenticación, o una VPN mesh, nunca el internet crudo.

La parte honesta

Escrito con claridad para que puedas decidir antes de instalarlo:

  • Windows es la plataforma de referencia — es contra lo que se construyeron la bandeja, la protección de VRAM por job object y los contadores de GPU por proceso. Linux está soportado y CI ejecuta ambos, y está menos probado en combate; la ruta de compilación desde fuente tiene su construcción de comandos probada pero nunca se ha ejercitado de punta a punta aquí. macOS no está soportado: no hay CUDA.
  • Solo NVIDIA. El planificador lee NVML, el motor es una build CUDA, y la afinidad de cuantización se expresa en capacidades de cómputo.
  • La estimación de VRAM es una estimación. Los pesos aterrizan dentro del 2% del tamaño del archivo y la KV es exacta desde la geometría por capas, pero el buffer de cómputo es una fracción calibrada, afinada una vez al arrancar, limitada a 0,03–0,15 y mantenida solo en memoria — así que una mala calibración se deshace con un reinicio. Dos historiales de calibración en esta máquina se contaminaron y ahora se ignoran por completo.
  • La división multi-GPU es proporcional, no medida. No modela el ancho de banda del interconectado, y mezclar generaciones corre al ritmo de la tarjeta más lenta.
  • Las estimaciones de concurrencia son aritmética. El estimador supone que los slots están a medio llenar y reduce los modelos MoE por una mitad plana, y --ctx-checkpoints no se modela en absoluto. Ambos errores apuntan hacia menos slots, que es la dirección segura — pero ejecuta el benchmark en paralelo antes de fiarte de 8.
  • Las estimaciones de velocidad usan cifras nominales del fabricante — 5090 a 1792 GB/s y 209 TFLOPS fp16, 3090 a 936 y 71, ninguna medida aquí. Hay exactamente dos anclas de calibración, ambas en este equipo, ambas a un slot: una 31B densa midió 39,4 tok/s frente a una estimación de 36,1, y una 122B MoE midió 37,3 frente a 47,4. Nada está validado a cuatro u ocho slots, ni en un modelo denso por encima de 31B.
  • Reservar una GPU para otro programa solo limita a mi planificador. Nada lo impone contra el otro programa, y nada evita que tome la memoria primero.
  • Una instancia por directorio de datos, y el lock cubre el directorio de datos en lugar de la biblioteca de modelos — dos instancias con directorios de datos distintos sobre una biblioteca siguen siendo dos escritores.
  • No se ha elegido licencia. A propósito no hay archivo LICENSE, y pyproject.toml lo dice en un comentario — Consíguelo tiene lo que eso significa en la práctica.
  • No hay auditoría de seguridad de terceros. Las afirmaciones de arriba describen lo que hace el código; yo escribí ambas cosas. Lee la fuente — por eso es una descarga y no un servicio.

2.503 tests unitarios pasan y 17 se saltan, en 327 segundos en esta máquina; CI ejecuta la misma suite en Windows y Ubuntu con la sonda de GPU forzada a un backend nulo. Una segunda suite carga pesos reales en GPU reales y está deseleccionada por defecto y protegida detrás de una variable de entorno — tirantes y cinturón, tras el incidente de huérfanos de arriba. Sin hacer, o sin activar: el A/B de micro-lote a 8 slots, los presets de sampler con nombre y mypy en CI; el self-update de la app está escrito pero sigue apagado hasta que pongas update.repo tú mismo.

Trampas

La lista honesta — cosas que de verdad mordieron, en orden aproximado del tiempo que costaron:

  • Primero hay que cerrar LM Studio en el 1234. Ambos no pueden ocupar el puerto; el preflight nombra al ocupante en lugar de imprimir un traceback de bind, y server.port lo mueve. La biblioteca sí se puede compartir.
  • Dos flags de llama.cpp no significan lo que parecen. --ctx-size es el presupuesto entre todos los slots, no por slot; --fit por defecto está on en upstream, junto a --n-gpu-layers auto — ambos arriba, bajo el planificador.
  • Los modelos de razonamiento devuelven una respuesta vacía bajo --reasoning-format auto. Mismo prompt, solo cambió el flag: content 0 caracteres y reasoning_content 316 bajo auto; content 323 bajo none. reasoning_content no está en el esquema de OpenAI, así que un cliente estándar lee una cadena vacía y concluye que el modelo no dijo nada. Yo ejecuto el 31B en deepseek porque mis clientes leen ese campo; todo lo demás recibe none.
  • Las prereleases vX.Y.Z de llama.cpp no llevan asset CUDA de Windows. Una etiquetada v0.1.2 quedó por encima de dos builds bNNNN normales sin ningún archivo precompilado, así que la pestaña Server ofrecía una actualización detrás de un botón que solo podía fallar. Ahora los tags se filtran a ^b\d+$.
  • Nunca hagas tree-kill de la raíz de la bandeja con una pestaña del navegador abierta en el panel. Bajo un stub de lanzador de venv, el vigilante es el nieto del servidor, y un reinicio una vez terminó con el servidor muerto, el vigilante muerto y nada lanzado. Usa el menú de la bandeja, el panel, o sfctl recover --restart.
  • pkill -f llama-server desde otra herramienta mata tus backends. bench-llm hace exactamente eso entre ejecuciones — arriba, bajo los clientes del arnés.
  • El PIN MCP no es una API key, y un servidor sin clave sigue necesitando una credencial de relleno en algunos clientes — arriba, bajo dsh.
  • La clave MCP de OpenClaw es mcp.servers, anidada bajo mcp. El mapa plano mcpServers no es una clave que conozca su esquema — arriba, bajo OpenClaw.
  • Dos atajos de medición te mentirán. Repetir un prompt en un benchmark especulativo cronometra la caché de prompt (+751% frente a +0,4%), y una fórmula de KV de capas × cabezas × contexto se equivoca en 4× en una Qwen3.5 — ambos arriba, bajo el planificador. /props también informa speculative.types: "none" mientras está redactando, así que lee timings.draft_n de una finalización real.
  • Los modelos de visión no obtienen beneficio de caché de prompt. llama.cpp desactiva por sí mismo la reutilización de caché para los modelos multimodales, y cada imagen se presupuesta en 1.024 tokens salvo que los metadatos del mmproj digan otra cosa — suficiente para que una ventana de 8k sea sobre todo imágenes.

Consíguelo

El zip de abajo es todo el conjunto: el árbol de código fuente etiquetado, los tests, los docs, los lanzadores, las unidades systemd, más una carpeta dist/ con ambos wheels y el sdist para que puedas instalar sin un paso de compilación. studioforge-2026-08.zip — release v1.26-08-23, 4.238.728 bytes (4,04 MiB), SHA-256:

84f4f828b5c75206236890f28e8651c96146a7bb39c14e21b13a0922a13f7d3f  studioforge-2026-08.zip

226 entradas bajo un único directorio de nivel superior, construido con git archive desde el tag anotado v1.26-08-23 en el commit 0610446, así que solo puede contener archivos rastreados — sin config.yaml, sin data/, sin sobrescrituras locales. El código fuente también está en github.com/LaserLloyd/StudioForge. Necesita Python 3.12+, uv, un driver NVIDIA de la serie 580 en adelante y una carpeta de GGUF. Licencia: aún no elegida, así que formalmente todos los derechos están reservados — en la práctica, trátalo como el resto de las descargas de este sitio: gratis para uso personal, y si lo quieres comercialmente, pregúntame.

Si algo se rompe, escríbeme — la dirección está en la página Acerca de — y envíame la forma del fallo en lugar de tu configuración: el error.code, las cifras de un 507, las últimas veinte líneas de logs/models/<model>.log. Nunca el PIN ni la clave. Y si construyes el planificador de bin-packing conjunto, los presets de sampler con nombre, o una ruta AMD que funcione de verdad antes que yo, preferiré fusionar la tuya a escribir la mía.

Dónde me deja esto

Lo que no esperaba era cuánto de esto resultó ser medición más que código. El planificador es aritmética que cualquiera podría escribir; lo que lo hizo fiable fue leer la propia geometría de KV de llama.cpp en lugar de una fórmula, y luego medir el codo de slots en 2 cuando el estimador decía 8. Casi cada decisión de ese registro empezó como un número que contradecía una creencia. Si ya ejecutas LM Studio en 1234, todo el experimento es un clon, un archivo batch, y apuntar models.dir a la carpeta que ya tienes — y si no se gana su puesto en una tarde, tu configuración anterior queda intacta.

Relacionado: DeepSeek Harness (dsh) (donde aparece por primera vez este servidor, como un bloque de proveedor sin explicar), bench-llm (de donde salen las cifras de tokens por segundo del equipo, y la herramienta que matará tus backends), Mi configuración de OpenClaw (el artículo que nombró el problema que esto resuelve), y DisPatch (la app de chat que hay delante).

Descargas

Gratis para uso personal. Si te ahorra una tarde, el botón del café está aquí cerca.


← Más de IA y LLM Local