MPI 集合通信实战:从 Bcast 到 Allreduce
1. 场景:一个进程对一群人说话
点对点通信是"一对一",而真实计算里大量是"一对多 / 多对一 / 多对多":根进程把输入广播给所有进程、每个进程算一部分再汇总。MPI 把这类通信封装成集合通信(Collective),一次调用完成全组协调,也更容易写出不出死锁的代码。
本文用三个例子(广播、并行求和、全局统计)跑通集合通信,并把结论接到 HPL 实测的网格参数上。
2. 例一:MPI_Bcast 广播
根进程(rank 0)把数组分发给所有进程。所有进程必须同时调用 MPI_Bcast,参数一致,仅根进程提供真实数据。
c
// bcast.c —— 根进程广播一段配置
#include <mpi.h>
#include <stdio.h>
#include <string.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);
char msg[64];
if (rank == 0) {
strcpy(msg, "HPL.N=3145728 NB=384 P*Q=80");
}
MPI_Bcast(msg, 64, MPI_CHAR, 0, MPI_COMM_WORLD);
printf("Rank %d 收到配置: %s\n", rank, msg);
MPI_Finalize();
return 0;
}bash
mpicc -O2 -o bcast bcast.c
mpirun -perhost 4 -np 8 -hostfile hostfile ./bcast要点:集合通信是集体动作——任何一个进程不参与,其他进程就会一直等(这是它与点对点的最大区别,也是排查"卡死"的第一思路)。
3. 例二:MPI_Reduce 并行求和
把"从 1 加到 N"分到 P 个进程:每个进程算自己的一段,MPI_Reduce 把各段部分和汇总到根进程。
c
// reduce.c —— 并行求和 1..N
#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);
long long N = 100000000; // 1 亿
long long n_per = N / size;
long long start = rank * n_per + 1;
long long end = (rank == size - 1) ? N : start + n_per - 1;
long long local = 0;
for (long long i = start; i <= end; i++) local += i;
long long total = 0;
MPI_Reduce(&local, &total, 1, MPI_LONG_LONG, MPI_SUM, 0, MPI_COMM_WORLD);
if (rank == 0) {
long long expect = N * (N + 1) / 2;
printf("并行求和 = %lld,期望 = %lld,%s\n",
total, expect, total == expect ? "一致 ✓" : "不一致 ✗");
}
MPI_Finalize();
return 0;
}bash
mpicc -O2 -o reduce reduce.c
mpirun -perhost 8 -np 16 -hostfile hostfile ./reduce16 进程实测:并行求和 = 5000000050000000,期望 = 5000000050000000,一致 ✓。这就是 HPL 里"把一个大矩阵分解到 P×Q 进程网格、各自消元、最后归约"的雏形。
4. 例三:MPI_Allreduce 全局统计
MPI_Reduce 把结果只给根进程;MPI_Allreduce 让所有进程都拿到结果(常用于判断是否收敛、求全局 max/min/avg)。
c
// allreduce.c —— 每个进程报一个数值,全局求 max/min/avg
#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);
double local = (rank + 1) * 1.5; // 模拟每个进程的指标
double g_max, g_min, g_sum;
MPI_Allreduce(&local, &g_max, 1, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD);
MPI_Allreduce(&local, &g_min, 1, MPI_DOUBLE, MPI_MIN, MPI_COMM_WORLD);
MPI_Allreduce(&local, &g_sum, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
printf("Rank %d: 全局 max=%.1f min=%.1f avg=%.2f\n",
rank, g_max, g_min, g_sum / size);
MPI_Finalize();
return 0;
}Allreduce 内部通常用"树形"或"环"算法,通信量随进程数呈对数/常数增长——这就是它能支撑上万进程训练/评测的原因。
5. 集合通信与 HPL 实测的对应关系
回顾《CPU HPL 实测》里 HPL.dat 的关键参数:
| HPL.dat 参数 | 实测值 | 对应通信 |
|---|---|---|
PMAP(进程映射) | 1(Column-major) | 决定 P×Q 网格按行还是按列编号,影响数据分布 |
Ps Qs | 10 × 8(80 进程) | 进程网格,P×Q=总进程数 |
BCASTs | 0(1rg,1 级递归) | 面板广播算法,直接依赖 MPI_Bcast 实现 |
DEPTHs(lookahead) | 0 | 流水线深度,隐藏广播延迟 |
经验:P×Q 越接近正方形,通信步数越少;但需配合节点拓扑(如 8 节点 × 10 进程)避免跨机通信热点。网格形状、广播算法与实测效率的关系,是 HPL 调优的核心,见下一篇的调优复盘。
6. 实战检查清单
- [ ] 集合通信所有进程必须同序调用、参数一致,否则"集体卡死"
- [ ]
MPI_Reduce只根进程拿到结果;需要全体同步结果用MPI_Allreduce - [ ] 大数组用
MPI_Scatter/MPI_Gather(分片/汇聚),不要用 Bcast 传数据 - [ ]
long long类型在跨平台时用MPI_LONG_LONG,避免 32 位溢出 - [ ] 大规模 HPL/训练前,先跑
bcast+allreduce小例确认集合通信正常,再上全规模