首页 / 资讯中心 / 文章详情

MPI集合通信四大原语详解:Bcast、Scatter、Gather与Reduce实战

MPI集合通信四大原语详解:Bcast、Scatter、Gather与Reduce实战 ★ FEATURED ARTICLE
MPI编程系列写到第4篇今天聊集合通信。前面我们处理进程间消息传递主要靠MPI_Send和MPI_Recv一对一干活代码看起来很直接。可一旦遇到一份参数要发给所有进程、所有人把结果汇总到根进程这类场景再手动写Send/Recv就非常痛苦一个广播就要写一长串循环而且还特别容易漏消息、配错tag。MPI标准里的MPI_Bcast、MPI_Gather、MPI_Scatter、MPI_Reduce这四个原语就是专门解决这类群体协作问题的它们统称集合通信。这一篇我会把四个函数各自到底管什么、参数有哪些坑、什么时候该用哪个讲清楚最后给一个能直接编译运行的并行求和Demo把四个原语串起来走一遍。适合正在学MPI、写过一点Send/Recv但还没系统接触集合通信的同学。照着本文的例子跑一遍再回自己的代码里对照一下应该能少踩不少坑。1. 为什么需要集合通信而不是手动Send/Recv1.1 点对点通信解决不了“群体协作”问题先回顾一下MPI里最基本的通信方式进程A调用MPI_Send把数据发给进程B进程B调用MPI_Recv接收。这个过程是点对点的一个发送对应一个接收逻辑非常清晰。但真实并行程序里通信模式很少是两个进程之间的事。你启动一个MPI作业往往一跑就是16个、64个、甚至上千个进程。这时你会频繁遇到这样的需求根进程通常是rank 0读入了一个配置文件里面有数组大小、迭代次数、路经名等参数所有进程都需要拿到这份数据才能各自干活。每个进程算完自己负责的那一段数据得到了一个局部结果最后要把所有局部结果汇总到根进程统一输出或者继续处理。根进程有一个大数组比如100万个浮点数需要按进程数切块分给其他进程做并行处理。所有进程各自算出了一个局部值需要对这些值做一次全局求和、求最大值或者求平均值。这些问题有一个共同点不是一对一而是一对多或多对一或多对多。如果全部用MPI_Send和MPI_Recv手动实现你得自己管理发送次数、接收次数、循环逻辑、tag编号、缓冲区状态代码量成倍增加不说还非常容易出隐藏的bug。1.2 集合通信到底解决了什么集合通信Collective Communication是MPI标准为这类群体协作场景设计的一类通信原语。它的核心特征有三个参与方是整个通信子通信子communicator里所有进程都必须调用同一个集合通信函数一个人不调整个调用就卡住。一次调用完成一组进程的协同你不需要写循环去逐个发送MPI实现内部帮你把数据从正确的地方取出来、发到正确的地方。有明确的语义和约束比如广播就是根进程的数据复制给所有人收集就是所有进程的数据按rank顺序拼到根进程归约就是所有进程的数据经过指定运算合并成一个结果。集合通信的另一个隐含优势是MPI实现可以针对硬件环境做优化。比如广播朴素做法是根进程给每个进程各发一份复杂度是O(P)步而好的MPI实现会使用树形广播复杂度能降到O(logP)步。这在后文性能部分会展开讲。1.3 看懂集合通信先盯住三个概念在拆解四个函数之前先统一一下概念后面代码里反复出现的根进程root集合操作里的中心节点。广播时数据从根进程发出收集时数据回到根进程归约结果也放在根进程。root只是一个逻辑角色用整数rank表示每个集合操作都可以指定不同的root。通信子comm所有参与这次集合操作的进程集合。大多数时候就是MPI_COMM_WORLD启动作业时全部进程也可以是自己划分出的子通信子。count和datatype一次集合操作传输多少个元素、每个元素是什么类型。它们共同决定数据在内存里占多大发送方和接收方的这两个参数必须匹配。把这些概念刻在脑子里后面看函数的用法就顺了。2. 集合通信四大原语的语义拆解2.1 MPI_Bcast一份数据发给所有进程先看最常见、也最好理解的一个MPI_Bcast广播。它做的事情是根进程把一份数据复制发送给通信子里的所有进程包括它自己。函数原型长这样int MPI_Bcast(void* buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm);参数含义buffer数据缓冲区。根进程从这里发送数据其他进程接收后也存放在这个缓冲区里。count缓冲区里有多少个元素。datatype元素类型比如MPI_INT、MPI_DOUBLE、MPI_CHAR。root根进程的rank。comm通信子。这里有个特别容易忽视的点MPI_Bcast不是MPI_Send的简单变形。在点对点通信里发送方调用MPI_Send接收方调用MPI_Recv两边的代码不一样。但在MPI_Bcast里所有进程调用的是同一个函数、同一个参数头部只是根进程的buffer里有有效数据非根进程的buffer只是一个接收容器。这个所有人都要调用的特点就是集合通信的集体性。MPI_Bcast的典型应用是广播配置参数。比如rank 0进程从文件里读入一个结构体里面包含网格大小、迭代次数、输出路径等然后一次MPI_Bcast把整个结构体广播出去所有进程就都知道了不用每个进程都去读一遍文件。这里有一个非常常用的技巧可以直接广播一个结构体对象只要结构体内部不包含指针。因为指针指向的地址在另一个进程里没有意义。2.2 MPI_Scatter把大数组均匀拆开分发MPI_Scatter和MPI_Bcast看着有点像都是根进程把数据分发给其他进程但本质区别很大MPI_Bcast分发的是同一份数据所有进程收获的内容完全一样。MPI_Scatter分发的是一段大数组的不同切片每个进程收到的是互不相同的一部分。函数原型int MPI_Scatter(const void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm);参数是两组发送/接收配对。根进程的sendbuf里放着一个大数组sendcount表示分给每个进程的元素个数不是总元素个数。假设有4个进程sendcount25那么根进程的sendbuf至少要有100个元素前25个给rank 0接着25个给rank 1依次类推。非根进程的sendbuf参数会被忽略但如果不传NULL也必须保证参数合法通常直接传NULL就行。接收侧每个进程的recvbuf大小只需要容纳recvcount个元素就行。这里的recvcount通常和sendcount相等但它的语义是我这个进程这次接收多少个元素而不是我一共要接收多少。MPI_Scatter最典型的场景就是数据并行根进程读入一个大数据集然后按进程数切块分给每个进程做计算。这里有个限制需要说清楚MPI_Scatter是均匀切分它要求总数据量能被进程数整除或者至少能分出sendcount给每个进程。如果你的总长度是100有8个进程想分给前4个进程每人25后4个进程每人0MPI_Scatter做不到。这种不规则的切分要交给MPI_Scatterv来处理我在第3章会给出替代方案。2.3 MPI_Gather按编号把数据收拢回根进程MPI_Gather可以理解为MPI_Scatter的逆操作所有进程包括根进程各自把一份数据发送给根进程根进程把这些数据按rank顺序拼接成一个大的接收数组。函数原型int MPI_Gather(const void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm);里面有一个全MPI里最容易搞混的参数recvcount。它在MPI_Gather里表示的是每个进程发送过来的元素个数也就是根进程收到的每一块的尺寸而不是所有块加起来的总量。假设有4个进程每个进程发10个int那么根进程的recvbuf需要至少40个int的空间但recvcount参数填10不是40。这一点几乎每个初学MPI的人都会错一次包括我自己。更准确地说receive buffer的布局是第i个发送者按rank排序的数据被放在recvbuf的起始偏移量i * recvcount * extent(recvtype)处。所以recvcount必须和每个进程的sendcount一致否则数据拼接会错位。MPI_Gather还有两个实用细节非根进程的recvbuf是无效参数可以传NULL也可以传一个分配好的指针反正MPI实现不会碰它。根进程自己也要调用MPI_Gather它自己的sendbuf数据也会被收集到recvbuf里位置按它的rank来。MPI_Gather的典型场景是每个进程算出了一个局部结果比如一个局部分数或者局部统计量根进程把它们收集起来形成一份完整的结果列表用于输出或者后续处理。2.4 MPI_Reduce一路归约最终只留一个结果MPI_Reduce和MPI_Gather做的事情很像都是把多个进程的数据往根进程聚集但区别在于Gather是把所有原始数据都收上来一个不少Reduce是在收的过程中就把数据按指定运算合并最终只剩一个结果。函数原型int MPI_Reduce(const void* sendbuf, void* recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm);这里的sendbuf是每个进程给出的局部值recvbuf在根进程接收最终合并结果count是每次处理的元素个数不是进程数op是归约操作符。MPI预定义了一组操作符最常用的有MPI_SUM求和MPI_MAX求最大值MPI_MIN求最小值MPI_PROD求乘积MPI_LAND / MPI_LOR逻辑与 / 逻辑或这些操作符必须满足结合律MPI标准里没有要求它们满足交换律实现内部可以用树形结构做归约。实际操作中我们用的MPI_SUM、MPI_MAX这些预定义操作符都是满足交换律和结合律的不用担心。如果你想用自定义操作符需要MPI_Op_create注册一个自定义函数同时自行保证它满足结合律这是另一个进阶话题本篇先不展开。MPI_Reduce返回后只有根进程的recvbuf里有有效数据其他进程的recvbuf参数会被忽略。这一点和MPI_Gather一样。典型场景每个进程算完自己的局部和用MPI_Reduce把所有局部和相加根进程拿到全局总和。第3章的Demo就是干这个的。四个函数的功能对比可以看这张表函数方向根进程数据特征典型场景MPI_Bcast一对多分发同一份数据所有进程收到相同数据广播配置参数、全局常量MPI_Scatter一对多分发不同切片每个进程收到部分数据大数据集并行分块MPI_Gather多对一收集全部数据并按rank拼接根进程拿到所有原始数据收集局部结果列表MPI_Reduce多对一收集后合并根进程只拿到一个结果全局求和、求极值3. 完整实操用四个原语写一个并行求和程序3.1 场景设计与数据划分理论讲再多不如跑一个代码。这里我设计一个经典的计算任务求1到100的所有整数之和。我们开4个进程来并行完成这件事。整体思路拆成四步根进程rank 0准备一个长度为100的数组元素分别是1到100。用MPI_Bcast把数组长度N广播给所有进程让每个进程都知道自己要处理多少数据。用MPI_Scatter把100个数据均匀分成4份每份25个分给4个进程。每个进程计算自己这25个数的局部和。用MPI_Reduce把4个局部和相加根进程得到最终结果5050。同时用MPI_Gather把4个局部和收集成一个长度为4的数组用来展示每个进程分别算出了多少。这个设计覆盖了全部四个原语Bcast广播元信息、Scatter分发数据、Gather收集中间结果、Reduce归约最终结果。3.2 第一步用MPI_Bcast广播数组大小先写主体框架#include stdio.h #include stdlib.h #include mpi.h int main(int argc, char** argv) { int rank, size, i; int N 0; int *data NULL; int local_n; int *local_data NULL; int local_sum 0; int total_sum 0; int *all_local_sums NULL; MPI_Init(argc, argv); MPI_Comm_rank(MPI_COMM_WORLD, rank); MPI_Comm_size(MPI_COMM_WORLD, size); if (rank 0) { N 100; data (int*)malloc(N * sizeof(int)); for (i 0; i N; i) { data[i] i 1; } }在根进程里初始化N和data其他进程的N还是0。现在需要所有进程都知道N直接用MPI_BcastMPI_Bcast(N, 1, MPI_INT, 0, MPI_COMM_WORLD);这里count1datatypeMPI_INTroot0。调用之后每个进程的N都变成了100。有的同学会问为什么不把整个data数组也一起Bcast出去因为不需要我们要做的是把数据切块分给不同进程而不是复制给所有进程。如果Bcast整个data数组那每个进程都有一份完整的100个元素后面的并行计算就失去意义了。参数广播和业务数据分发要区分开这是并行程序里很基本的设计判断。3.3 第二步用MPI_Scatter把数组分给每个进程分配好局部缓冲区然后调用MPI_Scatterlocal_n N / size; local_data (int*)malloc(local_n * sizeof(int)); MPI_Scatter(data, local_n, MPI_INT, local_data, local_n, MPI_INT, 0, MPI_COMM_WORLD);注意这里local_n25所以sendcount25sendbufdata包含100个元素。MPI实现会自动把data[0..24]给rank 0data[25..49]给rank 1data[50..74]给rank 2data[75..99]给rank 3。每个进程的local_data只填25个元素。我在这里踩过一个大坑sendcount应该填的是每个进程分到多少个而不是sendbuf里总共有多少个。如果我把sendcount误写成100MPI_Scatter会认为根进程的sendbuf里有至少400个元素然后读取到内存越界轻则数据全乱重则直接段错误。这个参数语义一定要记牢。3.4 第三步各进程计算局部和Scatter结束后每个进程手里有25个连续整数。接下来就是普通的求和循环for (i 0; i local_n; i) { local_sum local_data[i]; } printf(rank %d 收到 %d 个数据局部和 %d\n, rank, local_n, local_sum);这段代码在4个进程里是并行执行的每个进程只算自己那25个数。理论上rank 0算的是1加到25结果是325rank 1算的是26加到50结果是950rank 2是1575rank 3是2200。有个细节值得说明printf的输出顺序不保证按rank顺序展示。因为标准输出到控制台时各进程的输出会被系统调度打乱。实际运行里你可能看到rank 2的输出排在rank 0前面这完全正常不影响后面的计算结果。3.5 第四步用MPI_Reduce和MPI_Gather拿到结果现在每个进程手里有一个local_sum。为了演示两个收集型原语的区别我把它们同时用上all_local_sums (int*)malloc(size * sizeof(int)); MPI_Gather(local_sum, 1, MPI_INT, all_local_sums, 1, MPI_INT, 0, MPI_COMM_WORLD); MPI_Reduce(local_sum, total_sum, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);MPI_Gather把4个局部和收集到根进程的all_local_sums数组里数组下标对应rank于是all_local_sums[0]325all_local_sums[1]950以此类推。它们的总和恰好是5050存进total_sum。注意两个函数调用的顺序在4个进程里必须保持一致每个进程都是先执行Gather再执行Reduce。如果rank 0先执行Reduce再执行Gather而其他进程还按先Gather后Reduce的顺序整个通信序列就对不上了程序会在某个MPI调用处卡住。这种问题通常表现为程序挂死没有报错非常隐蔽。最后在根进程输出结果if (rank 0) { printf(每个进程的局部和); for (i 0; i size; i) { printf(rank%d %d, , i, all_local_sums[i]); } printf(\n); printf(最终总和 %d预期 5050\n, total_sum); free(data); free(all_local_sums); } free(local_data); MPI_Finalize(); return 0; }编译和运行命令mpicc -o sum_demo sum_demo.c mpirun -np 4 ./sum_demo在我的环境里4个进程跑出来的输出类似rank 2 收到 25 个数据局部和 1575 rank 0 收到 25 个数据局部和 325 rank 3 收到 25 个数据局部和 2200 rank 1 收到 25 个数据局部和 950 每个进程的局部和rank0 325, rank1 950, rank2 1575, rank3 2200, 最终总和 5050预期 5050有一个点值得留意我在根进程里先调用了MPI_Gather再调用MPI_Reduce。其实这两个调用之间没有数据依赖Gather收集原始局部和Reduce单独做归约两者是独立的。放在一起只是为了对照看它们的差异。3.6 处理“除不尽”的数据量上面例子能跑通有一个隐含前提100能被4整除。但实际项目里数据量很少正好等于进程数乘以某个整数。比如我有100个数据却只启动了8个进程100 / 8 12余4。如果还用MPI_Scatter每个进程分到12个那最后4个数据就没人处理了结果会缺一块。我见过不少新手在这里逻辑出错甚至有人直接断言MPI_Scatter只能处理能整除的情况。其实解决方式有几种最简单的方式是填充在根进程把数组长度从100填充到1048的倍数多出来的4个位置填0然后按104做Scatter。因为0不影响求和结果依然正确。缺点是内存有一点浪费但如果数组很大填充的比例可以忽略不计。更通用的方式是使用MPI_Scatterv它是MPI_Scatter的不规则版本允许给每个进程指定不同的接收元素个数。举例int *sendcounts malloc(size * sizeof(int)); int *displs malloc(size * sizeof(int)); int base N / size; int remain N % size; int offset 0; for (i 0; i size; i) { sendcounts[i] base (i remain ? 1 : 0); displs[i] offset; offset sendcounts[i]; } MPI_Scatterv(data, sendcounts, displs, MPI_INT, local_data, sendcounts[rank], MPI_INT, 0, MPI_COMM_WORLD);sendcounts数组告诉MPI每个进程各拿多少数据displs数组告诉MPI每个进程的数据在根进程sendbuf里从哪个偏移量开始。这套机制非常灵活甚至可以做非均匀切分。你不需要把自己限制在MPI_Scatter的均匀切分里遇到不规则数据量就上Scatterv。4. 常见问题与排查技巧实录4.1 最常见的count参数理解错位根据经验十个学MPI的人里至少有六个会在MPI_Gather的recvcount或MPI_Scatter的sendcount上出过错。这两个参数的语义都是单个进程的元素个数不是总元素个数。我曾经在代码里把MPI_Gather的recvcount填成了size * local_n结果根进程的recvbuf空间开得倒是足够大但MPI实现会往recvbuf里写入size份相同位置的块导致数组后面一大段内存被破坏程序偶尔崩溃偶尔不崩时好时坏。排查了很久才发现是recvcount的问题。建议在拿到一个集合通信函数时先明确自己填的count是每人多少个而不是一共多少个。4.2 非根进程的接收缓冲区不能乱分配Gather和Reduce这两个函数里非根进程的recvbuf参数实际上是被忽略的。有些人为了安全在每个进程里都分配一个和根进程同样大小的recvbuf这其实没有问题但如果分配的大小不对比如每个进程都分配size * local_n个元素在某些MPI实现里反而会因为recvcount不匹配导致运行时报错。最稳妥的写法是只在root分支里分配recvbuf非root进程直接传NULL。int *gather_buf NULL; if (rank 0) { gather_buf (int*)malloc(size * sizeof(int)); } MPI_Gather(local_val, 1, MPI_INT, gather_buf, 1, MPI_INT, 0, MPI_COMM_WORLD);这种做法在MPI标准里是允许的而且能避免很多内存相关的“玄学问题”。4.3 集合调用顺序不一致导致挂死集合通信最大的特点就是所有进程必须都调用。但都调用还不够调用的顺序也得一致。比如每个进程都需要执行一次Gather和一次Reduce如果rank 0先执行Gather再执行Reduce而rank 1先执行Reduce再执行Gather那么rank 0在Gather处等rank 1rank 1在Reduce处等rank 0两边互相等待程序就挂死了。一个实用建议把集合通信调用看成一个隐藏的同步点。代码里凡是出现集合通信一定要确认整个通信子里的进程都以相同顺序经过这些调用。如果某个进程走了一个if分支提前跳过了某个集合调用整个作业就会卡死或者报错。这里顺便提一下MPI集合通信的另一层语义集合通信不保证和点对点通信之间的时序关系。如果在Bcast之前某个进程用MPI_Send给另一个进程发了一条消息而接收方却先进入了Bcast就可能出现消息一直没被接收的情况。混合使用点对点通信和集合通信时要特别注意同步顺序。4.4 类型不匹配与缓冲区溢出集合通信里的datatype必须一致。发送端用的是MPI_INT接收端就不能用MPI_LONG。虽然int和long在有些平台上字节数一样但在另一些平台上不一样一旦环境变了错误就会冒出来。缓冲区溢出也是高发问题。MPI_Bcast时根进程的count100非根进程的buffer只有50个元素程序运行时会越界写内存表现出来就是栈被破坏、数据错乱、段错误。这类问题不好定位建议在写代码时就确认所有进程传给集合函数的count和datatype严格一致buffer的真实长度不小于count。我个人的习惯是在调试阶段给每个进程的缓冲区分配合法大小并统一打印打印关键内存区域的地址跑一两次就都清楚了。4.5 一组排查工具与土办法MPI程序的调试本身就比单机程序困难集合通信出错又常常是静默的。我总结了一套排查顺序先用小规模数据复现比如把N设成和进程数完全整除的小值跑一遍确定性场景看结果是否正确。在集合通信前后插入printf输出每个进程的rank、局部数据个数、局部结果。很多逻辑错误一眼就能看出来。如果程序挂死用gdb或mpirun自带的调试支持去attach到挂死的进程查看当前卡在哪个MPI调用上。卡在Bcast说明有人没到卡在Gather说明可能有人把顺序弄错了。确认没有越界访问。可以故意把缓冲区开大一些如果程序恢复正常说明原来的缓冲区大小确实有问题。以下是一个常见问题速查表排查时可以直接对照现象可能原因解决思路程序无输出且不退出某个进程没有调用集合函数或调用顺序不一致检查分支语句确保所有进程按相同顺序经过集合调用结果数值对不上Scatter/Gather的count语义理解反了确认sendcount/recvcount是“每个进程的个数”非总数偶发段错误或栈被破坏recvbuf分配的太小或datatype前后不一致统一datatype按count严格分配缓冲区根进程数据正常非根进程全为0非根进程在Bcast前没有准备好接收缓冲确认所有进程的buffer都已分配并传入有效指针多层循环里多次集合通信后性能急剧下降集合通信次数太多同步开销大考虑合并多次小集合通信为一次大集合通信5. 进阶集合通信的变体和性能优化到这里四个基础集合通信原语的用法应该都清楚了。但做并行计算的人不能只停留在能跑的层面还得关心跑得快不快。集合通信虽然方便但它附带了全局同步的语义使用不当会让性能拖垮整个程序。5.1 为什么集合通信比手动收发快我前面说MPI_Bcast如果手动写需要根进程给每个进程发一次。设进程数是P那么消息发送步数是P-1数据总量是原来广播数据的P-1倍。而MPI实现里的树形广播根进程只需要先发给一部分进程这些进程再转发给下一批类似二叉树扩散。整个过程只需要大约log2(P)步就能覆盖全部进程总数据传输量也远低于P-1份完整拷贝。归约操作类似。所有进程的局部值不需要全部集中到根进程再算中间进程可以先合并身边几个进程的结果再把合并结果往上传最终根进程得到最终结果。MPI_Reduce的复杂度也接近O(logP)。这就是为什么我强烈建议能用集合通信解决的问题别自己写Send/Recv循环集合。你写出来的版本大概率没有MPI实现内部针对特定网络拓扑优化过的版本跑得快。5.2 从一次广播到Allreduce常见的变体函数MPI标准里还有很多集合通信的变体实际项目里会经常碰到MPI_Allreduce和MPI_Reduce功能一样但归约结果不只是根进程有所有进程都拿一份。典型场景是所有进程都需要全局统计结果才能继续下一步计算。它等价于先做一次MPI_Reduce再跟着一次MPI_Bcast但MPI实现里通常有更高效的直接实现。MPI_Allgather所有进程都把自己的数据发给所有其他进程最终每个进程手上都有完整的收集结果。典型场景是分布式训练里的梯度同步前需要所有节点都知道全局参数。MPI_Alltoall每个进程给其他每个进程各发一份不同内容收到后拼出完整结果。这是转置类算法、桶排序、分布式矩阵乘里很常见的通信模式。MPI_Reduce_scatter先做全局归约再把归约结果按进程数进行切分每个进程只拿其中一部分。看似奇怪但在某些算法里比MPI_Reduce再MPI_Scatter的组合更高效。如果你发现自己的代码里频繁出现ReduceBcast或者GatherBcast通常都可以换成相应的Allreduce或Allgather会少一次同步程序结构也更清晰。5.3 我的三条优化心得最后分享几条我在实际中总结出的经验第一集合通信的代价不只是数据量还有同步点。每调用一次集合通信通信子里的所有进程都要隐式同步一次。假设有64个进程一个进程快一点先到了MPI_Bcast它不能立刻离开必须等其他63个进程都到达。如果一个进程因为负载不均衡跑得特别慢其他进程就都在同步点等它。所以后续要做负载均衡而不要在小循环里反复调用集合通信。第二合并小消息减少集合通信次数。我见过一段代码循环10000次每次都对一个长度为4的int数组做MPI_Allreduce。结果通信开销比计算开销还要大几个数量级。更好的做法是把循环里的多次归约攒成一个数组做一次大count的Allreduce再在本地拆开。这个优化思路几乎所有集合通信都适用。第三思考数据分布先想清楚再用函数。Bcast和Scatter都是数据分发但一个分的是同一份数据一个分的是不同切片。Gather和Reduce都是数据回收但一个收的是原始数据一个收的是归约后的结果。设计并行程序时先在纸上画出数据流谁拥有数据、谁需要数据、谁产出最终结果。画清楚了选择函数就是顺理成章的事而不是一个个试。最后说点个人体会吧。我最早用集合通信时在大循环里对一个小数组反复做MPI_Bcast程序比单进程还慢得多后来才意识到全局同步开销有多贵。从那以后我写并行程序都会先想清楚数据分布和同步次数再决定具体用哪个原语而不是拿最眼熟的函数硬套。MPI_Bcast、MPI_Gather、MPI_Scatter、MPI_Reduce这四大件是并行编程的基本功值得花时间彻底吃透。后面有机会再单独展开MPI_Alltoall通信子划分和自定义归约操作符这次先聊到这里。
阅读完成 · 觉得有帮助?
咨询建站