2014年05月19日

RPi - Raspberry Pi で High Performance Computing! スパコンを作る方法。

聞いただけでドキドキする言葉。 僕にとってスーパーコンピュータはその1つなんです。 今日は Raspberry Pi を使って HPC - High Performance Computing 環境を構築する方法をご紹介しますよ。
 
cray super computer

MPI - Message Passing Interface の概要


一般的なコンピュータを使って高速な処理を実現する場合には1つの目的を複数のコンピュータで分散・並列処理することになりますが、この実行基盤の1つが MPI (Message Passing Interface)と呼ばれる仕様・規格で MPICH, Open MPI など複数の実装系が存在しています。

MPI では Task と呼ばれる独立したアドレス空間で動作するプロセスをCPUコア/スレッド毎に生成し、このプロセス間でMPI 標準規格で定義された関数を使ってデータを分割・移動(Message Passing)しながら並列・協調動作を実現しています。

MPI overview

MPI による並列・分散処理関数の詳細はこちら(東京工業大学/廣瀬研究室 MPI リファレンス)をどうぞ。

Raspberry Pi を使った HPC プロジェクトとしては次の2つが有名。

まずはレゴで作成したシャーシに Raspberry Pi 64 ノードを詰め込んだ Southampton 大学発 Raspberry Pi supercomputer




もう一つは Kiepert さんが作った 32 ノード構成・ド派手な The RPiCluster 。



何れも MPI を採用していますが The RPiCluster は最小限のパッケージで構成され、リソースを最大限活用できる Arch Linux をプラットフォームに採用しています(詳しいレシピも公開されていますがこのド派手なマシンの性能はピーク時に約 10 GFLOPS、構築費用は約2000ドル)。

くうー、アホみたいな数のケーブルを見ているだけでドキドキしてきますね。


Raspberry Pi + MPI For Python による HPC 環境の構築


今日は Python MPI 実装である MPI4PY (http://mpi4py.scipy.org/)を使い動的型付け・オブジェクト指向といった言語パワーも享受できる HPC 環境を作ってみます。

ちなみに Python を採用した場合にも C 言語に近しい処理性能を叩き出すことは可能ですが、大きなデータ、メモリを扱う場合にはやはり不利で”最高の性能”を引き出したい場合には MPICH を使うのが一般的です。

今回は3台の Raspberry Pi を使いマスターx1、クラスターx2構成のミニ・スパコンを作り、円周率 Pi を計算してみます。

raspberry pi cluster

採用するディストリビューションは RASPBIAN。 初期設定はこちらをどうぞ。

RPi - Raspberry Pi ファースト・インプレッション + 押さえておきたい初期設定


まずは作業中に混乱しないよう /etc/hosts、/etc/hostname ファイルを編集して全てのマシンでホスト名を変更しておきましょう。

$ sudo vi /etc/hosts
127.0.0.1       localhost
...
#127.0.1.1      raspberrypi
127.0.1.1       pi-cluster1

$ sudo vi /etc/hostname
pi-cluster1

2つのファイルを編集後、再起動すればおしまい。

続いてマスター上で SSH 鍵を作成しましょう。

pi@pi-master ~ $ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/pi/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/pi/.ssh/id_rsa.
Your public key has been saved in /home/pi/.ssh/id_rsa.pub.
The key fingerprint is:
14:6e:e7:0e:c5:44:40:7b:e8:0d:4c:7c:f3:3a:e8:de pi@pi-cluster1
The key's randomart image is:
+--[ RSA 2048]----+
|       o=oo      |
|       +.*o      |
|        O.=o     |
|       + B  .    |
|        S.o.     |
|        .oo      |
|       .  ..     |
|        ..       |
|       .. E      |
+-----------------+


この鍵を全てのクラスターにコピーします(次の例では 192.168.0.128 というアドレスのクラスターへコピーしています)。

pi@pi-master ~ $ cat ~/.ssh/id_rsa.pub | ssh pi@192.168.0.128 "test -d ~/.ssh || mkdir .ssh;cat >> .ssh/authorized_keys"


これでユーザ、パスワード認証無しでログイン出来るようになったはずです。

pi@pi-master ~ $ ssh pi@192.168.0.128
Linux pi-cluster1 3.10.25+ #622 PREEMPT Fri Jan 3 20:41:00 GMT 2014 armv6l
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.


さあ、全てのマシン上で MPI4PY をインストール。

pi@pi-cluster1 ~ $ sudo apt-get install python-mpi4py


マスターのホームディレクトリ配下に mpi というディレクトリを作りましょう。 今後 MPI に関する作業は全てここで行います。

pi@pi-master ~ $ mkdir ~/mpi


この場所に machines.txt という名前で全てのホストをリストしたファイルを作り、

192.168.0.127 # masater
192.168.0.128 # cluster1
192.168.0.129 # cluster2


次のコマンドを実行してみましょう。 -n オプションで指定する 3 には合計3プロセス起動するという意味があります。

pi@pi-master ~/mpi $ mpirun -n 3 -machinefile machines.txt hostname
pi-master
pi-cluster1
pi-cluster2

全てのマシン(3台)で hostname コマンドが実行、その結果が表示されたはずです。

次の作業はマスターだけ。 MPI ではマスター、クラスター間でコードを共有する必要がありますが都度コピーするのは手間なので Network File System を使います。

次のコマンドでマスターに NFS 関連パッケージをインストール、

pi@pi-master ~ $ sudo apt-get install nfs-kernel-server nfs-common


/etc/exports ファイルにネットワーク情報で共有する場所、つまり先程作った~/mpiを定義(エクスポート)します。

pi@pi-master ~ $ sudo vi /etc/exports
...
/home/pi/mpi  *(rw,sync,fsid=0,crossmnt,no_subtree_check)

NFS サービスを起動したらマスターの準備はおしまい。

pi@pi-master ~ $ sudo service rpcbind start
Starting rpcbind daemon....

pi@pi-master ~ $ sudo service nfs-common start
Starting NFS common utilities: statd idmapd.

pi@pi-master ~ $ sudo service nfs-kernel-server start
Exporting directories for NFS kernel daemon....

NFS サービスを自動起動するならば次のコマンドをマスター上で実行しておきます。

pi@pi-master ~ $ sudo update-rc.d rpcbind enable
pi@pi-master ~ $ sudo update-rc.d nfs-common enable
pi@pi-master ~ $ sudo update-rc.d nfs-kernel-server enable


今度はクラスタ側の設定。 同じパスでマウント・ポイントを作り、マスターで(ここでは192.168.0.127)のエクスポートした共有ファイル・システムをマウントします。

pi@pi-cluster1 ~ $ sudo service rpcbind start
Starting rpcbind daemon...
pi@pi-cluster1 ~ $ mkdir ~/mpi
pi@pi-cluster1 ~ $ sudo mount -t nfs 192.168.0.127:/home/pi/mpi ~/mpi

こちらも自動起動するなら rpcbind を有効にしつつ、マウント情報を /etc/fstab に追加しておきましょう。

pi@pi-master ~ $ sudo update-rc.d rpcbind enable

pi@pi-cluster1 ~ $ sudo vi /etc/fstab
...
192.168.0.127:/home/pi/mpi /home/pi/mpi nfs defaults 0 0


さて、マスター上の ~/mpi (/home/pi/mpi)に hello.py という名前で次のようなコードを作成・保存します。 rank はタスク(プロセス)へ割り当てられる一意な ID、size は全体のプロセス数(≒CPUコアの全体数)、name はタスクの動作するマシンのホスト名です。

#!/usr/bin/env python

from mpi4py import MPI

size = MPI.COMM_WORLD.Get_size()
rank = MPI.COMM_WORLD.Get_rank()
name = MPI.Get_processor_name()

print "I am task %d of %d on %s." % (rank, size, name)


このファイルに実行権限を付与、mpirun を使って起動してみます。

pi@pi-master ~/mpi $ chmod 775 hello.py
pi@pi-master ~/mpi $ mpirun -n 3 -machinefile machines.txt ./hello.py
I am task 0 of 3 on pi-master.
I am task 1 of 3 on pi-cluster1.
I am task 2 of 3 on pi-cluster2.

お、うまく動いているみたい。 動作するマシンは違いますがそれぞれのプロセスに重複しない rank が割り当てられています。


円周率の計算速度を MPI / 非MPI で比較してみよう


突然ですが f(x) = 1 / (1 + x^2) という関数の区間 [0,1] の積分結果(面積)は円周率 π の近似値になることがわかっています。

円周率関数


積分では 0〜1 の区間を N 個に分割、それぞれを長方形として考え個々の面積を合算することで全体の近似値を求めるわけですが、当然分割数 N が大きくなるほど長方形の横幅は狭まり、幅を持たない1本の線に近づくので精度も上がっていきます(誤差が減ります)。

円周率積分


さあ、イメージが出来たら Python で円周率を計算するシンプル(非 MPI な)なコードを見てみましょう。

#!/usr/bin/env python

def f(x):
  return 4 / ( 1.0 + x**2 )

n = 10000000
sum = 0
step = 1.0 / n

for i in range(0, n):
  x = ( i + 0.5 ) * step
  sum += f(x)

pi = sum * step

print pi

このコードでは区間を 1000 万(10000000)個に分割し、各 x における f(x) 値を算出、その合計を求めています。

for ループで都度 f(x) * step すると分割した長方形の面積(高さ*横幅)を求めている!という実感が持てますが、家に帰るのが遅くなる(CPUリソースの無駄使いになる)ので最後に一度だけ高さの合算(sum)に横幅(step)を掛けています。

これを単一の Raspberry Pi 上で実行すると約4分30秒を要します。

同じ精度の計算を MPI4PY で分散・並列処理してみましょう。 まずは pi.py という名前でマスター・コードを用意します。

#!/usr/bin/env python

from mpi4py import MPI
import numpy
import sys

N = 1000000

print "--------------- PI ---------------"
print "N: %d" % N

comm = MPI.COMM_SELF.Spawn(sys.executable,
                           args=['cluster-pi.py'],
                           maxprocs=2)
# Broadcast a message from one process to all other processes in a group
comm.bcast(N, root=MPI.ROOT)

PI = 0.0
# Reduce
PI = comm.reduce(None, PI, op=None, root=MPI.ROOT)

print "PI =", PI

comm.Disconnect()

print "----------------------------------"

MPI.COMM_SELF.Spawn には cluster-pi.py (クラスタ側のコード)を指定したプロセス数だけサブ・プロセスとして起動する、という意味があります。

続いて comm.bcast で各タスクに円周率計算の初期条件になる区間分割数 N をブロードキャスト通知しています。  ちなみに、この bcast という関数を comm.Bcast とすると MPI 標準規格で定義されている原始的な配列ベースの MPI_Bcast 関数を使うことになり MPICH と同等の性能を引き出せるのですが、ここでは Python オブジェクトを直接扱うことが出来る bcast を使っています。

最後に各クラスターが計算した値を合計した円周率 PI を reduce を使って受け取っています(bcast と同様、性能面では comm.Reduce が有利)。

クラスターで実行する cluster-pi.py はこう。

#!/usr/bin/env python

from mpi4py import MPI
import sys

def f(x):
  return 4 / ( 1.0 + x**2 )

comm = MPI.Comm.Get_parent()
size = comm.Get_size()
rank = comm.Get_rank()
name = MPI.Get_processor_name()

print "starting task-%d on %s " % (rank, name)

N = None
N = comm.bcast(N, root=0)
step = 1.0 / N

start = rank
end = N
skip = size

sum = 0.0
cn = 0
for i in range(start, end, skip):
  x = (i + 0.5) * step
  sum += f(x)
  cn += 1

cpi = sum * step
print "c-pi (task %d on %s,n=%d): %f" % (rank, name, cn, cpi)
comm.reduce(cpi, None, op=MPI.SUM, root=0)

comm.Disconnect()

ここで注目するのは for ループの条件で range の引数に skip として全体のタスク(CPU コア)数を指定しているところ。

これによって全体数が 2 で自身の ID (rank)が 0 のタスクの場合で例えると、 0, 2(=0+2), 4(=2+2) ... < N というように自身の ID である 0 からスタートして全体のタスク数だけ都度スキップ/オフセットしながら他と重複しない範囲の f(x) 値だけを求め、その結果を comm.reduce でマスターに集約しているのです。

さあ、各ファイルに実行権限を付け、僕の作ったスパコン?で計算してみましょう。 マスター・プロセスである pi.py のコード中でサブ・プロセス数を定義しているので起動時に指定する -n オプションの値、つまり pi.py のプロセス数は 1 になります。

次のコマンドを実行するとマスター上で pi.py、2台のクラスター上では cluster-pi.py が動き出します。

pi@pi-master ~/mpi $ mpirun -n 1 -machinefile machines.txt ./pi.py

--------------- PI ---------------
N: 10000000
starting task-0 on pi-cluster1
starting task-1 on pi-cluster2
c-pi (task 1 on pi-cluster2,n=5000000): 1.570796
c-pi (task 0 on pi-cluster1,n=5000000): 1.570796
PI = 3.14159265359
----------------------------------

simple-pi.py と結果は同じですが要した時間は約2分40秒。 MPI ランタイムのオーバーヘッドもありますから半分とはいきませんが大幅に減ってますね!

MPI For Python のより詳しい使い方はこちら(mpi4py.pdf)をどうぞ。

オーバークロックするなら $ sudo raspi-config と入力、Overclock メニューからクロック数を変更しましょう。

raspberry pi overclock


今回の構成でクラスター2台を Turbo 化したところ所要時間はさらに縮まり約1分40秒になりましたよ。

同じ台数の Raspberry Pi という条件下でチームで性能競争(RPi TOP500?)したら楽しいだろうなあ。

それでは、より楽しい RPi ライフを。



スーパーコンピューターを20万円で創る (集英社新書)
伊藤 智義
集英社
売り上げランキング: 239,318

Posted by netbuffalo at 20:30│Comments(0)TrackBack(0) Python | Raspberry Pi


この記事へのトラックバックURL

コメントする

名前
 
  絵文字