MPI(Message Passing Interface)是分布式内存系统中最主流的并行编程标准。无论节点间通过 InfiniBand 还是以太网互联,MPI 提供了统一的通信原语。理解 MPI 通信模型是编写可扩展并行程序的基础。
MPI 程序基本结构
每个 MPI 程序遵循固定的生命周期模式:
- 初始化
MPI_Init:建立进程通信域 - 通信
MPI_Send/MPI_Recv:进程间交换数据 - 结束
MPI_Finalize:释放资源并退出
#include <mpi.h>
#include <stdio.h>
int main(int argc, char** argv) {
int rank, size;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
printf("Hello from rank %d of %d\n", rank, size);
MPI_Finalize();
return 0;
}
编译运行:
mpicc hello.c -o hello
mpirun -np 4 ./hello
点对点通信模型
点对点通信涉及明确的发送方和接收方,对应 MPI_Send 和 MPI_Recv:
// 进程 0 发送消息到进程 1
if (rank == 0) {
int data[100];
for (int i = 0; i < 100; i++) data[i] = i;
MPI_Send(data, 100, MPI_INT, 1, 0, MPI_COMM_WORLD);
}
// 进程 1 接收消息
if (rank == 1) {
int buf[100];
MPI_Status status;
MPI_Recv(buf, 100, MPI_INT, 0, 0, MPI_COMM_WORLD, &status);
int count;
MPI_Get_count(&status, MPI_INT, &count);
printf("Received %d integers\n", count);
}
| 参数 | 说明 |
|---|---|
buf | 数据缓冲区指针 |
count | 元素数量 |
datatype | MPI 数据类型(MPI_INT、MPI_DOUBLE 等) |
dest / source | 目标 / 源进程 rank |
tag | 消息标签,用于区分同一进程的不同消息 |
comm | 通信子(通常为 MPI_COMM_WORLD) |
一个常见的死锁场景:两个进程互相发送但未按序接收。解决方案是交错发送接收顺序,或使用非阻塞通信。
// 安全的交替配对通信:进程 0 先发后收,进程 1 先收后发
if (rank == 0) {
MPI_Send(sendbuf, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD);
MPI_Recv(recvbuf, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
} else if (rank == 1) {
MPI_Recv(recvbuf, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(sendbuf, n, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD);
}
集合通信原语
集合通信涉及通信子内所有进程的同步参与,比逐对发送更高效。
Broadcast 广播
根进程将数据分发给所有进程:
int root = 0;
int data = 0;
if (rank == root) data = 42;
MPI_Bcast(&data, 1, MPI_INT, root, MPI_COMM_WORLD);
// 所有进程此时 data == 42
Reduce 归约
进程计算结果汇总为标量值,支持 MPI_SUM、MPI_MAX、MPI_MIN、MPI_PROD 等操作:
int local_sum = rank + 1; // 进程 0:1, 1:2, ..., 3:4
int global_sum;
MPI_Reduce(&local_sum, &global_sum, 1, MPI_INT,
MPI_SUM, 0, MPI_COMM_WORLD);
// 进程 0 的 global_sum = 1+2+3+4 = 10
进程 0:1 ──┐
进程 1:2 ──┼─ Bottom-Up 二叉树归约 ──→ 根进程: 10
进程 2:3 ──┘
进程 3:4 ──┘
Scatter / Gather 分发与收集
// Scatter: 根进程的数组分段分发给每个进程
int sendbuf[16], recvbuf[4];
for (int i = 0; i < 16; i++) sendbuf[i] = i;
MPI_Scatter(sendbuf, 4, MPI_INT, recvbuf, 4, MPI_INT, 0, MPI_COMM_WORLD);
// rank 0 得到 [0,1,2,3],rank 1 得到 [4,5,6,7]...
// Gather: 收集各进程的 recbuf 到根进程的 recvbuf
int gathbuf[16];
MPI_Gather(recvbuf, 4, MPI_INT, gathbuf, 4, MPI_INT, 0, MPI_COMM_WORLD);
MPI_Allreduce:全局同步结果
与 MPI_Reduce 不同,MPI_Allreduce 将归约结果广播到所有进程,无需显式调用 MPI_Bcast:
double local_norm = ...;
double global_norm;
MPI_Allreduce(&local_norm, &global_norm, 1, MPI_DOUBLE,
MPI_SUM, MPI_COMM_WORLD);
// 所有进程都能访问 global_norm
这在迭代计算中极为常用——每个进程计算局部残差后需要判断是否全局收敛。
进程拓扑与 Cart 坐标映射
二维网格上的 Jacobi 迭代需要邻居进程通信,MPI_Cart_create 将一维 rank 映射到逻辑网格:
int dims[2] = {0, 0};
int periods[2] = {0, 0};
MPI_Dims_create(size, 2, dims); // 自动分解,如 4x4
MPI_Comm cart_comm;
MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 1, &cart_comm);
int coords[2];
MPI_Cart_coords(cart_comm, rank, 2, coords);
// 获取上下左右邻居
int north, south, east, west;
MPI_Cart_shift(cart_comm, 0, 1, &west, &east);
MPI_Cart_shift(cart_comm, 1, 1, &north, &south);
Cart 拓扑的优势在于:进程 rank 与非结构化邻居查找解耦,使得同一逻辑程序能自适应不同节点规模。
非阻塞通信:Isend / Irecv
阻塞通信在 MPI_Send 完成前不能继续执行后续代码。非阻塞版本立即返回一个 MPI_Request 句柄,允许在通信"在途"的同时执行本地计算:
MPI_Request req;
MPI_Isend(sendbuf, n, MPI_DOUBLE, 1, 0, MPI_COMM_WORLD, &req);
// 重叠:在数据传输期间执行本地计算
compute_local(update_stencil, domain);
// 确保通信完成
MPI_Wait(&req, MPI_STATUS_IGNORE);
非阻塞同步的两种典型模式:
// 模式一:多个请求批量等待
MPI_Request reqs[4];
MPI_Isend(..., &reqs[0]);
MPI_Irecv(..., &reqs[1]);
MPI_Isend(..., &reqs[2]);
MPI_Irecv(..., &reqs[3]);
MPI_Waitall(4, reqs, MPI_STATUSES_IGNORE);
// 模式二:非阻塞只等边界邻居,内部计算完全重叠
for (step = 0; step < max_iter; step++) {
// Phase 1: 启动边界交换
MPI_Isend(halo_north, ..., north, ..., &reqs[0]);
MPI_Irecv(halo_south, ..., south, ..., &reqs[1]);
MPI_Isend(halo_west, ..., west, ..., &reqs[2]);
MPI_Irecv(halo_east, ..., east, ..., &reqs[3]);
// Phase 2: 更新内部区域(不依赖边界)
jacobi_update(domain, FROM(1,1), TO(nx-1, ny-1));
// Phase 3: 等待边界到达
MPI_Waitall(4, reqs, MPI_STATUSES_IGNORE);
// Phase 4: 更新边界行/列
jacobi_update(domain, edges);
}
非阻塞通信的难点在于缓冲区生命周期——在 MPI_Wait 之前,sendbuf 不能被修改或释放。
通信优化要点
| 优化方向 | 具体措施 | 预期收益 |
|---|---|---|
| 减少消息数 | 聚合小消息为大数据块 | 减少延迟开销 |
| 重叠计算通信 | MPI_Isend/Irecv + Wait | 隐藏通信延迟 |
| 使用集体通信 | Allreduce 替代手写的 reduce+broadcast | 利用优化算法 |
避免密集 MPI_Send 到同一目标 | 检查缓冲策略(标准模式 vs 同步模式) | 避免内部额外拷贝 |
| 通信子拆分 | MPI_Comm_split 创建子域通信子 | 减少不必要同步 |
将阻塞通信改写为非阻塞循环允许 stencil 程序在典型 CPU 集群上获得 15%-30% 的性能提升。MPI 通信优化的核心口诀是:批量发送、计算重叠、集体优先。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。