HPF(High Performance Fortran)
Fortranのプログラムを並列化するもの。
OpenMPのように並列実行する部分などに指示行を追加することで並列化を行う。
HPFの考え方
分散共有型のスパコンにおいて、各ノードは個別にプロセッサとメモリを持っている。
HPFでは、これらをまとめて1つのグローバルなメモリ空間ととらえることで、プログラマ自身はノード間の通信を行っているということを意識することなく、
プログラミングをすることができる。
しかし、実際にはノード間の通信は存在し、速度を追求する場合にはこれらのことを意識することは重要である。
また、仮想的にグローバルな見方をしているためにプログラム自体は逐次として書くことができる。
プログラムの流れ
基本的な流れは以下のようになる
データの分散
↓
プロセッサの宣言
↓
ループの並列化・処理の分担
データの分散
分散しているメモリにどのようにデータを割り当てるかを決定する。
均等に割り当てること、必要に応じて均等にしないようにすることも可能。また、割り当てかたにも様々な方法をとることもできる。
また、計算を行うプロセスに最適なメモリのデータ配置をとるために「シャドウ」「アライン」といった指示文も用意されている。
プロセッサの宣言
どの部分をどのプロセッサが計算するかを決定する。
分散型であることから、仮想的にグローバルなメモリであるが、実際には各ノードがメモリとプロセッサを持っている。
参照するメモリと計算を実行するプロセッサが別の場合には、他のノードと通信を行い、一度計算実行するプロセッサのメモリにデータを置く必要がある。この場合には通信コストがかかることから遅くなる。このため、できるだけ計算のソースを保管しているメモリを持つプロセッサでの計算を行うほうがアクセスが早くなる。
ループの並列化
並列化するループ文を指定する。
通信を行う必要のないループ文に関しては「LOCAL」として通信を行わないように明示することができる。
その他の注意事項
スカラ変数
配列に関しては上記のようなメモリへの分散配置が基本になるが、スカラ変数についてはそうはいかない。
各プロセッサで、すべてそのスカラ変数が必要な場合には、すべてのプロセッサがそのスカラ変数をメモリ上に保持していなければならない。そのため、スカラ変数はすべてのプロセッサに重複割り付けされている。
読み出す場合には、各プロセッサが変数を保持する必要はない。が、速度面を考えると、すべてのプロセッサのメモリが変数を所持していることが望ましい。
書き込む場合には、1つのプロセッサが書き込みを行ったあと、すべてのプロセッサに対してブロードキャストする。
NEW変数
ループ内でのみしか使用されないローカルな変数。
ループの最初で定義され、ループを抜ければ解放される。
これは、自分で指定することが原則ではあるが、コンパイラが自動で判断することも可能な場合がある。
集計変数
これは、配列要素内の最大値や総和をとるときにスカラ変数が必要になるが、代入する変数をどこにとり、どのように計算を進めるかが計算結果や速度に影響を与えることによる。
指示文
基本的に使う指示文はそんなに多くはない。
DISTRIBUTE
!HPF$ DISTRIBUTE a(<分散形式>,…) または !HPF$ DISTRIBUTE (<分散形式>,…)::a,b,…
a,bは配列。分散形式によって、各ノードのメモリへの分散の仕方を決定し分散配置を行う。宣言部に書く。
例)
real a(100,200)
!HPF$ DISTRIBUTE a(*,BLOCK) ::aの配列を列でBLOCKの形で分割する
分散形式
- BLOCK…近傍要素の参照が多い場合によく用いる。配列を一定の大きさに分割する。
- CYCLIC…配列要素を頭から1つずつ順に各ノードに割り振り、それを繰り返す。ノードが3つの場合には、ノード1には配列の0,3,6,9,…の要素が格納される。
- CYCLIC(w)…wは整数。このwの数ずつでCYCLICで割り当てる。上記2つの中間的な割り振り方。
- 負荷の偏りが事前に分かっている場合などのために、不均等な分散の方法もある。
PROCESSORS
!HPF$ PROCESSORS p(n1,n2,…,nN),…
pはプロセッサの配列名。n1*…*nNはプロセッサ数。Nは分散次元数。
DISTRIBUTEなどでどのプロセッサがどのメモリを処理するかの分割の仕方を指定する。この場合にはDISTRIBUTEに下のように「ONTO」を追加する。
例)
!HPF$ PROCESSORS proc(8),p2(2,4) ::procはプロセッサを1列で8個、p2は2列で4個ずつ割り振る
real e(100,200),f(100,200)
!HPF$ DISTRIBUTE e(*,BLOCK) ONTO proc ::eをprocに準じて分割する。つまり、列で8分割する
!HPF$ DISTRIBUTE f(BLOCK,BLOCK) ONTO p2 ::fをp2に準じて分割する。つまり、2行4列で分割する
ALIGN
!HPF$ ALIGN A(変数,…) WITH B(変数,…) または !HPF$ ALIGN (変数,…) WITH B(変数,…) :: A1,A2,…
配列A(A1,A2,…)の整列方法を配列Bに合わせる。整列方法には、3つ組、縮退、複製、Single整列がある。
- これが必要になるのは、DISTRIBUTE指示で配列を分散したときに起こる問題を解決するときである。
DISTRIBUTEで複数の配列を個々に分散したとき、その分割の仕方は分割方法と配列の大きさによる。
すると、個々の配列の分割位置は異なることになり、各プロセッサで必要なデータが一方の配列のものはあるのに他方のものはないということが起こり、余計な通信が発生しパフォーマンス低下の原因となる。これを解消するためにALIGNを使う。これによって、各プロセッサに一方のデータに対応する他方のデータをきちんと割り当てておくことで、計算のたびに通信する必要がなくなる。
例)
real a(11),b(0:11)
!HPF$ PROCESSORS p(2)
!HPF$ DISTRIBUTE (BLOCK) onto p :: b ::bをpに準じて分割
!HPF$ ALIGN a(i) WITH b(i) ::aをbに合わせる
do 1 = 1,11
a(i) = b(i)
TEMPLATE
!HPF$ TEMPLATE T(上下限,…),…
メモリを消費しない仮想配列(テンプレート)を宣言する。
ALIGN指示文では、整列元が整列先からはみ出すような指定は禁止されている。つまり、極端にずれたものはALIGNだけではできない。
そこで、TEMPLATEを用いて配列の並び方だけを定義しておき、本当に存在する配列をこの並び方に対応させる。
実際には、このTEMPLATEで宣言した枠に各配列を割り当て、DISTRIBUTEでTEMPLATEの配列を分割する。
例)
INTEGER A(8),B(8)
!HPF$ PROCESSORS P(2)
!HPF$ TEMPLATE T(9) ::TEMPLATEを宣言
!HPF$ ALIGN B(l) WITH T(l+1) ::BをTに対して後ろ詰めで対応させる
!HPF$ ALIGN A(l) WITH T(l) ::AをTに対して前詰めで対応させる
!HPF$ DISTRIBUTE T(BLOCK) ONTO P ::A,BをTに対応するように2つに分割する
do l = 1,7
B(l) = A(l+1)
INDEPENDENT
!HPF$ INDEPENDENT
通常は並列化可能な部分はコンパイラが自動で判断して並列化を行うが、たまに並列化されないことなどもある。
この指示文では、並列化処理を行う部分をプログラマが明示することで並列化する。
ただし、並列化できないところに指示をした場合の動作は保証されない。
例)
integer a(100)
!HPF$ DISTRIBUTE a(BLOCK)
!HPF$ INDEPENDENT ::ループ文の直前に書いて並列処理を指定する
do i = 1,100
a(i) = i**2
end do
write(*,*) a
end
並列化の可否
並列化できないケース
- 順序どおりに実行しないと実行結果の変化するもの(逐次処理)
- ループ内から外への分岐があるもの(ループ内での分岐は可)
- STOP文
- EXIT文
コンパイラが判断できないケース
例)
do i = …
a(ix(i)) = …
enddo
- サブルーチン内でのaの同じ要素をアクセスするかもしれない
例)
do i = …
call subx(a,i)
enddo
ON
!HPF$ ON HOME(<ホーム変数>)
または
!HPF$ ON HOME(<ホーム変数>) BEGIN
<DOループ本体>
!HPF$ END ON
DOループ内においてどのプロセッサがどの計算をするかを指定する。
指定の仕方は以下の例を見たほうがわかりやすいと思います。
例1)
…
!HPF$ INDEPENDENT
do i = 1,99
!HPF$ ON HOME(a(i)) BEGIN ::a(i)を持つプロセッサに計算を指示
a(i) = b(i+1)**2
!HPF$ END ON
enddo
例2)
…
!HPF$ INDEPENDENT
do i = 1,99
!HPF$ ON HOME(b(i+1)) BEGIN ::b(i+1)を持つプロセッサに計算を指示
a(i) = b(i+1)**2
!HPF$ END ON
enddo
以上のように変数で指定し、後の計算などを考慮に入れてどのプロセッサに計算を担当させるかを指定することができる。
SHADOW
!HPF$ SHADOW a(シャドウ幅,…),…
または
!HPF$ SHADOW (シャドウ幅,…)::a,b,…
aやbは配列変数。<シャドウ幅>は<整数>のとき両側のサイズ、<整数>:<整数>のときはそれぞれ下、上のシャドウのサイズ
差分方程式などある程度の幅でデータが必要な計算を行う場合には、配列を分散させていると端の部分のデータについて他のプロセッサのメモリを参照する必要があり、通信が発生してしまう。このため、実効速度が低下してしまう。
これを解消するため、分散した各配列の端の部分のデータを余分に持たせて(つまり、全体で重複したデータを持つ)通信を行わないようにする。
宣言部で記述する。
例はREFLECTと一緒に。
REFLECT
!HPF$ REFLECT a,b,…
aやbはSHADOWを持つ配列変数
SHADOWで指定した変数に設定を反映する。
実行部で記述する。
例)
…
dimension u(lx,ly), uu(lx,ly)
!HPF$ DISTRIBUTE (*,BLOCK)::u,uu
!HPF$ SHADOW (0,1)::u ::SHADOWを指定、0行1列だけ拡張する
…
!HPF$ REFLECT u ::シャドウ反映
c..
!HPF$ INDEPENDENT NEW(ix,iy) ::並列化指定、NEWはループ内でのみの変数を定義する、ここではixとiy
do iy = 2, ly-1
!HPF$ ON HOME(uu(:,iy)), LOCAL BEGIN ::LOCALは指定した変数が通信なしでアクセスできるという宣言、変数を書かないときは全ての変数を指定
do ix = 2, lx-1
…
enddo
!HPF$ ENDDO
enddo
…
ミニスパコンでの使い方
プロ輪で使っていたスパコンで使うことができます。
使えるコマンドは「hpf」と「fhpf」がある。
hpf
製品版のHPFコンパイラ。MPI同様の様々なコンパイルオプションがある。
fhpf
フリーで使用可能なHPFコンパイラ。manでのマニュアルが出ないという代物。
一旦MPIに落としこんでから実行形式にするもの。
しっかりと確認はしていないが、端末を起動するごとにコマンドを有効にする必要がある。
そのためのコマンドは以下。
hpfを有効に
cshの場合
source /opt/NEChpf/bin/hpfvars.csh
shの場合
source /opt/NEChpf/bin/hpfvars.sh
fhpfを有効に
setenv PATH ${PATH}:/tmp/fhpf
上記コマンドを実行したあと、コマンドが有効になっていれば、
hpf:コンパイルオプション等が表示される
fhpf:バージョン情報が表示される
プログラムの実行方法はMPIなどと同じようにジョブを投げることで行う。
その際使う実行コマンド行は
mpirun_rsh -np 8 ${NQSII_MPIOPTS} ./a.out
となる。
#bf
Today's Access -
Yesterday's Access -
最終更新:2009年07月28日 12:16