MPIは元々は分散メモリ用の並列化計算用の規格だけれど,MPI-3から共有メモリについてもサポートしてます.この点について日本語解説記事やサイトでは殆ど触れられてない上,英語サイトでもほぼC言語用の説明になっており,Fortranで実装しようとしたときに少し苦労したので実装の仕方をメモしておきます.
基本的に下記ページの内容を改変してます.
https://stackoverflow.com/questions/24797298/mpi-fortran-code-how-to-share-data-on-node-via-openmp
言語仕様の詳しい事とかは公式やコンパイラのページでも見てください.
https://software.intel.com/en-us/articles/an-introduction-to-mpi-3-shared-memory-programming
-
MPIで共有メモリを使う利点:ノード内の計算コアがそのノード内に構築した共有メモリ空間にアクセスするので,それぞれの計算コア毎に計算高速化のためのテーブルなどを保存しておく必要がなくなります(1ノード36コアなら,メモリ効率を最大36倍にできる).メモリがあり余ってるなら計算遅くなるので使わないほうがいいです.
-
MPI+OpenMPのhybridでいいじゃん,という意見もあろうかと思いますが,OpenMPは簡単なループを並列化するときは便利だけどループ内での処理が複雑になってくると非常に書きにくいです(subroutineの引数に構造体つっこんだ時に戻り値がおかしくなるのは許さない).今回MPIを利用したのも半日openMPで粘ってキーボード叩き割りたくなったためです.
以下,テストコード
-
うまく計算できる事を確認した実装:Intel MPI, OpenMPI('18.09)
-
京では使えんかった…、MPI-3に対応してない('18.10)
-
富岳で使えた!('20.04)
-
sh_dataの配列データをノード内のCPUコアで共有.どのコアが同一ノードなのかは mpi_comm_split_type で自動判別してくれて,コミュニケーターとランクをそれぞれhost_commとhost_rankに割り当ててくれる.
-
sh_dataの配列サイズをdatashapeで定義.
-
mpi_win_fenceは片方向通信関数の同期をとってる.ここではsh_dataについてhost_rank==0からしかデータを書き込ませていないので問題になりにくいが,他のコアからもデータを書き込むときにはmpi_win_fenceやmpi_barrierをきちんと使用しないとconflictしてわやくちゃになる.
-
sh_dataの配列サイズが4byte整数型のサイズを超えてくる(>2^31)とIntel MPIについては2017(のver.2)以降でないとエラーが起きる.Cに8byteの整数型を渡せてないためですが,控えめに言って頭わるい.(参考:https://software.intel.com/en-us/forums/intel-clusters-and-hpc-technology/topic/519995 )
-
MacOSのアクティビティモニタで使用メモリ確認しても,全コアにメモリが割り振られてるように見える.htopとかで確認すれば,ひとつのコアのみメモリを消費してるのがきちんとわかる。。。けどメモリモニタ、変な挙動があるなぁ.
module shared
use mpi
use,intrinsic :: iso_c_binding, only : c_ptr, c_f_pointer
implicit none
integer::win1,host_comm,host_rank
contains
subroutine shared_data(datashape,sh_data)
integer(8),intent(in):: datashape(:)
real(kind(0d0)),pointer,intent(inout)::sh_data(:,:)
type(c_ptr) :: baseptr ! 一時記憶用
! win1の番号にdatashapeサイズの共有メモリを確保して,sh_dataでアクセスする
call shared_memory(datashape, kind(sh_data), baseptr)
! Cのポインタ型をFortran用に変換 baseptr -> sh_data
call c_f_pointer(baseptr, sh_data, datashape)
end subroutine shared_data
subroutine shared_memory(datashape,data_kind,baseptr) !{{{
integer(8),intent(in) :: datashape(:)
integer,intent(in)::data_kind
type(c_ptr),intent(out) :: baseptr
integer(kind=mpi_address_kind) :: windowsize
integer :: disp_unit=1,ierr
if (host_rank == 0) then
windowsize = int(product(datashape),mpi_address_kind)*data_kind
else
windowsize = 0_mpi_address_kind
end if
! windowsizeのメモリをwin1の場所に確保
call mpi_win_allocate_shared(windowsize, disp_unit, mpi_info_null, host_comm, baseptr, win1, ierr)
! rank=0に確保したメモリ空間win1のポインタを得る:baseptr
if (host_rank /= 0) then
call mpi_win_shared_query(win1, 0, windowsize, disp_unit, baseptr, ierr)
end if
end subroutine shared_memory
end module
program main
use mpi
use shared, only: shared_data, win1, host_comm, host_rank
implicit none
integer::comm_rank, comm_size, ierr
real(kind(0d0)),pointer::sh_data(:,:)
integer(8)::datashape(2)
integer::i,j
call mpi_init( ierr )
call mpi_comm_rank(mpi_comm_world,comm_rank,ierr)
call mpi_comm_size(mpi_comm_world,comm_size,ierr)
call mpi_comm_split_type(mpi_comm_world, mpi_comm_type_shared, 0, mpi_info_null, host_comm,ierr)
call mpi_comm_rank(host_comm, host_rank,ierr)
datashape=(/100,10/)
! subroutine内で,(100,10)の配列サイズのsh_dataをノード内で共有するよう定義している
call shared_data(datashape, sh_data)
! 保存したいデータを記述
if(host_rank==0) then
do j=1,10
do i=1,100
sh_data(i,j)=i*j
end do
end do
end if
! 片方向通信同期
call mpi_win_fence(0, win1, ierr)
! host_rank==0にしか書き込んでないが,他のrankからもデータを参照できる
print'(i3,4f10.4)',host_rank,sh_data(1:2,1:2)
call mpi_win_fence(0, win1, ierr)
call mpi_barrier(host_comm,ierr)
call MPI_win_free(win1,ierr)
call mpi_finalize(ierr)
end program