3D MPI笛卡尔拓扑下类MPI_Neighbor_allreduce功能的最优实现策略问询
我现在遇到个MPI编程的棘手问题——迫切需要类似MPI_Neighbor_allreduce的操作,但这个MPI函数根本不存在,得自己动手实现。先给大家理清楚背景:
背景说明
我基于描述3D物理域进程分布的3D MPI笛卡尔拓扑,写了一个probe函数,用来根据域内某点的三维坐标获取一个标量值(存入REAL :: val变量)。参与val计算的进程数量只会是1、2、4或8个,对应四种场景:
- 如果点在某进程子域内部,只有1个进程参与;
- 如果点在两个进程子域的分界面上,2个进程参与;
- 如果点在四个进程子域的交线上,4个进程参与;
- 如果点在八个进程子域的顶点处,8个进程参与。
调用probe之后,每个进程都持有val:参与计算的进程的val是有效数值,未参与的进程的val是0或NaN(可以通过注释/取消注释代码行切换)。每个进程通过LOGICAL :: found变量知道自己是否参与,但不知道是不是只有自己参与,也不知道其他参与的邻居是谁。
规则很明确:只有1个进程参与时,直接用这个进程的val就行;另外三种情况,需要把所有参与进程的val求和,再除以参与进程的总数(也就是自己加邻居的数量)。
核心问题
实现上述通信与计算的最优策略是什么?
我考虑过的几种方案
我自己琢磨了几个方向,但都有明显的缺点:
- 方案1:全局归约:所有进程在调用
probe前先把val设为0,之后用MPI_(ALL)REDUCE(参与进程的val非0,其余为0)。但这种方式处理多个点时必须串行执行,哪怕不同点的参与进程集完全不重叠,效率太低了。 - 方案2:邻居共享+点对点通信:所有进程调用
MPI_Neighbor_allgather共享found变量,让参与进程知道哪些邻居需要参与求和,再通过单独的MPI_SEND和MPI_RECV传递val。但这会让所有进程都卷入通信,哪怕某个进程根本不参与当前点的计算,有点浪费资源。 - 方案3:自定义通信子:或许最优的方式是每个进程创建一个包含自身及6个邻居的通信子,再基于这个通信子做后续的归约操作?不过我还没具体尝试,不确定可行性和效率。
补充:死锁问题的临时处理
针对之前有人提到的死锁风险,我一开始的处理方式是:正方向通信(对应who_is_involved里的偶数索引)先调用MPI_SEND再调用MPI_RECV,负方向则反过来。但这种方式在周期方向只有两个进程的特殊场景下会失效——此时每个进程会把对方同时视为正负两个方向的邻居,导致双方按相同顺序调用MPI_SEND和MPI_RECV,直接触发死锁。
后来我对found_neigh(代码里的变量名)做了临时修改来规避这个问题:
DO id = 1, ndims IF (ALL(found_neigh(2*id - 1:2*id))) found_neigh(2*id -1 + mycoords(id)) = .FALSE. END DO
目前已实现但不满意的代码
我现在已经写出了一个可行的实现,但总感觉不够优雅高效,代码如下:
found = ... ! 根据进程是否参与val计算赋值.TRUE.或.FALSE. IF ( found) val = ... ! 计算自身贡献值 IF (.NOT. found) val = NaN ! 与邻居共享found变量 found_neigh(:) = .FALSE. CALL MPI_NEIGHBOR_ALLGATHER(found, 1, MPI_LOGICAL, found_neigh, 1, MPI_LOGICAL, procs_grid, ierr) found_neigh = found_neigh .AND. found ! 修改found_neigh以处理周期方向仅两个进程的特殊情况 DO id = 1, ndims IF (ALL(found_neigh(2*id - 1:2*id))) found_neigh(2*id -1 + mycoords(id)) = .FALSE. END DO ! 与邻居交换贡献值 val_neigh(:) = NaN IF (found) THEN DO id = 1, ndims IF (found_neigh(2*id)) THEN CALL MPI_SEND(val, 1, MPI_DOUBLE_PRECISION, idp(id), 999, MPI_COMM_WORLD, ierr) CALL MPI_RECV(val_neigh(2*id), 1, MPI_DOUBLE_PRECISION, idp(id), 666, MPI_COMM_WORLD, MPI_STATUS_IGNORE, ierr) END IF IF (found_neigh(2*id - 1)) THEN CALL MPI_RECV(val_neigh(2*id - 1), 1, MPI_DOUBLE_PRECISION, idm(id), 999, MPI_COMM_WORLD, MPI_STATUS_IGNORE, ierr) CALL MPI_SEND(val, 1, MPI_DOUBLE_PRECISION, idm(id), 666, MPI_COMM_WORLD, ierr) END IF END DO END IF ! 合并自身与邻居的贡献值 val = somefunc(val, val_neigh)
有没有更高效、更简洁的实现方式?期待大家的建议!
内容的提问来源于stack exchange,提问作者Enlico




