電子計算記

個人的な検証を

17. MPIクラスターを作ろう! - STREAMでハイブリッド並列を試す

前回からのつづきです。
16. MPIクラスターを作ろう! - HPLのパラメータを検討 - 電子計算記

忘れてましたが、アドカレのコマ埋めの話のためにはじめたので、今回が最終回です。1ヶ月以上かかってしまいましたが。。。
今回はSTREAMをやります。HPC問わず定番のベンチマークソフトですし、シンプルで使いやすく様々な実装もあるので大変使いやすいのです。

www.cs.virginia.edu

STREAMといえば、メモリバンド幅のベンチマークによく使われます。MPIの実装もあるので、今回はそれを試してみます。

ビルドは簡単です。

mpiuser@compute-1:~$ mkdir /nfs/stream
mpiuser@compute-1:~$ cd /nfs/stream/
mpiuser@compute-1:/nfs/stream$
mpiuser@compute-1:/nfs/stream$ wget http://www.cs.virginia.edu/stream/FTP/Code/Versions/stream_mpi.c
mpiuser@compute-1:/nfs/stream$ mpicc -O3 stream_mpi.c

実行もこれまでと同じです。

mpiuser@compute-1:/nfs/stream$ mpirun -np 4 --hostfile ~/my_hosts ./a.out
-------------------------------------------------------------
STREAM version $Revision: 1.8 $
-------------------------------------------------------------
This system uses 8 bytes per array element.
-------------------------------------------------------------
Total Aggregate Array size = 10000000 (elements)
Total Aggregate Memory per array = 76.3 MiB (= 0.1 GiB).
Total Aggregate memory required = 228.9 MiB (= 0.2 GiB).
Data is distributed across 4 MPI ranks
   Array size per MPI rank = 2500000 (elements)
   Memory per array per MPI rank = 19.1 MiB (= 0.0 GiB).
   Total memory per MPI rank = 57.2 MiB (= 0.1 GiB).
-------------------------------------------------------------
Each kernel will be executed 10 times.
 The *best* time for each kernel (excluding the first iteration)
 will be used to compute the reported bandwidth.
The SCALAR value used for this run is 0.420000
-------------------------------------------------------------
Your timer granularity/precision appears to be 1 microseconds.
Each test below will take on the order of 2361 microseconds.
   (= 2361 timer ticks)
Increase the size of the arrays if this shows that
you are not getting at least 20 timer ticks per test.
-------------------------------------------------------------
WARNING -- The above is only a rough guideline.
For best results, please be sure you know the
precision of your system timer.
-------------------------------------------------------------
Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:          43360.4     0.004048     0.003690     0.005795
Scale:         42361.4     0.004132     0.003777     0.005845
Add:           40603.1     0.006053     0.005911     0.006244
Triad:         38144.5     0.006475     0.006292     0.006926
-------------------------------------------------------------
Solution Validates: avg error less than 1.000000e-13 on all three arrays
-------------------------------------------------------------

最終的なスコアは、Best Rate MB/sの部分で、4種類のテストが実施されます。4つのなかでもベンチマーク用の値としてはTriadがよく用いられていて、加算と乗算をしているので、4つのテストの中では1番複雑な計算をしています。

この例では、Array sizeが10Mで、クラスタ全体で必要なメモリは0.2GiBになっています。STREAMはメモリバンド幅に依存しますので、Array sizeが小さいすなわち計算に利用するメモリが小さいとメモリではなく、CPUのキャッシュに収まってしまいメモリ本来の値より高速な結果となってしまいます。
そのため、マシンの搭載メモリやクラスタ全体の搭載メモリの容量に合わせて大きな値を設定する必要があります。これは以下のようにコンパイル時のオプションで指定できます。デフォルトより10倍大きい100Mの例です。

mpiuser@compute-1:/nfs/stream$ mpicc -O3 -DSTREAM_ARRAY_SIZE=100000000 stream_mpi.c

これを実行すると、クラスタ全体で 2.2 GiB 必要となります。
では、このArray sizeを変えて実行してみます。

Light.S1の結果
f:id:fujish:20180131012528p:plain

HighCPU.M4の結果
f:id:fujish:20180131012545p:plain

S1のスコアがM4と大差ないですが、これはほとんどのテストが1〜2秒で終わってしまい、クロックキャッピングがかかる前に実行完了してしまうためと考えられます。

また、S1、M4ともに-npが16以上で急に高速な結果となりますが、これはノード数が多くなりプロセスあたりに必要なメモリ容量が小さくなることで、CPUのキャッシュに収まってしまうためで、Array sizeが10Mのとき、1プロセスにおける必要なメモリサイズは 14.3 MiB になり、今回の環境の物理CPUが搭載しているL3キャッシュが 40 MiB なのですっぽり収まってしまいます。とは言っても仮想化されていて、他の仮想マシンと共有しているため完全にキャッシュに収まるかはわからないですし、そのときどきで変わります。

ということで、完全にキャッシュに載り切らないArray sizeを指定できれば、Array sizeによるスコアへの影響はほぼないように見えます。

ハイブリッド並列化

と、STREAMが何となくわかったところで、ここからが本題です。そうハイブリッド並列です。
これまで並列化の実装としてMPIを用いてきました。MPIはプロセスによる並列化手法です。
一方、ノード内のマルチコア環境ではスレッドによる並列化が軽量で高速なためよく用いられその実装としてはOpenMPが定番です。
このMPIとOpenMPを両方使って並列化し、ノード間はMPI,ノード内はOpenMPを使って並列化します。

実はMPI版のSTREAMは、このMPIとOpenMPのハイブリッド並列に対応しているので、上記と同じコードで簡単に試すことができます。

コンパイルOpenMP対応のオプションとして、-fopenmpを追加するのみです。OpenMPのライブラリパッケージは、OpenMPIのインストール時にgccなどと一緒にインストールされています。

mpiuser@compute-1:/nfs/stream$ mpicc -O3 -fopenmp -DSTREAM_ARRAY_SIZE=100000000 stream_mpi.c

実行は、オプションを追加する必要があります。-bind-to board と入れることで、OpenMPのスレッドが各CPUコアを使えるようになります。boardのところは実行するマシン環境に合わせる必要が有り、IDCFクラウド環境やシングルソケットのマシンではboardを指定します。また、 -x OMP_NUM_THREADS=2 というように-xで環境変数が設定できるのでOpenMPのスレッド数も合わせて指定します。今回は2vCPUのHighCPU.M4のため2としています。

mpiuser@compute-1:/nfs/stream$ mpirun -np 16 -bind-to board -x OMP_NUM_THREADS=2 --hostfile ~/my_hosts ./a.out
-------------------------------------------------------------
STREAM version $Revision: 1.8 $
-------------------------------------------------------------
This system uses 8 bytes per array element.
-------------------------------------------------------------
Total Aggregate Array size = 100000000 (elements)
Total Aggregate Memory per array = 762.9 MiB (= 0.7 GiB).
Total Aggregate memory required = 2288.8 MiB (= 2.2 GiB).
Data is distributed across 16 MPI ranks
   Array size per MPI rank = 6250000 (elements)
   Memory per array per MPI rank = 47.7 MiB (= 0.0 GiB).
   Total memory per MPI rank = 143.1 MiB (= 0.1 GiB).
-------------------------------------------------------------
Each kernel will be executed 10 times.
 The *best* time for each kernel (excluding the first iteration)
 will be used to compute the reported bandwidth.
The SCALAR value used for this run is 0.420000
-------------------------------------------------------------
Number of Threads requested for each MPI rank = 2
Number of Threads counted for rank 0 = 2
-------------------------------------------------------------
Your timer granularity/precision appears to be 1 microseconds.
Each test below will take on the order of 2944 microseconds.
   (= 2944 timer ticks)
Increase the size of the arrays if this shows that
you are not getting at least 20 timer ticks per test.
-------------------------------------------------------------
WARNING -- The above is only a rough guideline.
For best results, please be sure you know the
precision of your system timer.
-------------------------------------------------------------
Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:         203224.7     0.009135     0.007873     0.013324
Scale:        156833.1     0.010802     0.010202     0.014634
Add:          175671.5     0.014061     0.013662     0.016308
Triad:        176416.6     0.013954     0.013604     0.014882
-------------------------------------------------------------
Solution Validates: avg error less than 1.000000e-13 on all three arrays
-------------------------------------------------------------

> Number of Threads requested for each MPI rank = 2
> Number of Threads counted for rank 0 = 2
の部分が違うくらいであとは同じです。

では、MPIのみとハイブリッドだとどう結果が違ってくるのか、Array sizeを100Mにして、HighCPU.M4のマシンを並べて実行してみましょう。

f:id:fujish:20180201005721p:plain

Hybridのnpが2のときスレッド含めた並列数は4になるので、例えばHybridのnp 2と比較するのは同じ並列数のMPI Onlyのnp 4を比べてみます。するとだいたい同じ値になっており、多少Hybridの方がスコアが低くなっています。
一方、クラスタ搭載のCPUをフルに使うHybridのnp 16とMPI Onlyのnp 32だと、多少Hybridの方がスコアが高くなっています。

結果からすると、ノード内で1プロセスしか走らせないならMPI Onlyが良いけど、ノード内で搭載コア数分のプロセスを走らせるならスレッド使った方が高速になると言えそうです。
今回は2vCPUのマシンだったので差が小さかったですが、もっとコア数が多いとか、問題規模が大きくなるとその差はもっともっと大きくなると考えられます。

以上、簡単にハイブリッド並列が試せましたね。

※HighCPU,M4(2vCPU)のときの-np 16まではslots=1として各ノード1プロセスづつしか動かしてません

まとめ

ここまでやってきたとおり、IDCFクラウドのLight.S1を並べて、安価にMPIクラスタ環境を構築できました。一方で、クロックキャッピングによる性能や実行時間への影響は考慮する必要があります。また、さくっと上位タイプへリサイズすれば、ハイブリッド並列化環境も揃えられるので、MPIコードの開発に活用できるのではないでしょうか。