# リモート実行仕様 (Remote Execution)

手元からコードを送り，研究室の複数の GPU サーバ（Newton・Archimedes）のうち**空いている GPU を自動で選んで**ジョブを実行し，結果を[回収ホワイトリスト](SPEC_01_アーキテクチャ設計.md)に従って持ち帰るツール `tias-remote` の仕様．SmTIAS-LightSim の `sm-remote` を移植し，複数サーバ対応と回収制限を加えたもの．

## 前提と制約 (Constraints)

- **臨床データを転送しない**．ツールが扱うのはコードの送信と結果の回収のみ．データは各サーバに事前配置する（[ENV_03](../02_ENV/ENV_03_管理者用環境構築手順.md)）
- サーバに GitHub の認証情報を置かない（コードは常に手元から送る）
- **非対話で動く**（ssh は `BatchMode=yes`．パスワード入力を求めない）
- 共用サーバのため，他のユーザーが使用中の GPU を勝手に使わない

## サーバ設定 (Server Configuration)

`configs/remote.yaml` に置き，pydantic で検証する（未知のキーは拒否）．

```yaml
jump_host: labpc              # 踏み台．null なら直接接続
connect_timeout_sec: 15
gpu_free:
  max_memory_used_mib: 1024   # 使用中メモリがこれ未満
  max_utilization_percent: 10 # かつ使用率がこれ未満なら「空き」
  observe_sec: 10             # この秒数だけ観測してから判定する（0 なら瞬間値のみ）
  sample_interval_ms: 500     # 観測の間隔
servers:
  - name: Newton
    address: 192.168.23.14
    user: hasegawa
    priority: 1               # 小さいほど優先
    remote_dir: ~/work/TIASJudgeModel
    data_dir: ~/data/TIASJudgeModel
    uv: ~/.local/bin/uv
    data_ready: true          # 臨床データが配置済みか
  - name: Archimedes
    address: 192.168.23.15
    user: hasegawa
    priority: 2
    remote_dir: ~/work/TIASJudgeModel
    data_dir: ~/data/TIASJudgeModel
    uv: ~/.local/bin/uv
    data_ready: false
```

| 検証規則 | 内容 |
| --- | --- |
| `servers` | 1 件以上．`name` は重複不可 |
| `priority` | 整数．同じ値のときは `servers` の記載順で優先 |
| `name` | 英数字・`_`・`-` のみ（コマンド引数・表示に使うため） |
| 閾値 | `max_memory_used_mib` は 0 以上．`max_utilization_percent` は 0〜100 |
| 観測 | `observe_sec` は 0〜120（0 は瞬間値のみで判定）．`sample_interval_ms` は 100〜5000 |
| `connect_timeout_sec` | 1 以上 |

環境変数による上書き:

| 環境変数 | 効果 |
| --- | --- |
| `TIAS_REMOTE_JUMP` | `jump_host` を上書きする．**空文字なら直接接続**（研究室 LAN 内にいるとき） |
| `TIAS_REMOTE_SSH` | 手元の ssh 実行ファイル（Windows の既定は `C:\Windows\System32\OpenSSH\ssh.exe`） |
| `TIAS_REMOTE_ALLOW_SELF` | `1` でサーバ上実行の検出を無効化する（誤検出時の逃げ道） |

## 接続 (Connection)

- ssh の接続先は `<user>@<address>`．`jump_host` があれば `-J <jump_host>` を付ける
- 常に `-o BatchMode=yes -o ConnectTimeout=<connect_timeout_sec>` を付ける
- サーバ上のコマンドは非対話シェルで動くため，`uv` は設定の絶対パス（`~` 展開）で呼び，`PATH` に `~/.local/bin` を加える（非対話 ssh では `~/.local/bin` が PATH に無い．ENV_02）
- **接続できないサーバは「接続不可」として扱い，他のサーバで処理を続ける**（1 台の不調で全体を止めない）．すべてのサーバに接続できない場合はエラー

## GPU の空き判定と選択 (GPU Selection)

### 取得

各サーバで次を取得する（ssh 1 回で行う）．

1. GPU の一覧（1 行 1 GPU）

   ```bash
   nvidia-smi --query-gpu=index,uuid,name,memory.used,memory.total,utilization.gpu --format=csv,noheader,nounits
   ```

2. **観測**: `observe_sec` 秒間，`sample_interval_ms` 間隔で GPU ごとの使用率と使用中メモリを繰り返し取得する（`nvidia-smi --query-gpu=index,utilization.gpu,memory.used --format=csv,noheader,nounits -lms <間隔>` を `timeout <秒>` で打ち切る等）
3. **プロセス別の観測**: 同じ時間，プロセスごとの SM 使用率を取得する（`nvidia-smi pmon -s u -d 1 -c <秒>`．上の観測と並行して実行する）
4. GPU 上の計算プロセス（`gpu_uuid` で GPU と対応付ける）

   ```bash
   nvidia-smi --query-compute-apps=gpu_uuid,pid,used_memory --format=csv,noheader,nounits
   ```

5. 各プロセスの所有ユーザーと**起動からの経過秒数**（`ps -o user=,etimes= -p <pid>` 等）

※ `--query-compute-apps` と `pmon` の `C`（計算）種別のみを数え，画面描画のプロセス（`G` 種別．Xorg 等）は含めない．`C+G` のように両方を含む種別は計算として数える．
※ nvidia-smi は「最後に使った時刻」の履歴を持たないため取得しない．代わりに観測中の使用率とプロセスの経過時間で判断する．

### 判定

**観測中の最大値**で閾値を判定する（瞬間値だけでは，起動と停止を繰り返す GPU を「空き」と誤認するため．2026-09-27 に Newton で 10 秒間に 0〜74% を往復する GPU を実測）．

| 状態 | 条件 | 選択 |
| --- | --- | --- |
| **未使用** | 観測中ずっと閾値内（使用率の最大 < `max_utilization_percent` かつ使用中メモリの最大 < `max_memory_used_mib`）で，**計算プロセスが 1 つも無い** | 最優先 |
| **空き（利用者あり）** | 観測中ずっと閾値内だが計算プロセスがある．または計算プロセスの一覧が取得できなかった（利用者不明） | 未使用が無いときのみ |
| 使用中 | 観測中に一度でも閾値を超えた | 選ばない |
| 状態不明 | GPU の行が解釈できない，または観測のサンプルが 1 つも得られない | 選ばない（取得できない GPU を使わないため） |

- 計算プロセスは**所有者を問わず数える**（自分の別ジョブが載っている GPU に次のジョブを重ねないため）
- 計算プロセスの一覧の取得・解釈に失敗した場合，そのサーバの閾値内の GPU はすべて「空き（利用者あり）」とする（未使用と誤認しないため）
- サーバ単位で GPU の一覧の取得に失敗した場合は，そのサーバの GPU をすべて候補から外す
- `observe_sec: 0`（または `--observe 0`）のときは従来どおり 1 回の瞬間値で判定する

### 使い方の型 (Usage Pattern)

表示のために，観測中の使用率から GPU ごとに型を付ける（判定には使わない．判定は上の表）．

| 型 | 条件 |
| --- | --- |
| 常時使用 | 使用率が 0% を超えたサンプルが全体の 80% 以上 |
| 断続的に使用 | 使用率が 0% を超えたサンプルが 1 つ以上・80% 未満 |
| メモリ確保のみ | 使用率が 0% を超えたサンプルが無く，計算プロセスがある |
| 未使用 | 使用率が 0% を超えたサンプルが無く，計算プロセスも無い |

プロセス別の観測（pmon）が得られた場合は，プロセスごとにも観測中の SM 使用率の最大値を表示する．得られなかった場合は GPU 単位の型だけを表示する．

### 選択（`run` で GPU を指定しないとき）

1. 候補サーバ: `data_ready: true` のサーバ（`--no-data` を付けたときは全サーバ）．`--server` を指定したときはそのサーバのみ
2. **全候補サーバを通して「未使用」の GPU を探し**，`priority` 昇順（同値は記載順）→ GPU の index 昇順で最初のものを選ぶ
3. 未使用が無ければ，同じ順で「空き（利用者あり）」の最初のものを選び，**警告として利用者（ユーザー名）を表示する**
4. どちらも無ければ，各サーバの状況を表示して 0 以外の終了コードで終わる（ジョブは起動しない）

例: Newton の空き GPU がすべて利用者ありで，Archimedes に未使用の GPU があれば，Newton の方が優先順位が高くても **Archimedes を選ぶ**．

明示指定:

- `--server NAME --gpu I` で GPU を直接指定できる．指定した GPU が「未使用」「空き（利用者あり）」なら起動する（後者は利用者を警告表示する）．**使用中・状態不明ならエラー**にし，`--force` を付けたときだけ警告を出して実行する
- `--gpu` だけの指定は不可（どのサーバか曖昧になるため `--server` を必須とする）
- サーバに存在しない GPU 番号を指定した場合はエラー（`--force` でも起動しない）
- `--server` に `data_ready: false` のサーバを指定した場合も，`--no-data` が無ければエラー（候補サーバの規則を明示指定にも適用する）

### 表示

`gpus`・`status` は各 GPU について次を表示する．

- 状態（未使用・空き（利用者あり）・使用中・状態不明）
- 使い方の型と，観測中の使用率の平均・最大（例: 断続的に使用・平均 21%・最大 74%）
- 計算プロセスごとの所有ユーザー（自分のものは「自分」）・起動からの経過時間（例: 27.7 時間）・観測中の SM 使用率の最大

`gpus` と `run` は `--observe SEC` で観測時間を上書きできる（`--observe 0` で瞬間値のみ．急ぐとき用）．観測は全サーバで並行して行う．

※ 判定から起動までの間に他者が使い始める競合は防げない（既知の制約）．

## コマンド (Commands)

| サブコマンド | 動作 |
| --- | --- |
| `gpus [--observe SEC]` | 全サーバの GPU 一覧（状態・使い方の型・利用者）と，`run` が選ぶ GPU を表示する |
| `push [--server NAME]` | コードを送る．省略時は接続できる全サーバ |
| `run --name NAME [--server S] [--gpu I] [--force] [--no-data] [--no-push] [--observe SEC] [--env K=V ...] -- CMD...` | GPU を選び，そのサーバへコードを送り（`--no-push` で省略），tmux でジョブを起動する |
| `status` | 全サーバの実行中ジョブと GPU の状況 |
| `logs NAME [-n N] [-f]` | ジョブのログを表示する．ジョブのあるサーバを自動で探す |
| `kill NAME` | ジョブを止める．ジョブのあるサーバを自動で探す |
| `pull RUN_ID [--server S] [--with-checkpoints]` | 実験結果を回収ホワイトリストに従って持ち帰る |

### push

- 送るもの: リポジトリ内のコード一式（`src/`・`scripts/`・`tests/`・`configs/`・`pyproject.toml`・`uv.lock`・`.python-version` 等）
- 送らないもの: `.git/`・`.venv/`・`__pycache__/`・`.pytest_cache/`・`.ruff_cache/`・`data/`・`runs/`・`.claude/`
- 方式: tar over ssh でサーバの `remote_dir` に上書き展開する（Windows に rsync が無いため）
- そのサーバでジョブが実行中なら警告を出す（上書き展開で実行中のジョブが新旧のコードを混ぜて読むおそれがある）

### run

- ジョブ名: 英数字で始まり，英数字・`_`・`-` のみ，1〜64 文字
- **いずれかのサーバで同名のジョブが実行中ならエラー**
- tmux セッション名をジョブ名とし，サーバの `remote_dir` で `<uv> run <CMD>` を実行する．接続が切れても継続する
- ジョブに渡す環境変数: `CUDA_VISIBLE_DEVICES=<選んだ GPU の index>`・`TIAS_DATA_ROOT=<そのサーバの data_dir>`・`CUDA_DEVICE_ORDER=PCI_BUS_ID`（CUDA の GPU 番号を nvidia-smi の index にそろえる．そろえないと空きと判定したのと別の GPU を使いうる）・`PYTHONUNBUFFERED=1`・`--env` で指定したもの．**前の 4 つは `--env` で上書きできない**（GPU の割り当てを迂回させないため）
- ログ: `remote_dir/runs/_remote_logs/<ジョブ名>.log` に，開始行（サーバ名・GPU・日時）と終了コード行（`=== [日時] exit <終了コード>`）を付けて追記する
- `--dry-run` を付けると GPU の選択までを行い，送信・起動はしない
- 起動後，選んだサーバと GPU を表示する

### pull

- 対象: サーバの `remote_dir/runs/<RUN_ID>/` 配下で，[SPEC_01「回収ホワイトリスト」](SPEC_01_アーキテクチャ設計.md)に**名前が一致するファイルのみ**．`checkpoints/*.pt` は `--with-checkpoints` のときだけ
- `figures/*.png` は `figures/` の**直下のみ**を対象とする（`figures/sub/x.png` は回収しない）
- `RUN_ID` に `/`・`\`・`..` を含むもの，空文字，`.`・`-` で始まるもの，制御文字・前後の空白を含むものは拒否する
- ジョブログ（`runs/_remote_logs/`）は `pull` の対象外．`logs` で表示する
- `--server` を省略したときは全サーバを探し，見つかったのが 1 台ならそこから回収する．複数台にあればエラー（`--server` の指定を求める）．どこにも無ければエラー
- 保存先: 手元の `runs/<RUN_ID>/`（同名ファイルは上書き）
- **二重の防御**:
  1. サーバ側で送るファイルをホワイトリストで絞り込む
  2. 手元で受け取った tar を**展開前に全メンバー検査**し，ホワイトリスト外の名前・パス外に出る名前（`..`・絶対パス・ドライブ文字・バックスラッシュ）・リンク（シンボリック・ハード）が 1 つでもあれば**何も展開せずに中止**する
- 回収しなかったファイルの件数を表示する（名前は表示しない．患者情報を含む名前の可能性があるため）

### サーバ上での誤実行の防止

手元のホスト名がいずれかのサーバの `name` と一致したら，自分自身へ ssh しようとしているとみなして止める（大文字小文字とドメイン部分は無視して比べる．`TIAS_REMOTE_ALLOW_SELF=1` で無効化）．

### 共通オプションと観測の有無 (Common Options)

- 全サブコマンドに `--config PATH`（既定はプロジェクトルートの `configs/remote.yaml`）
- 観測（`observe_sec` 秒）を行うのは `gpus`・`run`・`status`．`push`・`kill`・`logs`・`pull` はジョブやファイルの有無だけを見るため観測しない

## テスト方針 (Testing)

- ssh・tmux・nvidia-smi はモックする（テストが実サーバに接続しない．`.claude/rules/testing.md`）
- 純粋なロジックとして分離し，単体テストで固定する対象:
  - `nvidia-smi` 出力（GPU 状態・観測サンプル・pmon・計算プロセス）と `ps` 出力の解析，`gpu_uuid` による対応付けと 4 段階の判定（**観測中の最大値で判定すること**・状態不明・利用者不明・サンプル 0 件の扱いを含む）
  - 使い方の型の判定（常時・断続的・メモリ確保のみ・未使用の境界）と経過時間の表示
  - GPU の選択（**未使用をサーバの優先順位より優先すること**・優先順位・`data_ready`・`--server`・`--gpu`・`--force`・空き無し）
  - 接続できないサーバがあっても他のサーバで続行すること
  - ssh 引数の組み立て（踏み台の有無・`BatchMode`・タイムアウト）
  - 送信対象の選別（除外ディレクトリ）
  - **回収ホワイトリストの判定と tar 検査**（最重要．列挙外・パス外・リンクで中止し，何も展開しないこと）
  - 設定の検証と環境変数の上書き
