Metadata-Version: 2.4
Name: dgxllm
Version: 0.3.0
Summary: Run large LLMs across two NVIDIA DGX Sparks with vLLM — model picker, one-command start/stop, and an Anthropic-compatible endpoint for Claude Code.
Project-URL: Homepage, https://github.com/javasparrows/dgxllm
Project-URL: Issues, https://github.com/javasparrows/dgxllm/issues
Author: Yuki Kashiwada
License-Expression: MIT
License-File: LICENSE
Keywords: claude-code,dgx-spark,gb10,llm,tensor-parallel,vllm
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.28
Requires-Dist: pyyaml>=6.0
Requires-Dist: questionary>=2.0
Requires-Dist: rich>=13.9
Requires-Dist: typer>=0.15
Requires-Dist: uvicorn>=0.32
Description-Content-Type: text/markdown

# dgxllm

Run large LLMs across **two NVIDIA DGX Sparks** with vLLM — pick a model from a menu,
start it with one command, and use it from **Claude Code**.

```
$ dgxllm start

? 使用するモデルを選択してください:
❯ deepseek-v4-flash-0731  (TP=2, 1M ctx, KV fp8_ds_mla, spec k=5)
  qwen3.6-27b-nvfp4       (TP=2, 32K ctx)
  gemma-4-31b-it-nvfp4    (TP=2, 32K ctx)

deepseek-v4-flash-0731 を 2 ノードで起動します...
```

```
$ dgxllm claude      # Claude Code をこのモデルで起動
```

---

## なぜ必要か

DGX Spark を 2 台つないで大きなモデルを動かすのは、やってみると設定の落とし穴が多い。
`dgxllm` は実機で踏んだ落とし穴を最初から回避する。

| 落とし穴 | dgxllm の対処 |
|---|---|
| **1 つの物理 QSFP ポートが 2 つの論理 IF に見える。** 片方だけ `NCCL_IB_HCA` に渡すと約 100 Gbit/s で頭打ち | `phys_port_name` で同一物理ポートのレールを全部集めて渡す |
| **RoCEv2 の GID index はノードごとに違い、再起動でずれる。** 固定値を共有すると NCCL が初期化時に固まる | IPv4 が埋まった GID を sysfs から毎回解決する |
| **`--master-addr` を渡さないと `127.0.0.1` になりワーカーが繋がらない** | 常に明示的に渡す |
| **ヘッドを先に起動すると `mp` 初期化のレースで掴み損ねる** | ワーカーを先に起動する |
| **API キーを渡し忘れると LAN 上の誰でもアクセスできる** | キーを自動生成し、起動後に「キー無しで 401 か」を検証する |
| **ログインシェルが fish だと `ssh host "bash構文"` が壊れる** | スクリプトを常に stdin から渡す (`ssh host bash -ls`) |
| **GB10 は GPUDirect RDMA 非対応。** ログに GDRDMA が出なくても正常 | `NCCL_NET_GDR_LEVEL=0` を明示 |
| **同梱の NCCL プラグインが GB10 で RoCE を使っているように見えて性能が出ない** | `NCCL_NET_PLUGIN=none` |

---

## インストール

```bash
uv tool install dgxllm
```

試すだけなら:

```bash
uvx dgxllm --help
```

Mac / Linux のどちらからでも使える。**ノード側に入れる必要はない** — SSH 経由で操作する。

---

## セットアップ

### 1. クラスタを登録する

```bash
dgxllm init
```

SSH ホストとノード間リンクの IP を訊かれる。実機を見に行って GPU・メモリ・
ファブリック構成（物理ポートと論理 IF の対応、RoCE デバイス、MTU）を検出し、
API キーを生成する。

前提: 各ノードへ**パスワードなし SSH** が通り、`docker` が使えること。

### 2. モデルを登録する

```bash
dgxllm model add deepseek-ai/DeepSeek-V4-Flash-0731 \
  --name deepseek-v4-flash-0731 \
  --max-model-len 1048576 \
  --kv-cache-dtype fp8_ds_mla \
  --speculative-tokens 5
```

引数を省略すると対話的に訊かれる。

> **モデル名にスラッシュは使えない。** vLLM の served model name がそのまま
> Claude Code の model picker に出るため。

### 3. 起動する

```bash
dgxllm start          # 選択画面が出る
dgxllm start <name>   # 直接指定
```

起動前に「SSH で届くか」「イメージがあるか」「モデルがキャッシュにあるか」を
全ノードで確認してから始める。

### モデルを切り替える

```bash
dgxllm switch         # 選択画面。起動中のモデルには [起動中] と出る
dgxllm switch <name>
```

対象 deployment を止めてから新しいモデルを起動する。2 ノードとも使う
大きいモデル1つだけの構成なら、これで「今動いているモデル」を丸ごと
入れ替えることになる (同じノード集合を使う限り同時起動はできない)。
ノードを分けて複数のモデルを並行して動かす場合は、後述の
[複数の deployment を同時に動かす](#複数の-deployment-を同時に動かす)
を参照。

> Claude Code を開いている場合は、モデル名が変わるので `dgxllm claude` で
> 起動し直す必要がある。

---

## コマンド

複数の deployment (起動中のモデルの組) を同時に扱える前提のコマンド体系になっている。
`ps`/`stop [name]`/`switch [name]`/`claude [--name]` はどれも「どの deployment を
対象にするか」を選ぶ操作で、名前を省略すると対話的なピッカーが出る
(deployment が1つしか無ければピッカー無しでそれを使う)。

| コマンド | 説明 |
|---|---|
| `dgxllm init` | クラスタを対話的に設定する |
| `dgxllm start [name] [--nodes ...] [--name ...]` | deployment を起動する (モデル選択 → ノード選択 → 起動) |
| `dgxllm ps` | 起動中の deployment を一覧する |
| `dgxllm stop [name]` | deployment を停止する |
| `dgxllm switch [name]` | deployment を選び、モデルとノードを選び直して切り替える |
| `dgxllm logs [name] [-f]` | deployment のヘッドノードのログを表示する |
| `dgxllm claude [--name/-n name]` | この endpoint を backend にして Claude Code を起動する |
| `dgxllm dashboard [--host] [--port] [--open]` | Web ダッシュボードを起動する |
| `dgxllm model list` | 登録済みモデルプロファイルを一覧する |
| `dgxllm model add [hf_repo]` | モデルプロファイルを追加する |
| `dgxllm model remove <name>` | モデルプロファイルを削除する |

> **`dgxllm status` は廃止された。** 単一 deployment 前提のコマンドだったが、
> 複数 deployment を扱えるようにした際に `dgxllm ps` へ統合した
> (`ps` は「どの deployment がどのノードで動いているか」を表で見せる)。
> 呼ぶとその場でエラーになり、`ps` を使うよう案内が出る。

各コマンドの正確なオプションは `--help` で確認するのが確実
(例: `dgxllm start --help`、`dgxllm model add --help`)。

---

## Claude Code から使う

```bash
dgxllm claude
```

vLLM は **v0.11.1 から Anthropic 互換の `/v1/messages` を持つ**ので、
LiteLLM や claude-code-router のような変換プロキシは要らない。

`dgxllm claude` は「その起動だけ」に環境変数を効かせるため、
**別ターミナルで動いている通常の claude.ai セッションには影響しない。**

### 制約

- **動作中のセッションを `/model` で切り替えることはできない。** `ANTHROPIC_BASE_URL` は
  プロセス起動時に一度だけ読まれる
- **Anthropic モデルとの同一セッション併用は不可。** `ANTHROPIC_BASE_URL` を設定すると
  Anthropic モデルは無効になる
- Anthropic は非 Claude モデルを gateway 経由で使う構成を**公式にはサポートしていない**

---

## 設定ファイル

`~/.config/dgxllm/config.yaml`

```yaml
nodes:
  - ssh_host: dgx-spark-1
    fabric_ip: 192.168.100.10
  - ssh_host: dgx-spark-2
    fabric_ip: 192.168.100.11
models:
  - name: deepseek-v4-flash-0731
    hf_repo: deepseek-ai/DeepSeek-V4-Flash-0731
    min_nodes: 2
    max_model_len: 1048576
    kv_cache_dtype: fp8_ds_mla
    speculative_tokens: 5
default_image: ghcr.io/anemll/dspark-vllm-gx10:0.1.1
port_base: 8888
master_port_base: 25000
```

> **`tensor_parallel` という設定キーは存在しない。** TP サイズはモデルごとに
> 固定するものではなく、`dgxllm start` のたびに選んだノード数からその場で
> 決まる (2 ノードなら TP=2)。モデル側が持つのは「これより少ないノードでは
> 載らない」という下限の `min_nodes` だけ。`port`/`master_port` も
> v0.1.0 時点のキー名で、`port_base`/`master_port_base` に改名されている
> (複数 deployment を同時に扱うようになり、実際に使うポートは
> `port_base`/`master_port_base` を起点に deployment ごとへずらして
> 割り当てるため)。**旧キーで書かれた設定ファイルはそのまま読める** —
> 読み込み時に自動で新しいキー名へ読み替えるので、手で書き換える必要はない。

API キーは `~/.config/dgxllm/api-key` (mode 600) に分離してある。
設定ファイルをそのまま共有しても鍵は漏れない。

---

## 複数の deployment を同時に動かす

`dgxllm` はノードの部分集合ごとに独立した deployment を起動できる。
たとえば 2 ノードを全部使う大きいモデルを動かす代わりに、ノードを 1 台ずつに
分けて別々の小さいモデルを載せる、といった使い方ができる。

```bash
dgxllm start deepseek-v4-flash-0731 --nodes dgx-spark-1,dgx-spark-2 --name big
dgxllm start qwen3.6-27b-nvfp4       --nodes dgx-spark-1             --name small
```

- 同じノードを複数の deployment で取り合うことはできない (使用中のノードを
  選ぶとエラーになる)
- `--name` を省略すると deployment 名はモデル名になり、衝突時は `-2`/`-3` が
  付く
- `dgxllm ps` は全 deployment を一覧する。`dgxllm stop [name]`/`switch [name]`/
  `claude --name` は名前で対象を選ぶ (deployment が1つしか無ければ名前無しで
  そのまま使える)

> Claude Code の endpoint は deployment ごとに別ポートで立つため、
> どの deployment 向けに `dgxllm claude` を起動したかを覚えておくこと。

---

## Web ダッシュボード

```bash
dgxllm dashboard          # http://127.0.0.1:8000 で待受
dgxllm dashboard --open   # 起動後にブラウザで自動的に開く
```

ノードのリソース使用状況 (GPU/CPU/メモリ/温度) と、動いているモデルの
メトリクス (KV キャッシュ使用率、トークン生成レート等) をブラウザで見られる。
一覧・グラフ表示だけでなく、その場で deployment の起動・停止・切替もできる
(`dgxllm start`/`stop`/`switch` を CLI から打たなくてよい)。

- ノード側から2秒間隔でテレメトリを収集し、SSE (Server-Sent Events) で
  ブラウザへ配信する。タブを開いたままにしておけば自動的に更新され続ける
- 起動・停止・切替は SSE でログをリアルタイムに追える (`dgxllm start` の
  出力をブラウザで見ている状態に近い)
- ノードが1台落ちても、そのノードのカードだけ「到達不能」と表示され、
  残りのノードの更新は止まらない

### セキュリティモデル

ダッシュボードは操作ひとつで動作中のクラスタを止められるため、CLI とは
別に3層の防御を持つ。

- **起動ごとに使い捨てのトークンを発行する。** `dgxllm dashboard` の出力に
  `http://127.0.0.1:8000/?token=...` という URL が出るので、これをそのまま
  ブラウザにコピペして開く。この URL に含まれるトークンはプロセスを
  再起動すると変わる (Jupyter Notebook と同じ方式)
- **トップページを開くと Cookie が発行され、以後 URL のトークンは不要になる。**
  Cookie を受け取った時点でクエリ無しの `/` へリダイレクトするため、
  トークンがブラウザのアドレスバー・履歴に残り続けることもない
- **既定では `127.0.0.1` (ループバック) にしかバインドしない。** 同じマシン
  以外からは端から接続できない
- **`--host 0.0.0.0` で LAN に公開できるが、これを指定すると `Host`/`Origin`
  ヘッダの検証が両方とも無効になる。** ワイルドカード bind では「正規の
  接続元がどの LAN アドレス/ホスト名で来るか」を事前に確定できないため
  (DHCP で変わる、`.local` 名で来る、コンテナ越し等)、この2層は最初から
  検証しようがない。**その状態ではトークンだけがクラスタを守る唯一の層に
  なる** ので、`--host 0.0.0.0` は信頼できる LAN でのみ使うこと。ダッシュボード
  自身もこの旨を起動時にログへ警告として出す

---

## リモートから使う

`dgxllm dashboard` は既定では `127.0.0.1` にしかバインドしないため、Mac の
外 (スマホ・別の端末) からは見えない。Mac がスリープしていても常に見える
形で運用したい場合は、ヘッドノード上で `dgxllm dashboard` を systemd
ユーザーユニットとして常駐させ、`tailscale serve` の背後に置く構成が使える。
ノード常駐の具体的な手順とハマりどころは
[`docs/deploy/node-setup.md`](docs/deploy/node-setup.md) にまとめてある。
ここでは、その構成を理解するのに必要な2つの起動オプションと、
やらないと決めたことを書く。

### `--public-origin` と `--token-command`

- **`--public-origin`**: `tailscale serve` (や他のリバースプロキシ) は TLS
  を終端したあと平文 HTTP に変換してアプリへ転送し、`Host` ヘッダも書き換
  えない。つまりアプリはリクエストそのものからは「自分が https で外部公開
  されているか」を知りようがない。この判断材料を起動時の設定として明示的
  に渡すのが `--public-origin` (例: `https://<head>.<tailnet>.ts.net`) で
  あり、これによって `Host`/`Origin` の許可集合と Cookie の `Secure` 属性
  が決まる。
- **`--token-command`**: 既定の起動トークンは毎回ランダム生成される。
  フォアグラウンドで都度起動するだけなら問題ないが、systemd で常駐させる
  と再起動のたびにトークンが変わり、運用者が保存したブックマークが黙って
  死ぬ。`--token-command` は `cat ~/.config/dgxllm/dashboard-token` のよう
  な外部コマンドの標準出力を起動トークンとして使う口で、これを使えば
  トークンを固定できる。**コマンドの実行に失敗した場合はランダムトークン
  へフォールバックせず、起動そのものを中止する** (黙って別のトークンで
  動き続けるより、気付ける形で止まるほうを選んでいる)。

### HTTPS 証明書の有効化は不可逆

`tailscale serve` を使うには Tailscale 管理コンソールでそのノードの HTTPS
証明書を先に有効化しておく必要がある。**この有効化は取り消せない** —
有効化した時点で、そのノードのマシン名が Certificate Transparency (CT)
ログに永久公開される。個人が特定できるマシン名を付けている場合は、
有効化する前に見直すこと。

### `tailscale funnel` は使わない

`funnel` は tailnet の外・インターネット全体に公開する機能であり、この
dashboard は起動ひとつでクラスタを止められる操作をトークン1枚で守っている
ため、到達範囲を tailnet メンバーに限定する設計を最初から前提にしている。
`funnel` を使うとその前提そのものが崩れるため使わない。

### `dgxllm` から `tailscale` は呼ばない

`dgxllm` のコードから `tailscale` コマンドを直接呼ぶ実装はしない。VPN
ベンダ1社のツールをコード側に焼き込むことになるため、この判断は変えない
(後から「自動化しよう」と言い出さないための記録)。

---

## セキュリティ (vLLM API)

- API キーは自動生成され、**起動後に「キー無しアクセスが 401 になるか」を必ず検証する**。
  ならなければエラーで終了する
- 既定の bind は `0.0.0.0`。LAN 内から使う想定
- **ルーターでポートを開けないこと。** vLLM は単一の静的キーしか持たず、
  レート制限も監査ログもない。外から使うなら Tailscale か SSH トンネルを使う

---

## 動作確認済みの構成

- 2× DGX Spark (GB10, sm_121, ARM64, Ubuntu 24.04)
- 1 本の 200GbE ConnectX-7 QSFP DAC 直結
- `deepseek-ai/DeepSeek-V4-Flash-0731` を TP=2、1M コンテキスト

実測 (64K コンテキスト): TTFT 34.05 秒 / prefill 1,926 tok/s / decode 61.8 tok/s

> ベンチマークを取るときは **必ず 2 回以上回して 2 回目以降を採用する**こと。
> vLLM は初回リクエストで Triton カーネルを JIT コンパイルすることがあり、
> TTFT が 8 倍以上変わる。ログの `jit_monitor` 警告で確認できる。

---

## 謝辞

2 ノード DGX Spark で DeepSeek-V4-Flash を動かす方法は、以下の先行事例に多くを負っている。

- [MiaAI-Lab/DeepSeek-v4-Flash-DSpark-2x-DGX-Spark](https://github.com/MiaAI-Lab/DeepSeek-v4-Flash-DSpark-2x-DGX-Spark)
- [tonyd2wild/DeepSeek-v4-Flash-0731-DSpark-1M-NVFP4-KV-2x-DGX-Spark](https://github.com/tonyd2wild/DeepSeek-v4-Flash-0731-DSpark-1M-NVFP4-KV-2x-DGX-Spark)
- [NVIDIA/dgx-spark-playbooks](https://github.com/NVIDIA/dgx-spark-playbooks)

`dgxllm` はこれらのレシピを、モデルを差し替えられる形の CLI にまとめ直したもの。

## ライセンス

MIT
