MPI と相互接続#
スレッドは1 台のマシン内でスケールさせます。多数のマシン — クラスタ — を使うには、入門 の分散メモリモデル MPI が必要です。本章は MPI をゼロから導入し、qc-rs がそれを使う 2 つの方法を示し、実際の クラスタで誰もがつまずく部分 — 相互接続(マシン間のネットワーク)とその選び方 — を扱います。
MPI とは#
MPI(Message Passing Interface)は分散メモリ並列の標準です。MPI ジョブは複数のプロセス — ランクと
呼ばれ 0, 1, 2, … と番号付け — を、場合により異なるマシン上で起動します。各ランクは自分専用のメモリを持ち、
同じプログラムを走らせます;データを共有するには、ランクどうしがネットワーク越しにメッセージを送ります。
入門 の分散メモリモデルを具体化したものです:
ランクは仕事の単位(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 の下に再起動します:
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 で警告のみ)です。スレッドの章
の警告どおり、ノードあたりランク数 × nthread ≤ ノードあたりコア数 を保ってください。
2. 外部 mpirun + comm= — あなたが起動し、qc-rs が参加#
HPC の道:mpirun/mpiexec で自分でジョブを起動し、qc-rs にコミュニケータを渡します:
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 レシピ:
# (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 は高い
memlockulimit が必要(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 台の大ノードを要する
代わりに複数の小ノードに広がれる理由です。
練習 18
2 ノードの MPI ジョブが走りますが単一ノードより速くならず、
topはネットワークをほとんど使っていないと 示します。本章から考えられる原因を 2 つ挙げなさい。スレッドを足してもできないのに、MPI 実行が 1 台の RAM に収まらない積分の分子を扱えるのはなぜですか。
クラスタジョブがエラーなく起動時にハングします。相互接続を疑います。最初の 1 行チェックと、フォールバック すべき最も安全なトランスポートは。
解答 練習 18
(a) MPI が InfiniBand ではなく TCP/Ethernet に静かにフォールバックした(トランスポート未明示)ため、 ノード間メッセージが遅い;かつ/または (b) ジョブが通信律速 / 小さすぎるので Amdahl が利得を制限。ランクが オーバーサブスクライブや未バインド(
--bind-to none)でないかも確認。MPI は分散メモリ — 各ランクが積分の一部だけを保持するので、全体(1 ノードより大きい)がノードに広がり ます。スレッドは共有メモリで単一マシンの RAM に縛られ、それを超えられません。
cat /sys/class/infiniband/*/ports/*/state(ACTIVEのはず)。最も安全なフォールバックは共有メモリのみ--mca pml ob1 --mca btl self,sm(単一ノード)、またはノード跨ぎは明示インターフェースの TCP。
CPU — スレッドと分散 — は速さへの 1 つの道です。もう 1 つは GPU です。