StudioForge: 私のリグで LM Studio を置き換え、エージェントがリモートから操作する GPU オンリーの LLM サーバー
- カテゴリ
- AIとローカル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のやりくり」— そして以下がその答えであり、すべてのルールに、それを強制した実測値が添えられています。
要約
- 何か: llama.cpp の
llama-server上で動く GPU 専用・OpenAI 互換の LLM サーバー。ビルドb10425(CUDA 13.3)で実測。ゲートウェイが1234、コントロールパネルが8080、ウォッチドッグが1235、読み込み済みモデルごとに1つの子プロセスが18100–18200。 - できること: 最初の使用時にモデルを読み込み、その瞬間の空き VRAM に照らしてコンテキスト、KVキャッシュ種別、GPU配置、スロット数を計画します。アイドル状態ならアンロードし、ピン留めされたモデルは常駐させ、リクエストがあればカード全体を1つのモデルに渡し、MCP 経由で 29個のツールを公開します — 管理19個、リカバリー10個。
- 絶対にしないこと: CPU への流出(モデルは VRAM に完全に収まるか、数値付きで拒否されるかのどちらか)、ホームへの通信(外部への呼び出しは、モデル取得のための Hugging Face、固定した
llama-serverビルドとその更新チェックのための GitHub、オプトインの StudioForge リリースチェック、そしてリクエストが指定した画像 URL のみ)、さらに MCP 経由の推論 — コントロールプレーンには補完ツールがなく、大文字でその旨を明言しています。 - 必要なもの: 580系以降のドライバー上の NVIDIA GPU、Python 3.12+、uv、GGUF のフォルダ。モデルをローカルで動かしたことがない? こちらのガイドから始めてください。
- 手に入るもの: ネットワーク上のすべての OpenAI クライアントが変更なしで使う1つのベースURL、各カードの全ギガバイトが何に握られているかを名指しするパネル、そして「27B を 128k で 5090 2枚に読み込んで」と言えばそれが実際に起きるアシスタント。
- 正直なところ: Windows がリファレンスプラットフォームで、NVIDIA のみ対応、そして ライセンスファイルはまだありません — 先に 入手方法を読んでください。
何をするのか、ひと目で
これは何ではないか:
- モデルではない。 あなたが持っている 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 のまま。その間もstatusはokのままです — プロセスが生きているからで、死活監視ポーラーが尋ねるのはまさにそれだからです。GET /health?deep=trueは読み込み済みの全モデルに対して実際の8トークン補完(埋め込みモデルなら埋め込み呼び出し)を実行します — そして何も読み込まれていないときは、成功を返す代わりにno_models_loadedと答えます。失敗し得ないプローブは、プローブがないより悪いからです。local-modelエイリアスが解決される。local-model、default、auto、currentはすべて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_kind は general.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/s | 2803.6 tok/s |
2枚、-sm layer | 344.4 tok/s | 2722.5 tok/s |
2枚、-sm tensor | 294.3 tok/s | 1182.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 で起動します。そして問題は、スロットを何個持つ価値があるか — ここで私は自分の算術を信用するのをやめました。
| 同時接続 | ストリームあたり | 合計 | p50 | p95 | 実効バッチ |
|---|---|---|---|---|---|
| 1 | 302.8 tok/s | 302.8 tok/s | 0.41 s | 0.41 s | 1.00 |
| 2 | 225.3 tok/s | 425.3 tok/s | 0.46 s | 0.49 s | 1.84 |
| 4 | 134.5 tok/s | 436.0 tok/s | 0.83 s | 1.00 s | 3.46 |
| 8 | 83.3 tok/s | 576.9 tok/s | 1.57 s | 1.77 s | 6.03 |
Qwen2.5-1.5B-Instruct-Q4_K_M、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-mtp は 50.70 tok/s、+34.3%、受理率 0.528。深さ4は下がって 47.48 — 受理率が 0.446 に落ち、却下された各トークンの検証が無駄になるからです。ngram-mod は +0.4% にとどまり、ドラフトを1つも出しませんでした。
ここに罠があります: 同じプロンプトを3回繰り返すと、その 27B で +751% を計測しました。1つのプロンプトを繰り返せば、あなたはプロンプトキャッシュを計測してドラフトと呼んでいることになります。4スロットを超えると auto は none を返し、その理由を言います — "投機は単一ストリームでの勝利であり、飽和したバッチを傷つける" — これは、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 子プロセスでした。現在すべての保持者は分類されます — ours、child-of-live-process、orphan、other-instance、foreign — そして殺されるのは 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個のツールは health、get_config、set_config、restart_server、kill_model、nuke_all_models、reclaim_orphan_engines、tail_logs、gpu_status、rollback_update です。孤児ルールを、それを所有するモジュールをインポートするのではなくローカルで再実装しています — リカバリープロセスは、自分が修復するスタックをインポートしてはいけないのです。
コントロールパネル
8080 のパネルは同じプロセス内の2つ目の uvicorn サーバーで、ゲートウェイのオブジェクトグラフを参照で共有しています — 中には絶対 URL がまったくありません。これが、メッシュ VPN 上の素の HTTP と HTTPS フロントエンドの背後で、まったく同じように動く理由です。
9.7 tok/s のラベルはこのタブ自身の算術です — リクエストが送信された瞬間からの実時間に対するストリームチャンクで、プリフィル込み — エンジンの生成タイミング(モデルカードの 58.77 tok/s)ではありません。ここでのチャット成功は、クライアントが動く証拠であり、ずれ得るモック経路ではありません。
sfctl にシェルアウトするため、ペアリング PIN は設定ファイルに一切現れる必要がありません。
同じバッファのさらに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 と暴き、どのカードでなぜかを言っているところです — 出力レイヤーは最後のデバイスに載り、まさにそこに計上されたのに、それでも計上不足だったのです。
ハーネスのバックエンドとして使う
私のネットワーク上の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_slot、max_parallel、parallel_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_config と set_config はゲートウェイのツールと衝突するため、health は対称性のため。restart_server は素の名前のままです。死んだ管理ツールのエラーメッセージが、エージェントに呼ぶよう指示する名前がそれだからです。メインサーバーがダウンしていても、ブリッジは19個の管理ツールすべてを注記付きで広告し続けます — load_model が見えないエージェントは、その能力が存在すること自体を知らないからです。ブリッジが回すループ:
list_models(limit=N)— カタログ、新しいダウンロード順。推奨行を読んでください。load_model(**row["load_args"])— 変更せずにそのまま渡す。行を選んだエージェントは、選ぶ作業を終えています。load_recommended(model_id, ctx_size=N)— 知っているのが必要なコンテキストだけのとき。縮小ではなく拒否する唯一の読み込み経路です。- 推論は MCP ではなく HTTP で。 未読み込みモデルを名指しすると読み込まれますが、読んでいた行ではなくプランナーのデフォルトで読み込まれます。
model_options(model_id)— 推奨行では足りないとき: 速度付きのすべてのコンテキスト段階。search_models→repo_details→download_model— 新しいものを手に入れるために。pin_model— 常に応答する必要があるモデルのために。reserve_gpus/release_gpus— 自分のカードのために。server_statusとconnection_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つのワンライナー
dsh(DeepSeek Harness)は、1つの YAML ファイルを編集してモデルを切り替えます。次のリクエストのためにホットリロードされます。provider ブロックは camelCase — baseURL、apiKeyEnv:
# ~/.dsh/settings.yaml
llm-pi-ai:
providers:
gpu-rig:
displayName: StudioForge (GPU rig)
apiKeyEnv: STUDIOFORGE_PLACEHOLDER_KEY # a reference, not a value
api: openai-completions
baseURL: http://my-gpu-rig:1234/v1
defaultContextWindow: 131072
compat:
supportsDeveloperRole: false # many local servers reject role: "developer"
maxTokensField: max_tokens
models:
- id: <id from /v1/models>
apiKeyEnv は参照であり、リテラルではありません — そしてキーなしのローカルサーバーでも、何らかの資格情報を参照する必要があります。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 WebUI は OPENAI_API_BASE_URL。LibreChat は baseURL と models.fetch: true の custom エンドポイント。aider は OPENAI_API_BASE に続けて --model openai/<id>。Continue は provider: openai プラス apiBase。ベースURLキーの綴りはどれも違っており、このエコシステムで夜を無駄にする最も確実な原因です。
エージェントが選ぶもの
カタログはモデル選択を推測ではなく照会にします: 新しいダウンロード順にソートされ、コンテキスト段階ごとに1行。各行は、リアルタイムの空き VRAM に対する fits、devices、KV タイプ、max_parallel、recommended_parallel、confidence、if_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 5090 | 2× RTX 5090 | 4枚すべて |
|---|---|---|---|
| BF16 (51.8 GiB) | — 重みだけで収まらない | 32k(q8_0) | 256k |
| Q8_0 (27.9 GiB) | — | 256k | 256k |
| Q5_K_M (19.3 GiB) | 128k(q8_0) | 256k | 256k |
| IQ2_M (10.5 GiB) | 256k | 256k | 256k |
リポジトリの 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.21 | StudioForge 1.26-08-23 | |
|---|---|---|
| エンジン | 独自 llama.cpp ビルドと Apple 上の MLX | 上流の llama-server、固定ビルド1つ(b10425、CUDA 13.3)、有効化前にスモークテスト済み |
| ルーティング外のパス | エラーボディ付き 200 — ログは Returning 200 anyway と言う | JSON エンベロープ付き 404、すべてのステータスで JSON |
| エラー | クライアントが正規表現で照合する非構造化散文 | 安定した error.code、error.studioforge の下の診断情報 |
| 読み込み設定 | 2つの読み込み経路の1つで context_length が無視される。repetition_penalty は黙って無視される | 読み込み経路は1つ、すべてのフィールドが尊重され、実効値がエコーされる。サンプラーエイリアスも受け付ける |
| 収まらないとき | 「GPUオフロードサイズを自動的に減らし … 残りはシステムRAMに入れる」— 設計による無言の CPU 流出 | 必要・利用可能バイト、GPUごとの空き、収まる最大コンテキスト、順序付き提案を伴う 507 insufficient_vram |
| マルチ GPU | 優先または均等分割、GPUごとのトグル、0.4.15 以降テンソル並列 | 配置ごとにコンテキスト・KV タイプ・スロットをサイズ決めするプランナー、出力レイヤー向けに分割比率を傾ける |
| アイドル TTL | 60分。自動退避はジャストインタイム読み込みモデルを最大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つまで | 優先/均等分割、テンソル並列 | ❌ オフロードを減らし、残りは RAM | REST。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 | ✅ 製品全体がこれ · モデル/グループ単位の ttl | ❌ cmd 次第 | 該当なし — プロキシのみ | /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ステップ: Git、Python 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.json の downloadsFolder を調べます。
| サービス | デフォルトポート | 設定キー |
|---|---|---|
ゲートウェイ — /v1、/api、/mcp | 1234 | server.port |
| Web コントロールパネル | 8080 | gui.port |
| リカバリーウォッチドッグ | 1235 | watchdog.port |
llama-server 子プロセス(ループバックのみ) | 18100–18200 | gateway.child_port_start / _end |
この表が示すこと: 私の2台のマシンではポート 1234 が「ローカルモデルサーバー」を意味し、それは同じものではない — エージェントのマシンはループバックの 127.0.0.1:1234 で自分用のサーバーを動かし、リグはメッシュ VPN 越しに my-gpu-rig:1234 を提供します — そして到達可能なのは3つのポートだけ。設定検証は、任意のサービスポートと子プロセス範囲の衝突を読み込み時に拒否します。
データディレクトリのルールは、まず SF_DATA_DIR、次に --config ファイルのフォルダ、そしてチェックアウト内の <repo>/data — 完全な順序と、なぜ data_dir が config.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.hostと0.0.0.0のgui.hostが、パネルが丸裸なのに緑のまま読めたからです。 - クロスオリジンのブラウザリクエストは、ループバックでも「このマシン」ではない。
cors_origins: ["*"]では、あなたが訪れるどんなページも127.0.0.1:1234でPATCH /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.yaml も data/ もローカル上書きもありません。ソースは 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(その前面にあるチャットアプリ)。
ダウンロード
- StudioForge ソースをダウンロード(zip) 4.0 MB
個人利用は無料です。役に立ったら、コーヒーをおごってもらえると嬉しいです。