为何MPI_Sendrecv在并行C代码中比单独MPI_Ssend/MPI_Recv扩展性更优?
我编写了用于图像处理的基础MPI并行C代码,该代码会执行N次迭代,每次迭代通过MPI收发例程完成进程间的halo swap。最初采用单独的MPI_Ssend/MPI_Recv依次执行,在64个进程上运行100,000次数值例程迭代时,运行时间约为8秒;改用MPI_Sendrecv组合调用后,运行时间降至约0.5秒。
后续调整进程数与迭代次数进行测试,结果如下:
| 进程数 | 迭代次数 | 单独调用运行时间(秒) | 组合调用运行时间(秒) |
|---|---|---|---|
| 64 | 100,000 | 8 | 0.50 |
| 32 | 50,000 | 2.32 | 0.49 |
| 16 | 25,000 | 0.86 | 0.43 |
| 8 | 12,500 | 0.48 | 0.41 |
| 4 | 6,250 | 0.39 | 0.37 |
| 2 | 3,125 | 0.38 | 0.37 |
可见组合调用方案的弱扩展性极佳,而单独调用方案的扩展性差很多,请问这是什么原因?
代码实现如下:
// Combined send receive approach MPI_Sendrecv(&old[Nx_mpi][1], Ny_mpi, MPI_DOUBLE, right, 1, &old[0][1], Ny_mpi, MPI_DOUBLE, left, 1, cart_comm, &statusArray[0]); MPI_Sendrecv(&old[1][1], Ny_mpi, MPI_DOUBLE, left, 2, &old[Nx_mpi+1][1], Ny_mpi, MPI_DOUBLE, right, 2, cart_comm, &statusArray[1]); // Seperate sends and receives approach MPI_Ssend(&old[Nx_mpi][1], Ny_mpi, MPI_DOUBLE, right, 1, cart_comm); MPI_Recv(&old[0][1], Ny_mpi, MPI_DOUBLE, left, 1, cart_comm, &statusArray[0]); MPI_Ssend(&old[1][1], Ny_mpi, MPI_DOUBLE, left, 2, cart_comm); MPI_Recv(&old[Nx_mpi+1][1], Ny_mpi, MPI_DOUBLE, right, 2, cart_comm, &statusArray[0]);
其中cart_comm是一维笛卡尔拓扑,运行时注释掉其中一种方案。代码运行于ARCHER2超算,采用Cray MPI与GCC编译器,未使用优化编译选项。
1. 同步阻塞发送(MPI_Ssend)的串行等待瓶颈
MPI_Ssend是同步阻塞调用:它必须等到接收方完成对应MPI_Recv的数据接收后才会返回。在单独调用的方案中,每个进程的通信流程是串行的:
- 先调用
MPI_Ssend给右邻居,必须等右邻居的MPI_Recv完成才能继续 - 再调用
MPI_Recv等待左邻居的消息
当进程数增多时,这种串行收发会形成链式等待:比如进程0要等进程1完成接收才能继续,进程1要等进程2,以此类推。整个通信链的等待时间会随进程数线性累加,直接导致总运行时间急剧上升,扩展性变差。
2. MPI_Sendrecv的底层通信优化
MPI_Sendrecv是MPI专门设计的组合收发操作,它将发送和接收绑定为一个原子操作,MPI实现(如Cray MPI)可以对其进行针对性优化:
- 通信重叠执行:底层可以调度发送和接收操作同时进行,避免了单独调用时的串行等待开销
- 硬件级优化:对于笛卡尔拓扑的邻近通信,MPI可以直接利用超算的点对点链路硬件特性,减少中间缓冲区拷贝和上下文切换的开销
- 减少系统调用开销:一次
MPI_Sendrecv调用相当于两次单独的收发调用,但底层只需要一次系统调度,降低了调用本身的开销
3. 弱扩展性的本质差异
单独调用方案的时间随进程数增加显著上升,核心是每个迭代的通信延迟被串行等待放大;而MPI_Sendrecv方案中,通信操作是并行化执行的,新增进程不会带来额外的链式等待,每个迭代的通信时间基本稳定,因此弱扩展性远优于单独调用方案。
内容的提问来源于stack exchange,提问作者Arthur Scott

