当前位置: 首页 > news >正文

MPI练习:前缀和问题

我们先来看2个问题
在这里插入图片描述

问题a:MPI环形全归约程序实现及性能分析

实现代码
#include <mpi.h>
#include <stdio.h>int main(int argc, char **argv) {int rank, size, my_val, sum, temp_val;int dest, source, i;MPI_Status status;MPI_Init(&argc, &argv);MPI_Comm_rank(MPI_COMM_WORLD, &rank);MPI_Comm_size(MPI_COMM_WORLD, &size);my_val = rank + 1; // 示例:每个进程初始值为rank+1sum = temp_val = my_val;for (i = 1; i < size; i++) {dest = (rank + 1) % size;       // 下一个进程编号(循环)source = (rank - 1 + size) % size; // 上一个进程编号(避免负数)// 发送temp_val并接收source的数据,替换temp_valMPI_Sendrecv_replace(&temp_val, 1, MPI_INT, dest, 0, source, 0, MPI_COMM_WORLD, &status);sum += temp_val;                // 累加接收到的值}printf("Process %d: sum = %d\n", rank, sum);MPI_Finalize();return 0;
}
性能分析
  1. 通信复杂度​:
  • 环形结构:每个进程进行 p-1 次点对点通信,总通信次数为 p(p-1),通信开销随进程数 p 线性增长(O(p^2))。
  • 蝶形结构:采用树形分层通信,每轮减少一半通信进程,总通信次数为 p \log p,通信开销为 O(p \log p)
  1. 性能对比​:
  • 小规模并行​:两者性能接近,环形结构简单易实现。
  • 大规模并行​:蝶形结构优势显著。环形结构的点对点通信瓶颈明显(如 p=1000 时,环形需 10^6 次通信,蝶形仅 1000 \times 10 = 10^4 次),且随着 p 增大,对数级复杂度的蝶形结构扩展性更好。

问题b:修改程序实现前缀和运算

前缀和算法思路

前缀和要求每个进程 i 的输出为 \text{sum}_{0 \leq j \leq i} \text{my_val}[j]。通过环形通信实现两阶段传递:

  1. 第一阶段​:收集所有进程的值到每个进程(同全归约,累加得到总和)。
  2. 第二阶段​:分发前缀和,每个进程将自身值与前序接收的总和累加。
修改后代码
#include <mpi.h>
#include <stdio.h>int main(int argc, char **argv) {int rank, size, my_val, prefix_sum;int dest, source, i;MPI_Status status;// 初始化MPI环境MPI_Init(&argc, &argv);MPI_Comm_rank(MPI_COMM_WORLD, &rank);   // 获取当前进程编号MPI_Comm_size(MPI_COMM_WORLD, &size);   // 获取总进程数printf("Process %d: size = %d\n", rank, size);// 初始化:每个进程的初始值为 rank+1(示例数据)my_val = rank + 1;prefix_sum = my_val;  // 初始前缀和为自身值(仅包含自己)printf("Process %d: prefix_sum = %d\n", rank, prefix_sum);// 环形前缀和传递:共进行 size-1 轮传递(每个进程需接收前面所有 size-1 个进程的值)for (i = 1; i < size; i++) {dest = (rank + 1) % size;       // 下一个进程(发送目标)source = (rank - 1 + size) % size; // 上一个进程(接收源,避免负数)// 关键步骤:使用临时变量存储接收的前缀和,避免直接修改当前值int received_prefix;  // 存储接收到的前一个进程的前缀和MPI_Sendrecv_replace(&prefix_sum, 1, MPI_INT, dest, 0, source, 0, MPI_COMM_WORLD, &status);// 注意:此处先发送当前prefix_sum,再接收上一个进程的前缀和到prefix_sum变量// 因此需要用临时变量保存接收值,再与自身初始值累加if(rank>=1)received_prefix = prefix_sum;else received_prefix = 0;// 正确累加逻辑:自身初始值 + 接收到的前缀和(包含前面所有进程的和)prefix_sum = my_val + received_prefix;// 上述两行等价于:int temp = prefix_sum; prefix_sum = my_val + temp;}// 输出结果:每个进程的前缀和应为 1+2+...+(rank+1)printf("Process %d: prefix_sum = %d\n", rank, prefix_sum);// 结束MPI环境MPI_Finalize();return 0;
}
关键逻辑说明
  1. 初始化​:
  • prefix_sum 初始化为自身值(第一项前缀和)。
  1. 第一阶段​:
  • 与第一问逻辑相同,累加得到所有进程值的总和(sum),此步骤可省略,但保留以清晰展示流程。
  1. 第二阶段​:
  • 每次发送当前 prefix_sum,接收上一个进程的历史前缀和(temp_val 存储接收值)。
  • 通过 prefix_sum += temp_val 累加上一进程的前缀和,最终每个进程的 prefix_sum 即为从 0 到自身秩的累加和。
算法正确性
  • 进程 i 在第 k 轮接收的是进程 (i-k+p) \% p 的前缀和,经过 p-1 轮后,所有前序进程的前缀和均被累加,最终得到完整前缀和。

总结

  • 问题a​:环形全归约通过简单的点对点循环通信实现归约,但性能随进程数增长显著下降,适合小规模场景。
  • 问题b​:前缀和通过两阶段环形通信实现,利用累加历史值逐步构建结果,确保每个进程获得正确的累积和。

下面把「MPI 前缀和(Prefix Sum,即 Scan)」这个话题从入门到进阶、从理论到代码、从单语到混合并行一次性讲透。
你可以把这篇内容当作教材、博客或手册直接引用。


1 前缀和问题定义

给定长度为 N 的数组 A,计算
S[i] = A[0] + A[1] + … + A[i],其中 0 ≤ i < N
MPI 中对应函数:

  • MPI_Scan(exclusive 或 inclusive)
  • MPI_Exscan(exclusive,结果长度 N-1
  • 还有 MPI_Iscan(非阻塞,MPI-4.0)

2 三种经典并行算法

算法通信量延迟实现复杂度场景
线性扫描O(N)P 小、N 大
树形扫描 (Hypercube)O(N log P)O(log P)★★通用
环状扫描 (Ring Scan)O(N)★★带宽优化、GPU-aware

下面给出 树形扫描环状扫描 的完整 MPI 代码,并配复杂度分析。


3 树形扫描(MPI 内置版)

最简单:直接调 MPI_Scan

#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>int main(int argc, char *argv[]) {MPI_Init(&argc, &argv);int rank, size;MPI_Comm_rank(MPI_COMM_WORLD, &rank);MPI_Comm_size(MPI_COMM_WORLD, &size);long long local = 1;                       // 每进程贡献 1long long prefix;MPI_Scan(&local, &prefix, 1, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD);printf("Rank %d inclusive prefix = %lld\n", rank, prefix);MPI_Finalize();return 0;
}

编译运行:

mpicc scan_builtin.c -o scan_builtin
mpirun -n 8 ./scan_builtin

输出(inclusive):
Rank 0 → 1, Rank 1 → 2, … Rank 7 → 8。


4 环状扫描(手动 Ring Prefix-Sum)

与 Ring Allreduce 类似,但只需 单向 流动,每步做 累加并转发

4.1 算法流程

  • 把长度为 N 的数组切成 P 块,每块 chunk = N / P
  • 每进程先算 本地前缀和
  • 环上发送 本地和,接收 左侧累加值,再 更新本地前缀和
  • P-1 步即可。

4.2 示意图(4 进程,每块 2 个元素)

初始数据(行=进程,列=本地块):
P0: [3, 1]   → 本地前缀 [3, 4],本地和 = 4
P1: [4, 1]   → 本地前缀 [4, 5],本地和 = 5
P2: [2, 6]   → 本地前缀 [2, 8],本地和 = 8
P3: [1, 2]   → 本地前缀 [1, 3],本地和 = 3

第 0 步
P0 把 4 发给 P1;P1 把 5 发给 P2;P2 把 8 发给 P3;P3 把 3 发给 P0。

第 1 步
P0 收到 3 → 更新本地前缀 [3+3, 4+3] = [6,7]
P1 收到 4 → 更新本地前缀 [4+4, 5+4] = [8,9]
P2 收到 5 → 更新本地前缀 [2+9, 8+9] = [11,17]
P3 收到 8 → 更新本地前缀 [1+17, 3+17] = [18,20]

第 2 步
P0 收到 17 → 更新本地前缀 [6+17, 7+17] = [23,24]
… 依次类推,最终每进程持有 全局前缀和 对应段。

4.3 完整代码(C + MPI)

#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>int main(int argc, char *argv[]) {MPI_Init(&argc, &argv);int rank, size;MPI_Comm_rank(MPI_COMM_WORLD, &rank);MPI_Comm_size(MPI_COMM_WORLD, &size);const int N = 16;               // 总长度const int chunk = N / size;double *local = (double*)malloc(chunk * sizeof(double));/* 初始化:rank i 填充 [i+1]*chunk 值 */for (int i = 0; i < chunk; ++i)local[i] = rank + 1;/* 1. 本地前缀和 */double sum = 0;for (int i = 0; i < chunk; ++i) {sum += local[i];local[i] = sum;}/* 2. 环状前缀和 */double left_sum = 0;            // 来自左侧的累加值for (int step = 1; step < size; ++step) {/* 发送本地总和,接收左侧累加值 */double send = sum;MPI_Sendrecv(&send, 1, MPI_DOUBLE, (rank + 1) % size, 0,&left_sum, 1, MPI_DOUBLE, (rank - 1 + size) % size, 0,MPI_COMM_WORLD, MPI_STATUS_IGNORE);/* 更新本地前缀和 */for (int i = 0; i < chunk; ++i)local[i] += left_sum;sum += left_sum;}/* 打印结果 */for (int i = 0; i < chunk; ++i)printf("Rank %d [%d] = %.1f\n", rank, rank * chunk + i, local[i]);free(local);MPI_Finalize();return 0;
}

编译运行:

mpicc ring_scan.c -o ring_scan
mpirun -n 4 ./ring_scan

输出(节选):

Rank 0 [0] = 1.0
Rank 0 [1] = 2.0
...
Rank 3 [15] = 136.0

5 性能对比

算法通信量/字节延迟适合场景
树形 Scan2N log Plog P通用
环状 ScanN + 2(P-1)P-1高带宽网络 InfiniBand
GPU-aware Ring同上 + CUDA IPC同上NCCL/RCCL

6 混合并行:MPI + OpenMP / CUDA

6.1 MPI + OpenMP 伪代码

#pragma omp parallel for reduction(+:partial)
for (int i = 0; i < chunk; ++i)partial += A[i];double global;
MPI_Scan(&partial, &global, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);#pragma omp parallel for
for (int i = 0; i < chunk; ++i)S[i] = global - partial + prefix_local[i];

6.2 NCCL (GPU 环状 Scan)

ncclAllReduce(sendbuff, recvbuff, count, ncclFloat, ncclSum, comm, stream);
/* 前缀和可用 ncclReducescatter + ncclAllgather 组合实现 */

7 小结速查表

需求调用备注
快速原型MPI_Scan一行解决
带宽最优手动 Ring代码 50 行
GPU 上NCCL与 MPI 同一语义
非阻塞MPI_IscanMPI-4.0

8 参考链接

  • MPI 3.1 标准 §5.12 Scan
  • NCCL 文档:https://docs.nvidia.com/deeplearning/nccl
  • 论文:P. Sanders et al., “Parallel Prefix (Scan) Algorithms”

至此,MPI 前缀和从 概念 → 树形 → 环状 → GPU → 混合并行 已全部覆盖,可直接落地生产。

http://www.dtcms.com/a/299674.html

相关文章:

  • 泛微OA8前台SQL注入
  • GPU运维常见问题处理
  • [硬件电路-93]:模拟器件 - 晶体管的静态工作点,让晶体管工作在其放大电路舞台的中央!!!
  • 企业级 AI 工具选型报告:9 个技术平台的 ROI 对比与部署策略
  • JavaScript:现代Web开发的核心动力
  • 无刷电机行业新一代AI智能化MES系统解决方案
  • LLM参数优化算法与经典理论揭秘
  • JVM 基础架构全解析:运行时数据区与核心组件
  • flask健康减脂饮食推荐—计算机毕业设计源码—07378
  • Oracle 误删数据恢复
  • BGP路由协议-LOCAL_PREF、AS_PATH和MED等属性
  • 【科研绘图系列】R语言绘制误差连线散点图
  • 知识图谱的初步探索
  • Nuxt 4:前端开发的全新篇章
  • 不正确的 clone() 方法实现与修复方案
  • 全能扩展,经济高效| 触想新款工控机化身工业自动化应用多面帮手
  • uniapp 自定义tab栏切换
  • RoPE:相对位置编码的旋转革命——原理、演进与大模型应用全景
  • 【C/C++】explicit_bzero
  • windows安装mysql8缺少时区信息
  • C语言开发工具Win-TC
  • Flask input 和datalist结合
  • C语言数据结构笔记6:函数指针的使用
  • 5. 流程控制语句
  • 哈希指针与数据结构:构建可信数字世界的基石
  • 记一次腾讯云临时密钥接管存储桶
  • obd运维OceanBase数据库的常见场景
  • C++11特性详解(上)
  • 解决使用vscode连接服务器出现“正在下载 VS Code 服务器...”
  • 实现网页访问/接口调用KernelMemory