MPIによる共有メモリの利用 （Fortran編）

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とかで確認すれば，ひとつのコアのみメモリを消費してるのがきちんとわかる。。。けどメモリモニタ、変な挙動があるなぁ．


```fortran:makeshare.f90
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
```