MPI Fortran 代码:如何通过 OpenMP 在节点间共享数据?

12

我正在处理一个已经使用MPI的Fortan代码。

现在,我面临一个情况,一组数据非常大,但对于每个进程都相同,因此我希望将其仅存储在每个节点的内存中,并且该节点上的所有进程都可以访问相同的数据。

为每个进程存储它将超出可用RAM。

是否有可能通过openMP实现这样的目标?

只需要按节点共享数据,不需要其他节点并行处理,因为这已经通过MPI完成了。


2
是的,你想做的是可能的,通常被称为混合编程,它是MPI和OpenMP的结合。请使用你喜欢的搜索引擎学习更多。在这里,在SO上查找来自Hristo Iliev和Jonathan Dursi的一些答案,我相信他们两个都涵盖了这个主题,并且当这些人涵盖一个主题时,它会保持覆盖状态。 - High Performance Mark
我猜问题在于如何在没有使用OpenMP并行化所有计算的情况下完成它。 - Vladimir F Героям слава
感谢您的建议!是的,正如Vladimir F所指出的那样,这正是我所考虑的,数据共享但不进行进一步的并行化。那么我会更仔细地研究混合编程。 - chris
3个回答

20

如果只是为了共享一块数据,你不需要实现混合MPI + OpenMP代码。你需要做的是:

1)将全局通信器分割成跨越相同主机/节点的组。如果你的MPI库实现了MPI-3.0,这非常容易 - 你只需要调用MPI_COMM_SPLIT_TYPE并将split_type设置为MPI_COMM_TYPE_SHARED

USE mpi_f08

TYPE(MPI_Comm) :: hostcomm

CALL MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, 0, &
                         MPI_INFO_NULL, hostcomm)

MPI-2.2或更早版本不提供MPI_COMM_SPLIT_TYPE操作,因此需要有些创意。例如,您可以使用我在Github上找到的简单的按主机拆分实现,该实现方法在此处

2)现在,同一节点上的进程属于同一个通信器hostcomm,它们可以创建一块共享内存,并使用它来交换数据。再次提醒,MPI-3.0提供了一个相对容易且可移植的方法来实现这一点:

USE mpi_f08
USE, INTRINSIC :: ISO_C_BINDING, ONLY : C_PTR, C_F_POINTER

INTEGER :: hostrank

INTEGER(KIND=MPI_ADDRESS_KIND) :: size
INTEGER :: disp_unit
TYPE(C_PTR) :: baseptr
TYPE(MPI_Win) :: win

TYPE(MY_DATA_TYPE), POINTER :: shared_data

! We only want one process per host to allocate memory
! Set size to 0 in all processes but one
CALL MPI_Comm_rank(hostcomm, hostrank)
if (hostrank == 0) then
   size = 10000000 ! Put the actual data size here
else
   size = 0
end if
disp_unit = 1
CALL MPI_Win_allocate_shared(size, disp_unit, MPI_INFO_NULL, &
                             hostcomm, baseptr, win)

! Obtain the location of the memory segment
if (hostrank /= 0) then
   CALL MPI_Win_shared_query(win, 0, size, disp_unit, baseptr)
end if

! baseptr can now be associated with a Fortran pointer
! and thus used to access the shared data
CALL C_F_POINTER(baseptr, shared_data)

! Use shared_data as if it was ALLOCATE'd
! ...

! Destroy the shared memory window
CALL MPI_Win_free(win)
代码的工作方式是使用MPI-3.0功能来分配共享内存窗口。 MPI_WIN_ALLOCATE_SHARED在每个进程中分配一块共享内存。由于您希望共享一块数据,因此只有将其分配给单个进程而不是分散在多个进程中才有意义,因此在进行调用时对size除了一个秩以外都设置为0。使用MPI_WIN_SHARED_QUERY查找共享内存块在调用进程的虚拟地址空间中映射的地址。一旦知道了地址,C指针就可以使用C_F_POINTER()子程序与Fortran指针相关联,并且后者可以用于访问共享内存。完成后,必须通过使用MPI_WIN_FREE销毁共享内存窗口来释放共享内存。
MPI-2.2或更早版本不提供共享内存窗口。在这种情况下,必须使用特定于操作系统的API来创建共享内存块,例如标准POSIX序列shm_open() / ftruncate() / mmap()。必须编写一个可从Fortran中调用的实用C函数来执行这些操作。请参见此代码以获取一些灵感。mmap()返回的void *可以直接传递给Fortran代码中的C_PTR类型变量,然后可以将其与Fortran指针相关联。

哇,这听起来非常有前途!非常感谢!我必须认真研究一下。还有一个小问题:是否有可用于生产的MPI-3实现?编辑:不用担心,open-mpi具有完整的MPI-3标准支持。 - chris
MPICH也支持完整的MPI-3。我相信一些供应商的实现也支持它,或者很快也会支持。 - Wesley Bland
Open MPI中MPI-3.0支持在1.8系列中存在,但尚未完全达到“稳定版”标签的要求。 - Hristo Iliev
@HristoIliev 我有一个类似于Chris的问题。我有一个使用MPI的代码,但是我有一个向量对于每个进程都是相同的。现在,在每个时间步之后,每个进程修改其相应的向量块,然后将该块发送到所有其他进程,同时从其他进程接收所有其他块。我的教授告诉我这很慢,也不是很优化。我能像Chris一样做些什么吗? - Gundro
@Gundro,你应该考虑在MPI之上添加OpenMP并行化。这比MPI共享内存窗口更简单,而且代码会更清晰。 - Hristo Iliev

7
通过这篇回答,我想添加一个完整的可运行代码示例(适用于ifort 15和mvapich 2.1)。MPI共享内存概念仍然相当新,特别是对于Fortran而言,很少有代码示例。它基于Hristo的答案和mvapich邮件列表中非常有用的电子邮件(http://mailman.cse.ohio-state.edu/pipermail/mvapich-discuss/2014-June/005003.html)。
该代码示例基于我遇到的问题,并通过以下方式添加到Hristo的答案中:
  • 使用mpi而不是mpi_f08(一些库尚未提供完整的Fortran 2008接口)
  • 在相应的MPI调用中添加了ierr
  • 显式计算windowsize元素*elementsize
  • 如何使用C_F_POINTER将共享内存映射到多维数组
  • 修改共享内存后,请记得使用MPI_WIN_FENCE
  • Intel mpi(5.0.1.035)需要在MPI_FENCE之后添加额外的MPI_BARRIER,因为它只保证“在两个MPI_Win_fence调用之间,所有RMA操作都已完成。”(https://software.intel.com/en-us/blogs/2014/08/06/one-sided-communication
致谢Hristo和Michael Rachner。
program sharedmemtest
  USE, INTRINSIC :: ISO_C_BINDING, ONLY : C_PTR, C_F_POINTER
  use mpi
  implicit none
  integer, parameter :: dp = selected_real_kind(14,200)
  integer :: win,win2,hostcomm,hostrank
  INTEGER(KIND=MPI_ADDRESS_KIND) :: windowsize
  INTEGER :: disp_unit,my_rank,ierr,total
  TYPE(C_PTR) :: baseptr,baseptr2
  real(dp), POINTER :: matrix_elementsy(:,:,:,:)
  integer,allocatable :: arrayshape(:)

  call MPI_INIT( ierr )

  call MPI_COMM_RANK(MPI_COMM_WORLD,MY_RANK,IERR)  !GET THE RANK OF ONE PROCESS                                                                                                                                                                                                
  call MPI_COMM_SIZE(MPI_COMM_WORLD,Total,IERR)  !GET THE TOTAL PROCESSES OF THE COMM                                                                                                                                                                                          
  CALL MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, 0, MPI_INFO_NULL, hostcomm,ierr)
  CALL MPI_Comm_rank(hostcomm, hostrank,ierr)

  ! Gratefully based on: https://dev59.com/aGAf5IYBdhLWcg3wUBNc                                                                                                                                                     
  ! and https://gcc.gnu.org/onlinedocs/gfortran/C_005fF_005fPOINTER.html                                                                                                                                                                                                       
  ! We only want one process per host to allocate memory                                                                                                                                                                                                                       
  ! Set size to 0 in all processes but one                                                                                                                                                                                                                                     
  allocate(arrayshape(4))
  arrayshape=(/ 10,10,10,10 /)
  if (hostrank == 0) then
     windowsize = int(10**4,MPI_ADDRESS_KIND)*8_MPI_ADDRESS_KIND !*8 for double ! Put the actual data size here                                                                                                                                                                
  else
     windowsize = 0_MPI_ADDRESS_KIND
  end if
  disp_unit = 1
  CALL MPI_Win_allocate_shared(windowsize, disp_unit, MPI_INFO_NULL, hostcomm, baseptr, win, ierr)    

  ! Obtain the location of the memory segment                                                                                                                                                                                                                                  
  if (hostrank /= 0) then
     CALL MPI_Win_shared_query(win, 0, windowsize, disp_unit, baseptr, ierr)     
  end if

  ! baseptr can now be associated with a Fortran pointer                                                                                                                                                                                                                       
  ! and thus used to access the shared data                                                                                                                                                                                                                                    
  CALL C_F_POINTER(baseptr, matrix_elementsy,arrayshape)

  !!! your code here!                                                                                                                                                                                                                                                          
  !!! sample below                                                                                                                                                                                                                                                             

  if (hostrank == 0) then
     matrix_elementsy=0.0_dp
     matrix_elementsy(1,2,3,4)=1.0_dp
  end if
  CALL MPI_WIN_FENCE(0, win, ierr)

  print *,"my_rank=",my_rank,matrix_elementsy(1,2,3,4),matrix_elementsy(1,2,3,5)

  !!! end sample code                                                                                                                                                                                                                                                          

  call MPI_WIN_FENCE(0, win, ierr) 
  call MPI_BARRIER(MPI_COMM_WORLD,ierr) 
  call MPI_Win_free(win,ierr)     
  call MPI_FINALIZE(IERR)

  end program

谢谢你发布这么有用的文章。我可以问一下你是否有意省略了MPI_WIN_FREE调用?我已经同时使用了MPI_WIN_FREE和MPI_FINALIZE,但却得到了后者的段错误。 - painfulenglish
1
谢谢您指出这个问题。我并没有故意遗漏它们。程序的正确结尾应该是:CALL MPI_WIN_FENCE(0, win, ierr); call MPI_BARRIER(MPI_COMM_WORLD,ierr); CALL MPI_Win_free(win,ierr); call MPI_FINALIZE(IERR)。我已经相应地更正了帖子。谢谢。 - ftiaronsem
1
你知道 Stack Overflow 有一个自动版本控制吗?如果你点击 edited + date,你会看到完整的历史记录,包括日期。没有理由去写像 ==== EDIT 1/7/2016 ==== 这样的东西。 - Vladimir F Героям слава
1
谢谢您指出这一点,弗拉基米尔。我之前不知道,现在已经相应地修改了我的回答。 - ftiaronsem
2
虽然在这个例子中只有节点0写入内存,但我建议使用MPI_Win_fence将内存初始化步骤matrix_elementsy=0.0_dp与使用内存步骤matrix_elementsy(1,2,3,4)=1.0_dp分开。当我尝试在下面的代码循环中进行操作时,这个问题困扰了我。这是用户错误,但也可以作为其他人的参考点。 - Jim Parker

2
为了增加Fortran共享内存MPI示例,我想扩展ftiaronsem的代码以包含一个循环,以便MPI_Win_fence和MPI_Barrier的行为更清晰(至少对我来说是这样)。具体来说,尝试运行带有MPI_Win_Fence或MPI_Barrier命令中的一个或两个被注释掉的循环的代码,以查看效果。或者,反转它们的顺序。删除MPI_Win_Fence允许写语句显示尚未更新的内存。删除MPI_Barrier允许其他进程在进程有机会写入之前运行下一次迭代并更改内存。先前的答案真的帮助我在我的MPI代码中实现了共享内存范例。谢谢。
program sharedmemtest
  USE, INTRINSIC :: ISO_C_BINDING, ONLY : C_PTR, C_F_POINTER
  use mpi
  implicit none
  integer, parameter :: dp = selected_real_kind(14,200)
  integer :: win,win2,hostcomm,hostrank
  INTEGER(KIND=MPI_ADDRESS_KIND) :: windowsize
  INTEGER :: disp_unit,my_rank,ierr,total, i
  TYPE(C_PTR) :: baseptr,baseptr2
  real(dp), POINTER :: matrix_elementsy(:,:,:,:)
  integer,allocatable :: arrayshape(:)

  call MPI_INIT( ierr )

  call MPI_COMM_RANK(MPI_COMM_WORLD,my_rank, ierr)  !GET THE RANK OF ONE PROCESS
  call MPI_COMM_SIZE(MPI_COMM_WORLD,total,ierr)  !GET THE TOTAL PROCESSES OF THE COMM
  CALL MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, 0, MPI_INFO_NULL, hostcomm,ierr)
  CALL MPI_Comm_rank(hostcomm, hostrank,ierr)

  ! Gratefully based on: https://dev59.com/aGAf5IYBdhLWcg3wUBNc
  ! and https://gcc.gnu.org/onlinedocs/gfortran/C_005fF_005fPOINTER.html
  ! We only want one process per host to allocate memory
  ! Set size to 0 in all processes but one
  allocate(arrayshape(4))
  arrayshape=(/ 10,10,10,10 /)
  if (hostrank == 0) then
     windowsize = int(10**4,MPI_ADDRESS_KIND)*8_MPI_ADDRESS_KIND !*8 for double ! Put the actual data size here
  else
     windowsize = 0_MPI_ADDRESS_KIND
  end if
  disp_unit = 1
  CALL MPI_Win_allocate_shared(windowsize, disp_unit, MPI_INFO_NULL, hostcomm, baseptr, win, ierr)

  ! Obtain the location of the memory segment
  if (hostrank /= 0) then
     CALL MPI_Win_shared_query(win, 0, windowsize, disp_unit, baseptr, ierr)
  end if

  ! baseptr can now be associated with a Fortran pointer
  ! and thus used to access the shared data
  CALL C_F_POINTER(baseptr, matrix_elementsy,arrayshape)

  !!! your code here!
  !!! sample below
  if (hostrank == 0) then
     matrix_elementsy=0.0_dp
  endif
  call MPI_WIN_FENCE(0, win, ierr)
  do i=1, 15
     if (hostrank == 0) then
        matrix_elementsy(1,2,3,4)=i * 1.0_dp
        matrix_elementsy(1,2,2,4)=i * 2.0_dp
     elseif ((hostrank > 5) .and. (hostrank < 11)) then  ! code for non-root nodes to do something different
        matrix_elementsy(1,2,hostrank, 4) = hostrank * 1.0 * i
     endif
     call MPI_WIN_FENCE(0, win, ierr)
     write(*,'(A, I4, I4, 10F7.1)') "my_rank=",my_rank, i, matrix_elementsy(1,2,:,4)
     call MPI_BARRIER(MPI_COMM_WORLD, ierr)
  enddo
  !!! end sample code

  call MPI_WIN_FENCE(0, win, ierr)
  call MPI_BARRIER(MPI_COMM_WORLD,ierr)
  call MPI_Win_free(win,ierr)
  call MPI_FINALIZE(IERR)

  end program

这个解决方案对我大部分情况下都有效,谢谢。但是我发现在 NERSC 的 Cori 上,在 MPI_FINALIZE 行会出现 segfault。另外,MPI_WIN_FREE 是否也会释放共享内存? - Lazer
1
我无法评论Cori和标准MPI之间的任何差异,因此不确定为什么会出现段错误。很奇怪它发生在MPI_FINALIZE时,因为此时所有操作都已完成。实际上,MPI_WIN_FREE仅释放共享内存(更具体地说是由第一个参数指向的内容)。 - Jim Parker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接