ACSE6 L3 Collective communications

简介

前面提到的通信都是点到点通信,这里介绍组通信。MPI 组通信和点到点通信的一个重要区别就在于它需要一个特定组内的所有进程同时参加通信,而不是像点对点通信那样只涉及到发送方和接收方两个进程。组通信在各个进程中的调用方式完全相同,而不是像点对点通信那样在形式上有发送和接收的区别
组通信一般实现三个功能
(1) 通信:主要完成组内数据的传输
(2) 同步:实现组内所有进程在特定点的执行速度保持一致
(3) 计算:对给定的数据完成一定的操作

1. 消息通信

对于组通信来说,按照通信方向的不同,可以分为以下三种:一对多通信,多对一通信和多对多通信,下面是这三类通信的示意图:
一对多通信
在这里插入图片描述
在这里插入图片描述

2. 同步

组通信提供了专门的调用以完成各个进程之间的同步,从而协调各个进程的进度和步伐。下面是 MPI 同步调用的示意图
在这里插入图片描述

3. 计算

MPI 组通信提供了计算功能的调用,通过这些调用可以对接收到的数据进行处理。当消息传递完毕后,组通信会用给定的计算操作对接收到的数据进行处理,处理完毕后将结果放入指定的接收缓冲区。

通信

1. MPI_Bcast

  1. MPI_Bcast是一对多通信的典型例子,它可以将 root 进程中的data发送到组内的其它进程,同时包括它自身。在执行调用时,组内所有进程(不管是 root 进程还是其它的进程)都使用同一个通信域 comm 和根标识 root,其执行结果是将根进程消息缓冲区的消息拷贝到其他的进程中去。下面是 MPI_Bcast 的函数原型:
int MPI_Bcast(
    void * buffer,          // 通信消息缓冲区的起始位置
    int count,              // 广播 / 接收数据的个数
    MPI_Datatype datatype,  // 广播 / 接收数据的数据类型
    int root,               // 广播数据的根进程号
    MPI_Comm comm           // 通信域
);

(1) On root process root buffer is a pointer to the data to be sent. On other processes buffer is a pointer to where the sent data is to be stored. After the operation buffer will contain the same data on all processes.
(2) 下面是广播前后各进程缓冲区中数据的变化
在这里插入图片描述
2. MPI_Bcast example

#include <mpi.h>
#include <iostream>
#include <cstdlib>
#include <time.h>
using namespace std;
int id, p;
int main(int argc, char* argv[])
{
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &id);
	MPI_Comm_size(MPI_COMM_WORLD, &p);
	srand(time(NULL) + id * 10);
	int num_send = 1;
	int* data = new int[num_send];

	if (id == 0) {
		for (int i = 0; i < num_send; i++)
			data[i] = rand();
	}
	MPI_Bcast(data, num_send, MPI_INT, 0, MPI_COMM_WORLD);
	if (id == 0) {
		cout << "Process 0 sent this data: ";
	}
	else {
		cout << "Process " << id << " received this data: ";
	}
	for (int i = 0; i < num_send; i++)
		cout << "\t" << data[i];
	cout << endl;
	cout.flush();
	delete[] data;
	MPI_Finalize();
}
/*
PS D:\桌面\C++ Assi\MPI\x64\Debug> mpiexec -n 5 MPI.exe 
Process 0 sent this data:       27091
Process 4 received this data:   27091
Process 1 received this data:   27091
Process 2 received this data:   27091
Process 3 received this data:   27091
*/

2. MPI_Scatter

  1. MPI_Scatter是一对多的组通信调用, 和广播不同的是,root 进程向各个进程发送的数据可以是不同的(广播从单个进程向所有进程发送相同的数据). MPI_ScatterMPI_Gather 的效果正好相反,两者互为逆操作下面是 MPI_Scatter 的函数原型
int MPI_scatter(
    void * send_data,         // 发送缓冲区的起始地址
    int send_count,          // 发送数据的个数
    MPI_Datatype send_datatype,  // 发送数据类型
    void * recv_data,         // 接收缓冲区的起始地址
    int recv_count,          // 接收数据的个数
    MPI_Datatype  recv_datatype,  // 接收数据的类型
    int root,               // 根进程的编号
    MPI_Comm comm           // 通信域
);

(1) send_data is a pointer to the location of the source data. 请注意,这仅在 root上很重要,在其他进程上可以为nullptr
(2) send_count is the number of items to be sent to each process, not the total amount data. 例如如果您要向每个进程发送10个整数,并且涉及到12个进程,则send_data将需要包含120个整数,但是send_count将为10
(3) recv_data is a pointer to the location where the data is to stored. Cannot be null on root 因为它也会收到其共享的数据
(4) recv_count is the number of items expected. 通常,这与send_count相同,但是例如,如果您将数据作为字节发送,然后作为整数接收,则可能会有所不同
2. 下面是 scatter 的示意图:
在这里插入图片描述
(1) With MPI_Scatter, the data in send_data is sent to processes in the order of the process id
在这里插入图片描述
(2) send_data的大小因此应为send_count乘以进程总数. If you wish to scatter an amount of data not exactly divisible by the number of processes, 则可能需要填充数据
(3) 请注意,root 的数据也会复制到 root 的recv_data中

3. MPI_Gather

  1. 不需要经过发送, 通过 MPI_Gather可以将其他进程中的数据收集到root. 根进程接收这些消息,并把它们按照进程号 rank 的顺序进行存储. 对于所有非根进程,接收缓冲区会被忽略,但是各个进程仍需提供这一参数. 在 gather 调用中,发送数据的个数 send_count 和发送数据的类型 send_datatype 接收数据的个数 recv_count 和接受数据的类型 recv_datatype 要完全相同. 下面是 MPI_Gather 的函数原型
int MPI_Gather(
    void * send_data,         // 发送缓冲区的起始地址
    int send_count,          // 发送数据的个数
    MPI_Datatype send_datatype,  // 发送数据类型
    void * recv_data,         // 接收缓冲区的起始地址
    int recv_count,          // 接收数据的个数
    MPI_Datatype recv_datatype,  // 接收数据的类型
    int root,               // 根进程的编号
    MPI_Comm comm           // 通信域
);

(1) MPI_Gather所需的参数与MPI_Scatter相同.最大的区别是,现在send_data仅具有send_count项,而recv_data将需要空间来存储recv_count乘以数据处理数
(2) 在除根目录以外的其他进程上,recv_data可以为NULL(或nullptr)
(3) recv_data will have the data sent from process zero stored in the first recv_count elements, the data for process one stored in the next recv_count elements etc
在这里插入图片描述
2. An example using MPI_Scatter and MPI_Gather

#include <mpi.h>
#include <iostream>
#include <cstdlib>
#include <time.h>
using namespace std;


int id, p;
// 计算平均值
double calc_ave(double* list, int num) {
	double total = 0.0;
	for (int i = 0; i < num; i++)
		total += list[i];
	return total / num;
}

int main(int argc, char* argv[])
{
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &id); //进程id
	MPI_Comm_size(MPI_COMM_WORLD, &p);  //进程size
	srand(time(NULL) + id * 10);
	double* send_data1 = nullptr, send_data2;
	double* recv_data1 = nullptr, * recv_data2 = nullptr;
	int num_sendrecv = 20;

	if (id == 0)
	{
		// 总的数据应该为 p×每一个进程的长度
		send_data1 = new double[num_sendrecv * p];
		// 为send_data赋值
		for (int i = 0; i < num_sendrecv * p; i++)
			send_data1[i] = ((double)rand() / (double)RAND_MAX) * 100.0;
	}
	// 0进程和非0进程进行发送操作
	recv_data1 = new double[num_sendrecv];
	MPI_Scatter(send_data1, num_sendrecv, MPI_DOUBLE, recv_data1, num_sendrecv, MPI_DOUBLE, 0, MPI_COMM_WORLD);
	// 计算recv_data1的平均数
	send_data2 = calc_ave(recv_data1, num_sendrecv);
	delete[] recv_data1; //Can be called on all processes as it is NULL where not used
	delete[] send_data1;

	if (id == 0)
		recv_data2 = new double[p];
	MPI_Gather(&send_data2, 1, MPI_DOUBLE, recv_data2, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
	if (id == 0)
	{
		cout << endl;
		cout << "The following averages were calculated for each set of data:" << endl;
		for (int i = 0; i < p; i++)
			cout << i << " " << recv_data2[i] << endl;
	}
	delete[] recv_data2;
	MPI_Finalize();
}

/*
PS D:\桌面\C++ Assi\MPI\x64\Debug> mpiexec -n 5 MPI.exe                                                        
The following averages were calculated for each set of data:
        0 46.8307
        1 55.2474
        2 60.6323
        3 42.1201
        4 49.4018
*/
  1. Worksheet 3 Exercise 1
    让每个进程计算一个介于0到100之间的随机数。稍后,我们希望根据这些数字的顺序进行通信(您无需实现此通信).You now need to ensure that every process knows what this order is. 换句话说 the process with the lowest random number should be first in the list, the one with the second lowest second etc. (如果它们具有相同的编号,则应根据进程ID对其进行排序)。
    一种方法是将所有随机数收集到一个进程中,使用您选择的任何排序算法对列表进行排序,同时跟踪与每个数字相对应的id(气泡排序非常容易实现,而快速排序 速度更快,但操作起来有些棘手。您也可以只依赖std的sort算法,尽管您需要考虑按顺序获取索引). Scatter the list of id order back to all the processes (i.e. send each one there number in the sorted order).
#include <mpi.h>
#include <iostream>
#include <cstdlib>
#include <time.h>

using namespace std;

int id, p;


// 排序
void bubble_sort(int* list, int* index)
{
	bool swapped;
	int temp;

	do
	{
		swapped = false;

		for (int i = 0; i < p - 1; i++)
		{
			if (list[i] > list[i + 1])
			{
				temp = list[i];
				list[i] = list[i + 1];
				list[i + 1] = temp;

				temp = index[i];
				index[i] = index[i + 1];
				index[i + 1] = temp;
				swapped = true;
			}
		}

	} while (swapped);
}


int main(int argc, char* argv[])
{
	MPI_Init(&argc, &argv);

	MPI_Comm_rank(MPI_COMM_WORLD, &id);  // 进程 id
	MPI_Comm_size(MPI_COMM_WORLD, &p);   // 进程size
	srand(time(NULL) + id * 10);

	// 随机数 0 - 101
	int rand_priority = rand() % 101;  

	int* recv_list = nullptr;
	int* order_list = nullptr;
	int* send_list = nullptr;

	//Only process zero is actually receiving data
	if (id == 0)
	{
		recv_list = new int[p];
		order_list = new int[p];
		send_list = new int[p];
	}

	// 所有的进程已经产生了这个数字. 不需要发送也可以收集
	MPI_Gather(&rand_priority, 1, MPI_INT, recv_list, 1, MPI_INT, 0, MPI_COMM_WORLD);

	if (id == 0)
	{
		for (int i = 0; i < p; i++)
			order_list[i] = i;

		bubble_sort(recv_list, order_list);

		for (int i = 0; i < p; i++)
		{
			send_list[order_list[i]] = i;
		}
	}

	int id_order;

	MPI_Scatter(send_list, 1, MPI_INT, &id_order, 1, MPI_INT, 0, MPI_COMM_WORLD);

	cout << "Processor " << id << " is " << id_order << " in the list (" << rand_priority << " priority number)" << endl;

	MPI_Finalize();

	delete[] recv_list;
	delete[] order_list;
	delete[] send_list;
}

/*
PS D:\桌面\C++ Assi\AMPI\x64\Debug> mpiexec -n 6 AMPI.exe
Processor 0 is 3 in the list (60 priority number)
Processor 2 is 1 in the list (25 priority number)
Processor 1 is 5 in the list (93 priority number)
Processor 4 is 4 in the list (90 priority number)
Processor 3 is 2 in the list (57 priority number)
Processor 5 is 0 in the list (22 priority number)
*/

4. MPI_Allgather

  1. MPI_Gather是将数据收集到 root 进程,而 MPI_Allgather 相当于每个进程都作为 root 进程执行了一次 MPI_Gather 调用,即一个进程都收集到了其它所有进程的数据, 因此不需要root。下面是 MPI_Allgather 的函数原型:
int MPI_Allgather(
    void * send_data,         // 发送缓冲区的起始地址
    int  send_count,         // 向每个进程发送的数据个数
    MPI_Datatype send_datatype,  // 发送数据类型
    void * recv_data,         // 接收缓冲区的起始地址
    int recv_count,          // 接收数据的个数
    MPI_Datatype recv_datatype,  // 接收数据的类型
    MPI_Comm comm           // 通信域
);
  1. 下面是MPI_Allgather的示意图
    在这里插入图片描述

5. MPI_Alltoall

  1. MPI_Alltoall是组内进程完全交换,每个进程都向其它所有的进程发送消息,同时每一个进程都从其他所有的进程接收消息。它与 MPI_Allgather 不同的是:MPI_Allgather 接收完消息后每个进程接收缓冲区的数据是完全相同的,但是 MPI_Alltoall接受完消息后接收缓冲区的数据一般是不同的,下面是 MPI_Alltoall的示意图,如果将进程和对应的数据看做是一个矩阵的话,MPI_Alltoall 就相当于把矩阵的行列置换了一下:
    在这里插入图片描述
  2. 下面是 MPI_Alltoall 的函数原型:
int MPI_Alltoall(
void* send_data,
int send_count,
MPI_Datatype send_datatype,
void* recv_data,
int recv_count,
MPI_Datatype recv_datatype,
MPI_Comm MPI_Comm comm
);

(1) The total size of both send_data and recv_data must be equal to the count times the number of processes

同步

1. MPI_Barrier

  1. MPI_Barrier 会阻塞进程, 直到组中的所有成员都调用了它,组中的进程才会往下执行
  2. 下面是 MPI_Barrier 的函数原型:
int MPI_Barrier(
    MPI_Comm comm
);

计算

1. MPI_Reduce

1. MPI_Reduce 用来将组内每个进程输入缓冲区中的数据按给定的操作 op 进行预案算,然后将结果返回到序号为 root 的接收缓冲区中。操作 op 始终被认为是可以结合的,并且所有 MPI 定义的操作被认为是可交换的。用户自定义的操作被认为是可结合的,但是可以不是可交换的。下面是 MPI_Reduce 的示意图:
在这里插入图片描述
2. 下面是 MPI_Reduce 的函数原型

int MPI_Reduce(
    void * send_data,         // 发送缓冲区的起始地址      
    void * recv_data,         // 接收缓冲区的起始地址
    int count,              // 发送/接收 消息的个数
    MPI_Datatype  datatype,  // 发送消息的数据类型
    MPI_Op op,              // 规约操作符
    int root,               // 根进程序列号
    MPI_Comm comm           // 通信域
);

(1) MPI_Reduce是一个非常有用的集体操作,它与MPI_Gather有一些相似之处, 因为数据是从所有进程发送并整理到根目录. 两者的最大区别是数据不是单独存储, 而是使用操作op合并. 这也是为什么只有一个count and datatype的原因,因为操作必须涉及一个单一类型. The operation is applied separately to each of the items in the data.
(2) 这也是为什么只有一个count and datatype的原因,因为操作必须涉及一个单一类型
3. There are a large number of different reduce operations that are available:

操作含义
MPI_MAX最大值
MPI_MIN最小值
MPI_SUM求和
MPI_PROD求积
MPI_LANDCarries out a logical “and” on all the values
MPI_BANDCarries out a bit-wise “and” on all the values
MPI_LOR逻辑或
MPI_BOR按位或
MPI_LXOR逻辑xor
MPI_BXOR按位xor
MPI_MAXLOC最大值且相应位置
MPI_MINLOC最小值且相应位置
  1. Example: MPI_Reduce
#include <mpi.h>
#include <iostream>
#include <cstdlib>
#include <time.h>
using namespace std;

int id, p;
// 计算平均值
double calc_ave(double* list, int num) {
	double total = 0.0;
	for (int i = 0; i < num; i++)
		total += list[i];
	return total / num;
}
int main(int argc, char* argv[])
{
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &id);  // 进程id
	MPI_Comm_size(MPI_COMM_WORLD, &p);   // 进程size
	srand(time(NULL) + id * 10);

	double* send_data1 = nullptr, send_data2;
	double* recv_data1 = nullptr, recv_data2;
	int num_sendrecv = 20;
	if (id == 0)
	{
		send_data1 = new double[num_sendrecv * p];
		for (int i = 0; i < num_sendrecv * p; i++)
			send_data1[i] = ((double)rand() / (double)RAND_MAX) * 100.0;
	}

	recv_data1 = new double[num_sendrecv];
	// 对所有进程进行Scatter数据
	MPI_Scatter(send_data1, num_sendrecv, MPI_DOUBLE, recv_data1, num_sendrecv, MPI_DOUBLE, 0, MPI_COMM_WORLD);

	send_data2 = calc_ave(recv_data1, num_sendrecv);
	delete[] recv_data1; //Can be called on all processes as it is NULL where not used
	delete[] send_data1;
	// 对每个进程的recv_data2进行求和
	MPI_Reduce(&send_data2, &recv_data2, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
	if (id == 0)
		cout << "The average of all the data is " << recv_data2 / p << endl;
	MPI_Finalize();
}

/*
PS D:\桌面\C++ Assi\MPI\x64\Debug> mpiexec -n 10 MPI.exe
The average of all the data is 52.8276
*/
  1. Worksheet 3 Exercise 2
    In simulations it is quite common for all processes to calculate a number, but then require that every process know, for instance , the smallest or largest number calculated. 一个示例可能是在有限元模拟中基于元素大小和流体速度计算时间步长,其中在每个过程中计算的最大时间步长是不同的,但实际使用的时间步长必须是在所有过程中计算出的最小时间步长。
    让每个进程计算一个随机的“时间步长”,然后结合使用MPI_Reduce和MPI_Bcast以确保每个进程都知道相同的最小时间步长。
#include <mpi.h>
#include <iostream>
#include <cstdlib>
#include <time.h>

using namespace std;

int id, p;

int main(int argc, char* argv[])
{
	MPI_Init(&argc, &argv);

	MPI_Comm_rank(MPI_COMM_WORLD, &id); // 进程id
	MPI_Comm_size(MPI_COMM_WORLD, &p);  //进程size
	srand(time(NULL) + id * 1000);
	
	// 对于每个process都有自己的time step
	double time_step = (double)rand() / RAND_MAX;

	cout << "Time step of process " << id << " is " << time_step << endl;

	double min_time_step;
	// send所有进程的time_step, s使用min_time_step进行接收, 但是接收MPI_MIN 即最小值 
	MPI_Reduce(&time_step, &min_time_step, 1, MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD);
	// 向所有的进程广播
	MPI_Bcast(&min_time_step, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);

	cout << "Process " << id << " has a minimum time step of " << min_time_step << endl;

	MPI_Finalize();
}
/*
PS D:\桌面\C++ Assi\AMPI\x64\Debug> mpiexec -n 3 AMPI.exe
Time step of process 2 is 0.830653
Time step of process 1 is 0.730979
Time step of process 0 is 0.631336
Process 2 has a minimum time step of 0.631336
Process 0 has a minimum time step of 0.631336
Process 1 has a minimum time step of 0.631336
*/

2. MPI_Allreduce

  1. MPI_AllreduceMPI_Reduce相似, 除了the data is received on all processes。 因此不需要root. 下面是 MPI_Allreduce 的函数原型
int MPI_Reduce(
    void * sendbuf,         // 发送缓冲区的起始地址      
    void * recvbuf,         // 接收缓冲区的起始地址
    int count,              // 发送/接收 消息的个数
    MPI_Datatype datatype,  // 发送消息的数据类型
    MPI_Op op,              // 规约操作符
    MPI_Comm comm           // 通信域
);
  1. Woorksheet3 Exercise3
    Use MPI_Allreduce to combine the MPI_Reduce and MPI_Bcast in the previous example into a single operation.
#include <mpi.h>
#include <iostream>
#include <cstdlib>
#include <time.h>

using namespace std;

int id, p;

int main(int argc, char* argv[])
{
	MPI_Init(&argc, &argv);

	MPI_Comm_rank(MPI_COMM_WORLD, &id);
	MPI_Comm_size(MPI_COMM_WORLD, &p);
	srand(time(NULL) + id * 1000);

	double time_step = (double)rand() / RAND_MAX;

	cout << "Time step of process " << id << " is " << time_step << endl;

	double min_time_step;

	MPI_Allreduce(&time_step, &min_time_step, 1, MPI_DOUBLE, MPI_MIN, MPI_COMM_WORLD);

	cout << "Process " << id << " has a minimum time step of " << min_time_step << endl;

	MPI_Finalize();
}

Non-blocking collectives

  1. 您可以使用集体操作的非阻塞版本,该版本可让您执行诸如在等待其完成时继续工作之类的操作. 我们不会在这里专门讨论非阻塞集合,但是它们与阻塞版本的区别类似于MPI_SendMPI_Isend之间的区别. 它们还需要一个请求变量,并保证在调用MPI_Wait之后已完成. 也可以使用MPI_Test检查它们是否完成
  2. The non-blocking equivalents all have an I in their name. E.g.MPI_Gather is MPI_Igather
  3. 除了多余的MPI_Request *变量外,它们采用的参数及其用法与它们的非阻塞等效项相同。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值