MPI 点对点通信实战:环形接力与延迟实测
1. 场景:让进程真正"传话"
Hello World 只验证了"所有进程能跑起来",进程之间并没有通信。点对点通信(Point-to-Point)是 MPI 最基础的传递方式:一个进程发、一个进程收。本文用两个可编译例子——环形接力和 ping-pong 延迟测量——把点对点通信跑通、跑准。
测试环境沿用知识库《MPI Hello World 实战》的 oneAPI 环境,命令:
mpirun -perhost 4 -np 8 -genv I_MPI_FABRICS shm:ofi -genv FI_PROVIDER tcp -hostfile hostfile ./ring-perhost 4 表示每个节点起 4 个进程,hostfile 内容为节点列表(每行一个主机名)。
2. 例一:环形接力(Ring)
8 个进程围成一圈:rank 0 把消息传给 rank 1,rank 1 传给 rank 2,……,rank 7 传回 rank 0。全程只用一个 MPI_Sendrecv(先收后发,避免死锁)。
// ring.c —— 环形接力:每个进程收到后 +1 再传给下一个
#include <mpi.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
MPI_Init(&argc, &argv);
int size, rank;
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
int next = (rank + 1) % size;
int prev = (rank - 1 + size) % size;
int recv_buf = 0, send_buf = rank + 1;
if (rank == 0) {
// rank 0 发起第一棒
MPI_Send(&send_buf, 1, MPI_INT, next, 0, MPI_COMM_WORLD);
MPI_Recv(&recv_buf, 1, MPI_INT, prev, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("Rank %d: 收到接力值 %d,环形一圈完成\n", rank, recv_buf);
} else {
// 其余进程:先收后发
MPI_Recv(&recv_buf, 1, MPI_INT, prev, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(&recv_buf, 1, MPI_INT, next, 0, MPI_COMM_WORLD);
printf("Rank %d: 收到 %d,传给 rank %d\n", rank, recv_buf, next);
}
MPI_Finalize();
return 0;
}编译与运行:
mpicc -O2 -o ring ring.c
mpirun -perhost 4 -np 8 -genv I_MPI_FABRICS shm:ofi -genv FI_PROVIDER tcp -hostfile hostfile ./ring8 进程实测输出(关键行):
Rank 7: 收到 7,传给 rank 0
Rank 3: 收到 3,传给 rank 4
Rank 0: 收到接力值 8,环形一圈完成要点:MPI_Sendrecv 在同一调用里完成"发下一棒 + 收上一棒",天然避免进程两两等待的死锁;这里用分开的 Send/Recv 演示了非环状进程必须注意收发顺序——若所有进程都先 Send 再 Recv,当 -np 较大且无缓冲时可能互相等待(缓冲区耗尽),这是点对点最经典的坑。
3. 例二:ping-pong 延迟实测
衡量网络/通信质量最常用的微基准是 ping-pong:rank 0 发一条消息给 rank 1,rank 1 原样送回,来回记一次时间。消息越小越接近"延迟"(latency),越大越能看出"带宽"(bandwidth)。
// pingpong.c —— 来回测延迟/带宽
#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
MPI_Init(&argc, &argv);
int size, rank;
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (size != 2) {
if (rank == 0) printf("请用 -np 2 运行本程序\n");
MPI_Finalize();
return 1;
}
int iters = 1000;
int max_bytes = 1 << 20; // 1 MB
for (int nbytes = 4; nbytes <= max_bytes; nbytes *= 2) {
char *buf = (char *)malloc(nbytes);
double t0 = MPI_Wtime();
for (int i = 0; i < iters; i++) {
if (rank == 0) {
MPI_Send(buf, nbytes, MPI_CHAR, 1, 0, MPI_COMM_WORLD);
MPI_Recv(buf, nbytes, MPI_CHAR, 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
} else {
MPI_Recv(buf, nbytes, MPI_CHAR, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(buf, nbytes, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
}
}
double t1 = MPI_Wtime();
double sec_per_pingpong = (t1 - t0) / iters; // 一次来回
double latency_us = sec_per_pingpong / 2 * 1e6; // 单程
double bw_mbps = (double)nbytes * 8 / sec_per_pingpong / 1e6;
if (rank == 0)
printf("%8d bytes | 单程延迟 %8.1f us | 带宽 %8.1f Mbps\n",
nbytes, latency_us, bw_mbps);
free(buf);
}
MPI_Finalize();
return 0;
}运行与观察:
mpicc -O2 -o pingpong pingpong.c
mpirun -perhost 1 -np 2 -genv I_MPI_FABRICS shm:ofi -genv FI_PROVIDER tcp -hostfile hostfile ./pingpong实测要点(2 节点跨网):
| 消息大小 | 单程延迟 | 带宽 | 说明 |
|---|---|---|---|
| 4 B | 约 10~30 us | 低 | 接近网络"延迟"底数(TCP 路径偏高) |
| 4 KB | 随 fabric 提升 | 数百 Mbps~Gbps | 进入带宽竞争区 |
| 1 MB | 带宽受限 | 接近链路上限 | 大消息看吞吐 |
实测结论:用
shm:ofi时同节点进程走共享内存(延迟最低),跨节点走FI_PROVIDER tcp(延迟明显高于 IB/RoCE 直通)。这就是为什么 HPL 这类大规模测试必须调好 fabric 与 provider——详见《MPI + Slurm 实战:作业调度与性能调优复盘》。
4. 实战检查清单
- [ ]
mpicc编译通过(找不到命令时先source /opt/intel/oneapi/setvars.sh) - [ ]
hostfile每行一个节点名,且 ssh 免密可达 - [ ] 环形程序 1 进程→N 进程都正常(
-np 1时next==prev需要特殊处理,建议最小从 2 起) - [ ] ping-pong 必须
-np 2,否则报错退出 - [ ] 跨节点延迟异常偏高时,优先检查
I_MPI_FABRICS/FI_PROVIDER与防火墙
下一篇《MPI 集合通信实战》在此基础上把 Bcast/Allreduce 跑通,并解释 HPL 网格参数 P×Q 为什么依赖集合通信性能。