(2008.10.8)
Linux機とWindows機が混在する (ヘテロ) 環境で、並列にプログラムを動かす方法について。
並列プログラミングはいろいろな方法がありますが、ここではMPIライブラリを使ってみます。
MPI (Message Passing Interface) は、その名前のとおり、並列に動作するプログラム間で、メッセージをやり取りするための仕様です。MPI-1, MPI-2 として標準化されています。
複数の機械(ノード)を結んで、あたかも一つの計算機として計算させるために用いられます。(クラスタ)
MPIは、
クラスタを構成する各ノードでは、同じ場所(パス)にプログラムを置きます。普通は、すべてのノードのCPUのアーキテクチャ、OSなどを揃えておいて、NFSなどでプログラム自体を共有するようです。
各ノードのプログラムの起動は、マスタノードから一括しておこないます。
私は、Windows機も参加させたかったので、ソースだけを配布し、各機械でコンパイルしました。WindowsではCygwin環境を使いました。
MPIライブラリには、Open MPI, LAM/MPI, MPICH, GridMPI などがあります。
商用製品もあります。例えば、
Fedora 9 Linuxには、Open MPI のパッケージが含まれます。Open MPI でいこうかと思いましたが、試したときには、Cygwinで (ソースからコンパイルして) 動かすとクラッシュしてしまいました。
MPICH2 は、移植性を重視したライブラリです。こちらは Linux でも Cygwinでも大丈夫でした。以降では、MPICH2を使います。
MPICH2は次のところから入手できます。
2009.1現在の最新版はバージョン1.0.8です。Windows用のバイナリもありますが、Cygwin で動かすときは、ソースを取ってきて自分でコンパイルします。
あらかじめ Python をインストールしておかないといけません。
configureスクリプトに与えるオプションなどは MPICH2 Installer's Guide を参照してください。例えば、次のようにします。
$ ./configure --enable-fast=O3 --with-device=ch3:ssm
--with-device はノード間の通信方法を選択します。
ch3:ssmは、違うノード間はソケットで、同一ノード内のCPU間は共有メモリを利用します。マルチスレッドと併用するときは、ch3:sock (すべてソケット) にします。
$ make $ su # make install
次に、秘密の文字列を含む .mpd.conf ファイルを各ノードのホームディレクトリに作ります。
内容は、例えば、次のようにします。
secretword=hogehogefunifuni
secretwordの内容はすべてのノードで同じにします。私は最初、ノードごとに変えるのかと勘違いし、動かなくて悩みました。
.mpd.confファイルのパーミッションを変更します。
$ chmod 600 ~/.mpd.conf
mpdコマンドを起動して、1台で動作するか確認します。mpdtraceコマンドでホスト名が表示されるはずです。
$ mpd & $ mpdtrace $ mpdallexit
mpdallexitは、すべてのノードのmpdを終了させます。
もう一度テストします。
$ mpd & $ mpiexec -n 1 /bin/hostname $ mpdallexit
MPICH2は SSH 経由で各ノードのプログラムを起動します。そのときにパスワードなしでコマンドを送れるようにします。
次の手順で鍵を各マシンに登録します。
鍵ペアは ssh-keygen コマンドで作ります。-t オプションで rsa か dsa を指定します。dsa は FIPS 186 Digital Signature Standard で定義されるデジタル署名アルゴリズムです。
ここではrsaで作ってみます。
$ ssh-keygen -t rsa
デフォルトで、~/.ssh/id_rsa, ~/.ssh/id_rsa.pub ファイルが作られます。
秘密鍵は本人だけが知っているべき鍵ですから、ログインする側の機械(クライアント)だけが持ちます。マスタノードがクライアントに、それ以外の機械がサーバになります。
各ノードに(秘密鍵ではなく)公開鍵を配布し、それぞれのノードで決められたファイルに追記します。
scpコマンドでコピーしてもいいでしょう。
$ cat id_rsa.pub >> ~/.ssh/authorized_keys $ chmod 600 ~/.ssh/authorized_keys
秘密鍵は ~/.ssh/id_rsaに保存しておきます。
マスタノードで ssh-agentを起動します。ssh-agentはforkして常駐し、必要な環境変数を出力します。
evalしてシェルに環境変数を登録します。
次に ssh-add を起動します。引数なしだと、~/.ssh にある id_rsaまたはid_dsa ファイルを ssh-agentに登録します。ssh-agentが常駐している間、sshコマンドは自動的にその秘密鍵を使います。
$ eval `ssh-agent` $ ssh-add Enter passphrase for /home/.../id_rsa: パスワードを入力
では、パスワードなしで別ノードのプログラムを起動できるかテストします。
$ ssh 別ノードのホスト名 date
ノードの一覧を記録したmpd.hostsファイルを作ります。1行1ノードで、先頭を#にするとコメントになります。
ホスト名の後ろに「:プロセス数」を付けることもできます。mpiexecコマンド(後述)に渡すことで、ノード毎の起動プロセス数を制御できます。
例えば次のように書きます。自ホストも書かなければならないことに注意してください。
host1 host2:2
マスタノードでmpdbootコマンドを実行します。mpdbootはクラスタに参加する各ノードでmpdコマンドを常駐させます。
$ mpdboot -n 開始するノード数 -f mpd.hosts
mpdtraceで起動中のノードを表示させます。-lオプションを付けるとホスト名とIPアドレスを表示します。
$ mpdtrace host1 host2
ごく簡単なプログラムを書いてみます。
C++はmpicxxコマンドでコンパイルします。
$ mpicxx -Wall first.cc
各ノードで実行ファイルを作ったら、mpiexecコマンドで実行します。-nオプションはプロセス数で、ノード数より多い数でも構いません。
$ mpiexec -n 2 ./a.out
あるいは、mpd.hostsファイルでノード毎の起動プロセスをコントロールします。このときは-nオプションでmpd.hosts内のプロセス数より多い数を指定するとエラーになります。
$ mpiexec -machinefile mpd.hosts -n 6 ./cpi