StudioForge: um servidor de LLM somente GPU que substituiu o LM Studio no meu rig, operado remotamente pelos meus agentes

Publicado em
23 agosto 2026
Por
Jacob Lloyd — escrito com ajuda de IA, depois do projeto
Tempo de leitura
60 min de leitura

Em termos simples: Este é um programa que roda no computador que tem as placas de vídeo. Os aplicativos pedem a ele uma resposta de um modelo de IA, e ele descobre em quais placas aquele modelo cabe, o inicia, o mantém aquecido enquanto está em uso e o desliga de novo quando fica em silêncio. A regra que ele nunca quebra é que um modelo ou cabe inteiramente nas placas de vídeo ou é recusado — ele não roda silenciosamente metade dele no processador lento e deixa você se perguntando por que tudo ficou arrastado. O porém: ele precisa de placas NVIDIA, é melhor testado no Windows, e eu ainda não escolhi uma licença para ele.

O StudioForge é o servidor de modelos local que escrevi para substituir o LM Studio no meu rig com GPU: um supervisor do llama.cpp com uma API compatível com OpenAI na porta 1234 — a porta do LM Studio, de propósito — um painel de controle no navegador, um sidecar de recuperação que responde quando o processo principal não responde, e um plano de gerenciamento publicado via MCP para que um agente em outra máquina possa operá-lo sem shell.

Ele existe por causa de um registro de incidentes que deixou de ser engraçado. Um 200 que não provava nada: o LM Studio responde caminhos não roteados com um status de sucesso e um corpo de erro, com o próprio log dizendo Returning 200 anyway. O /v1/models listando tudo que foi baixado em vez do que foi carregado, então "o que está realmente residente?" não tinha resposta. Modelos de raciocínio estourando uma janela de 8.192 tokens, porque o contexto que eu definia na configuração do cliente não era o contexto que o carregamento usava — no LM Studio a janela é fixada no momento do carregamento, e nada me avisava. E o caro: um modelo que não cabia era reduzido em vez de recusado — o comportamento documentado é que ele "vai reduzir automaticamente o tamanho do offload de GPU … e o restante na RAM do sistema", o que troca velocidade de GPU por velocidade de RAM do sistema e reporta sucesso de qualquer jeito.

Se você leu o artigo sobre o dsh, já conheceu este servidor: o bloco de provedor rotulado como "StudioForge (GPU rig)" é isto. É a ele que a stack de oito agentes recorre pela rede para trabalho pesado, com quem o DisPatch conversa, e o que o OpenClaw Email pede um modelo de chat e um modelo de embeddings. Meu artigo sobre o OpenClaw nomeou o problema — "minutos de tempo de carregamento de modelo, malabarismo de VRAM entre serviços" — e o que vem a seguir é a resposta, com cada regra carregando a medição que a forçou.

tl;dr

  • O que é: um servidor de LLM somente GPU, compatível com OpenAI, sobre o llama-server do llama.cpp, medido no build b10425 (CUDA 13.3). Um gateway em 1234, um painel de controle em 8080, um watchdog em 1235, um processo filho por modelo carregado em 18100–18200.
  • O que ele faz: carrega um modelo no primeiro uso, planejando seu contexto, tipo de cache KV, colocação de GPU e número de slots contra a VRAM livre naquele instante; o descarrega por inatividade; mantém modelos fixados residentes; entrega placas inteiras a um único modelo quando solicitado; e publica 29 ferramentas via MCP — 19 de gerenciamento, 10 de recuperação.
  • O que ele nunca faz: transbordar para a CPU (um modelo cabe inteiramente na VRAM ou é recusado com os números), ligar para casa (as únicas chamadas de saída são Hugging Face para modelos, GitHub para o build fixado do llama-server e sua verificação de atualização, a verificação de lançamento do StudioForge que é opcional, e URLs de imagem que uma requisição nomeia), ou rodar inferência via MCP — o plano de controle não tem ferramenta de completion e diz isso em maiúsculas.
  • O que você precisa: GPUs NVIDIA em um driver da série 580 ou mais recente, Python 3.12+, uv, uma pasta de GGUFs. Nunca rodou um modelo localmente? Comece aqui.
  • Com o que você fica: uma base URL que todo cliente OpenAI na sua rede usa sem alteração, um painel que nomeia o que ocupa cada gigabyte em cada placa, e um assistente que pode dizer "carregue o 27B em 128k nas duas 5090s" e ver isso acontecer.
  • A parte honesta: o Windows é a plataforma de referência, somente NVIDIA, e ainda não há arquivo de licença — leia Como obter primeiro.

O que ele faz, em uma imagem

O que ele não é:

  • Não é um modelo. Ele roda os GGUFs que você tem e busca mais na mesma pasta e layout que o LM Studio usa.
  • Não é um motor de inferência. O llama.cpp faz a matemática; isto decide qual processo roda com quais flags em quais placas.
  • Não é um app de chat. Existe uma aba Chat, e ela existe para provar que o caminho real de requisição funciona.
  • Não é um cluster. Uma máquina, suas próprias placas. O backend RPC do llama.cpp existe e não está conectado.
  • Não é um servidor de inferência em CPU. Não há nenhum valor de --n-gpu-layers além de 999 em lugar algum da base de código.
  • Não é uma segunda superfície de API. Sem /api/generate do Ollama, sem API do KoboldCpp — a superfície OpenAI, um espelho /api/v0 com sabor de LM Studio e o REST de gerenciamento /api, e é só isso.

O que roda onde

PeçaOndePara que serve
Gatewayo host de GPU, um processo, 1234/v1, /mcp (19 ferramentas), /api. Mantém o registro, o planejador e o supervisor.
Painel de controlemesmo processo, segundo uvicorn, 8080Dashboard, Setup, Models, Download, Chat, Server, Logs.
Watchdogum processo separado, 1235Seu próprio servidor MCP, 10 ferramentas de recuperação. Ele sobrevive ao que supervisiona.
Filhos llama-server18100–18200, somente loopbackUm por modelo carregado; uma falha derruba um modelo, nunca o gateway.
Companion sfctla máquina do agenteUm cliente HTTP puro — Python 3.11+, sem CUDA, sem dependência de servidor. Também a ponte stdio MCP.
Biblioteca GGUFmodels.dir, onde já estáIndexada no lugar; nada copiado. O LM Studio continua usando a mesma pasta.

O que a tabela diz:

  • Só o host de GPU instala alguma coisa. Um cliente precisa de uma base URL; um agente precisa de um pacote Python pequeno, e somente para as ferramentas de gerenciamento.
  • Os processos filhos são invisíveis de fora. Eles se vinculam a 127.0.0.1 e nada mais, então o gateway é a única superfície pública — o que faz de uma única chave de API uma fronteira real.

Como uma requisição flui

Tudo que um cliente pode errar é verificado antes do primeiro byte, porque uma requisição inválida deve receber um 4xx real (um 404 para um id de modelo que não existe) com um corpo JSON, em vez de um frame de erro enterrado dentro de um stream SSE 200 — clientes lidam bem com o primeiro e rotineiramente lidam mal com o segundo. O que o GET /health diz no meu 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}
  • O can_serve é a resposta ao problema do 200-que-não-prova-nada. Ele é false enquanto a primeira varredura da biblioteca roda, enquanto o status permanece ok porque o processo está vivo e é isso que um verificador de liveness pergunta. O GET /health?deep=true roda uma completion real de 8 tokens (uma chamada de embeddings para modelos de embeddings) contra cada modelo carregado — e sem nada carregado ele responde no_models_loaded em vez de passar, porque uma sonda que não pode falhar é pior do que nenhuma sonda.
  • O alias local-model resolve. local-model, default, auto e current todos mapeiam para models.default_model. Clientes do LM Studio caem para essa string literal, então retornar 404 para ela os quebra à toa.
  • Um modelo frio não parece um travamento. O stream aberto carrega : loading <model id> (5s) a cada cinco segundos, e então : prefilling <model id> (Ns) até o primeiro token real — linhas de comentário SSE que todo parser ignora. Um socket em silêncio por tanto tempo dispara um read timeout, e um cliente que faz retry empilha mais prefill sobre um lote saturado.
  • Um carregamento por vez, na máquina inteira. Dois modelos frios uma vez foram planejados simultaneamente para as mesmas placas; um morreu com CUDA error: out of memory, seu retry despejou o outro modelo novo, e a requisição daquele cliente caiu em um processo filho morto.
  • Um ttl no nível da requisição move o temporizador de inatividade e nada mais. ttl: 0 é a forma no wire de fixado em todo lugar aqui, então ele é ignorado em vez de honrado — um cliente enviando {"ttl": 60} costumava desfixar o que o dono dele tinha fixado.

A verdade sobre a VRAM: o planejador

Esta é a parte que ninguém mais faz, então aqui está ela em detalhe em vez de adjetivos. O core/planner.py tem 3.188 linhas respondendo a uma pergunta antes de um modelo iniciar: dada a VRAM genuinamente livre em cada placa agora, qual é a melhor janela, qualidade de cache e número de slots que este modelo pode ter — e se a resposta for nenhuma, o que devo lhe dizer?

A escada, e o que ela se recusa a trocar

Os degraus desenhados ali são ilustrativos, não os padrões enviados: o mecanismo é exato, os números são um exemplo. O que o config.example.yaml envia é target_ctx: 1048576 como alvo e default_ctx: 8192 como piso, com o alvo limitado primeiro à janela treinada do modelo — embora na primeira execução o tune_for_hardware escreva default_ctx: 16384 quando a menor placa tem 24 GiB ou mais, e 8192 de 12 GiB para cima; meu rig roda com um piso de 128000. Passar de uma janela treinada exige escala RoPE e degrada a qualidade, então um nível acima dela nunca é oferecido. Um ctx_size explícito é uma escada de um degrau.

Duas passadas, a segunda somente quando a primeira falhou em todo lugar. O motivo é um carregamento às 12:03 numa tarde: 79.832 MB livres, mais 19.423 MB recuperáveis de um modelo ocioso. Com esse orçamento, 262144 com cache q4_0 cabia em 96.004 MB, e 65536 com f16 completo cabia em 95.236 MB. O que carregou foi 8192/f16 em 89.860 MB — o modelo pagou o preço total da despejo e recebeu a menor janela da escada.

KV não é um número por modelo

Quase toda calculadora de VRAM online calcula o KV como camadas × cabeças × dim-de-cabeça × 2 × bytes × contexto. Certo para um Llama, muito errado para duas famílias que as pessoas realmente rodam: o Gemma 3 e 4 intercalam cinco camadas de janela deslizante para cada uma completa, com metade da dimensão de cabeça, e o Qwen3.5, 3.6 e 3.8 declaram full_attention_interval = 4, então um cache KV existe a cada quarta camada e o resto são camadas recorrentes Gated-DeltaNet com um estado fixo por sequência.

O custo de errar isso: um Gemma-4 31B solicitado com 262.144 tokens foi estimado em 480 GiB de KV e limitado a 65.536, enquanto o log de calibração registrava predicted_mb=95615 actual_mb=40037 por semanas. Depois da correção de geometria, o previsto e o real caem ambos em 38 GiB em duas 5090s com n_ctx=262144 — um destravamento de 4× que levou junto toda a frota Gemma-4. Cobrar KV de todas as camadas de um Qwen3.5 era o mesmo bug com outro chapéu: uma sobrecarga direta de 4×.

Dois detalhes que valem roubar. A contagem de células de janela deslizante precisa espelhar o llama.cpp exatamente; um multiplicador fixo de 1,25× estava errado na direção perigosa, 3,6× abaixo em quatro slots. E o attention_kind é derivado da geometria das camadas em vez de general.architecture — onde não pode ser derivado, ele reporta unknown, que significa "desconfie de todo número de KV aqui", nunca "suponha o caso barato".

A qualidade do cache é escolhida dentro de cada degrau em vez de trocada por uma janela mais larga: f16/f16 → q8_0/q8_0 → q8_0 K + q4_0 V, com o q4_0 simétrico removido de todo caminho automático. Não é gosto — com um cache q4_0 na chave K, o Qwen2.5-7B reproduz apenas 11,7% dos tokens que seu eu f16 produz, enquanto um par q8_0/q8_0 casado fica em divergência KL de 0,0018.

Quatro placas, duas gerações

O rig tem duas RTX 5090 e duas RTX 3090 — nominalmente placas de 32 GB e 24 GB, que o painel conta como 31,84 GiB e 24,0 GiB, 111,7 GiB no total — em driver 610.88, driver CUDA 13.3. Placa única primeiro, sempre — um modelo dividido em PCIe sem NVLink é significativamente mais lento — e o planejador só anula isso quando a colocação em placa única fica limitada a um slot, a divisão pelo menos o dobra, toda placa adicionada é pelo menos tão capaz e você deixou o número de slots no automático. Essa última condição não é gentileza: uma divisão roda no ritmo do membro mais lento.

Colocação (1.5B Q4_K_M, duas 3090s, ctx 8k)GeraçãoProcessamento de prompt
Uma 3090352,5 tok/s2.803,6 tok/s
Duas, -sm layer344,4 tok/s2.722,5 tok/s
Duas, -sm tensor294,3 tok/s1.182,0 tok/s
Duas, -sm rowfalha: error loading model: device CUDA2 does not support split buffers

O que a tabela diz:

  • Uma placa venceu duas nos dois eixos. A divisão por camadas custa cerca de 2% da geração; a divisão por tensor custa 17% da geração e 58% do processamento de prompt — então o modo tensor é opcional, e só uma medição pode escolhê-lo.
  • O -sm row está morto no CUDA. O parser o aceita e o carregamento então falha, então ele é bloqueado antes de o processo filho ser gerado, não depois.

Dois detalhes de colocação só aparecem quando você mede por placa. A camada de saída é cobrada do último dispositivo, porque os quantizadores mantêm os tensores de embedding e saída em Q6_K ou Q8_0 mesmo dentro de um arquivo Q4: um 27B planejado como --device CUDA1,CUDA0 --tensor-split 0.5079,0.4921 pousou 15,52 GiB na CUDA0 — o último dispositivo, aquele que a divisão deu menos — contra 14,48 da CUDA1. E o llama.cpp abre um contexto CUDA em todo dispositivo visível — ~0,22 GiB em uma 3090, 0,43 GiB em uma 5090 — por isso a coluna de colocação tem um piso de 512 MiB.

Quantas conversas uma colocação vale

O --ctx-size do llama.cpp é o orçamento total de KV compartilhado entre os slots, não a janela por slot — uma flag amplamente mal interpretada, e o README do upstream não explica isso. Um carregamento com --ctx-size 4096 e sem --parallel reporta total_slots: 4: 1.024 tokens por conversa. O StudioForge inicia com ctx_per_slot × parallel. Então: quantos slots valem a pena, que é onde eu parei de confiar na minha própria aritmética.

ConcorrentesPor streamAgregadop50p95Lote alcançado
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, uma RTX 3090, 8.192 tokens por slot, KV f16, oito slots iniciados, prompts de 512 tokens, 192 tokens gerados cada.

O que a tabela diz:

  • O estimador disse 8. A medição disse 2. Em quatro slots cada stream cai para 44% da velocidade solo, abaixo de um piso de 65%; a regra pega o maior de 1/2/4/8 que fica acima desse piso e ainda ganha 15% de agregado sobre o nível abaixo.
  • O agregado nunca para de subir — oito slots movem 1,9× os tokens de um — enquanto uma única conversa desaba para 27%. Uma regra que maximiza o agregado escolheria 8, e todo usuário experimentaria um modelo três vezes mais lento do que a placa consegue rodar.
  • O batching é real, não enfileiramento. O lote alcançado subindo de 1,00 → 1,84 → 3,46 → 6,03 prova passos de decodificação compartilhados; reproduzido em três execuções com variação de 2%.

Uma segunda execução em duas 3090s com 32.768 por slot foi de 301,7 → 230,5 por stream e respondeu 2 de novo. Aliás, o catálogo havia previsto 308,0 tok/s para essa colocação e a execução mediu 301,7 — 2% de diferença, melhor do que eu esperava. As linhas agora carregam max_parallel (quantos cabem) ao lado de recommended_parallel (quantos valem rodar).

Duas flags que medi antes de confiar

A decodificação especulativa é uma vitória de stream único. Qwen3.8-27B Q5_K_S com cabeça MTP, uma 3090, quatro prompts distintos de 256 tokens, cache de prompt desligado: sem especulação 37,75 tok/s; draft-mtp em profundidade 3 deu 50,70 tok/s, +34,3%, com aceitação de 0,528. A profundidade 4 cai para 47,48, porque a aceitação cai para 0,446 e todo token rejeitado extra foi verificado à toa. O ngram-mod conseguiu +0,4% e não emitiu nenhum rascunho.

O que traz a armadilha: o mesmo prompt três vezes mediu +751% naquele 27B. Repita um prompt e você está medindo o cache de prompt e chamando isso de rascunho. Acima de quatro slots, o auto agora retorna none e diz o porquê — "a especulação é uma vitória de stream único e prejudica um lote saturado" — depois que uma execução carregou um 27B com --parallel 8 e o auto ainda escolheu draft-mtp, vendo a cabeça MTP e não o número de slots.

O micro-batch compra prefill com VRAM. O mesmo 1.5B, um prompt de 5.166 tokens: -ub 512 (o padrão do motor) deu 15.232 tok/s em 1.492 MiB; -ub 1024, 17.307 tok/s (+13,6%) em 1.562 MiB; -ub 2048, 18.061 tok/s (+18,6%) em 1.702 MiB. Ele ficou desligado por muito tempo porque o buffer de computação cresce com -ub, o planejador não o modelava, e um buffer não modelado transforma um encaixe em um out-of-memory. O planejador o cobra agora, arredondado para cima de modo a errar em direção a recusar, e aumenta o micro-batch automaticamente apenas acima de quatro slots.

A recusa, com os números

Toda execução passa --fit off e --n-gpu-layers 999, e o segundo é uma constante, não uma configuração. Isso importa mais do que antes: o build fixado b10425 envia -fit, --fit [on|off]"se deve ajustar argumentos não definidos para caber na memória do dispositivo"padrão on, ao lado de --n-gpu-layers auto; ambos chegaram ao upstream no PR #16653 em dezembro de 2025. Esse par é precisamente um caminho silencioso de offload parcial: um padrão razoável para um servidor de uso geral, e exatamente o comportamento que este projeto existe para recusar. Quando nada cabe, a resposta é HTTP 507 com a aritmética no corpo, aparado aqui:

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

Essa é uma recusa real, capturada enquanto eu escrevia isto: o 31B pediu para carregar em uma única RTX 3090. O texto nomeia a falta e os detentores; o error.studioforge carrega a mesma falha como dados — os bytes necessários e disponíveis, VRAM livre por placa, a estimativa dividida em pesos, KV, buffer de computação, modelo de projeção e contexto CUDA, todo processo segurando VRAM, e notes dizendo em qual degrau da escada ele estava quando desistiu. Quando uma janela menor caberia, o max_ctx_that_fits a nomeia, calculado na geometria por camada para que a oferta seja uma que o próximo carregamento aceite; aqui nem o piso coube, então é null em vez de um número que falharia. Uma recusa que não é sobre um modelo ocupado não carrega retry_after_s, porque "tente de novo mais tarde" é um conselho ruim quando nada vai mudar.

Fixações, TTLs, locações e o rebalanceador

Uma fixação é um estado desejado, não uma isenção. Ela costumava significar TTL zero, exclusão de toda escada de despejo e um aquecimento na inicialização — e não a quarta coisa: um modelo fixado nunca era recarregado, então um processo filho que entrasse em loop de crash além do max_restarts por modelo ficava em state="failed" sem segurar nada. Um reconciliador agora acompanha a varredura de 15 segundos, recuando de 60 s até um teto de 900 s. A única coisa que vence uma fixação é uma pessoa: uma descarga explícita marca o id como suprimido e ele permanece desligado.

Uma locação é uma placa pertencente a um modelo. Uma placa locada está ausente da visão de GPU de todos os outros modelos — não classificada por último, não é uma opção — e o dono é forçado exatamente àquelas placas, dimensionado pelo estimador, no modo de divisão que o próprio benchmark dele mediu como mais rápido ali. Uma placa já locada é um conflito 409, nunca uma tomada. Uma locação sem modelo segura placas para algo fora do servidor: reserve_gpus(devices=[3], reason="ComfyUI render") é o motivo de minha geração de imagens e meus modelos de linguagem terem parado de brigar.

O rebalanceador corrige a boa decisão de ontem. Às 13:42, um 27B foi planejado para as placas [1, 3] — uma divisão entre camadas compartilhando a GPU1 — porque um 31B segurava [1, 0, 2] e um app de geração de imagens segurava 7,5 GiB da GPU2. Às 13:53, o 31B encolheu para [0, 1]; a partir daí [2, 3] ficou livre e estritamente melhor, e o 27B permaneceu onde estava. Então um modelo ocioso agora é movido quando um plano sem despejo em suas configurações atuais exatas o coloca fora de toda placa compartilhada. Tokens por segundo estimados nunca justificam uma mudança.

Ele olha uma vez por minuto, e só quando o mundo mudou: só em uma máquina tranquila, só para um modelo ocioso há cinco minutos, uma mudança por modelo a cada 30 minutos — porque uma realocação é um recarregamento e um recarregamento descarta o cache de prompt, e na carga de trabalho de conversa longa deste rig aquele cache era 93% de um prompt de 98 mil tokens. A despejo tem três regras rígidas além disso — nunca um modelo fixado, nunca um no meio de uma requisição, nunca uma instância carregando — e uma placa locada não está na visão do planejador, para começar. Um carregamento just-in-time nunca pode definir force.

Quando as coisas quebram

A VRAM morre com o processo que a tomou. No Windows, os filhos vivem em um job object anônimo criado com JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, então o kernel mata cada membro quando o último handle fecha — não importa como o pai termine. Anônimo, porque um job nomeado seria compartilhado com qualquer coisa que adivinhasse o nome. O Linux ganha um shim PR_SET_PDEATHSIG, que cobre um kill -9 do gateway; é um melhor esforço em vez de uma garantia do kernel, então a varredura de inicialização e o reclaim_orphan_engines pegam o que escapa.

Por isso existe uma varredura de inicialização. Em 18 de agosto de 2026, ~10 GiB na GPU0 e ~15,6 GiB na GPU1 estavam indisponíveis com "tudo parado". Os detentores eram três filhos llama-server.exe de uma execução python -m pytest tests -q que um agente de codificação havia iniciado e que desde então saiu. Todo detentor agora é classificado — ours, child-of-live-process, orphan, other-instance, foreign — e apenas orphan é morto, seguro por construção porque nada mais inicia binários da nossa árvore de motores.

Nomear quem segura o quê levou duas tentativas. O NVML reporta zero de memória usada por processo no Windows, então os tamanhos vêm do contador que a coluna "Dedicated GPU memory" do Gerenciador de Tarefas lê — mas isso é um total por processo entre adaptadores, então a coluna de dispositivo estava errada: um processo reportado em CUDA0,1,2,3 na verdade segurava 15,52 GiB na CUDA0 e nada nas 3090s. A correção une o LUID do adaptador a um endereço de barramento PCI, e uma armadilha ganha o parágrafo: o número do barramento no busId do NVML é hexadecimal. "00000000:42:00.0" é barramento 66, não 42 — uma resposta errada e confiante, indistinguível de uma certa.

Uma descarga é verificada, não anunciada. O supervisor recheca o pid com uma proteção de tempo de criação contra reuso de pid, escala um sobrevivente para um tree-kill forçado e registra a VRAM antes e depois; um sobrevivente levanta um 500 dizendo para você matá-lo manualmente. Uma descarga que reporta sucesso enquanto o processo permanece residente é a mentira mais cara que este sistema pode contar, porque todo carregamento posterior é então planejado contra uma VRAM que não está livre.

Códigos de saída são vocabulário. 2 é um erro de configuração nomeando a chave. 3 é um conflito de porta, e a bandeja nunca reergue na porta em conflito — ela espera o detentor responder /health como um servidor StudioForge e se anexa. 75 é "reinicialização solicitada": o servidor drena, define o código e encerra graciosamente — 1,0 s da solicitação à saída, medido — e a bandeja reergue sem gastar uma tentativa de crash. Essa distinção existe porque um reinício via GUI uma vez produziu dois servidores disputando o 1234, e três crashes contados depois a bandeja ficou em Crashed — see the logs folder ao lado de um servidor saudável que ela não conseguia mais parar.

Quando o gateway está travado em vez de morto, você fala com o watchdog: um processo separado sempre ativo em 1235, construído com argparse e logging da stdlib, então ele inicia mesmo quando o config.yaml é a coisa quebrada. Suas dez ferramentas são health, get_config, set_config, restart_server, kill_model, nuke_all_models, reclaim_orphan_engines, tail_logs, gpu_status e rollback_update. Ele reimplementa a regra de órfão localmente em vez de importar o módulo que a possui — o processo de recuperação não deve importar a stack que ele conserta.

O painel de controle

O painel em 8080 é um segundo servidor uvicorn dentro do mesmo processo, compartilhando o grafo de objetos do gateway por referência — não há URLs absolutas nele, o que o faz funcionar de forma idêntica sobre HTTP puro em uma VPN mesh e atrás de um front end HTTPS.

O Dashboard do StudioForge no meio de uma requisição: um cabeçalho lendo 2 carregados, 4 GPUs, 45,5 de 111,7 GiB livres; quatro placas de GPU — duas RTX 5090, duas RTX 3090 — com números de usado, livre e utilização; uma lista de detentores de VRAM com dois processos llama-server.exe marcados como ours e cinco foreign; um painel de locações de GPU vazio; e dois cartões de modelo carregado, um em prefill e um fixado e ocioso
O dashboard no meio de uma requisição: um 31B dividido entre as duas 5090s fazendo prefill de um prompt de 2.700 tokens, um 26B fixado em uma 3090, e todo processo segurando VRAM em qualquer placa nomeado com sua fatia. O painel de detentores existe porque 25 GiB uma vez sumiram.
Um close dos dois cartões de modelo carregado: o 31B mostra porta 18100, ctx real 262144, 1 ocupado / 0 ocioso, GPU0, GPU1 (layer), TTL 15m 00s, requisições ativas 1, total 3, último 58,77 tok/s, build b10425, e uma linha de slot lendo Processing prompt 2202/2742 (80%), cache hit 7/2742; o 26B mostra porta 18101, GPU2, TTL fixado, último 46,67 tok/s, slot 0 Idle
Os mesmos dois cartões, recortados do frame acima: a porta que cada modelo realmente recebeu, o contexto que realmente obteve, as placas em que realmente pousou, a contagem regressiva de TTL — e então uma linha por slot do motor. "Real" é lido de volta do processo filho, não do que pedimos.
A aba Setup: uma checklist encabeçada por Ready to serve, but fix before exposing it: Network exposure, com linhas verdes para o diretório de dados, 50 arquivos GGUF em E:\LLM\Models, 34 modelos indexados, 4 GPUs no driver 610.88 e CUDA 13.3, motor b10425 smoke-testado e o gateway em 0.0.0.0:1234, uma linha âmbar de Network exposure com um botão Set API key; depois a seção Model library e os campos Defaults for every load — piso de contexto 128000, alvo 1048576, piso de modelo de raciocínio 32768
Uma instalação nova abre aqui, e "nova" é medida em vez de lembrada — sem diretório de modelos, nada indexado, ou sem motor. Toda linha não atendida carrega o botão que a corrige; no meu rig, a única linha âmbar é o gateway escutando em toda interface sem chave de API, o que é uma escolha, não um acidente.
A tabela da biblioteca na aba Models: colunas para Name, Downloaded, Size, Quant, Arch, Features, Last used, Status e Actions; onze linhas de dois builds Qwen3.8-27B em 21,29 e 17,95 GiB até o Qwen2.5-0.5B em 0,49 GiB, com ícones de recurso para chat, visão e embeddings; a linha gemma-4-26B carrega um selo pinned e um chip verde loaded; seis botões pequenos de ação terminam cada linha
Uma tabela em vez de cartões, porque com algumas dezenas de modelos as perguntas são comparativas — qual chegou por último, qual é o grande, qual consegue ver imagens. A ordenação padrão é o download mais recente primeiro, e os chips de status à direita são a verdade viva sobre quais deles estão residentes.
O diálogo de configurações por modelo para gemma-4-31B-it-QAT-Q4_0 (16,44 GiB, ctx treinado 262144): um bloco Optimal settings com uma linha por conjunto de GPUs — duas RTX 5090 em ctx 262144, q8_0, 3 slots, cerca de 62 tok/s; duas RTX 3090 em 1 slot, cerca de 34 tok/s; as quatro GPUs em 3 slots, cerca de 40 tok/s, marcadas fits now; uma RTX 5090 em ctx 131072, cerca de 66 tok/s — cada uma com botões Load here e Measure parallel; uma linha Load at exactly com 64K, 128K, 256K e 512K; um veredito verde de encaixe lendo Fits across GPU1, GPU0, GPU3 (split: layer) com cache KV q8_0, flash-attn ligado e VRAM projetada de 32,55 GiB detalhada por placa; e os campos Basic — comprimento de contexto, tipo de KV K e V, TTL, um checkbox Pinned, modelo de rascunho e device override
Este diálogo é o argumento inteiro em uma tela: o que o modelo poderia fazer em cada conjunto de placas se elas estivessem livres, um veredito de encaixe que é reexecutado a cada mudança com a VRAM projetada por placa, e então cada botão para anulá-lo (os níveis Advanced e Expert continuam abaixo do recorte). Tamanhos de contexto inalcançáveis são esmaecidos com o motivo em vez de ocultos — "por que este modelo não consegue 512k" é a pergunta que o botão existe para responder.
A aba Download após buscar por gemma-4 no Hugging Face, ordenada por Downloads (30d): doze repositórios de unsloth, lmstudio-community e google, cada linha mostrando o publicador, uma contagem de downloads de 1.187.348 até 458.712, curtidas, o número de quantizações e quando foi atualizado, com um link de model card e um botão Quants
A busca do Hugging Face e a fila de download em uma aba. O rótulo de ordenação diz "Downloads (30d)" de propósito — esse número é uma contagem móvel de trinta dias, não um total de todos os tempos — e o botão Quants é onde a matemática de encaixe começa.
O topo do seletor de quants para unsloth/Qwen3.8-27B-GGUF: uma nota de cabeçalho dizendo que o encaixe é uma estimativa só de pesos até o cabeçalho do modelo ser lido, uma linha de geometria lendo attention hybrid, 65 layers, KV 65 KB per token, e então dezesseis linhas de BF16 em 51,77 GiB com um chip âmbar needs-multiple-GPUs até Q5_K_M em 19,28 GiB, cada uma com um chip verde fits-one-GPU, uma linha de contexto por colocação para uma 5090, duas 5090s e as quatro placas, e um botão Download
Antes de qualquer download, cada linha é verificada contra a VRAM que está realmente livre agora: um chip de encaixe por quant, e abaixo dele o contexto que cada colocação teria — uma 5090, duas, ou as quatro placas. O seletor preenche duas vezes: tamanho do arquivo contra a VRAM livre, para aparecer na tela de imediato, e então uma leitura remota do cabeçalho do GGUF para que o selo se torne a resposta do próprio planejador. Nem sempre foi tão limpo: até uma correção feita enquanto eu escrevia isto, o seletor também listava o módulo de rascunho MTP do repositório e seu arquivo imatrix como se fossem quants do modelo. O diálogo continua abaixo do recorte — 25 linhas no total.
A aba Chat: um interruptor Use the loaded model resolvendo para gemma-4-31B-it-QAT-Q4_0, mais recentemente usado de 2 carregados; um seletor de modelo, temperature 0.7, top_p 0.95, max_tokens 1024 e um rótulo de taxa lendo 9,7 tok/s; uma caixa de system prompt; e uma transcrição com o prompt do usuário pedindo uma explicação em três frases do que é um cache KV e por que seu tamanho depende da janela de contexto, seguida da resposta concluída do modelo
Um harness de teste, não um app de chat: ele chama o mesmo caminho de carregamento e faz stream da mesma porta de processo filho que os endpoints OpenAI usam. O rótulo 9,7 tok/s é a aritmética desta aba — chunks em streaming sobre o tempo de relógio desde o momento em que a requisição saiu, prefill incluído — não o tempo de geração do motor, que é o 58,77 tok/s no cartão do modelo. Um chat bem-sucedido aqui é evidência de que um cliente vai funcionar, não um caminho simulado que pode divergir.
O topo da aba Server: uma faixa Ready to serve listando o diretório de modelos, 34 modelos indexados, 4 GPUs e o motor b10425; o painel Point OpenClaw at this server expandido para mostrar o par OPENAI_BASE_URL e OPENAI_API_KEY gerado, o JSON do servidor MCP com openclaw_cli, openclaw_config em mcp.servers e um bloco genérico mcpServers, e a configuração do companion; depois um bloco Health lendo version 1.26-08-23, uptime, o caminho de configuração e os dois modelos carregados
A aba Server escreve a configuração do cliente para você — o bloco exato que colei no OpenClaw, nas duas formas — e mostra as linhas de health abaixo; mais para baixo na mesma aba ficam a lista de motores e um relatório de quanto da biblioteca a máquina consegue realmente rodar. Como a entrada MCP invoca o sfctl, o PIN de pareamento nunca precisa aparecer em um arquivo de configuração.
A aba Logs: source StudioForge server, level INFO, Follow marcado, acompanhando um ring buffer em memória de 84 linhas — linhas repetidas de load planned de studioforge.core.planner lendo chosen ctx=262144 kv=q8_0 parallel=1, devices=[1, 0, 3] e estimate_mb=33331, depois unloading idle model at ttl_s=900 e model_unload_verified com vram_reclaimed_mb=37086
A aba Logs acompanha o ring buffer em memória: aqui o planejador anota cada decisão antes de agir — o contexto, o tipo de cache, o número de slots e os dispositivos que escolheu, e o que esperava que o carregamento custasse — depois uma descarga por inatividade e a VRAM que verificou de volta. Nada aqui é reconstruído depois; é o que o planejador pensou na hora.

Quatro linhas mais abaixo no mesmo buffer são as que eu leio quando um carregamento me surpreende — a linha de comando que ele construiu, o processo que respondeu, e o planejador corrigindo o próprio dever de casa (timestamps e nomes de logger aparados):

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'

Isso é o planejador se pegando em 3 GB em uma placa, 4 GB no par, e dizendo qual placa e por quê — a camada de saída pousou no último dispositivo, exatamente onde ele a cobra, e a cobrança ainda assim ficou curta.

O menu da área de notificação do Windows para o StudioForge: uma linha de cabeçalho lendo Running — 1 model loaded, 23,4 GiB free, depois Open control panel, Open API docs, Open logs folder, Open models folder, Unload all models (free VRAM), Restart engines, Start server esmaecido, Stop server, Restart server, Copy MCP URL, Copy MCP PIN, um Start at login marcado e Quit
No dia a dia, a coisa inteira é um ícone de bandeja. A linha de cabeçalho é o status completo — o que está carregado e quanto de VRAM resta — e o PIN é a única credencial que a bandeja coloca na área de transferência.

Usando-o como backend de harness

Seis clientes na minha rede falam com essa coisa e apenas um deles sabe que é o StudioForge. Essa é a intenção. Cinco deles estão desenhados abaixo; o OpenClaw Email é o sexto.

Qualquer cliente OpenAI

Duas variáveis de ambiente. server.api_key é null por padrão, então qualquer string não vazia funciona — a maioria dos clientes OpenAI se recusa a iniciar com uma vazia.

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

O GET /v1/models lista tudo que está baixado, no estilo do LM Studio, adicionando state e — quando residente — ctx_per_slot, max_parallel e parallel_limited_by, porque um comprimento de contexto sozinho é ambíguo quando um modelo roda mais de um slot. Os ids fazem round-trip: o id completo publisher/repo/file, um nome de arquivo simples, ou publisher/name, sem diferenciar maiúsculas. O DisPatch precisou só de uma nova base URL; o OpenClaw Email é o cliente mais exigente, querendo um modelo de chat e um modelo de embeddings e chamando /v1/models na inicialização para perguntar o que está realmente sendo servido.

OpenClaw, em outra máquina

A máquina do agente instala um pequeno wheel: sfctl, que tem como alvo o Python 3.11 em vez do 3.12 do servidor porque a máquina que roda o agente frequentemente fica atrás do rig, e que deliberadamente não depende do pacote do servidor — sem CUDA, sem planejador, sem registro.

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

Ou à mão — o detalhe que custa uma tarde às pessoas. A chave do OpenClaw é mcp.servers, aninhada sob mcp: o mapa plano mcpServers está certo para o Claude Code, o Cline e o LibreChat, e não é uma chave que o schema do OpenClaw conhece. A inferência é um caminho separado, sob models.providers — note baseUrl, com rl em minúsculas:

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

O que o agente ganha é uma lista mesclada de 29 ferramentas: as 19 do gateway mais as 10 do watchdog, três renomeadas como recovery_*get_config e set_config porque colidem com ferramentas do gateway, health por simetria. O restart_server mantém o nome simples, porque é o nome que a mensagem de erro de uma ferramenta de gerenciamento morta diz ao agente para chamar. Quando o servidor principal está fora, a ponte ainda anuncia todas as 19 ferramentas de gerenciamento com uma nota anexada — um agente que não consegue ver load_model não sabe que a capacidade existe. O loop que ele roda:

  1. list_models(limit=N) — o catálogo, download mais recente primeiro. Leia a linha recomendada.
  2. load_model(**row["load_args"]) — passe-o adiante sem alterar; um agente que escolheu uma linha terminou de escolher.
  3. load_recommended(model_id, ctx_size=N) quando o que você sabe é o contexto de que precisa — o único caminho de carregamento que recusa em vez de encolher.
  4. Inferência por HTTP, não MCP. Nomear um modelo não carregado o carrega, com padrões do planejador em vez da linha que você estava lendo.
  5. model_options(model_id) quando a linha recomendada não é suficiente: todo nível de contexto, com velocidades.
  6. search_modelsrepo_detailsdownload_model para obter algo novo.
  7. pin_model para o modelo que deve sempre responder; reserve_gpus / release_gpus para placas só dele.
  8. server_status e connection_info — o que está residente, quem segura VRAM, todo endereço em que responde.

E o parágrafo de que mais me orgulho, servido a todo cliente na conexão:

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

O dsh (DeepSeek Harness) troca de modelo editando um arquivo YAML, recarregado a quente para a próxima requisição. O bloco de provedor é 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>

O apiKeyEnv é uma referência, nunca um literal — e um servidor local sem chave ainda precisa de alguma credencial referenciada, porque o cliente compatível com OpenAI insiste em um bearer token. O Claude Code, por outro lado, não consegue rotear a própria inferência para cá de jeito nenhum: a referência de protocolo de gateway dele lista Anthropic Messages, Bedrock e Vertex, e nenhum é /v1/chat/completions. Então o StudioForge é a ferramenta dele, não o cérebro — claude mcp add studioforge -- sfctl mcp (o -- é obrigatório) entrega a ele todas as 29.

O bench-llm produziu os números do rig que as pessoas citam de volta para mim — Gemma 4 26B-A4B (QAT, Q4) a 229,0 tok/s, 110 ms até o primeiro token, medido através do LM Studio na época — e é um cliente OpenAI simples, então uma base URL é tudo de que precisa; mas ele roda pkill -f llama-server entre benchmarks, o que mata todo backend do StudioForge na máquina. O resto é uma linha cada: Open WebUI, OPENAI_API_BASE_URL; LibreChat, um endpoint custom com baseURL e models.fetch: true; aider, OPENAI_API_BASE e então --model openai/<id>; Continue, provider: openai mais apiBase. A chave da base URL é escrita de forma diferente em cada um deles, que é a fonte mais confiável de noites desperdiçadas neste ecossistema.

A partir do que um agente escolhe

O catálogo transforma a escolha de um modelo em uma consulta em vez de um chute: ordenado com o download mais recente primeiro, uma linha por nível de contexto, cada uma carregando fits contra a VRAM livre ao vivo, devices, tipos de KV, max_parallel, recommended_parallel, uma confidence, uma coluna if_gpus_idle e load_args. Uma linha é uma chamada de plano real, então ela não pode prometer o que um carregamento recusaria — e if_gpus_idle é a diferença entre um agente desistir e um agente chamar unload_model.

O repo_details é o que vale nomear: ele lê um cabeçalho de GGUF remotamente, por requisições HTTP Range — 2–15 MB, na maioria os arrays de strings com prefixo de comprimento do tokenizador, em cache no disco — em vez de baixar 20 GB para descobrir se cabe, e uma CDN que responde a uma requisição range com um 200 e o corpo inteiro é detectada e recusada. O que volta é uma matriz context_fit do mesmo planejador que um carregamento real usa:

Quant1× RTX 50902× RTX 5090As quatro placas
BF16 (51,8 GiB)— só os pesos não cabem32k em q8_0256k
Q8_0 (27,9 GiB)256k256k
Q5_K_M (19,3 GiB)128k em q8_0256k256k
IQ2_M (10,5 GiB)256k256k256k

Do guia OpenClaw do repositório, calculado neste rig, para unsloth/Qwen3.8-27B-GGUF. O max_ctx é a maior janela em um cache f16 de qualidade total; um número q8_0 aparece apenas onde ele alcança mais longe.

O que a tabela diz: a quantização decide a janela, não o número de placas — de BF16 para Q5_K_M transforma "não cabe de jeito nenhum" em 128k em uma placa — e é a resposta do próprio planejador, então onde um nível só é alcançável quantizando o cache, a matriz diz "em q8_0" em vez de contar como vitória. Para números reais, o roteiro tem três passos: um benchmark de colocação (cada modo de GPU sob a própria locação, throughput dos próprios tempos do llama-server), então benchmark_parallel no vencedor, e então reserve_gpus para fixar. Nunca faça benchmark de um modelo com o qual alguém está no meio de uma conversa.

Substituto direto do LM Studio

A compatibilidade foi a restrição de design, e o repositório lista o que foi emprestado para que ninguém precise adivinhar: a porta 1234; o /v1/models listando baixados em vez de carregados; carregamento just-in-time; TTL de inatividade; o ttl por requisição; o layout publisher/repo/ usado no lugar, então não há etapa de importação e os dois programas compartilham uma biblioteca; o espelho /api/v0/models; e o deep link lmstudio://open_from_hf. Migrar um cliente é uma mudança de host, não uma mudança de host e porta.

LM Studio 0.4.21StudioForge 1.26-08-23
Motorbuilds próprios do llama.cpp mais MLX na Applellama-server upstream, um build fixado (b10425, CUDA 13.3), smoke-testado antes da ativação
Caminhos não roteados200 com um corpo de erro — o log dele diz Returning 200 anyway404 com um envelope JSON, e JSON em todo status
Errosprosa não estruturada que os clientes casam com regexum error.code estável, diagnósticos em error.studioforge
Configuração de carregamentocontext_length ignorado em um dos dois caminhos de carregamento; repetition_penalty ignorado silenciosamenteum caminho de carregamento, todo campo honrado, valores efetivos ecoados; aliases de sampler aceitos
Quando não cabe"vai reduzir automaticamente o tamanho do offload de GPU … e o restante na RAM do sistema" — um transbordo silencioso para a CPU, por design507 insufficient_vram com bytes necessários e disponíveis, livre por GPU, o maior contexto que caberia, sugestões ordenadas
Multi-GPUdivisão por prioridade ou uniforme, toggles por GPU, tensor parallel desde a 0.4.15um planejador dimensionando contexto, tipo de KV e slots por colocação, inclinando as frações de divisão para a camada de saída
TTL de inatividade60 minutos; auto-evict mantém no máximo 1 modelo carregado JIT1.800 s enviados (15 min no meu rig), varridos a cada 15 s; quantos couberem, com fixações e locações decidindo quem fica
Gerenciamento remoto/api/v1 load/unload/download; LM Link em preview, a ser monetizado, descoberta via o hub do LM StudioREST /api, sfctl, 29 ferramentas MCP; o alcance é sua LAN ou sua própria VPN mesh
MCPsomente host — ele consome servidores MCPum servidor MCP para o próprio gerenciamento, mais um no watchdog
Fontefechada; gratuito para uso pessoal e empresarial internofonte no zip, e nenhuma licença escolhida ainda

Verificado contra o changelog, a documentação e o bug tracker do próprio LM Studio em 2026-08-23, versão 0.4.21 (lançada em 12 de agosto de 2026). A linha 2 está no bug tracker público dele; as linhas 3–4 são o que o meu próprio cliente teve que contornar na API 0.3.x. Não retestei nenhuma delas contra a 0.4.21, então leia-as como "documentado em algum momento", não "quebrado hoje".

O que a tabela diz: ele é um app de desktop melhor do que isto jamais será — GUI polida, MLX no Apple Silicon, um endpoint compatível com Anthropic, um companion móvel, uma equipe lançando quinzenalmente — e a divisão é filosófica, não de recursos. A postura padrão do LM Studio é de melhor esforço: faça rodar de algum jeito. A minha é recusar-e-explicar. Onde um agente decide o que carrega, o melhor esforço é o padrão errado, porque nada rio abaixo consegue distinguir rápido de lento sem medir.

Como ele se compara

Servidor · motor · licençaHot-swap / TTL de inatividadeColocação multi-GPURecusa transbordo para CPUGerenciamento remoto / MCP
StudioForge 1.26-08-23
llama.cpp, um build fixado · licença: nenhuma ainda
✅ JIT · TTL de 1.800 s, varredura de 15 splanejado por modelo, placas mistas, fixações + locações-ngl 999 + --fit off, 507 com números✅ REST + 29 ferramentas MCP
LM Studio 0.4.21
llama.cpp próprio + MLX · fechado
✅ JIT · 60 min, auto-evict para 1divisão por prioridade/uniforme, tensor parallel❌ reduz o offload, resto na RAMREST; MCP apenas host
Ollama 0.32.15
llama.cpp/GGML; MLX na Apple · MIT
keep_alive 5 min · 3 por GPU residentesespalhamento automático entre as placas❌ transborda, mostra % de CPU em ollama ps/api/* rico; sem servidor MCP
roteador llama-server (b105xx, agosto de 2026)
é o llama.cpp · MIT
✅ um filho por modelo · --sleep-idle-seconds, LRU por contagem-sm / -ts / -dev manuais--fit on encolhe seu plano/models/load|unload
llama-swap v251
um proxy que gera outros · MIT
✅ o produto inteiro · ttl por modelo/grupo❌ o que o seu cmd dissern/d — só proxy/ui + rotas do upstream
vLLM 0.27.1
próprio (PagedAttention) · Apache-2.0
❌ um modelo por processoparallel de tensor / pipeline / expertparcial — sem caminho de offload de camadassó LoRA, "local dev"
KoboldCpp 1.119
fork do llama.cpp + imagem/áudio · AGPL-3.0
--admin + --routermode--tensor_split manual❌ transborda/api/admin/*; só cliente MCP
TextGen (ex-oobabooga) 4.9
5 loaders incl. ExLlamaV3, TRT-LLM · AGPL-3.0
✅ troca sem reiniciar · TTL ?--tensor-split manual❌ transborda/v1/internal/model/*; cliente MCP
TabbyAPI (rolling)
só ExLlamaV3 — sem GGUF · AGPL-3.0
✅ admin + carregamento inline · TTL ?gpu_split_auto ligado por padrão✅ na prática — o ExLlama não tem caminho de CPUadmin-key /v1/model/load
Jan 0.8.4
modo router do llama.cpp · Apache-2.0
✅ via o router · TTL ?herdado do llama.cpp❌ transborda/v1/orchestrations; cliente MCP
LocalAI 4.9.0
60+ backends como imagens de contêiner · MIT
✅ sob demanda · WATCHDOG_IDLE_TIMEOUT"automatic GPU model fitting"❌ "No GPU required"REST + UI; só cliente MCP
GPUStack 2.2.3
vLLM, SGLang, MindIE, VoxBox · Apache-2.0
parcial — implantações de clusterSpread/Binpack automático, multi-nó?API completa de gerenciamento de cluster

Verificado em fontes primárias em 2026-08-23. Um ponto de interrogação significa desconhecido, não "não". Os workers do GPUStack são só Linux; o vLLM não tem suporte nativo ao Windows (só WSL ou forks).

O que a tabela diz:

  • Carregamento JIT e TTL de inatividade não são novidade, e eu não estou reivindicando isso. LM Studio, Ollama, llama-swap e LocalAI fazem ambos, e as flags por grupo swap / exclusive / persistent do llama-swap são um motor de políticas genuinamente elegante.
  • Três coisas ali são raras: recusar a rodar em vez de transbordar para a CPU (só o TabbyAPI chega perto, e só porque o ExLlama não tem caminho de CPU); um planejador que dimensiona contexto e slots para as placas que realmente encontrou; e gerenciamento publicado como ferramentas MCP, que nada mais na categoria faz, pelo que encontrei.
  • A coisa mais próxima no upstream é o próprio router do llama.cpp, uma briga justa: multi-modelo com isolamento de processo, gratuito, no binário que você tem. O que lhe falta é despejo baseado em memória em vez de contagem, fixações, locações e uma recusa carregando números; ele tem um sleep de inatividade (--sleep-idle-seconds), embora uma consulta a /metrics o acorde.

Várias boas ideias são emprestadas, e o repositório diz quais. O Modelfile do Ollama virou modelos virtuais, então duas personas sobre uma base compartilham um único llama-server, e o keep_alive dele virou o ttl por requisição. A superfície de configurações em três níveis do TextGen foi adotada diretamente, com "extra flags" brutas incluídas, com as flags validadas contra o próprio --help do motor fixado no momento de salvar. A filosofia de artefato único do KoboldCpp é o motivo de os motores viverem em diretórios versionados.

Instalação

Windows, a plataforma de referência, em quatro passos: instale o Git, o Python 3.12+, o uv e um driver NVIDIA atual; clone o repositório ou descompacte o download; clique duas vezes em launchers\Update StudioForge.bat, que apesar do nome é a etapa de primeira execução — ele constrói o virtualenv, instala o lançamento mais novo do llama.cpp que tem um build para o seu driver, faz smoke-test e fixa nele (b10425 é o build em que este artigo foi medido, não o que você vai receber); e então launchers\Start StudioForge.bat, ou launchers\StudioForge Tray.bat se quiser na área de notificação, e o painel abre em http://127.0.0.1:8080 na aba Setup.

Linux, quatro linhas — mais cmake e um CUDA toolkit cujo nvcc corresponda ao seu driver, porque o upstream não publica nenhum arquivo CUDA para Linux em tag alguma e o motor é construído a partir da fonte uma vez por versão:

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 uma máquina headless, o deploy/ contém duas units user do systemd — units user de propósito, porque o processo deve rodar como o usuário de login que é dono da biblioteca de modelos, do venv e dos device nodes de GPU. O watchdog deliberadamente não usa BindsTo= no gateway e usa Restart=always: ele existe para estar de pé quando o gateway não está. Então sudo loginctl enable-linger "$USER", o mesmo padrão do artigo sobre ComfyUI headless. A primeira execução abre na Setup, onde Detect LM Studio library sonda primeiro o downloadsFolder em ~/.lmstudio/settings.json.

ServiçoPorta padrãoChave de configuração
Gateway — /v1, /api, /mcp1234server.port
Painel de controle web8080gui.port
Watchdog de recuperação1235watchdog.port
Filhos llama-server (somente loopback)18100–18200gateway.child_port_start / _end

O que a tabela diz: a porta 1234 significa "o servidor de modelo local" nas duas minhas máquinas e elas não são a mesma coisa — a máquina do agente roda o seu em 127.0.0.1:1234, loopback, enquanto o rig serve my-gpu-rig:1234 pela VPN mesh — e apenas três portas são alcançáveis, com a validação de configuração recusando uma colisão entre qualquer porta de serviço e a faixa de filhos no momento do carregamento.

A regra do diretório de dados é SF_DATA_DIR primeiro, depois a pasta de um arquivo --config, depois <repo>/data em um checkout — a ordem completa, e o porquê de data_dir nunca ser gravado de volta no config.yaml, está no docs/SETUP.md. Uma instância é dona de um diretório de dados, imposto por um lock exclusivo do SO; uma segunda instância é somente leitura.

Segurança, honestamente

Uma regra por baixo de tudo: leituras, inferência e residência permanecem abertas; mudar a máquina não. Com server.api_key não definida, uma requisição mutadora a uma rota que muda a máquina só é aceita de um chamador nesta máquina, ou com o PIN MCP enviado como X-MCP-Pin ou como bearer token — qualquer outra coisa recebe 403 remote_admin_requires_credential. O conjunto bloqueado é configuração, reinicializações, motores, atualizações, recuperação de VRAM, downloads, locações, exclusões e as duas escritas por modelo que sobrevivem à instância. O problema que isso corrigiu era meu: qualquer um na LAN podia PATCH /api/config, definir server.api_key ele mesmo e me trancar para fora — enquanto a ferramenta MCP set_config, mesma capacidade no mesmo processo, exigia o PIN.

  • O PIN protege só o MCP. É um código de pareamento que você lê no banner de inicialização, com escopo nas ferramentas de gerenciamento. Não é uma chave de API.
  • server.api_key é a credencial real e é null por padrão. Defina-a e ela cobre /v1, /api, /mcp e o watchdog; o PIN continua funcionando nos dois endpoints MCP ao lado.
  • O bind enviado é 0.0.0.0 nos três listeners. A linha Network exposure da aba Setup fica âmbar e obrigatória no momento em que qualquer listener é exposto sem chave — verificando os três, porque server.host em loopback com gui.host em 0.0.0.0 costumava aparecer verde enquanto o painel estava totalmente aberto.
  • Uma requisição de navegador cross-origin não é "esta máquina", mesmo em loopback. Com cors_origins: ["*"], qualquer página que você visite poderia fazer preflight de PATCH /api/config em 127.0.0.1:1234 e chegar parecendo local — então a comparação de origem inclui a porta, e Origin: null conta como estrangeiro. O CORS governa o que uma página pode ler, nunca em quem o servidor confia. O websocket do painel tem uma versão só-para-host do mesmo portão, porque o painel é alcançado pela porta de onde foi servido.
  • Um navegador remoto em uma instalação sem chave recebe leituras e inferência, 403 em mudanças na máquina, e o PIN retido — caso contrário, qualquer coisa na LAN poderia ler o PIN de um endpoint aberto e usá-lo. O PIN era teatro exatamente quando importava.
  • Imagens são buscadas sob uma proteção de SSRF que bloqueia loopback, link-local, privado, ULA e espaço CGNAT — a faixa 100.64/10, onde todo peer de VPN mesh vive — e resolve uma vez, conectando ao endereço validado com o Host original e o SNI.
  • Nada sai da máquina sem ser pedido. As únicas chamadas de saída são Hugging Face para modelos, GitHub para o build fixado do llama-server e sua verificação de atualização, a verificação de lançamento do StudioForge que é opcional, e URLs de imagem que uma requisição nomeia; o self-update reporta "não configurado" sem uma chamada de rede até você definir update.repo, e um teste unitário fixa isso.

Dois limites declarados em vez de ocultos: uma verificação de endereço de peer confia no que está em loopback, que atrás de um proxy reverso é o proxy, então coloque o proxy atrás de server.api_key; e não há autenticação além de uma única chave compartilhada — sem contas, sem rate limiting. A regra da casa do artigo sobre o OpenClaw ainda vale: loopback mais um proxy autenticador, ou uma VPN mesh, nunca a internet crua.

A parte honesta

Escrito de forma clara para que você decida antes de instalar:

  • O Windows é a plataforma de referência — é contra ele que a bandeja, a proteção de VRAM por job object e os contadores de GPU por processo foram construídos. O Linux é suportado e a CI roda os dois, e é menos testado em batalha; o caminho de build a partir da fonte tem a construção de comandos testada, mas nunca foi exercitado de ponta a ponta aqui. O macOS não é suportado: sem CUDA.
  • Somente NVIDIA. O planejador lê o NVML, o motor é um build CUDA, e a afinidade de quant é expressa em compute capabilities.
  • A estimativa de VRAM é uma estimativa. Os pesos ficam dentro de 2% do tamanho do arquivo e o KV é exato a partir da geometria por camada, mas o buffer de computação é uma fração calibrada, ajustada uma vez na inicialização, limitada a 0.03–0.15 e mantida só em memória — então uma calibração ruim é desfeita por uma reinicialização. Dois históricos de calibração nesta máquina foram contaminados e agora são ignorados por completo.
  • A divisão multi-GPU é proporcional, não medida. Ela não modela a largura de banda do interconecte, e misturar gerações roda no ritmo da placa mais lenta.
  • As estimativas de concorrência são aritméticas. O estimador assume que os slots ficam meio cheios e reduz modelos MoE por uma metade fixa, e --ctx-checkpoints não é modelado de forma alguma. Ambos os erros apontam para menos slots, que é a direção segura — mas rode o benchmark paralelo antes de confiar em 8.
  • As estimativas de velocidade usam números nominais do fabricante — 5090 a 1792 GB/s e 209 TFLOPS fp16, 3090 a 936 e 71, nenhum medido aqui. Há exatamente duas âncoras de calibração, ambas neste rig, ambas em um slot: um 31B denso mediu 39,4 tok/s contra uma estimativa de 36,1, e um 122B MoE mediu 37,3 contra 47,4. Nada é validado em quatro ou oito slots, nem em um modelo denso acima de 31B.
  • Reservar uma GPU para outro programa restringe apenas o meu planejador. Nada impõe isso contra o outro programa, e nada o impede de tomar a memória primeiro.
  • Uma instância por diretório de dados, e o lock cobre o diretório de dados em vez da biblioteca de modelos — duas instâncias com diretórios de dados diferentes sobre uma biblioteca ainda são duas escritoras.
  • Nenhuma licença foi escolhida. Deliberadamente não há arquivo LICENSE, e o pyproject.toml diz isso em um comentário — Como obter tem o que isso significa na prática.
  • Não há auditoria de segurança de terceiros. As afirmações acima descrevem o que o código faz; eu escrevi ambas. Leia a fonte — é por isso que é um download e não um serviço.

2.503 testes unitários passam e 17 pulam, em 327 segundos nesta máquina; a CI roda a mesma suíte no Windows e no Ubuntu com a sonda de GPU forçada a um backend nulo. Uma segunda suíte carrega pesos reais em GPUs reais e é desselecionada por padrão e bloqueada atrás de uma variável de ambiente — cinto e suspensório, depois do incidente de órfão acima. Não feito, ou não ligado: o A/B de micro-batch de 8 slots, presets de sampler nomeados e mypy na CI; o self-update do app está escrito mas fica desligado até você definir update.repo você mesmo.

Pegadinhas

A lista honesta — coisas que realmente morderam, em ordem aproximada de quanto tempo custaram:

  • O LM Studio na 1234 deve ser fechado primeiro. Os dois não podem segurar a porta; o preflight nomeia o detentor em vez de imprimir um traceback de bind, e server.port a move. A biblioteca pode ser compartilhada sem problema.
  • Duas flags do llama.cpp não significam o que parecem. --ctx-size é o orçamento entre todos os slots, não por slot; --fit tem padrão on no upstream, ao lado de --n-gpu-layers auto — ambos acima, sob o planejador.
  • Modelos de raciocínio retornam uma resposta vazia sob --reasoning-format auto. Mesmo prompt, só a flag mudou: content 0 caracteres e reasoning_content 316 sob auto; content 323 sob none. O reasoning_content não está no schema OpenAI, então um cliente padrão lê uma string vazia e conclui que o modelo não disse nada. Eu rodo o 31B em deepseek porque meus clientes leem esse campo; todo o resto recebe none.
  • Pré-lançamentos vX.Y.Z do llama.cpp não carregam ativo CUDA para Windows. Uma tag v0.1.2 ficou acima de dois builds comuns bNNNN sem nenhum arquivo pré-compilado, então a aba Server oferecia uma atualização atrás de um botão que só podia falhar. As tags agora são filtradas para ^b\d+$.
  • Nunca faça tree-kill na raiz da bandeja com uma aba do navegador aberta no painel. Sob um stub de launcher de venv, o watchdog é o neto do servidor, e um reinício uma vez terminou com o servidor morto, o watchdog morto e nada gerado. Use o menu da bandeja, o painel, ou sfctl recover --restart.
  • O pkill -f llama-server de outra ferramenta mata seus backends. O bench-llm faz exatamente isso entre execuções — acima, sob os clientes de harness.
  • O PIN MCP não é uma chave de API, e um servidor sem chave ainda precisa de uma credencial placeholder em alguns clientes — acima, sob o dsh.
  • A chave MCP do OpenClaw é mcp.servers, aninhada sob mcp. O mapa plano mcpServers não é uma chave que o schema dele conhece — acima, sob o OpenClaw.
  • Dois atalhos de medição vão mentir para você. Repetir um prompt em um benchmark especulativo cronometra o cache de prompt (+751% contra +0,4%), e uma fórmula de KV de camadas × cabeças × contexto erra em 4× em um Qwen3.5 — ambos acima, sob o planejador. O /props também reporta speculative.types: "none" enquanto rascunha, então leia timings.draft_n de uma completion real.
  • Modelos de visão não têm benefício de cache de prompt. O llama.cpp desabilita o reuso de cache para modelos multimodais ele mesmo, e cada imagem é orçada em 1.024 tokens a menos que os metadados do mmproj digam o contrário — o bastante para uma janela de 8k ser majoritariamente imagens.

Como obter

O zip abaixo é a coisa inteira: a árvore de fonte com tag, os testes, a documentação, os launchers, as units do systemd, mais uma pasta dist/ com os dois wheels e o sdist para que você possa instalar sem uma etapa de build. studioforge-2026-08.zip — lançamento v1.26-08-23, 4.238.728 bytes (4,04 MiB), SHA-256:

84f4f828b5c75206236890f28e8651c96146a7bb39c14e21b13a0922a13f7d3f  studioforge-2026-08.zip

226 entradas sob um diretório de nível superior, construído com git archive da tag anotada v1.26-08-23 no commit 0610446, então ele só pode conter arquivos rastreados — sem config.yaml, sem data/, sem overrides locais. A fonte também está em github.com/LaserLloyd/StudioForge. Ele precisa de Python 3.12+, uv, um driver NVIDIA da série 580 para cima, e uma pasta de GGUFs. Licença: ainda não escolhida, então todos os direitos estão formalmente reservados — na prática, trate-o como o resto dos downloads deste site: gratuito para uso pessoal, e se você o quiser comercialmente, me pergunte.

Se algo quebrar, me mande um email — o endereço está na página Sobre — e envie o formato da falha em vez da sua configuração: o error.code, os números de um 507, as últimas vinte linhas de logs/models/<model>.log. Nunca o PIN ou a chave. E se você construir o planejador de bin-packing conjunto, os presets de sampler nomeados, ou um caminho AMD que realmente funcione antes de mim, eu prefiro mesclar o seu a escrever o meu.

Onde isso me deixa

O que eu não esperava era quanto disso acabou sendo medição em vez de código. O planejador é aritmética que qualquer um poderia escrever; o que o tornou confiável foi ler a própria geometria de KV do llama.cpp em vez de uma fórmula, e então medir o joelho de slots em 2 quando o estimador disse 8. Quase toda decisão naquele log começou como um número que discordava de uma crença. Se você já roda o LM Studio na 1234, o experimento inteiro é um clone, um arquivo batch, e apontar models.dir para a pasta que você já tem — e se ele não ganhar o lugar dele em uma tarde, sua configuração antiga fica intacta.

Relacionados: DeepSeek Harness (dsh) (onde este servidor aparece pela primeira vez, como um bloco de provedor sem explicação), bench-llm (de onde vêm os números de tokens por segundo do rig, e a ferramenta que vai matar seus backends), Minha configuração do OpenClaw (o post que nomeou o problema que isto resolve), e DisPatch (o app de chat na frente dele).

Downloads

Gratuito para uso pessoal. Se isso te economizar uma tarde, o botão do café está logo ali.


← Mais de IA e LLM Local