# MPI と相互接続

スレッドは**1 台**のマシン内でスケールさせます。**多数**のマシン — クラスタ — を使うには、[入門](parallel-computing-primer.md)
の分散メモリモデル **MPI** が必要です。本章は MPI をゼロから導入し、qc-rs がそれを使う 2 つの方法を示し、実際の
クラスタで誰もがつまずく部分 — **相互接続**（マシン間のネットワーク）とその選び方 — を扱います。

## MPI とは

**MPI**（Message Passing Interface）は分散メモリ並列の標準です。MPI ジョブは複数の**プロセス** — **ランク**と
呼ばれ `0, 1, 2, …` と番号付け — を、場合により異なるマシン上で起動します。各ランクは**自分専用のメモリ**を持ち、
同じプログラムを走らせます;データを共有するには、ランクどうしがネットワーク越しに**メッセージを送り**ます。
[入門](parallel-computing-primer.md) の分散メモリモデルを具体化したものです:

- **ランク**は仕事の単位（1 ランク ≈ 1 つの CPU コア群、しばしば NUMA ドメインごとに 1 つ）。
- 各ランクは大きなデータの**一部**だけを保持する（例:積分の自分のスラブ）ので、問題全体は 1 台のメモリより
  はるかに大きくできます。
- ランクは**集団通信**（全ランクが値を合わせる — 和、ブロードキャスト）と**点対点**メッセージで調整します。

qc-rs は MPI を使って、メモリを食う積分/Fock の仕事をランクに分散し、1 ノードには大きすぎる計算を複数ノードに
収めます。

## qc-rs で MPI を走らせる 2 つの方法

### 1. `run(nmpi=..., hosts=...)` — qc-rs が再起動してくれる

易しい道:`.run()` にランクを頼むと、qc-rs がサブプロセスで**自身を `mpirun` の下に再起動**します:

```python
import qc
m = qc.chk.new(atom="...", ao="cc-pvtz")
m = m.scf(ref="r").run(nthread=8, nmpi=4, hosts="nodeA:2,nodeB:2")
```

- **`nmpi`** = ランク数（`nmpi=1`、既定はインプロセス実行 — MPI なし）。
- **`nthread`** = ランクあたりコア数（`nmpi>1` で必須）。
- **`hosts`** = ランクの配置先:`None`（全ローカル）、`"nodeA:2,nodeB:2"`（明示スロット）、`"nodeA,nodeB"`
  （均等分割）。

`nmpi>1` は**並列プランのバナー** — 総ランク数、ランクあたりスレッド、ノード、そして予算 `ランク × nthread` を
ノードのコア数と対比し、**オーバーサブスクリプション**（⚠）を示すノードごとの表 — を表示します。オーバーサブ
スクリプションは既定でエラー（`parallel.strict` IOP;`False` で警告のみ）です。[スレッドの章](threads-and-blas.md)
の警告どおり、`ノードあたりランク数 × nthread ≤ ノードあたりコア数` を保ってください。

### 2. 外部 `mpirun` + `comm=` — あなたが起動し、qc-rs が参加

HPC の道:`mpirun`/`mpiexec` で自分でジョブを起動し、qc-rs にコミュニケータを渡します:

```python
import mpi4py
mpi4py.rc.thread_level = "serialized"     # UCX PML に必須 — 注意点を参照
from mpi4py import MPI
import qc

m = qc.chk.new(atom="...", ao="cc-pvtz")
m = m.scf(ref="r").run(nthread=8, comm=MPI.COMM_WORLD, log_rank="root")
```

`mpirun -np 4 python script.py` で実行します。`comm` があると `nmpi` は無視されます（ランクは起動側が既に設定）。
**`log_rank`** はどのランクが表示するかを制御:`"root"`（既定、ランク 0 のみ）、`"all"`、`"gather"`、またはリスト
— 128 ランクのジョブが出力で溢れるのを防ぎます。

:::{tip} どちらを使うか
`nmpi=`/`hosts=` はノート PC や対話ノードに便利です。**`comm=` の道はスケジューラ（PBS/Slurm）のあるクラスタで
標準**で、ジョブスクリプトが既に正しいホストリストで `mpirun` の下で走ります。ホスト跨ぎの `nmpi=` 実行は
追加で、再起動の一時ファイル用の**共有ファイルシステム**が必要です。
:::

## 相互接続:なぜネットワークが非常に効くのか

クラスタではランクが**ネットワーク**越しに通信し、その速度が通信の多いステップを支配しえます。2 種が重要です:

- **Ethernet / TCP** — 通常のネットワーク。どこにでもあるが**遅い**（1 GbE で ~0.1 GB/s）で、MPI の自動選択が
  時に静かにこれにフォールバックします。
- **InfiniBand（IB）** — RDMA（リモート直接メモリアクセス）を持つ高性能相互接続。1 GbE の約 **35 倍の帯域**
  （ここでは ~3.9 GB/s を測定）で、ランクが大きな配列を交換するときの正しい選択です。

**トランスポートを明示的に選んで**ください — 自動選択に任せるとハングしたり、静かに遅い Ethernet を使ったりし
ます。実際に動く OpenMPI の 3 レシピ:

```bash
# (1) InfiniBand (UCX) — 速いノード間経路（~35x TCP）
mpirun --mca pml ucx \
  -x UCX_TLS=rc_verbs,ud_verbs,sm,self -x UCX_NET_DEVICES=mlx4_0:1 \
  -x PATH -x LD_LIBRARY_PATH -x MKL_THREADING_LAYER \
  --host nodeA:128,nodeB:128 -np <N> python script.py

# (2) TCP フォールバック（IB なし / IB ダウン）
mpirun --mca pml ob1 --mca btl tcp,sm,self --mca btl_tcp_if_include eno1 \
  -x PATH -x LD_LIBRARY_PATH -x MKL_THREADING_LAYER \
  --host nodeA:128,nodeB:128 -np <N> python script.py

# (3) 共有メモリのみ（単一ノード;ネットワークが壊れているときの安全な道でもある）
mpirun --mca pml ob1 --mca btl self,sm -np <N> python script.py
```

まずリンクを確認:`cat /sys/class/infiniband/*/ports/*/state` が `ACTIVE` を示すはずです。

## さもないとハングする注意点

実際のクラスタが投げてくる非自明な失敗 — いずれも苦い経験から記録:

- **mpi4py は UCX PML のため `MPI_THREAD_SERIALIZED` で初期化せねばならない。** mpi4py は既定で `THREAD_MULTIPLE`
  で、この UCX は選択できません（*"PML UCX could not be selected"*）。**`from mpi4py import MPI` の前に
  `import mpi4py; mpi4py.rc.thread_level = "serialized"`** を置きます。qc-rs はランクのメインスレッドからのみ MPI
  を呼ぶので serialized で十分です。
- **UCX RC は `ud` 補助トランスポートが必要。** `UCX_TLS=rc_verbs,sm,self`（`ud_verbs` なし）は *"no auxiliary
  transport … Destination is unreachable"* で失敗します。常に `ud_verbs` を含めます。
- **`-x` で環境を転送する。** リモートランクはログインシェルを継承しないので、`-x PATH -x LD_LIBRARY_PATH
  -x MKL_THREADING_LAYER` を渡します（`MKL_THREADING_LAYER=GNU` を保つ）。
- **`/tmp` はノードローカル。** ノード跨ぎのスクリプトとスクラッチは `/tmp` でなく共有ファイルシステム
  （`$HOME` / `/home1`）に置きます。
- **1 ホストに複数ランクを詰めるときは NUMA ノードにバインド**:`--map-by numa:PE=<threads> --bind-to core`。
  既定の `--bind-to none` は thrash してハングに見えます。
- **InfiniBand は高い `memlock` ulimit が必要**（RDMA がメモリをピン留めする）。バッチジョブはしばしば低い上限を
  継承します（`ibv_reg_mr ... Cannot allocate memory`）;管理者が計算ノードの `LimitMEMLOCK` を上げます。

qc-rs は **OpenMPI**・**MPICH**・**MVAPICH**（いずれも UCX 経由で InfiniBand を駆動可能）に対応します;使う MPI に
合わせて `mpi4py` を再ビルドしてください。網羅的なクラスタ別レシピは README の HPC 付録にあります。

## 裏側:分散メモリを正しく

qc-rs は行列全体を素朴にブロードキャストしません。その分散バックエンドは**片側 RMA**（リモート直接メモリ
アクセス）層の上に築かれます:各ランクは**自分の**スラブをその場で埋めてコピーなしで他に供し、factor 全体を
再構成する代わりに一度に**1 つ**のリモートブロックをストリームし、大きなテンソルではなく*小さな*補助中間を簡約
します。その成果は、ランクあたりメモリが `全体 / ランク数` でスケールすること — これが RI-JK と RI-MP2 が
「1 ノードの RAM」の壁を破る方法です。これらの管理は不要です;同じ `eri="ri-ram"` 計算が、1 台の大ノードを要する
代わりに複数の小ノードに広がれる理由です。

:::{exercise}
:label: ex-mpi

1. 2 ノードの MPI ジョブが走りますが単一ノードより速くならず、`top` はネットワークをほとんど使っていないと
   示します。本章から考えられる原因を 2 つ挙げなさい。
2. スレッドを足してもできないのに、MPI 実行が 1 台の RAM に収まらない積分の分子を扱えるのはなぜですか。
3. クラスタジョブがエラーなく起動時にハングします。相互接続を疑います。最初の 1 行チェックと、フォールバック
   すべき最も安全なトランスポートは。
:::

:::{solution} ex-mpi
:class: dropdown

1. (a) MPI が InfiniBand ではなく **TCP/Ethernet** に静かにフォールバックした（トランスポート未明示）ため、
   ノード間メッセージが遅い;かつ/または (b) ジョブが**通信律速 / 小さすぎる**ので Amdahl が利得を制限。ランクが
   オーバーサブスクライブや未バインド（`--bind-to none`）でないかも確認。
2. MPI は**分散メモリ** — 各ランクが積分の*一部*だけを保持するので、全体（1 ノードより大きい）がノードに広がり
   ます。スレッドは**共有メモリ**で*単一*マシンの RAM に縛られ、それを超えられません。
3. `cat /sys/class/infiniband/*/ports/*/state`（`ACTIVE` のはず）。最も安全なフォールバックは**共有メモリのみ**
   `--mca pml ob1 --mca btl self,sm`（単一ノード）、またはノード跨ぎは明示インターフェースの TCP。
:::

CPU — スレッドと分散 — は速さへの 1 つの道です。もう 1 つは [GPU](gpu-cuda.md) です。
