HOME > システム > SR16000 > SR16000 利用の手引き > 第9章 使用例
第9章 使用例
9.1 概要
9.2 初歩的な例
ここで、本節については MPI、OpenMP などのノード間、ノード内並列に詳しかったり、すでに本システムにて十分に計算を行われた経験があったりする方は読み飛ばしていただいて問題はありません。
9.2.1 プログラムの作成とコンパイル
プログラムソースの例を示し、日立最適化 FORTRAN90 コンパイラを用いてコンパイルしてみます。
まず、エディタである vi や emacs を用いて、以下のように Fortran90 でプログラムを作成し、ファイル名「gethostn.f90」として保存しておきます。
program gethostn integer statnum,hostnm character*12 name statnum = hostnm(name) write(*,*) 'HOSTNAME is "',name,'"(',statnum,')' end
ここでは例として、実行する計算ノードのホスト名を取得し、それを標準出力するプログラムになっています。具体的には日立最適化 FORTRAN90 コンパイラで利用可能なサービスサブルーチンの一つである HOSTNM を用いて、実行時に name 変数にホスト名を格納し、ホスト名が取得できた場合に statnum に「0」を格納します。その後、write 文にてそれぞれの値を出力します。
日立最適化 FORTRAN90 コンパイラにおいて HOSTNM のようなコンパイラでサービスするサブルーチンについては日立より用意されているマニュアル「最適化 FORTRAN 言語」及び「最適化 FORTRAN 使用の手引」に記載があります。今回の場合、「最適化 FORTRAN 言語」の索引から先頭の文字 H の箇所を参照してください。マニュアルにはこの他にも各オプションの解説や、プログラミングに必要な注意書きなどが記載してあります。適宜参照するようにしてください。
それでは、このプログラムをコンパイルするために、以下のようなオプションを含むコマンドラインを実行してください。-lf90c により、サブルーチンの HOSTNM の呼び出しが可能になるからです。
% f90 gethostn.f90 -lf90c f90: compile start : gethostn.f90 *OFORT90 V03-01-/B entered. *program name = GETHOSTN *end of compilation : GETHOSTN *program units = 0001, no diagnostics generated.
このコマンドラインでは上記の様に特にエラーを出力せず、実行オブジェクトとして、a.out が生成されます。ここで、プログラムソース中の statnum = hostnm(name) の一行上に意図的に間違った文法となる statnum == 1 という行を追加してコンパイルすると次のようなエラーになります。
% f90 gethostn.f90 -lf90c f90: compile start : gethostn.f90 *OFORT90 V03-01-/B entered. KCHF049K 12 5 unrecognizable fortran statement. *program name = GETHOSTN *program units = 0001, 0001 diagnostics generated, highest severity code is 12
この KCHF の一行が日立最適化 FORTRAN90 コンパイラにおける診断メッセージのコードを表しており、その一行下が診断メッセージ本文となります。今回の場合はコード:KCHF049K のエラー(Fortran で分類できない文がある)が発生しており、エラーレベルは 12(文法の誤りがあり、オブジェクトモジュールは生成しない)でエラー個所はソースコードの 5 行目ということを表してします。もし、診断メッセージだけでは不十分であった場合はデバッグオプションである -debug をコンパイルオプションに追加して試してみるとより詳細な情報が得られることがあります。通常はこのメッセージを参考にプログラム作成を行ったり、本センターの SR16000 の環境で正常に動作するよう、手持ちのプログラムのデバッグ作業を実施します。
また、この診断メッセージのコードを元にマニュアルの関連する記述を参照することができます。上記の例の場合は「最適化 FORTRAN 使用の手引」の「11章コンパイル時の診断メッセージ(11.1 診断メッセージ)」の KCHF049K の項を参照してください。
次に、コンパイル可能な状態のプログラムソースを使って、試しにオプション「-lf90c」を外してコンパイルしてみます。
% f90 gethostn.f90 f90: compile start : gethostn.f90 *OFORT90 V03-01-/B entered. *program name = GETHOSTN *end of compilation : GETHOSTN *program units = 0001, no diagnostics generated. ld: 0711-317 ERROR: Undefined symbol: .HOSTNM ld: 0711-345 Use the -bloadmap or -bnoquiet option to obtain more information. find: bad status-- /tmp/.20120806165543720948F90
先ほどとは異なるエラーが出ました。ld で Undefined symbol なので、HOSTNM が解釈できていないことになります。HOSTNM はプログラムソース中の hostnm と異なり、大文字と小文字の差がありますが、今回のサービスサブルーチンの場合、適宜コンパイラが解釈してくれるので、問題となる箇所ではありません。サブルーチン名の大文字と小文字を解釈してくれるオプション「-i,L」を用いると次のようになります。このオプションは今後外部のライブラリを用いる場合には必要なケースがあります。
% f90 gethostn.f90 -i,L f90: compile start : gethostn.f90 *OFORT90 V03-01-/B entered. *program name = gethostn *end of compilation : gethostn *program units = 0001, no diagnostics generated. ld: 0711-317 ERROR: Undefined symbol: .hostnm ld: 0711-345 Use the -bloadmap or -bnoquiet option to obtain more information. find: bad status-- /tmp/.201208061718375374832F90
以上より、このようなすでにコンパイル可能な例、あるいは手持ちのプログラムを用いて、オプションを変更したり、プログラムの記述をわざと変更したりすることで、本システムではどの様なエラーメッセージが出力されるかを把握しておくことは今後のプログラムのデバッグ作業にも有用となります。
9.2.2 ジョブ実行
9.2.1 でコンパイル、生成された a.out を、ジョブスクリプトを記述して、計算ノードで実行してみます。その際、ジョブスクリプトのパラメータを少し変更して出力の変化を見てみます。
ジョブスクリプト(job.csh)は以下のようにします。parallel キュー(-q parallel)において1ノード(-N 1)を用い、1 プロセス/1 ノード(-J T01,T は task の「t」)で実行してみます。$QSUB_WORKDIR は qsub コマンドを実行した際のカレントディレクトリが代入されます。今回の場合、job.csh と a.out は同じディレクトリに保存されているとします。よって、このジョブスクリプトは、計算ノードにおいて $QSUB_WORKDIR にカレントディレクトリを変更し、$QSUB_WORKDIR に置かれている a.out を実行するというジョブスクリプトになります。
#!/bin/csh #@$-q parallel #@$-N 1 #@$-J T01 cd $QSUB_WORKDIR ./a.out
qsub job.csh にてジョブ実行を開始し、問題なく終了すると job.csh.oXXXXX (XXXXX は数字)というテキストファイルが生成されるので、それを参照してみます。以下のような 1 レコードの出力になっているはずです。
HOSTNAME is "htcfWWcWWpWW"( 0 )
「htcfWWcWWpWW」が取得された計算ノードのホスト名です。a.out はこの計算ノード上で実行されました。次に 2 ノード(-N 2)を用いて実行してみます。
#!/bin/csh #@$-q parallel #@$-N 2 #@$-J T01 cd $QSUB_WORKDIR ./a.out
出力されたファイルには先ほどと同様に1レコードしか得られていないはずです。これは 1 ノード(-N 1)を用い、2 プロセス/1 ノード(-J T02)で実行しても同様です。また、下記のように a.out を 2 つ実行するようにしても、-N や -J を変化させても同じホスト名で出力されたレコードが 2 つ出力されます。
#!/bin/csh #@$-q parallel #@$-N 2 #@$-J T01 cd $QSUB_WORKDIR ./a.out & ./a.out & wait
以上より、単純に a.out を実行する限りでは NQS のパラメータを操作しても、ある 1 ノード上でしか実行されないことが分かります。ここで、本センターのシステムは、1 つの a.out を、複数の計算ノードを跨いだすべての CPU を使って実行する仕組みではありません。これは例えば OpenMP でノード内並列(スレッド並列)されたプログラムが生成するスレッドにおいても同様です。逆に並列化されていなかったり、スレッド並列化されていても 1 ノード使えれば十分なプログラムの場合は、これまでの 1 ノード、1 プロセス実行で基本的な対応ができます。次に、NQS オプションで計算ノード数個分、プロセス数個分の a.out を実行するためには mpirun コマンドを使います(MPI 実行)。
#!/bin/csh #@$-q parallel #@$-N 2 #@$-J T01 cd $QSUB_WORKDIR mpirun ./a.out
この場合は以下の出力が得られるはずです。各計算ノードで a.out が実行できました。
HOSTNAME is "htcfWWcWWpWW"( 0 ) HOSTNAME is "htcfXXcXXpXX"( 0 )
また、 1 ノード(-N 1)を用い、2 プロセス/1 ノード(-J T02)で実行するとa.out をバックグラウンドで 2 つ記述して実行せずとも以下の出力が得られます。
HOSTNAME is "htcfWWcWWpWW"( 0 ) HOSTNAME is "htcfWWcWWpWW"( 0 )
このように mpirun を使うと、プログラムを並列に実行できますが、単純に同じ a.out を計算ノード数分あるいはプロセス個数分実行しただけで、プログラム自体が MPI で並列化されたわけではありません。一つのプログラムを効率よく複数計算ノードあるいは複数プロセスを使って実行するためには MPI を用いて適切にプログラムソースを書き換える必要があります。
9.3 OpenMP
9.3.1 OpenMP プログラムとコンパイル
OpenMP は、共有メモリを用いた並列化を記述するための指示文、ライブラリ、及び環境変数を規定したもので、並列実行単位にスレッドを用います。各スレッドが各CPU に分散され並列実行されます。ここでは以下のようなサンプルプログラムを作成し、ファイル名「omp_sample.f90」として保存しておきます。
OpenMP についてはマニュアル「最適化 FORTRAN 使用の手引」の「7. OpenMP」に詳細な記述があります。コマンドなどについては FAQ 「OpenMP は使えますか。使い方を教えてください。」にまとめてありますので、ご覧ください。
program omp_sample implicit none include 'omp_lib.h' double precision t1, t2, diff integer, parameter :: N = 1000 integer i, j, k, nt double precision :: a(N,N), b(N,N), c(N,N) double precision tmp_val t1 = 0.0d0 t2 = 0.0d0 !$omp parallel !$omp master nt = omp_get_num_threads() !$omp end master !$omp end parallel do i=1, N do j=1, N a(j,i) = dble(i+j) b(j,i) = dble(i+j) c(j,i) = 0.0d0 end do end do t1 = omp_get_wtime() !$omp parallel !$omp do private(j,k,tmp_val) do i=1, N do j=1, N tmp_val = 0.0d0 do k=1, N tmp_val = tmp_val + a(k,i) * b(j,k) end do c(j,i) = tmp_val end do end do !$omp end do !$omp end parallel t2 = omp_get_wtime() diff = t2 - t1 print *, nt, 'thread(s), Etime is:', diff, '[s]' end program omp_sample
このプログラムは適当な行列積を実行し、その計算にかかった経過時間を表示するものです。実行時のスレッド数を取得する関数(omp_get_num_threads)と経過時間を取得する関数(omp_get_wtime)を使用するため、まず omp_lib.hをインクルードします。OpenMP 実行時ライブラリのヘッダファイルであるomp_lib.h に対しては標準でライブラリパスが通っているので、omp_lib.h をフルパスなどで記述する必要はありません。
コンパイルには、日立最適化 FORTRAN90 コンパイラで OpenMP ディレクティブを有効にするオプション -omp を使います。
% f90 -omp -Oss omp_sample.f90 f90: compile start : ./omp_sample.f90 *OFORT90 V03-01-/B entered. KCHF475K 00 C the variable is defined, but is never referred. *program name = OMP_SAMPLE *end of compilation : OMP_SAMPLE *end of compilation : _user_parallel_func_1__hf_main *end of compilation : _user_parallel_func_2__hf_main *program units = 0001, 0001 diagnostics generated, highest severity code is 00
行列積の結果を保存した配列 c の出力などを行っていないため、KCHF475Kが出力されます。これはエラーではなく警告メッセージ相当の出力であり、コンパイルは中断せず、オブジェクトファイルは生成されます。詳細は「最適化 FORTRAN 使用の手引」を参照してください。
-Oss で最適化しない場合は -Oss に含まれていた -parallel を明記します。
% f90 -omp -parallel omp_sample.f90
9.3.2 ジョブ実行
以下のようなジョブスクリプト(job.csh)を記述します。parallel キュー(-q parallel)において1ノード(-N 1)を用い、1 プロセス/1 ノード(-J T01)で実行します。
#!/bin/csh #@$-q parallel #@$-N 1 #@$-J T01 setenv OMP_NUM_THREADS 8 cd $QSUB_WORKDIR ./a.out
OpenMP を使用したプログラムを実行する際のスレッド数は環境変数の $OMP_NUM_THREADSに与えます。上記では 1 プロセス立ち上げたプログラムから 8 つのスレッドを生成し、並列実行するものです。出力ファイル(job.csh.oXXXXX)を確認します。
8 thread(s), Etime is: 0.412459999999999938 [s]
$OMP_NUM_THREADS を 1 から 64 まで変化させてそれぞれ実行し、結果を比較してみてください。ここで、基本的に性能面での理由から、指定する $OMP_NUM_THREADS の値が32(CPU コア数) x SMT(=2) = 64 を超えないように注意してください。
9.4 MPI
9.4.1 簡単な MPI プログラムとコンパイル
MPI(Message Passing Interface) は MPI Forum によって標準化されたメッセージ通信ライブラリのインタフェース規約です。ノード間及びノード内のプロセス間通信に MPI 通信ライブラリを持ちいたメッセージ通信が可能です。MPIは多くの計算機に実装されており、移植性の高いインタフェースとなります。
本センターの SR16000 では MPI 2.1 をサポートしています。ここでは以下のようなサンプルプログラムを作成し、ファイル名「mpi_sample.f90」として保存しておきます。コマンドなどの詳細については、FAQ 「MPI プログラムをコンパイルする際のコマンド名について教えてください。」をご覧ください。
program getmpirank implicit none include 'mpif.h' integer :: size,rank,ierr call MPI_INIT(ierr) call MPI_COMM_RANK(MPI_COMM_WORLD,rank,ierr) call MPI_COMM_SIZE(MPI_COMM_WORLD,size,ierr) write(*,*) 'proc=',rank, 'size=',size call MPI_FINALIZE(ierr) end program getmpirank
MPI 関数を使用するため、まず mpif.h をインクルードします。MPI ライブラリのヘッダファイルである mpif.h に対しては標準でライブラリパスが通っているので、mpif.h をフルパスなどで記述する必要はありません。上記では、MPI実行環境の初期化(MPI_INIT)と実行環境の終了(MPI_FINALIZE)、MPI コミュニケータから、call したプロセスのランクの取得(MPI_COMM_RANK)と全プロセス数の取得(MPI_COMM_SIZE)というプログラム制御関連の MPI 関数のみ用いており、通信を行う関数(MPI_Send など)を使用していない単純な例となっています。また、MPI コミュニケータには全プロセスで構成される MPI_COMM_WORLD を指定しています。取得できるランク番号は 0 から始まる点ご注意ください。
コンパイルには、MPI を使用するにあたって必要なヘッダファイルやライブラリ、コンパイルオプションを設定し、f90 コンパイラを呼び出す mpif90 コマンドを使います。今回は以下のように -Oss で最適化するものの、このオプションには自動並列化オプションが含まれるため、自動並列化を -noparallel オプションで無効にします。ピュア MPI なので、各プロセスは単一スレッドでの動作となります。
% mpif90 -Oss -noparallel mpi_sample.f90 f90: compile start : mpi_sample.f90 *OFORT90 V03-01-/B entered. *program name = GETMPIRANK *end of compilation : GETMPIRANK *program units = 0001, no diagnostics generated.
9.4.2 ジョブ実行(ピュア MPI)
以下のようなジョブスクリプト(job.csh)を記述します。parallel キュー(-q parallel)において2ノード(-N 2)を用い、32 プロセス/1 ノード(-J T32)で実行します。
#!/bin/csh #@$-q parallel #@$-N 2 #@$-J T32 cd $QSUB_WORKDIR mpirun ./a.out
合計 64 プロセスでの実行なので、出力されるランク番号は 0 から 63 の 64 個になっています。出力ファイル(job.csh.oXXXXX)を確認します。
proc= 0 size= 64 proc= 1 size= 64 .... proc= 62 size= 64 proc= 63 size= 64
ここで、指定するプロセス数(-J)には 32(CPU コア数) x SMT(=2) = 64 を超えた指定を行うことはできません。
9.4.3 OpenMP + MPI ハイブリッド実行
OpenMP + MPI ハイブリッド実行についてはコンパイル方法とジョブスクリプトの例を示すに留めることにします。プログラムソース(hybrid_sample.f90)には OpenMP ディレクティブが書かれているとすると、次のようにしてコンパイルします。なお、コマンドなどについては、FAQ 「MPI 並列化したプログラムですが、ノード内を OpenMP で並列化していますが利用可能でしょうか。また、コンパイル方法について教えてください。」にもまとめてありますので、ご覧ください。
% mpif90 -omp -Oss hybrid_sample.f90
-Oss で最適化しない場合は -Oss に含まれていた -parallel を明記します。
% mpif90 -omp -parallel hybrid_sample.f90
このプログラムを 1 ノードあたり 8 プロセス、 1 プロセスあたり 4 スレッド、8 ノードで実行する際のジョブスクリプトは以下のようになります。
#!/bin/csh #@$-q parallel #@$-N 8 #@$-J T08 setenv OMP_NUM_THREADS 4 cd $QSUB_WORKDIR mpirun ./a.out
parallel キュー(-q parallel)において 8 ノード(-N 8)を用い、8 プロセス/1 ノード(-J T08)での実行となります。また、各プロセスは OpenMP のスレッド 4 本で並列化するので、環境変数 OMP_NUM_THREADS に 4 を設定します。
ここで、プログラムの作りにもよりますが、基本的に性能面での理由から、指定するプロセス数(-J) x OMP_NUM_THREADS の値が32(CPU コア数) x SMT(=2) = 64 を超えないように注意してください。プログラムの作り次第で計算ノード上のメモリを使い果たしてしまい、一部プロセスがダウンしてしまうなどの原因になります。