Skip to content

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 ./reduce

16 进程实测:并行求和 = 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 Qs10 × 8(80 进程)进程网格,P×Q=总进程数
BCASTs0(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 小例确认集合通信正常,再上全规模

AI-HPC 智算超算专家 · Contact: openaihpc@gmail.com