[3.4] 集合通信 理论+代码

文章目录

    • [3.1 MPI_Bcast广播](#3.1 MPI_Bcast广播)
    • [3.2 MPI_Scatter, MPI_Gather, and MPI_Allgather](#3.2 MPI_Scatter, MPI_Gather, and MPI_Allgather)
      • [3.2.1 MPI_Scatter](#3.2.1 MPI_Scatter)
      • [3.2.2 MPI_Gather](#3.2.2 MPI_Gather)
      • [3.2.3 MPI_Allgather](#3.2.3 MPI_Allgather)

集体通信指的是一个涉及 communicator 里面所有进程的一个方法。关于集体通信需要记住的一点是它在进程间引入了同步点的概念。这意味着所有的进程在执行代码的时候必须首先 到达一个同步点才能继续执行后面的代码。MPI 有一个特殊的函数来做同步进程的这个操作:

cpp 复制代码
MPI_Barrier(MPI_Comm comm)

这个方法会构建一个屏障,任何进程都没法跨越屏障,直到所有的进程都到达屏障。这边有一个示意图。假设水平的轴代表的是程序的执行,小圆圈代表不同的进程。

这里四个时间内不同进程的执行逻辑是:进程0在时间点 (T 1) 首先调用 MPI_Barrier。然后进程0就一直等在屏障之前,之后进程1和进程3在 (T 2) 时间点到达屏障。当进程2最终在时间点 (T 3) 到达屏障的时候,其他的进程就可以在 (T 4) 时间点再次开始运行。MPI_Barrier 在很多时候很有用。其中一个用途是用来同步一个程序,使得分布式代码中的某一部分可以被精确的计时。

注意:在 MPI 中,所有的集体通信 (如 MPI_BcastMPI_GatherMPI_Scatter 等)都是同步的 ,也就是说:必须让所有相关进程都参与到同一次集体通信中,否则有一个进程掉队,其他进程就会一直等待,导致死锁。

3.1 MPI_Bcast广播

广播 (broadcast) 是标准的集体通信技术之一。一个广播发生的时候,一个进程会把同样一份数据传递给一个 communicator 里的所有其他进程。广播的主要用途之一是把用户输入传递给一个分布式程序,或者把一些配置参数传递给所有的进程。它的函数签名:

cpp 复制代码
MPI_Bcast(void *buffer, int count, MPI_Datatype datatype,
					int root, // 广播的根进程的rank值(进程号)
					MPI_Comm comm)

当根节点(在我们的例子是节点0)调用 MPI_Bcast 函数的时候,buffer 变量里的值会被发送到其他的节点上。当其他的节点调用 MPI_Bcast 的时候,buffer 变量会被赋值成从根节点接受到的数据。我们可以使用MPI_SendMPI_Recv来实现广播,代码很简单如下:

cpp 复制代码
void my_bcast(void* data, int count, MPI_Datatype datatype, int root,
              MPI_Comm communicator) {
  int world_rank;
  MPI_Comm_rank(communicator, &world_rank);
  int world_size;
  MPI_Comm_size(communicator, &world_size);

  if (world_rank == root) {
    // If we are the root process, send our data to everyone
    int i;
    for (i = 0; i < world_size; i++) {
      if (i != world_rank) {
        MPI_Send(data, count, datatype, i, 0, communicator);
      }
    }
  } else {
    // If we are a receiver process, receive the data from the root
    MPI_Recv(data, count, datatype, root, 0, communicator,
             MPI_STATUS_IGNORE);
  }
}

根节点把数据传递给所有其他的节点,其他的节点接收根节点的数据。但是这里的效率很低,因为每次并不是一下就完成了所有的进程发送和接收。只是使用了进程0的一次次的传递数据。这里有一些优化算法,如:基于树的沟通算法。

3.2 MPI_Scatter, MPI_Gather, and MPI_Allgather

3.2.1 MPI_Scatter

MPI_Scatter是一个类似MPI_Bcast的集体通信机制。它会会设计一个指定的根进程,根进程会将数据发送到 communicator 里面的所有进程,但是给每个进程发送的是一个数组的一部分数据

MPI_Bcast 在根进程上接收一个单独的数据元素,复制给其他进程。

MPI_Scatter 接收一个数组,并把元素按进程的秩分发出去。尽管根进程(进程0)拥有整个数组的所有元素,MPI_Scatter 还是会把正确的属于进程0的元素放到这个进程的接收缓存中。

cpp 复制代码
int MPI_Scatter(
		const void *sendbuf,   // 发送缓存(存储要发送的数据的起始地址)
    int sendcount,         // 发送数据的数量(每个进程接收的元素个数)
    MPI_Datatype sendtype, // 发送数据的类型(如 MPI_INT, MPI_FLOAT 等)
    void *recvbuf,         // 接收缓存(存储接收到的数据的起始地址)
    int recvcount,         // 接收数据的数量(每个进程接收的元素个数)
    MPI_Datatype recvtype, // 接收数据的类型(如 MPI_INT, MPI_FLOAT 等)
    int root,              // 根进程(发送数据的源进程)
    MPI_Comm comm)         // 通信器(指定通信域,如 MPI_COMM_WORLD)

3.2.2 MPI_Gather

顾名思义这里的MPI_Gather是和MPI_Scatter 相反的。它是从多个进程里面收集数据到一个进程上面,这个机制对很多平行算法很有用,比如并行的排序和搜索。如图:

元素是根据接收到的进程的秩排序的。函数签名如下:

cpp 复制代码
int MPI_Gather(
    const void *sendbuf,   // 发送缓冲区(存储要发送的数据的起始地址)
    int sendcount,         // 发送数据的数量(每个进程发送的数据元素个数)
    MPI_Datatype sendtype, // 发送数据的类型(如 MPI_INT, MPI_FLOAT 等)
    void *recvbuf,         // 接收缓冲区(存储接收到的数据的起始地址,只有 root 进程需要设置)
    int recvcount,         // 接收数据的数量(从每个进程接收的数据元素个数)
    MPI_Datatype recvtype, // 接收数据的类型(如 MPI_INT, MPI_FLOAT 等)
    int root,              // 根进程(用于收集数据的目标进程)
    MPI_Comm comm          // 通信器(指定通信域,如 MPI_COMM_WORLD)
);

只有根进程需要一个有效的接收缓存。所有其他的调用进程可以传递NULLrecvbuf。另外,别忘记*recvcount*参数是从每个进程接收到的数据数量,而不是所有进程的数据总量之和。

范例:用Scatter和Gather计算平均数

首先生成一个随机数的数组,scatter给不同进程,每个进程的到相同多数量的随机数,每个进程计算各自的avg,然后最后求总的avg。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <mpi.h>
#include <assert.h>

// Creates an array of random numbers. Each number has a value from 0 - 1
float *create_rand_nums(int num_elements) {
    float *rand_nums = (float *)malloc(sizeof(float) * num_elements);
    assert(rand_nums != NULL);
    int i;
    for (i = 0; i < num_elements; i++) {
        rand_nums[i] = (rand() / (float)RAND_MAX);
    }
    return rand_nums;
}

// Computes the average of an array of numbers
float compute_avg(float *array, int num_elements) {
    float sum = 0.f;
    for (int i = 0; i < num_elements; i++) {
        sum += array[i];
    }
    return sum / num_elements;
}

int main(int argc, char** argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: avg num_elements_per_proc\n");
        exit(1);
    }

    int num_elements_per_proc = atoi(argv[1]);
    // Seed the random number generator to get different results each time
    srand(time(NULL));

    MPI_Init(NULL, NULL);

    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);

    // Create a random array of elements on the root process. Its total
    // size will be the number of elements per process times the number
    // of processes
    float *rand_nums = NULL;
    if (world_rank == 0) {
        rand_nums = create_rand_nums(num_elements_per_proc * world_size);
    }

    // For each process, create a buffer that will hold a subset of the entire array
    float *sub_rand_nums = (float *)malloc(sizeof(float) * num_elements_per_proc);
    assert(sub_rand_nums != NULL);

    // Scatter the random numbers from the root process to all processes in the MPI world
    MPI_Scatter(rand_nums, num_elements_per_proc, MPI_FLOAT, sub_rand_nums,
                num_elements_per_proc, MPI_FLOAT, 0, MPI_COMM_WORLD);

    // Compute the average of your subset
    float sub_avg = compute_avg(sub_rand_nums, num_elements_per_proc);

    // Gather all partial averages down to the root process
    float *sub_avgs = NULL;
    if (world_rank == 0) {
        sub_avgs = (float *)malloc(sizeof(float) * world_size);
        assert(sub_avgs != NULL);
    }
    MPI_Gather(&sub_avg, 1, MPI_FLOAT, sub_avgs, 1, MPI_FLOAT, 0, MPI_COMM_WORLD);

    // Now that we have all of the partial averages on the root, compute the
    // total average of all numbers. Since we are assuming each process computed
    // an average across an equal amount of elements, this computation will
    // produce the correct answer.
    if (world_rank == 0) {
        float avg = compute_avg(sub_avgs, world_size);
        printf("Avg of all elements is %f\n", avg);

        // Compute the average across the original data for comparison
        float original_data_avg = compute_avg(rand_nums, num_elements_per_proc * world_size);
        printf("Avg computed across original data is %f\n", original_data_avg);
    }

    // Clean up
    if (world_rank == 0) {
        free(rand_nums);
        free(sub_avgs);
    }
    free(sub_rand_nums);

    MPI_Barrier(MPI_COMM_WORLD);
    MPI_Finalize();

    return 0;
}
/******************************************************************
(base) joker@joker-2 4.2 Collective % mpic++ avg_scatter_gather.cc -o avg_scatter_gather    
(base) joker@joker-2 4.2 Collective % mpirun -np 4 ./avg_scatter_gather 2
Avg of all elements is 0.444133
Avg computed across original data is 0.444133
*******************************************************************/

3.2.3 MPI_Allgather

前面出现了一对多,多对一,一对一等的通信模式,那么MPI_Allgather就是多对多。准确来说是收集所有进程的数据然后发到所有进程上,不涉及根进程了,所以可以看到函数签名里面少了int root

cpp 复制代码
int MPI_Allgather(
    const void *sendbuf,   // 发送缓冲区(存储要发送的数据的起始地址)
    int sendcount,         // 发送数据的数量(每个进程发送的数据元素个数)
    MPI_Datatype sendtype, // 发送数据的类型(如 MPI_INT, MPI_FLOAT 等)
    void *recvbuf,         // 接收缓冲区(存储接收到的数据的起始地址)
    int recvcount,         // 每个进程接收的数据数量
    MPI_Datatype recvtype, // 接收数据的类型(如 MPI_INT, MPI_FLOAT 等)
    MPI_Comm comm          // 通信器(指定通信域,如 MPI_COMM_WORLD)
);

对上一个计算平均数的代码修改,将最后输出的每个线程都是全局平均值。代码如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <mpi.h>
#include <assert.h>

// Creates an array of random numbers. Each number has a value from 0 - 1
float *create_rand_nums(int num_elements) {
    float *rand_nums = (float *)malloc(sizeof(float) * num_elements);
    assert(rand_nums != NULL);
    int i;
    for (i = 0; i < num_elements; i++) {
        rand_nums[i] = (rand() / (float)RAND_MAX);
    }
    return rand_nums;
}

// Computes the average of an array of numbers
float compute_avg(float *array, int num_elements) {
    float sum = 0.f;
    for (int i = 0; i < num_elements; i++) {
        sum += array[i];
    }
    return sum / num_elements;
}

int main(int argc, char** argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: avg num_elements_per_proc\n");
        exit(1);
    }

    int num_elements_per_proc = atoi(argv[1]);
    // Seed the random number generator to get different results each time
    srand(time(NULL));

    MPI_Init(NULL, NULL);

    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);

    // Create a random array of elements on the root process. Its total
    // size will be the number of elements per process times the number
    // of processes
    float *rand_nums = NULL;
    if (world_rank == 0) {
        rand_nums = create_rand_nums(num_elements_per_proc * world_size);
    }

    // For each process, create a buffer that will hold a subset of the entire array
    float *sub_rand_nums = (float *)malloc(sizeof(float) * num_elements_per_proc);
    assert(sub_rand_nums != NULL);

    // Scatter the random numbers from the root process to all processes in the MPI world
    MPI_Scatter(rand_nums, num_elements_per_proc, MPI_FLOAT, sub_rand_nums,
                num_elements_per_proc, MPI_FLOAT, 0, MPI_COMM_WORLD);

    // Compute the average of your subset
    float sub_avg = compute_avg(sub_rand_nums, num_elements_per_proc);

    // Gather all partial averages down to the root process
    float *sub_avgs = (float*)malloc(sizeof(float) * num_elements_per_proc);
    assert(sub_avgs != NULL);
    MPI_Allgather(&sub_avg, 1, MPI_FLOAT, sub_avgs, 1, MPI_FLOAT, MPI_COMM_WORLD);

    // Now that we have all of the partial averages on the root, compute the
    // total average of all numbers. Since we are assuming each process computed
    // an average across an equal amount of elements, this computation will
    // produce the correct answer.
    float avg = compute_avg(sub_avgs, world_size);
    printf("Avg of all elements from proc is %f\n", avg);

    // Compute the average across the original data for comparison
    // float original_data_avg = compute_avg(rand_nums, num_elements_per_proc * world_size);
    // printf("Avg computed across original data is %f\n", original_data_avg);
    

    // Clean up
    if (world_rank == 0) {
        free(rand_nums);
        free(sub_avgs);
    }
    free(sub_rand_nums);

    MPI_Barrier(MPI_COMM_WORLD);
    MPI_Finalize();

    return 0;
}
/******************************************************************
(base) joker@joker-2 4.2 Collective % mpic++ avg_allgather.cc -o avg_allgather
(base) joker@joker-2 4.2 Collective % mpirun -np 4 ./avg_allgather 3          
Avg of all elements from proc is 0.579840
Avg of all elements from proc is 0.579840
Avg of all elements from proc is 0.579840
Avg of all elements from proc is 0.579840
*******************************************************************/
相关推荐
小汉堡编程25 分钟前
数据结构——vector数组c++(超详细)
数据结构·c++
SHIPKING39325 分钟前
【机器学习&深度学习】什么是下游任务模型?
人工智能·深度学习·机器学习
写代码的小球2 小时前
求模运算符c
算法
weixin_472339463 小时前
高效处理大体积Excel文件的Java技术方案解析
java·开发语言·excel
枯萎穿心攻击4 小时前
响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法
开发语言·unity·c#·游戏引擎
子燕若水4 小时前
Unreal Engine 5中的AI知识
人工智能
Eiceblue5 小时前
【免费.NET方案】CSV到PDF与DataTable的快速转换
开发语言·pdf·c#·.net
极限实验室5 小时前
Coco AI 实战(一):Coco Server Linux 平台部署
人工智能
tan180°5 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
m0_555762906 小时前
Matlab 频谱分析 (Spectral Analysis)
开发语言·matlab