StudioForge: 私のリグで LM Studio を置き換え、エージェントがリモートから操作する GPU オンリーの LLM サーバー

公開日
2026年8月23日
著者
Jacob Lloyd — プロジェクト完了後、AIの支援を受けて執筆
読了時間
約78分で読めます

かんたんに言うと: これは、グラフィックスカードを積んだコンピューター上で動くプログラムです。アプリが AI モデルからの回答を求めると、そのモデルがどのカードに収まるかを計算し、起動し、使われている間は温めておき、静かになると再びシャットダウンします。決して破らないルールは、モデルがグラフィックスカードに完全に収まるか、さもなければ拒否されるかのどちらかだということです — 半分を遅いプロセッサーでこっそり動かして、なぜ全部が遅いのか疑問に思わせるようなことはしません。注意点: NVIDIA カードが必要で、最もよくテストされているのは Windows で、まだライセンスを選んでいません。

StudioForge は、私が GPU リグで LM Studio を置き換えるために書いたローカルモデルサーバーです: ポート 1234 で OpenAI 互換 API を提供する llama.cpp スーパーバイザー — 意図的に LM Studio と同じポート — ブラウザのコントロールパネル、メインプロセスが応答しないときに代わりに応答するリカバリーサイドカー、そして MCP 経由で公開される管理プレーン。別のマシン上のエージェントがシェルなしでこのサーバーを操作できます。

このサーバーが存在するのは、笑えなくなった障害記録のせいです。何も証明しない 200: LM Studio はルーティングされていないパスに対して成功ステータスとエラーボディで応答し、自分のログには Returning 200 anyway と記録されます。/v1/models が読み込み済みではなくダウンロード済みの全モデルを一覧するため、「実際に常駐しているのはどれか?」に答えが出ませんでした。推論モデルが 8,192 トークンのウィンドウをあふれさせる — クライアント設定で指定したコンテキストが実際の読み込みで使われたコンテキストではなかったからです。LM Studio ではウィンドウは読み込み時に固定され、それを教えてくれるものは何もありませんでした。そして最も高くついた一件: 収まらないモデルが拒否されるのではなく縮小されました — 文書化された挙動は「GPUオフロードサイズを自動的に減らし … 残りはシステムRAMに入れる」というもので、GPUの速度をシステムRAMの速度と引き換えにしつつ、どちらにせよ成功を報告します。

dsh の記事を読んだ方なら、このサーバーにはもう出会っています: 「StudioForge (GPU rig)」というラベルの provider ブロックがこれです。8エージェント構成が重い処理のためにネットワーク越しに接続する先であり、DisPatch が話しかける相手であり、OpenClaw Email がチャットモデルと埋め込みモデルを要求する相手です。私の OpenClaw の記事は問題に名前を付けました — 「モデル読み込みに数分、サービス間でのVRAMのやりくり」— そして以下がその答えであり、すべてのルールに、それを強制した実測値が添えられています。

要約

何をするのか、ひと目で

これは何ではないか:

  • モデルではない。 あなたが持っている GGUF を動かし、LM Studio と同じフォルダとレイアウトに追加分を取得します。
  • 推論エンジンではない。 計算をするのは llama.cpp。これを決めるのは、どのプロセスがどのフラグでどのカード上で動くかです。
  • チャットアプリではない。 Chat タブはありますが、それは本物のリクエスト経路が機能することを証明するためのものです。
  • クラスターではない。 1台のマシンと、そのマシン自身のカード。llama.cpp の RPC バックエンドは存在しますが、配線されていません。
  • CPU推論サーバーではない。 コードベースのどこにも、999 以外の --n-gpu-layers 値は存在しません。
  • 第二のAPIサーフェスではない。 Ollama の /api/generate も KoboldCpp API もありません — OpenAI サーフェス、LM Studio 風味の /api/v0 ミラー、/api 管理 REST。それだけです。

何がどこで動くのか

構成要素場所役割
ゲートウェイGPU ホスト、1プロセス、1234/v1/mcp(19ツール)、/api。レジストリ、プランナー、スーパーバイザーを保持。
コントロールパネル同一プロセス内、2つ目の uvicorn、8080ダッシュボード、セットアップ、モデル、ダウンロード、チャット、サーバー、ログ。
ウォッチドッグ独立したプロセス1235独自の MCP サーバーと10個のリカバリーツール。監督対象より長生きします。
llama-server 子プロセス18100–18200、ループバックのみ読み込み済みモデルごとに1つ。クラッシュしても倒れるのは1モデルだけで、ゲートウェイは倒れません。
sfctl コンパニオンエージェントのマシン純粋な HTTP クライアント — Python 3.11+、CUDA なし、サーバー依存なし。stdio の MCP ブリッジでもあります。
GGUF ライブラリmodels.dir、元々ある場所その場でインデックス化。コピーは一切なし。LM Studio は同じフォルダを使い続けます。

この表が示すこと:

  • 何かをインストールするのは GPU ホストだけ。 クライアントに必要なのはベースURL。エージェントは小さな Python パッケージ1つだけで、しかも管理ツールのためだけに必要です。
  • 子プロセスは外部から見えない。 子プロセスは 127.0.0.1 にだけバインドするため、ゲートウェイが唯一の公開サーフェスになります — だからこそ1つの API キーが本当の境界になるのです。

リクエストの流れ

クライアントが間違え得ることはすべて、最初の1バイトよりにチェックされます。不正なリクエストには、200 の SSE ストリームの中に埋もれたエラーフレームではなく、JSON ボディ付きの本物の 4xx(存在しないモデル ID なら 404)が返るべきだからです — クライアントは前者を正しく処理し、後者をほぼ確実に誤処理します。私のリグでの GET /health の応答:

{"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 こそ「何も証明しない200」問題への答え。 最初のライブラリスキャンが実行されている間は false のまま。その間も statusok のままです — プロセスが生きているからで、死活監視ポーラーが尋ねるのはまさにそれだからです。GET /health?deep=true は読み込み済みの全モデルに対して実際の8トークン補完(埋め込みモデルなら埋め込み呼び出し)を実行します — そして何も読み込まれていないときは、成功を返す代わりに no_models_loaded と答えます。失敗し得ないプローブは、プローブがないより悪いからです。
  • local-model エイリアスが解決される。 local-modeldefaultautocurrent はすべて models.default_model にマップされます。LM Studio クライアントはそのリテラル文字列にフォールバックするので、404 を返すと理由もなくクライアントを壊してしまいます。
  • コールドモデルの読み込みはハングに見えない。 開かれたストリームは5秒ごとに : loading <model id> (5s) を運び、最初の本物のトークンまで : prefilling <model id> (Ns) を運びます — すべてのパーサーが無視する SSE コメント行です。これだけ長く沈黙したソケットは読み取りタイムアウトを引き起こし、リトライするクライアントが飽和したバッチにプリフィルを積み上げることになります。
  • マシン全体で読み込みは一度に1つ。 かつて2つのコールドモデルが同時に同じカードへ計画されたことがあります。1つは CUDA error: out of memory で死に、そのリトライがもう一方の新しいモデルを退避させ、そのクライアントのリクエストは死んだ子プロセスに当たりました。
  • リクエストレベルの ttl が動かすのはアイドルタイマーだけで、それ以外は何も動かさない。 ここではどこでも ttl: 0 はピン留めのワイヤー形式なので、尊重されるどころか無視されます — かつて {"ttl": 60} を送ったクライアントが、所有者がピン留めしたモデルのピンを外していました。

VRAM の真実: プランナー

ここが他の誰もやらない部分なので、形容詞ではなく詳細でお見せします。core/planner.py は3,188行で、モデルが起動する前に1つの問いに答えます: 各カードの今まさに空いている VRAM を前提に、このモデルが持てる最良のウィンドウ、キャッシュ品質、スロット数は何か — そして答えが「なし」なら、何を伝えるべきか?

はしご、そして何と引き換えないか

そこに描かれた段は説明用であり、出荷時のデフォルトではありません: 仕組みは正確で、数値は一例です。config.example.yaml が出荷するのは、狙いとして target_ctx: 1048576、下限として default_ctx: 8192 — 狙いはまずモデルの学習済みウィンドウにクランプされます。ただし初回実行時に tune_for_hardware は、最小のカードが24 GiB 以上あれば default_ctx: 16384 を書き、12 GiB 以上なら 8192 を書きます。私のリグの下限は 128000 です。学習済みウィンドウを超えるには RoPE スケーリングが必要で品質が落ちるため、その上の段は決して提供されません。明示的な ctx_size は一段だけのはしごです。

2パス構成で、2回目は1回目がどこでも失敗したときだけ。理由は、ある午後12:03の読み込みです: 空き 79,832 MB、アイドルモデル1つから回収可能な 19,423 MB。その予算なら q4_0 キャッシュの 262144 が 96,004 MB で収まり、完全な f16 の 65536 が 95,236 MB で収まりました。実際に読み込まれたのは 8192/f16 で 89,860 MB — モデルは退避の全額を支払い、はしごで最小のウィンドウを得たのです。

KV はモデルごとに1つの数値ではない

オンラインのほぼすべての VRAM 計算機は、KV を レイヤー × ヘッド × ヘッド次元 × 2 × バイト × コンテキスト と計算します。Llama には正しく、実際に使われている2つのモデルファミリーには大きく間違っています: Gemma 3 と Gemma 4 は、フルアテンション1つにつきスライディングウィンドウレイヤーを5つ、ヘッド次元は半分でインターリーブし、Qwen3.5、3.6、3.8 は full_attention_interval = 4 を宣言するため、KV キャッシュは4レイヤーごとに存在し、残りは固定のシーケンスあたり状態を持つ Gated-DeltaNet リカレントレイヤーです。

間違えた場合の代償: 262,144 トークンを要求した Gemma-4 31B が、KV 推定 480 GiB とされて 65,536 に制限されました。一方キャリブレーションログは数週間、predicted_mb=95615 actual_mb=40037 と記録していました。形状修正の後、予測と実測はどちらも、2枚の 5090 で n_ctx=262144 のとき 38 GiB に落ち着きます — 4倍の解放で、Gemma-4 ファミリー全体がついてきました。Qwen3.5 の全レイヤーに KV を課金していたのは、同じバグが帽子を変えたものです: まっすぐな4倍の過大請求です。

盗む価値のある細部が2つ。スライディングウィンドウのセル数は llama.cpp と正確に一致させる必要があります。フラットな 1.25× 倍率は危険な方向に誤っていて、4スロットでは 3.6× も過小でした。そして attention_kindgeneral.architecture ではなくレイヤー形状から導出されます — 導出できない場所では unknown を報告します。これは「ここの KV 数値は全部信用しない」を意味し、「安いケースを仮定する」を意味しません。

キャッシュ品質は、より広いウィンドウと引き換えるのではなく、各段の内部で選ばれます: f16/f16 → q8_0/q8_0 → q8_0 K + q4_0 V。対称 q4_0 はすべての自動経路から消えました。好みの問題ではありません — q4_0 の K キャッシュでは Qwen2.5-7B が、f16 の自分が生成するトークンのうち 11.7% しか再現できません。一方、一致した q8_0/q8_0 のペアは KL ダイバージェンス 0.0018 に収まります。

4枚のカード、2世代

リグは RTX 5090 2枚と RTX 3090 2枚 — 名目32 GB と 24 GB のカードで、パネルは 31.84 GiB と 24.0 GiB、合計 111.7 GiB と数えます — ドライバー 610.88、CUDA ドライバー 13.3。常にまず単一カード — NVLink のない PCIe 上の分割モデルは明確に遅い — そしてプランナーがそれを覆すのは、単一カード配置が1スロットで不足し、分割が少なくとも2倍になり、追加するカードがすべて同等以上の能力を持ち、さらにスロット数を自動のままにしている場合だけです。最後の条件は礼儀ではありません: 分割は最も遅いメンバーのペースで走るのです。

配置(1.5B Q4_K_M、3090 2枚、8k ctx)生成プロンプト処理
3090 1枚352.5 tok/s2803.6 tok/s
2枚、-sm layer344.4 tok/s2722.5 tok/s
2枚、-sm tensor294.3 tok/s1182.0 tok/s
2枚、-sm row失敗: error loading model: device CUDA2 does not support split buffers

この表が示すこと:

  • 両軸で1枚が2枚に勝った。 レイヤー分割は生成の約2%を犠牲にし、テンソル分割は生成の17%とプロンプト処理の58%を犠牲にします — だからテンソルモードはオプトインで、実測だけがそれを選べます。
  • -sm row は CUDA 上では死んでいる。 パーサーは受け入れ、その後で読み込みが失敗します。だから子プロセスの起動前ではなく、起動前にゲートされます。

配置に関する細部は、カード単位で実測したときだけ姿を現します。出力レイヤーは最後のデバイスに計上されます。量子化ツールは Q4 ファイルの中でも埋め込みテンソルと出力テンソルを Q6_K か Q8_0 に保つからです: --device CUDA1,CUDA0 --tensor-split 0.5079,0.4921 と計画された 27B は、CUDA0 に 15.52 GiB — 分割が少なく割り当てた側である最後のデバイス — 対する CUDA1 は 14.48 でした。そして llama.cpp は見えるすべてのデバイスに CUDA コンテキストを開きます — 3090 で約0.22 GiB、5090 で 0.43 GiB — これが配置列に 512 MiB の下限がある理由です。

1つの配置で何件の会話が成立するか

llama.cpp の --ctx-size はスロット間で共有される 合計 KV 予算であり、スロットあたりのウィンドウではありません — 広く誤読されているフラグで、上流の README も明記していません。--ctx-size 4096--parallel なしで読み込むと total_slots: 4 と報告されます: 会話あたり 1,024 トークンです。StudioForge は ctx_per_slot × parallel で起動します。そして問題は、スロットを何個持つ価値があるか — ここで私は自分の算術を信用するのをやめました。

同時接続ストリームあたり合計p50p95実効バッチ
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、RTX 3090 1枚、スロットあたり 8,192 トークン、f16 KV、起動スロット8、512トークンのプロンプト、各192トークン生成。

この表が示すこと:

  • 推定器は8と言った。実測は2と言った。 4スロットでは各ストリームが単独時の44%まで落ち、65%の下限を下回ります。ルールは、その下限をクリアしつつ1段下のレベルより合計が15%以上伸びる 1/2/4/8 の最大値を取ります。
  • 合計は伸び続ける — 8スロットは1つの1.9倍のトークンを捌く — 一方、単一の会話は27%にまで崩れる。 合計を最大化するルールなら8を選び、すべてのユーザーが、カード本来の3倍遅いモデルを体験することになります。
  • バッチ処理は本物で、キューイングではない。 実効バッチが 1.00 → 1.84 → 3.46 → 6.03 と上がるのは共有デコードステップの証明であり、3回の実行で2%以内で再現されました。

3090 2枚でスロットあたり 32,768 の2回目の実行は、ストリームあたり 301.7 → 230.5 となり、答えはまた2でした。ちなみにカタログはその配置を 308.0 tok/s と予測し、実測は 301.7 — 2%差で、私の予想より良好でした。行は現在、max_parallel(いくつ収まるか)の横に recommended_parallel(いくつ動かす価値があるか)を載せています。

信頼する前に実測した2つのフラグ

投機的デコードは単一ストリームでの勝利。 MTP ヘッド付きの Qwen3.8-27B Q5_K_S、3090 1枚、4つの異なる256トークンプロンプト、プロンプトキャッシュオフ: 投機なし 37.75 tok/s。深さ3の draft-mtp50.70 tok/s、+34.3%、受理率 0.528。深さ4は下がって 47.48 — 受理率が 0.446 に落ち、却下された各トークンの検証が無駄になるからです。ngram-mod+0.4% にとどまり、ドラフトを1つも出しませんでした。

ここに罠があります: 同じプロンプトを3回繰り返すと、その 27B で +751% を計測しました。1つのプロンプトを繰り返せば、あなたはプロンプトキャッシュを計測してドラフトと呼んでいることになります。4スロットを超えると autonone を返し、その理由を言います — "投機は単一ストリームでの勝利であり、飽和したバッチを傷つける" — これは、27B を --parallel 8 で読み込み、auto が MTP ヘッドを見てスロット数を見ずに draft-mtp を選んだ実行の後のことです。

マイクロバッチは VRAM と引き換えにプリフィルを買う。 同じ 1.5B で 5,166 トークンのプロンプト: -ub 512(エンジンデフォルト)は 1492 MiB で 15,232 tok/s。-ub 1024 は 1562 MiB で 17,307 tok/s(+13.6%)。-ub 2048 は 1702 MiB で 18,061 tok/s(+18.6%)。長い間オフのままでした。コンピュートバッファが -ub とともに大きくなり、プランナーがそれをモデル化せず、モデル化されていないバッファが「収まる」を「メモリ不足」に変えるからです。プランナーは今それを計上し、拒否側に誤るよう切り上げ、マイクロバッチは4スロットを超えたときだけ自動的に上げます。

数値付きの拒否

すべての起動は --fit off--n-gpu-layers 999 を渡します。後者は設定ではなく定数です。これは以前より重要になっています: 固定ビルド b10425-fit, --fit [on|off]"未設定の引数をデバイスメモリに収まるよう調整するかどうか" — をデフォルトで on として出荷し、--n-gpu-layers auto と並んでいます。両方とも2025年12月に上流の PR #16653 で入りました。このペアはまさに無言の部分オフロード経路です: 汎用サーバーには合理的なデフォルトであり、このプロジェクトが拒否するために存在する挙動そのものです。何も収まらないとき、答えはボディに計算を載せた HTTP 507 です。以下は切り詰めたもの:

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

これは本物の拒否で、この記事を書いている間に取得したものです: 31B が RTX 3090 1枚への読み込みを要求されました。文章部分は不足額と保持者を名指しし、error.studioforge は同じ失敗をデータとして運びます — 必要バイト数と利用可能バイト数、カードごとの空き VRAM、重み・KV・コンピュートバッファ・プロジェクションモデル・CUDA コンテキストに分解された推定、VRAM を保持するすべてのプロセス、そして諦める時に立っていたはしごの段を示す notes。より小さいウィンドウなら収まる場合、max_ctx_that_fits がその値を示します — レイヤーごとの形状で計算されるため、次の読み込みが受け入れる提案になります。ここでは下限でさえ収まらないので、失敗する数値ではなく null です。ビジーなモデルが原因でない拒否には retry_after_s がありません — 何も変わらないときに「後で再試行」は悪いアドバイスだからです。

ピン留め、TTL、リース、そしてリバランサー

ピン留めは望ましい状態であり、免除ではない。 かつては TTL ゼロ、あらゆる退避のはしごからの除外、起動時の1回のウォームアップを意味しました — そして4つ目のことが欠けていました: ピン留めモデルは読み込みされず、モデルごとの max_restarts を超えてクラッシュループした子プロセスは、何も保持しない state="failed" のまま放置されたのです。今はリコンサイラーが15秒スイープに乗り、60秒から900秒の天井までバックオフします。ピンに勝る唯一のものは人間です: 明示的なアンロードは ID を抑制済みとマークし、ダウンのままにします。

リースは、1つのモデルに属するカード。 リースされたカードは他のすべてのモデルの GPU 視界から消えます — 最下位にランクされるのでも、選択肢になるのでもなく — 所有者は推定器がサイズを決め、その場所で自分のベンチマークが最速と測った分割モードで、まさにそれらのカードに強制されます。すでにリースされたカードは409 コンフリクトであり、乗っ取りではありません。モデルを伴わないリースは、サーバーの外の何かのためにカードを保持します: reserve_gpus(devices=[3], reason="ComfyUI render") が、私の画像生成と言語モデルの争いを止めた理由です。

リバランサーは昨日の良い判断を直す。 13:42 に 27B がカード [1, 3] に計画されました — GPU1 を共有する世代またぎ分割です。31B が [1, 0, 2] を保持し、画像生成アプリが GPU2 の 7.5 GiB を保持していたからです。13:53 に 31B が [0, 1] に縮小されました。それ以降 [2, 3] は空いて厳密に優位になりましたが、27B はそこに留まりました。そこで今、アイドルモデルは、現在の正確な設定での無退避プランがすべての共有カードから外れる場合に移動されます。推定トークン毎秒は移動を正当化しません。

チェックは1分に1回、しかも世界が変わったときだけです: 静かなマシンでだけ、5分間アイドルのモデルにだけ、モデルごとに30分に1回の移動 — 移動は再読み込みであり、再読み込みはプロンプトキャッシュを落とすからです。このリグの長い会話ワークロードでは、そのキャッシュは98kトークンのプロンプトの93%でした。退避には他に3つの絶対ルールがあります — ピン留めモデルは不可、リクエスト処理中のモデルは不可、読み込み中のインスタンスは不可 — そしてリースされたカードはそもそもプランナーの視界にありません。ジャストインタイムの読み込みは force を決して設定できません。

壊れたとき

VRAM は、それを取ったプロセスとともに死ぬ。 Windows では子プロセスは JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 付きで作られた匿名ジョブオブジェクトの中にいるため、親がどう終わろうと、最後のハンドルが閉じた時点でカーネルが全メンバーを殺します。匿名なのは、名前付きジョブは名前を推測した何とでも共有されるからです。Linux には PR_SET_PDEATHSIG のシムがあり、ゲートウェイへの kill -9 をカバーします。カーネルの保証ではなくベストエフォートなので、起動時スイープと reclaim_orphan_engines がすり抜けたものを拾います。

だから起動時スイープがある。 2026年8月18日、GPU0 の約10 GiB と GPU1 の約15.6 GiB が「すべて停止している」のに使えませんでした。保持者は、コーディングエージェントが起動してすでに終了していた python -m pytest tests -q 実行の、3つの llama-server.exe 子プロセスでした。現在すべての保持者は分類されます — ourschild-of-live-processorphanother-instanceforeign — そして殺されるのは orphan だけです。他に私たちの engines ツリーからバイナリを起動するものが存在しないため、これは構造上安全です。

誰が何を保持しているかを名指しするのに2回かかった。 NVML は Windows でプロセスごとの使用メモリをゼロと報告するため、サイズはタスクマネージャーの「専用 GPU メモリ」列が読むカウンターから取ります — しかしそれはアダプターをまたぐプロセス合計なので、デバイス列が間違っていました: CUDA0,1,2,3 で報告されたプロセスが、実際には CUDA0 に 15.52 GiB を保持し、3090 には何も保持していませんでした。修正はアダプターの LUID を PCI バスアドレスに結合します。そして1つの罠が段落1つ分の価値があります: NVML の busId のバス番号は16進数です。 "00000000:42:00.0" はバス 66 であり、42 ではありません — 正解と見分けがつかない、自信に満ちた誤答です。

アンロードは宣言ではなく検証される。 スーパーバイザーは pid 再利用に対する作成時刻ガードで pid を再チェックし、生存者を強制ツリーキルへエスカレーションし、前後の VRAM を記録します。生存者がいれば、手動で殺すよう伝える 500 を返します。プロセスが常駐したまま成功を報告するアンロードは、このシステムが語り得る最も高価な嘘です。それ以降のすべての読み込みが、空いていない VRAM に対して計画されるからです。

終了コードは語彙である。 2 はキーを名指しする設定エラー。3 はポート競合で、トレイは競合するポートに再生成しません — 保持者が StudioForge サーバーとして /health に応答するのを待ち、代わりに接続します。75 は「再起動要求」: サーバーはドレインし、コードを設定して、優雅にシャットダウンします — 要求から終了まで1.0秒、実測 — トレイはクラッシュ回数を消費せずに再生成します。この区別が存在するのは、かつて GUI の再起動が 1234 を争う2つのサーバーを生み、クラッシュを3回数えた後、トレイが停止できない健康なサーバーの隣で Crashed — see the logs folder のまま座っていたからです。

ゲートウェイが死んでいるのではなくフリーズしているとき、あなたはウォッチドッグと話します: 1235 で常時稼働する独立プロセスで、argparse と stdlib ロギングだけで作られているため、config.yaml 自体が壊れていても起動します。10個のツールは healthget_configset_configrestart_serverkill_modelnuke_all_modelsreclaim_orphan_enginestail_logsgpu_statusrollback_update です。孤児ルールを、それを所有するモジュールをインポートするのではなくローカルで再実装しています — リカバリープロセスは、自分が修復するスタックをインポートしてはいけないのです。

コントロールパネル

8080 のパネルは同じプロセス内の2つ目の uvicorn サーバーで、ゲートウェイのオブジェクトグラフを参照で共有しています — 中には絶対 URL がまったくありません。これが、メッシュ VPN 上の素の HTTP と HTTPS フロントエンドの背後で、まったく同じように動く理由です。

リクエスト処理中の StudioForge ダッシュボード: ヘッダーは「2 読み込み済み、4 GPU、111.7 GiB 中 45.5 空き」; GPU カード4枚 — RTX 5090 2枚、RTX 3090 2枚 — 使用・空き・利用率の数値付き; VRAM 保持者リストは ours マークの llama-server.exe プロセス2つと foreign の5つ; 空の GPU リースパネル; そして読み込み済みモデルのカード2枚 — 1つはプリフィル中、1つはピン留めでアイドル
リクエスト処理中のダッシュボード: 2,700トークンのプロンプトをプリフィルする 5090 2枚にまたがる 31B、1枚の 3090 にピン留めされた 26B、そしてどのカードでも VRAM を保持するすべてのプロセスがその分け前とともに名指しされています。保持者パネルが存在するのは、かつて 25 GiB が消えたからです。
読み込み済みモデルのカード2枚の拡大: 31B はポート 18100、実コンテキスト 262144、1 ビジー / 0 アイドル、GPU0、GPU1(レイヤー)、TTL 15分00秒、アクティブリクエスト 1、合計 3、最終 58.77 tok/s、ビルド b10425、スロット行には「プロンプト処理 2202/2742(80%)、キャッシュヒット 7/2742」; 26B はポート 18101、GPU2、TTL ピン留め、最終 46.67 tok/s、スロット 0 アイドル
同じ2枚のカードを上のフレームから切り出したもの: 各モデルに実際に割り当てられたポート、実際に得たコンテキスト、実際に載ったカード、TTL のカウントダウン — そしてエンジンスロットごとの行。「実際」は私たちが要求した値ではなく、子プロセスから読み戻されます。
セットアップタブ: 「提供準備完了だが、公開前に修正すべきこと: ネットワーク公開」という見出しのチェックリスト。データディレクトリ、E:\LLM\Models 配下の GGUF ファイル50個、インデックス化されたモデル34個、ドライバー 610.88 と CUDA 13.3 上の GPU 4枚、スモークテスト済みのエンジン b10425、0.0.0.0:1234 のゲートウェイの行は緑。ネットワーク公開の行は1つだけアンバー色で、API キー設定ボタン付き。その下はモデルライブラリセクションと、すべての読み込みに適用されるデフォルト項目 — コンテキスト下限 128000、目標 1048576、推論モデル下限 32768
新規インストールはここで開きます。そして「新規」は記憶ではなく実測です — モデルディレクトリなし、インデックスなし、エンジンなし。満たされていない行にはすべて、それを直すボタンが付いています。私のリグでアンバー色の行は1つだけ — API キーなしですべてのインターフェースで待ち受けるゲートウェイで、これは事故ではなく選択です。
モデルタブのライブラリ表: 列は名前、ダウンロード済み、サイズ、量子化、アーキテクチャ、機能、最終使用、ステータス、アクション。Qwen3.8-27B のビルド2つ(21.29 GiB と 17.95 GiB)から Qwen2.5-0.5B(0.49 GiB)までの11行。チャット・ビジョン・埋め込みの機能アイコン付き。gemma-4-26B の行にはピン留めバッジと緑の読み込み済みチップ。各行の末尾に小さなアクションボタン6個
カードではなく表なのは、モデルが数十個あれば質問は比較になるからです — どれが最後に追加されたか、どれが大きいか、どれが画像を見られるか。デフォルトの並び順は新しいダウンロード順で、右側のステータスチップは、どれが常駐しているかの生の真実です。
gemma-4-31B-it-QAT-Q4_0(16.44 GiB、学習済みコンテキスト 262144)のモデルごと設定ダイアログ: GPU の組み合わせごとに1行の最適設定ブロック — RTX 5090 2枚で 262144 ctx、q8_0、3スロット、約62 tok/s; RTX 3090 2枚で 1スロット、約34 tok/s; 4枚すべてで 3スロット、約40 tok/s、「今なら収まる」マーク付き; RTX 5090 1枚で 131072 ctx、約66 tok/s — それぞれに「ここに読み込む」と「並列数を計測」ボタン。64K・128K・256K・512K の「正確にこのサイズで読み込む」行。緑のフィット判定は「GPU1、GPU0、GPU3 に収まる(分割: レイヤー)、KV キャッシュ q8_0、flash-attn オン、予測 VRAM 32.55 GiB をカードごとに内訳」。そして基本フィールド — コンテキスト長、KV タイプ K と V、TTL、ピン留めチェックボックス、ドラフトモデル、デバイス上書き
このダイアログは、1画面に収めた議論全体です: 各カードの組み合わせでモデルが自由なら何ができるか、変更のたびに再実行されるフィット判定(カードごとの予測 VRAM 付き)、そしてそれを覆すためのすべてのつまみ(上級・エキスパート層は切り取りの下に続きます)。到達できないコンテキストサイズは隠すのではなく理由付きでグレーアウトされます — 「なぜこのモデルは 512k を出せないのか」は、そのボタンが存在する目的の問いだからです。
Hugging Face で gemma-4 を検索した後のダウンロードタブ、「ダウンロード数(30日)」でソート済み: unsloth、lmstudio-community、google のリポジトリ12個。各行は発行元、1,187,348 から 458,712 までのダウンロード数、いいね数、量子化の数、更新日時を表示し、モデルカードのリンクと量子化ボタン付き
Hugging Face 検索とダウンロードキューを1つのタブに。ソートラベルが「ダウンロード数(30日)」なのは意図的です — その数字は直近30日間のカウントであり、全期間の合計ではありません — そしてフィット計算が始まるのは量子化ボタンからです。
unsloth/Qwen3.8-27B-GGUF の量子化ピッカーの上部: モデルヘッダーを読むまではフィットは重みのみの推定であるというヘッダー注記、アテンション ハイブリッド・65レイヤー・KV 65 KB/トークンと読める形状行、そして 51.77 GiB の BF16(アンバー色の「複数GPU必要」チップ付き)から 19.28 GiB の Q5_K_M までの16行。各行に緑の「1GPUに収まる」チップ、5090 1枚・5090 2枚・4枚すべての配置ごとのコンテキスト行、そしてダウンロードボタン
何かをダウンロードする前に、すべての行が今まさに空いている VRAM に対してチェックされます: 量子化ごとのフィットチップ、その下に各配置が得るコンテキスト — 5090 1枚、2枚、または4枚すべて。ピッカーは2段階で埋まります: ファイルサイズと空き VRAM の照合で即座に表示され、次に GGUF ヘッダーのリモート読み取りで、バッジがプランナー自身の回答になります。 これは以前からずっとこうだったわけではありません: この記事を書いている間に直した修正まで、ピッカーはリポジトリの MTP ドラフトモジュールと imatrix ファイルを、モデルの量子化版であるかのように一覧していました。ダイアログは切り取りの下に続きます — 全部で25行です。
チャットタブ: 読み込み済みモデル2つのうち直近使用の gemma-4-31B-it-QAT-Q4_0 に解決される「読み込み済みモデルを使う」スイッチ; モデルピッカー、温度 0.7、top_p 0.95、max_tokens 1024、9.7 tok/s と読めるレートラベル; システムプロンプトボックス; そして、KV キャッシュとは何か、なぜそのサイズがコンテキストウィンドウに依存するのかを3文で説明するよう求めるユーザープロンプトと、それに続くモデルの回答が完成したトランスクリプト
チャットアプリではなくテストハーネス: 同じ読み込み経路を呼び、OpenAI エンドポイントが使うのと同じ子プロセスポートからストリーミングします。9.7 tok/s のラベルはこのタブ自身の算術です — リクエストが送信された瞬間からの実時間に対するストリームチャンクで、プリフィル込み — エンジンの生成タイミング(モデルカードの 58.77 tok/s)ではありません。ここでのチャット成功は、クライアントが動く証拠であり、ずれ得るモック経路ではありません。
サーバータブの上部: モデルディレクトリ、インデックス化されたモデル34個、GPU 4枚、エンジン b10425 を一覧する「提供準備完了」ストリップ; 展開された「OpenClaw をこのサーバーに向ける」パネル — 生成された OPENAI_BASE_URL と OPENAI_API_KEY のペア、openclaw_cli・openclaw_config を mcp.servers の下に持つ MCP サーバー JSON と汎用の mcpServers ブロック、そしてコンパニオン設定; その下にバージョン 1.26-08-23、稼働時間、設定パス、読み込み済みモデル2つを読めるヘルスブロック
サーバータブはクライアント設定をあなたの代わりに書きます — 私が OpenClaw に貼り付けたブロックそのものを、両方の形状で — その下にヘルス行を表示します。同じタブのさらに下には、エンジン一覧と、ボックスがライブラリのどれだけを実際に実行できるかのレポートがあります。MCP エントリは sfctl にシェルアウトするため、ペアリング PIN は設定ファイルに一切現れる必要がありません。
ログタブ: ソース StudioForge server、レベル INFO、追従オン、84行のインメモリリングバッファをテーリング — studioforge.core.planner からの「読み込み計画」行の繰り返しで、chosen ctx=262144 kv=q8_0 parallel=1、devices=[1, 0, 3]、estimate_mb=33331 と読み取れ、続いて ttl_s=900 でのアイドルモデルのアンロードと、vram_reclaimed_mb=37086 の model_unload_verified
ログタブはインメモリリングバッファをテーリングします: ここではプランナーが行動する前にすべての決定を書き留めています — 選んだコンテキスト、キャッシュ種別、スロット数、デバイス、そして読み込みにかかると予想したコスト — その後にアイドルアンロードと、検証して戻した VRAM。ここで再構成されたものは何もありません。当時プランナーが考えていたことそのものです。

同じバッファのさらに4行下が、読み込みが私を驚かせたときに私が読む行です — 組み立てたコマンドライン、応答したプロセス、そしてプランナーが自分の宿題を採点している様子(タイムスタンプとロガー名は省略):

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'

これはプランナーが自分の誤りを1枚のカードで 3 GB、ペアで 4 GB と暴き、どのカードでなぜかを言っているところです — 出力レイヤーは最後のデバイスに載り、まさにそこに計上されたのに、それでも計上不足だったのです。

StudioForge の Windows 通知領域メニュー: 「実行中 — モデル1個読み込み、23.4 GiB 空き」と読めるヘッダー行、続いてコントロールパネルを開く、API ドキュメントを開く、ログフォルダを開く、モデルフォルダを開く、すべてのモデルをアンロード(VRAM 解放)、エンジンを再起動、サーバーを起動(グレーアウト)、サーバーを停止、サーバーを再起動、MCP URL をコピー、MCP PIN をコピー、チェック済みの「ログイン時に起動」、そして終了
日々の運用では、全体がトレイアイコンです。ヘッダー行が完全なステータス — 何が読み込まれ、VRAM がどれだけ残っているか — そして PIN は、トレイがクリップボードに載せる唯一の資格情報です。

ハーネスのバックエンドとして使う

私のネットワーク上の6つのクライアントがこのサーバーと話しますが、それが StudioForge だと知っているのは1つだけです。それがポイントです。5つを下に描きます。6つ目は OpenClaw Email です。

どんな OpenAI クライアントでも

環境変数は2つ。server.api_key は初期状態で null なので、空でない文字列なら何でも機能します — ほとんどの OpenAI クライアントは空のキーでは起動を拒否するからです。

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 は LM Studio 方式でダウンロード済みのすべてを一覧し、state を、常駐中なら ctx_per_slotmax_parallelparallel_limited_by を追加します — モデルが複数スロットを動かすとコンテキスト長だけでは曖昧だからです。ID は往復します: 完全な publisher/repo/file ID、素のファイル名、または publisher/name を、大文字小文字を無視して。DisPatch は新しいベースURLだけで済みました。OpenClaw Email はより要求の多いクライアントで、チャットモデル埋め込みモデルの両方を欲し、起動時に /v1/models を呼んで実際に提供されているものを尋ねます。

別のマシン上の OpenClaw

エージェントのマシンは小さな wheel を1つインストールします: sfctl です。これはサーバーの 3.12 ではなく Python 3.11 を対象にしています — エージェントを動かすマシンはしばしばリグより遅れているからです — そして意図的にサーバーパッケージに依存しません — CUDA なし、プランナーなし、レジストリなし。

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

または手動で — これが人々の午後を奪う細部です。OpenClaw のキーmcp の下に入れ子になる mcp.servers: フラットな mcpServers マップは Claude Code、Cline、LibreChat には正しいものの、OpenClaw のスキーマが知るキーではありません。推論は models.providers の下の別経路です — baseUrl と、小文字の rl に注意:

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

エージェントが得るのは 統合された29個のツールリスト1つ: ゲートウェイの19個プラスウォッチドッグの10個。3つは recovery_* に改名されています — get_configset_config はゲートウェイのツールと衝突するため、health は対称性のため。restart_server は素の名前のままです。死んだ管理ツールのエラーメッセージが、エージェントに呼ぶよう指示する名前がそれだからです。メインサーバーがダウンしていても、ブリッジは19個の管理ツールすべてを注記付きで広告し続けます — load_model が見えないエージェントは、その能力が存在すること自体を知らないからです。ブリッジが回すループ:

  1. list_models(limit=N) — カタログ、新しいダウンロード順。推奨行を読んでください。
  2. load_model(**row["load_args"]) — 変更せずにそのまま渡す。行を選んだエージェントは、選ぶ作業を終えています。
  3. load_recommended(model_id, ctx_size=N) — 知っているのが必要なコンテキストだけのとき。縮小ではなく拒否する唯一の読み込み経路です。
  4. 推論は MCP ではなく HTTP で。 未読み込みモデルを名指しすると読み込まれますが、読んでいた行ではなくプランナーのデフォルトで読み込まれます。
  5. model_options(model_id) — 推奨行では足りないとき: 速度付きのすべてのコンテキスト段階。
  6. search_modelsrepo_detailsdownload_model — 新しいものを手に入れるために。
  7. pin_model — 常に応答する必要があるモデルのために。reserve_gpus / release_gpus — 自分のカードのために。
  8. server_statusconnection_info — 何が常駐し、誰が VRAM を保持し、どのアドレスで応答しているか。

そして、私が最も気に入っている段落。接続時にすべてのクライアントへ配信されます:

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、そして5つのワンライナー

dshDeepSeek Harness)は、1つの YAML ファイルを編集してモデルを切り替えます。次のリクエストのためにホットリロードされます。provider ブロックは camelCase — baseURLapiKeyEnv:

# ~/.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参照であり、リテラルではありません — そしてキーなしのローカルサーバーでも、何らかの資格情報を参照する必要があります。OpenAI 互換クライアントがベアラートークンを要求するからです。一方 Claude Code は、ここに自前の推論をルーティングできません: そのゲートウェイプロトコルのリファレンスには Anthropic Messages、Bedrock、Vertex が並び、/v1/chat/completions はありません。だから StudioForge は Claude Code のツールであり、頭脳ではありません — claude mcp add studioforge -- sfctl mcp-- は必須)が29個すべてを渡します。

bench-llm が、人々が私に引用して返すリグの数値を生み出しました — Gemma 4 26B-A4B(QAT、Q4)で 229.0 tok/s、最初のトークンまで110 ms(当時は LM Studio 経由で実測)— これは素の OpenAI クライアントなので、ベースURLだけで十分です。ただしベンチマークの合間に pkill -f llama-server を実行し、ボックス上のすべての StudioForge バックエンドを殺します。残りはそれぞれ1行です: Open WebUIOPENAI_API_BASE_URLLibreChatbaseURLmodels.fetch: truecustom エンドポイント。aiderOPENAI_API_BASE に続けて --model openai/<id>Continueprovider: openai プラス apiBase。ベースURLキーの綴りはどれも違っており、このエコシステムで夜を無駄にする最も確実な原因です。

エージェントが選ぶもの

カタログはモデル選択を推測ではなく照会にします: 新しいダウンロード順にソートされ、コンテキスト段階ごとに1行。各行は、リアルタイムの空き VRAM に対する fitsdevices、KV タイプ、max_parallelrecommended_parallelconfidenceif_gpus_idle 列、そして load_args を運びます。1行は1回の本物の計画呼び出しなので、読み込みが拒否するものを約束できません — そして if_gpus_idle は、エージェントが諦めるか、unload_model を呼ぶかの違いです。

repo_details は名前を挙げる価値があるものです: GGUF ヘッダーをリモートで、HTTP の Range リクエスト越しに読みます — 2〜15 MB、主にトークナイザーの長さプレフィックス付き文字列配列で、ディスクにキャッシュされます — 20 GB をダウンロードして収まるか調べる代わりに。レンジ要求に 200 とボディ全体で応答する CDN は検出して拒否されます。返ってくるのは、実際の読み込みが使うのと同じプランナーによる context_fit 行列です:

量子化1× RTX 50902× RTX 50904枚すべて
BF16 (51.8 GiB)— 重みだけで収まらない32k(q8_0)256k
Q8_0 (27.9 GiB)256k256k
Q5_K_M (19.3 GiB)128k(q8_0)256k256k
IQ2_M (10.5 GiB)256k256k256k

リポジトリの OpenClaw ガイドから、このリグで計算したもの。unsloth/Qwen3.8-27B-GGUF 用。max_ctx はフル品質の f16 キャッシュでの最大ウィンドウ。q8_0 の数値は、それがより遠くまで届く場合にだけ現れます。

この表が示すこと: ウィンドウを決めるのはカードの枚数ではなく量子化 — BF16 から Q5_K_M へは、「まったく収まらない」を1枚のカードで 128k に変えます — そしてこれはプランナー自身の回答なので、ある段階にキャッシュの量子化でしか到達できない場合、行列は勝利として数えるのではなく「q8_0 で」と書きます。実際の数値のための手順書は3ステップ: 配置ベンチマーク(各 GPU モードを独自のリースの下で、llama-server 自身のタイミングからスループットを取得)、次に勝者で benchmark_parallel、そして reserve_gpus で固定。誰かが会話の途中のモデルをベンチマークしてはいけません。

LM Studio の置き換え(ドロップイン)

互換性が設計の制約であり、リポジトリは借りたものを列挙して、誰も推測しなくて済むようにしています: ポート 1234。ダウンロード済みを一覧する /v1/models。ジャストインタイム読み込み。アイドル TTL。リクエストごとの ttlその場で使われる publisher/repo/ レイアウト — インポートステップがなく、両プログラムが1つのライブラリを共有します。/api/v0/models ミラー。そして lmstudio://open_from_hf ディープリンク。クライアントの移行はホストの変更であり、ホストとポートの変更ではありません。

LM Studio 0.4.21StudioForge 1.26-08-23
エンジン独自 llama.cpp ビルドと Apple 上の MLX上流の llama-server、固定ビルド1つ(b10425、CUDA 13.3)、有効化前にスモークテスト済み
ルーティング外のパスエラーボディ付き 200 — ログは Returning 200 anyway と言うJSON エンベロープ付き 404、すべてのステータスで JSON
エラークライアントが正規表現で照合する非構造化散文安定した error.codeerror.studioforge の下の診断情報
読み込み設定2つの読み込み経路の1つで context_length が無視される。repetition_penalty は黙って無視される読み込み経路は1つ、すべてのフィールドが尊重され、実効値がエコーされる。サンプラーエイリアスも受け付ける
収まらないとき「GPUオフロードサイズを自動的に減らし … 残りはシステムRAMに入れる」— 設計による無言の CPU 流出必要・利用可能バイト、GPUごとの空き、収まる最大コンテキスト、順序付き提案を伴う 507 insufficient_vram
マルチ GPU優先または均等分割、GPUごとのトグル、0.4.15 以降テンソル並列配置ごとにコンテキスト・KV タイプ・スロットをサイズ決めするプランナー、出力レイヤー向けに分割比率を傾ける
アイドル TTL60分。自動退避はジャストインタイム読み込みモデルを最大1つ保持出荷値 1,800 秒(私のリグでは15分)、15秒ごとにスイープ。収まるだけ常駐、誰が残るかはピン留めとリースが決める
リモート管理/api/v1 の読み込み/アンロード/ダウンロード。LM Link はプレビュー中で、将来課金予定。発見は LM Studio のハブ経由/api REST、sfctl、MCP ツール29個。到達範囲はあなたの LAN か自分だけのメッシュ VPN
MCPホストのみ — MCP サーバーを消費する自身の管理のための MCP サーバー、ウォッチドッグ上にもう1つ
ソースクローズド。個人利用と社内業務利用は無料ソースは zip 内にあり、ライセンスはまだ未選択

2026-08-23 に LM Studio 自身のチェンジログ、ドキュメント、バグトラッカーと照合。バージョン 0.4.21(2026年8月12日リリース)。行2は公開バグトラッカーに載っています。行3〜4は、私自身のクライアントが 0.3.x API で回避しなければならなかったものです。どれも 0.4.21 に対して再テストしていないので、「どこかの時点で文書化された」と読み、「今日壊れている」とは読まないでください。

この表が示すこと: LM Studio は、これが永遠になれないほど優れたデスクトップアプリです — 磨かれた GUI、Apple Silicon 上の MLX、Anthropic 互換エンドポイント、モバイルコンパニオン、隔週リリースのチーム — そして違いは機能ではなく哲学です。 LM Studio のデフォルトの姿勢はベストエフォート: とにかく動かす。私のは拒否して説明する。エージェントが何を読み込むかを決める場面では、ベストエフォートは間違ったデフォルトです。下流の何も、実測なしでは速いと遅いを区別できないからです。

比較

サーバー · エンジン · ライセンスホットスワップ / アイドル TTLマルチ GPU 配置CPU 流出を拒否リモート管理 / MCP
StudioForge 1.26-08-23
llama.cpp、固定ビルド1つ · ライセンス: 未定
✅ JIT · TTL 1,800 秒、15秒スイープモデルごとに計画、カード混在、ピン留め + リース-ngl 999 + --fit off、数値付き 507✅ REST + MCP ツール29個
LM Studio 0.4.21
独自 llama.cpp + MLX · クローズド
✅ JIT · 60分、自動退避で1つまで優先/均等分割、テンソル並列❌ オフロードを減らし、残りは RAMREST。MCP はホストのみ
Ollama 0.32.15
llama.cpp/GGML。Apple は MLX · MIT
keep_alive 5分 · GPUあたり3つ常駐カード間で自動分散❌ 流出する。ollama ps に CPU % 表示充実した /api/*。MCP サーバーなし
llama-server router(b105xx、2026年8月)
そのもの llama.cpp · MIT
✅ モデルごとに子プロセス · --sleep-idle-seconds、件数ベースの LRU手動 -sm / -ts / -dev--fit on が計画を縮小/models/load|unload
llama-swap v251
他を起動するプロキシ · MIT
✅ 製品全体がこれ · モデル/グループ単位の ttlcmd 次第該当なし — プロキシのみ/ui + 上流ルート
vLLM 0.27.1
独自(PagedAttention)· Apache-2.0
❌ プロセスごとに1モデルテンソル / パイプライン / エキスパート並列一部 — レイヤーオフロード経路なしLoRA のみ、「ローカル開発」
KoboldCpp 1.119
llama.cpp フォーク + 画像/音声 · AGPL-3.0
--admin + --routermode手動 --tensor_split❌ 流出する/api/admin/*。MCP クライアントのみ
TextGen(旧 oobabooga)4.9
ExLlamaV3、TRT-LLM を含む5つのローダー · AGPL-3.0
✅ 再起動なしで切替 · TTL ?手動 --tensor-split❌ 流出する/v1/internal/model/*。MCP クライアント
TabbyAPI(ローリング)
ExLlamaV3 のみ — GGUF なし · AGPL-3.0
✅ 管理 + インライン読み込み · TTL ?gpu_split_auto デフォルトオン✅ 実質的に — ExLlama に CPU 経路がない管理キー /v1/model/load
Jan 0.8.4
llama.cpp ルーターモード · Apache-2.0
✅ ルーター経由 · TTL ?llama.cpp 由来❌ 流出する/v1/orchestrations。MCP クライアント
LocalAI 4.9.0
60以上のバックエンドをコンテナイメージとして · MIT
✅ オンデマンド · WATCHDOG_IDLE_TIMEOUT「自動 GPU モデルフィッティング」❌ 「GPU 不要」REST + UI。MCP クライアントのみ
GPUStack 2.2.3
vLLM、SGLang、MindIE、VoxBox · Apache-2.0
一部 — クラスターデプロイ自動 Spread/Binpack、マルチノード?完全なクラスター管理 API

2026-08-23 に一次情報源から照合。疑問符は「不明」を意味し、「いいえ」ではありません。GPUStack のワーカーは Linux のみ。vLLM はネイティブの Windows サポートなし(WSL またはフォークのみ)。

この表が示すこと:

  • ジャストインタイム読み込みとアイドル TTL は新規ではなく、新規だと主張もしません。 LM Studio、Ollama、llama-swap、LocalAI はすべて両方を備え、llama-swap のグループ単位の swap / exclusive / persistent フラグは、本当に洗練されたポリシーエンジンです。
  • 珍しいのは3つ: CPU に流すくらいなら動かないことを選ぶこと(近いのは TabbyAPI だけ。しかもそれは ExLlama に CPU 経路がないからだけ)、実際に見つけたカードに合わせてコンテキストとスロットをサイズ決めするプランナー、そして MCP ツールとして公開された管理 — このカテゴリーで他にやっているものは、私が探した限りありません。
  • 上流で最も近いのは llama.cpp 自身のルーターで、対等な勝負です: プロセス分離によるマルチモデル、無料、すでに手元のバイナリに入っています。欠けているのは、件数ベースではなくメモリベースの退避、ピン留め、リース、そして数値を伴う拒否。アイドルスリープ(--sleep-idle-seconds)はありますが、/metrics のポーリングがそれを起こします。

いくつかの良いアイデアは借用され、リポジトリがどれかを明言しています。Ollama の Modelfile は仮想モデルになり、1つのベースモデル上の2つのペルソナが単一の llama-server を共有し、その keep_alive はリクエストごとの ttl になりました。TextGen の3層設定サーフェスはそのまま採用され、生の「追加フラグ」も含みます。フラグは保存時に固定エンジン自身の --help と照合されます。KoboldCpp の単一アーティファクト哲学が、エンジンをバージョン付きディレクトリに置く理由です。

インストール

Windows(リファレンスプラットフォーム)は4ステップ: GitPython 3.12+uv、最新の NVIDIA ドライバーをインストール。リポジトリをクローンするかダウンロードを展開。launchers\Update StudioForge.bat をダブルクリック — 名前とは裏腹にこれが初回実行ステップで、virtualenv を構築し、ドライバー用のビルドがある最新の llama.cpp リリースをインストールし、スモークテストして固定します(b10425 はこの記事が実測したビルドであり、あなたが入手するものではありません)。それから launchers\Start StudioForge.bat、通知領域に入れたいなら launchers\StudioForge Tray.bat。パネルは http://127.0.0.1:8080 の Setup タブで開きます。

Linux は4行 — さらに cmake と、nvcc がドライバーと一致する CUDA ツールキット。上流がどのタグでも Linux 用 CUDA アーカイブを公開せず、エンジンはバージョンごとにソースから1回ビルドされるためです:

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

ヘッドレスボックス用に、deploy/ は systemd の ユーザーユニットを2つ保持しています — 意図的にユーザーユニットです。プロセスは、モデルライブラリ、venv、GPU デバイスノードを所有するログインユーザーとして実行されなければならないからです。ウォッチドッグは意図的にゲートウェイに BindsTo= されておらず、Restart=always を使います: ゲートウェイが落ちているときに起きているために存在するからです。それから sudo loginctl enable-linger "$USER"ヘッドレス ComfyUI の記事と同じパターンです。初回実行は Setup で開き、LM Studio ライブラリを検出が最初に ~/.lmstudio/settings.jsondownloadsFolder を調べます。

サービスデフォルトポート設定キー
ゲートウェイ — /v1/api/mcp1234server.port
Web コントロールパネル8080gui.port
リカバリーウォッチドッグ1235watchdog.port
llama-server 子プロセス(ループバックのみ)18100–18200gateway.child_port_start / _end

この表が示すこと: 私の2台のマシンではポート 1234 が「ローカルモデルサーバー」を意味し、それは同じものではない — エージェントのマシンはループバックの 127.0.0.1:1234 で自分用のサーバーを動かし、リグはメッシュ VPN 越しに my-gpu-rig:1234 を提供します — そして到達可能なのは3つのポートだけ。設定検証は、任意のサービスポートと子プロセス範囲の衝突を読み込み時に拒否します。

データディレクトリのルールは、まず SF_DATA_DIR、次に --config ファイルのフォルダ、そしてチェックアウト内の <repo>/data — 完全な順序と、なぜ data_dirconfig.yaml に書き戻されないかは docs/SETUP.md にあります。1つのインスタンスが1つのデータディレクトリを所有し、OS の排他ロックで強制されます。2つ目のインスタンスは読み取り専用です。

セキュリティ、正直に

すべての根底にある1つのルール: 読み取り、推論、常駐は開いたまま。ボックスを変更することは開かない。 server.api_key が未設定のとき、ボックスを変更するルートへの変更リクエストは、このマシン上の呼び出し元からか、MCP PIN を X-MCP-Pin またはベアラートークンとして送った場合にだけ受け付けられます — それ以外はすべて 403 remote_admin_requires_credential です。ゲートされる集合は、設定、再起動、エンジン、更新、VRAM 回収、ダウンロード、リース、削除、そしてインスタンスより長生きする2つのモデル単位の書き込みです。これが直した問題は私自身のものでした: LAN 上の誰でも PATCH /api/config でき、自分で server.api_key を設定して私を締め出せました — 一方、同じプロセス内で同じ能力を持つ MCP の set_config ツールは PIN を要求していました。

  • PIN が守るのは MCP だけ。 起動バナーで読み取るペアリングコードで、管理ツールに限定されます。API キーではありません。
  • server.api_key が本当の資格情報で、デフォルトは null 設定すれば /v1/api/mcp ウォッチドッグをカバーします。PIN はその隣の2つの MCP エンドポイントで引き続き機能します。
  • 出荷時のバインドは3つのリスナーすべてで 0.0.0.0 Setup タブのネットワーク公開行は、キーなしでリスナーが1つでも公開されると即座にアンバー色になって必須表示になります — 3つすべてをチェックします。かつてループバックの server.host0.0.0.0gui.host が、パネルが丸裸なのに緑のまま読めたからです。
  • クロスオリジンのブラウザリクエストは、ループバックでも「このマシン」ではない。 cors_origins: ["*"] では、あなたが訪れるどんなページも 127.0.0.1:1234PATCH /api/config をプリフライトして、ローカルに見えるリクエストとして届き得ます — だからオリジン比較にはポートが含まれ、Origin: null は外部扱いです。CORS が決めるのはページが読み取れるものだけで、サーバーが誰を信頼するかは決めません。パネルの WebSocket には同じゲートのホスト専用版があります。パネルは配信元のポート経由で到達するからです。
  • キーなしインストールへのリモートブラウザは、読み取りと推論は得られ、ボックス変更は 403、PIN は伏せられる — そうでなければ LAN 上の何かが開いたエンドポイントから PIN を読んで使えてしまいます。PIN が劇場になったのは、まさにそれが重要だった瞬間でした。
  • 画像は SSRF ガードの下で取得される — ループバック、リンクローカル、プライベート、ULA、そしてCGNAT 空間(すべてのメッシュ VPN ピアがいる 100.64/10 レンジ)をブロックし、1回だけ解決して、元の Host と SNI 付きで検証済みアドレスに接続します。
  • 何も、尋ねられずにボックスを出ない。 外部への呼び出しは、モデル取得のための Hugging Face、固定した llama-server ビルドとその更新チェックのための GitHub、オプトインの StudioForge リリースチェック、そしてリクエストが指定した画像 URL だけ。自己更新は、あなたが update.repo を設定するまで、ネットワーク呼び出しなしで「未設定」と報告します — ユニットテストがそれを固定しています。

隠さずに明言する2つの限界: ピアアドレスチェックはループバック上のものを信頼しますが、リバースプロキシの背後ではそれがプロキシになるので、プロキシを server.api_key の背後に置いてください。そして認証は共有キー1つのみ — アカウントもレート制限もありません。OpenClaw の記事の家訓は今も適用されます: ループバックと認証プロキシ、またはメッシュ VPN。素のインターネットは決して。

正直な部分

インストールする前に判断できるよう、率直に書き下ろします:

  • Windows がリファレンスプラットフォーム — トレイ、ジョブオブジェクトの VRAM ガード、プロセスごとの GPU カウンターはすべてこれに向けて作られました。Linux はサポートされ、CI は両方で走りますが、実戦での鍛えられ方は劣ります。ソースビルド経路はコマンド構築がテストされていますが、ここでエンドツーエンドに実行されたことはありません。macOS は非対応: CUDA がないからです。
  • NVIDIA のみ。 プランナーは NVML を読み、エンジンは CUDA ビルドで、量子化の相性はコンピュートケーパビリティで表現されます。
  • VRAM 推定は推定である。 重みはファイルサイズの2%以内に収まり、KV はレイヤーごとの形状から正確ですが、コンピュートバッファはキャリブレーションされた割合であり、起動時に一度調整され、0.03〜0.15 にクランプされ、メモリにだけ保持されます — だから悪いキャリブレーションは再起動でやり直せます。このボックスの2つのキャリブレーション履歴は汚染され、現在は完全に無視されています。
  • マルチ GPU 分割は比例配分であり、実測ではない。 インターコネクト帯域幅をモデル化せず、世代混在は遅いカードのペースで走ります。
  • 同時実行の推定は算術である。 推定器はスロットが半分埋まると仮定し、MoE モデルを一律に半分に割り引き、--ctx-checkpoints はまったくモデル化されません。両方の誤差は少なめのスロットを指す — 安全な方向です — ただし 8 を信頼する前に並列ベンチマークを実行してください。
  • 速度推定は名目上のベンダー数値を使う — 5090 は 1792 GB/s と 209 fp16 TFLOPS、3090 は 936 と 71。ここでは何も実測していません。キャリブレーションの基準点は正確に2つあり、どちらもこのリグ上で、どちらも1スロット: 高密度 31B が推定 36.1 に対して 39.4 tok/s を実測し、122B MoE が 47.4 に対して 37.3 を実測。4スロットや8スロット、または 31B を超える高密度モデルでは何も検証されていません。
  • 別のプログラムのための GPU 予約は、私のプランナーを制約するだけ。 相手のプログラムに対しては何も強制せず、相手が先にメモリを取るのを止めるものもありません。
  • データディレクトリごとに1インスタンス、 そしてロックがカバーするのはモデルライブラリではなくデータディレクトリ — 1つのライブラリ上で異なるデータディレクトリを持つ2つのインスタンスは、依然として2つの書き手です。
  • ライセンスはまだ選ばれていない。 意図的に LICENSE ファイルはなく、pyproject.toml がコメントでそう述べています — 入手方法に、それが実際には何を意味するかがあります。
  • 第三者によるセキュリティ監査はない。 上記の主張はコードが何をするかを述べたもので、両方書いたのは私です。ソースを読んでください — それがダウンロードであってサービスでない理由です。

このボックスで2,503個のユニットテストが通り、17個がスキップされ、327秒。CI は同じスイートを Windows と Ubuntu で、GPU プローブをヌルバックエンドに強制して実行します。2つ目のスイートは実 GPU に実重みを読み込み、デフォルトで選択解除されている上に環境変数の背後にゲートされています — 先ほどの孤児インシデントの後の、二重の安全策です。やっていないこと、またはオンにしていないこと: 8スロットのマイクロバッチ A/B、名前付きサンプラープリセット、CI での mypy。アプリの自己更新は書かれていますが、あなた自身が update.repo を設定するまでオフのままです。

落とし穴

正直なリスト — 実際に刺さったものたちを、かかった時間の大まかな順で:

  • 1234 の LM Studio は先に終了しなければならない。 両方はポートを保持できません。事前チェックがバインドのトレースバックを印刷する代わりに保持者を名指しし、server.port で移動できます。ライブラリは共有しても問題ありません。
  • 2つの llama.cpp フラグは、見た目通りの意味ではない。 --ctx-size はスロットあたりではなく全スロットにまたがる予算。--fit は上流でオンがデフォルトで、--n-gpu-layers auto と並びます — どちらも上記、プランナーの節で。
  • 推論モデルは --reasoning-format auto の下で空の返信を返す。 同じプロンプトで、フラグだけが違います: auto では content が0文字、reasoning_content が316。none では content が323。reasoning_content は OpenAI スキーマにないため、標準クライアントは空文字列を読んで「モデルは何も言わなかった」と結論します。私は 31B を deepseek で動かしています — 私のクライアントがそのフィールドを読むからです。それ以外はすべて none です。
  • llama.cpp の vX.Y.Z プレリリースは Windows 用 CUDA アセットを載せない。 v0.1.2 とタグ付けされた1つが、プリビルドアーカイブがまったくない普通の bNNNN ビルド2つの上に座っていて、Server タブは絶対に失敗するだけのボタンの背後に更新を用意していました。今はタグが ^b\d+$ にフィルタリングされています。
  • パネルでブラウザタブを開いたまま、トレイのルートをツリーキルしてはいけない。 venv ランチャースタブの下ではウォッチドッグはサーバーのプロセスで、かつて1回の再起動が、サーバー死・ウォッチドッグ死・何も起動されない、という結末を迎えました。トレイメニュー、パネル、または sfctl recover --restart を使ってください。
  • 別のツールからの pkill -f llama-server は、あなたのバックエンドを殺す。 bench-llm は実行の合間にまさにそれを行います — 上記、ハーネスクライアントの節で。
  • MCP PIN は API キーではない が、キーなしサーバーでも一部のクライアントはプレースホルダーの資格情報を必要とします — 上記、dsh の節で。
  • OpenClaw の MCP キーは、mcp の下に入れ子になる mcp.servers フラットな mcpServers マップは、そのスキーマが知るキーではありません — 上記、OpenClaw の節で。
  • 2つの測定の近道はあなたに嘘をつく。 投機的ベンチマークで1つのプロンプトを繰り返すとプロンプトキャッシュを計測します(+751% 対 +0.4%)。レイヤー × ヘッド × コンテキストの KV 式は Qwen3.5 で4倍ずれます — どちらも上記、プランナーの節で。/props も、ドラフト中は speculative.types: "none" と報告するので、本物の補完から timings.draft_n を読んでください。
  • ビジョンモデルはプロンプトキャッシュの恩恵を受けない。 llama.cpp 自身がマルチモーダルモデルのキャッシュ再利用を無効にし、各画像は mmproj メタデータが別のことを言わない限り 1,024 トークンに予算化されます — 8k ウィンドウがほとんど画像になるには十分な量です。

入手する

下の zip がすべてです: タグ付けされたソースツリー、テスト、ドキュメント、ランチャー、systemd ユニット、さらにビルドステップなしでインストールできるよう wheel と sdist の両方を保持する dist/ フォルダ。studioforge-2026-08.zip — リリース v1.26-08-23、4,238,728 バイト(4.04 MiB)、SHA-256:

84f4f828b5c75206236890f28e8651c96146a7bb39c14e21b13a0922a13f7d3f  studioforge-2026-08.zip

1つのトップレベルディレクトリの下に226エントリ。コミット 0610446 の注釈付きタグ v1.26-08-23 から git archive で作られているため、追跡されたファイルしか含めません — config.yamldata/ もローカル上書きもありません。ソースは github.com/LaserLloyd/StudioForge にもあります。必要なのは Python 3.12+、uv、580系以降の NVIDIA ドライバー、GGUF のフォルダ。ライセンス: まだ未選択なので、形式的には全著作権が留保されます — 実際には、このサイトの他のダウンロードと同じ扱いで: 個人利用は無料、商用利用したければ私に聞いてください。

何かが壊れたら、私にメールしてください — アドレスはAbout ページにあります — 設定ではなく失敗の形を送ってください: error.code、507 から出た数値、logs/models/<model>.log の最後の20行。PIN やキーは決して送らないでください。そして、統合ビンパッキングプランナー、名前付きサンプラープリセット、または実際に動く AMD 経路を私より先に作ったなら、私が自分のを書くよりあなたのをマージしたいです。

このプロジェクトが私に残したもの

私が予想していなかったのは、この多くがコードではなく実測になったことです。プランナーは誰でも書ける算術です。それを信頼できるものにしたのは、公式を読む代わりに llama.cpp 自身の KV 形状を読み、推定器が8と言ったときにスロット数の変曲点を2で実測したことでした。あの記録の中のほとんどすべての決定は、信念と一致しない数値として始まりました。すでに 1234 で LM Studio を動かしているなら、この実験全体は、クローン1つ、バッチファイル1つ、models.dir をすでに持っているフォルダに向けるだけで済みます — そして午後1つで居場所を勝ち取れなければ、あなたの古い環境は無傷です。

関連: DeepSeek Harness (dsh)(このサーバーが最初に登場する場所。説明のない provider ブロックとして)、bench-llm(リグのトークン毎秒の数値の出所であり、あなたのバックエンドを殺すツール)、私の OpenClaw セットアップ(この問題に名前を付けた記事)、そして DisPatch(その前面にあるチャットアプリ)。

ダウンロード

個人利用は無料です。役に立ったら、コーヒーをおごってもらえると嬉しいです。


← AIとローカルLLMをもっと見る