前回は多対多の集団通信について学びました。ですが、実際「となりのプロセスにだけデータを送りたい」というような場合もよくあります。今回はそういう場合に使う1対1通信について学びます。
目次(予定)
- 並列処理とは
- [プロセス並列の基本] (https://qiita.com/items/5cce1174ac48367948bc)
- 集団通信
- 1対1通信 ←今ココ
- オブジェクト指向コードの並列化
1対1通信
MPI_sendとMPI_recv
文字通りデータを送ります。
ブロッキング通信なので、通信が正常に終了しなければ終了しません。
サンプル
このサンプルでは、右隣に自身のランクを送信し、左隣からランクを受信します。
subroutine test_01()
integer :: n_data
integer :: dest ! 送り先
integer :: origin ! 受信データの送信元
integer :: buf ! 受け取るデータ
integer,allocatable :: ista(:) ! ステータス格納用
allocate(ista(mpi_status_size))
write(LOGUNIT,*) "test 01"
buf = me
n_data = 1
if(me == nnn-1) then
dest = 0
else
dest = me+1
end if
if(me == 0) then
origin = nnn-1
else
origin = me-1
end if
write(LOGUNIT,*) "origin,dest=", origin,dest
call mpi_send(buf, n_data, mpi_integer , dest, &
me, mpi_comm_world, ierr)
call mpi_recv(buf, n_data, mpi_integer, origin, &
origin, mpi_comm_world,ista,ierr)
write(LOGUNIT,*) "buf:", buf
end subroutine test_01
MPI_sendrecv
sendとrecvは基本的にセットで呼ばれるので、両方まとめたものがこれです。
subroutine test_02()
integer :: n_data
integer :: dest
integer :: origin
integer :: buf
integer,allocatable :: ista(:)
allocate(ista(mpi_status_size))
write(LOGUNIT,*) "test 02"
buf = me
n_data = 1
if(me == nnn-1) then
dest = 0
else
dest = me+1
end if
if(me == 0) then
origin = nnn-1
else
origin = me-1
end if
write(LOGUNIT,*) "origin,dest=", origin,dest
call mpi_sendrecv(me, n_data, mpi_integer , dest, me,&
buf, n_data,mpi_integer, origin, origin, &
mpi_comm_world, ista, ierr)
write(LOGUNIT,*) "buf:", buf
end subroutine test_02
MPI_IsendとMPI_Irecv
mpi_sendとmpi_recvはブロッキング通信ですが、ブロッキングでないものがmpi_isendとmpi_irecvです。
送受信を完了するにはMPI_waitを使う必要があります。
subroutine test_03()
integer :: n_data
integer :: dest
integer :: origin
integer :: buf
integer :: ireq
integer,allocatable :: ista(:)
allocate(ista(mpi_status_size))
write(LOGUNIT,*) "test 03"
buf = me
n_data = 1
if(me == nnn-1) then
dest = 0
else
dest = me+1
end if
if(me == 0) then
origin = nnn-1
else
origin = me-1
end if
write(LOGUNIT,*) "origin,dest=", origin,dest
call mpi_isend(me, n_data, mpi_integer , dest, &
me, mpi_comm_world, ireq, ierr)
call mpi_irecv(buf, n_data, mpi_integer, origin, &
origin, mpi_comm_world,ireq,ierr)
call mpi_wait(ireq, ista, ierr)
write(LOGUNIT,*) "buf:", buf
end subroutine test_03
#デッドロック
集団通信と異なり、1対1通信では厄介な問題が生じることがあります。それはデッドロックです。
一般にデッドロックとは、複数の処理同士が互いの処理の終了を待っている状態になり、処理が完全に停止してしまうことを言います。排他制御などをかじったことがある人なら知っているかも知れません。
MPIでは次のようにしてデッドロックが生じます。以下のコードはデッドロックを起こし得ます。
! n_dataはかなり大きい
call mpi_recv(buf_recv, n_data, mpi_double_precision, dest, tag, mpi_comm_world, ista,ierr)
call mpi_send(buf_send, n_data, mpi_double_precision, dest, tag, mpi_comm_world, ,ierr)
このコードを実行すると、両方がデータの受信待ちになるものの、データをsendしてくれる相手がいないので、デッドロックになります。特にデータサイズが大きいときにおこりやすいです。
逆にしても同じです。
call mpi_send(buf_send, n_data, mpi_double_precision, dest, tag, mpi_comm_world, ierr)
call mpi_recv(buf_recv, n_data, mpi_double_precision, dest, tag, mpi_comm_world, ista,ierr)
mpi_sendは受信する相手がいないと正常に終了しません。なので順番を変えてもデッドロックになります。
厄介なことに、デッドロックは場合によっては起こったり起こらなかったりします。ログが出ることもありません。その上abortするわけでもありません1 。そのため「MPIプログラムが沈黙したらデッドロックを疑う」と知っていなければいけません。
解決策
1.MPI_sendrecvを使う
MPI_sendrecvを使えばデッドロックを防げます。
2. MPI_isendとMPI_irecvに置き換える
デッドロックはブロッキング通信であることが原因なので、ノンブロッキングであるMPI_isendとMPI_irecvに置き換えれば解消できます。
構造体やクラスを通信するには
さて、ここまでテストで扱ってきたmpiコマンドは、すべてmpiデータ型(mpi_integer
,mpi_double_precision
など)を引数に取ってきました。ではここで自分で定義した構造体やクラスなどを通信するにはどうすればよいでしょうか。
実は僕も1行で書けるコードは知りません。mpi_datatype_null
を駆使すれば行けるのかもしれないですが、僕が見たことがあるコードでは、構造体を1種類しか扱っていなかったので、その構造体に対してsendやbcastをそれぞれ定義していました。
これについては次回。
参考サイト
-
いつもの概説サイト
http://www.cv.titech.ac.jp/~hiro-lab/study/mpi_reference/chapter4.html -
デッドロックについて詳説
https://qiita.com/kaityo256/items/a8690d4dc0d82dd7617c
-
この「abortしない」というのが非常に厄介です。「月曜日に結果を見るつもりで金曜日に計算を回してうまく行ってそうだったのに、月曜日になっても計算が終了しておらず結果も白紙のまま」ということが多々ありました。orz ↩