MPI 快速入门

news2024/12/30 4:05:52

浅学 MPI。

MPI

分布式内存多处理器:

  • 处理器 + 辅助组件 => 节点
  • 一堆节点 => 高性能计算系统
    • 节点 => 进程
  • 节点之间:消息传递

MPI:消息传递接口

安装

还是用 Docker 方便。

宿主机:

sudo docker run -idt --name openmpi -v /home/openmpi/:/home/openmpi -p 22001:22 alpine

sudo ufw allow 22001 comment 'openmpi:ssh'

sudo docker exec -it openmpi sh

容器内:

apk add build-base  # 国内网络有时候要多运行几次
apk add perl  # Open MPI requires perl
apk add linux-headers  # #include <linux/unistd.h>
apk add bash vim
apk add gcompat libstdc++ curl

apk add openssh
vi /etc/ssh/sshd_config # PermitRootLogin yes
passwd # 重新设个 root 密码
/usr/sbin/sshd  # 开 ssh 服务后台

# 下载、安装 OpenMPI
wget https://download.open-mpi.org/release/open-mpi/v4.1/openmpi-4.1.4.tar.gz
tar xzf openmpi-4.1.4.tar.gz
cd openmpi-4.1.4
./configure --prefix=/usr/local
make all install

# 测试安装
cd /openmpi-4.1.4/examples/
mpicc -o hello_c hello_c.c
mpirun -n 4 --allow-run-as-root --oversubscribe hello_c
    # --allow-run-as-root: root 硬跑
    # --oversubscribe: 没有多处理器,单核单线程硬跑

退出来,宿主机,把刚才装好的做成镜像备用,可以方便以后重开:

sudo docker ps # 找一下刚才那个的 id
sudo docker commit 37c628532bae openmpi:v0.0.0

以后再次搭这个环境就方便了:

sudo docker run -idt --name openmpi -v /home/openmpi/:/home/openmpi -p 22001:22 openmpi:v0.0.0

# 这个没有 entrypoint,
# 如果要用 ssh,需要手动进去手动开一下 sshd
sudo docker exec -it openmpi-with-sshd sh
容器内 # /usr/sbin/sshd

看着多是由于好多步骤是在弄 SSH,弄好了 SSH,搭集群也就方便了。但我暂时没有兴趣。

MPI 基本命令

#include <mpi.h>  // 导入包

int main(int argc, char *argv[]) {
    MPI_Init(&argc, &argv);  // 任意其他 MPI 调用前
    ...
    MPI_Finalize(); // 任意其他 MPI 调用后
    ...
}

Hello World

#include <mpi.h>
#include <stdio.h>

int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);
    
    printf("Hello, world!\n");
    
    MPI_Finalize();
    
    return 0;
}

编译运行:

$ mpicc hello.c -o hello
$ mpirun -n 4 --allow-run-as-root --oversubscribe hello
Hello, world!
Hello, world!
Hello, world!
Hello, world!

--allow-run-as-root--oversubscribe 是由于我要强制在单核单线程的虚拟机里用 Docker 里的 root 用户运行 MPI 程序,正常环境上不用。)

通信器

上面的 Hello World:无共享。每个进程做自己的,没有交互,无法协调工作。

MPI 的并发进程交互:通信器(communicator):

  • 地址空间:包含一组 MPI 进程
  • 其他各种属性

MPI 自带提供一个开箱即用的通信器:MPI_COMM_WORLD,包含该 MPI 程序的所有并发进程。

size & rank

sizerank 是两个常用的通信器属性。

  • size:通信器的大小,即构成通信器的进程数量;
  • rank:通信器中每个进程的标识(唯一进程 ID, ≥ 0 \ge 0 0 的整数),称为 rank;

两个属性的 getter(不是 setter):

int size, rank;

MPI_Comm_size(MPI_COMM_WORLD, /* out */ &size); 
MPI_Comm_rank(MPI_COMM_WORLD, /* out */ &rank);

(其实这两个函数有 int 类型的返回值,目测成功都是 0。)

e.g. 带 rank 和 size 的 Hello World:

#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);

    printf("Hello from rank %d out of %d processes in MPI_COMM_WORLD\n", rank, size);

    MPI_Finalize();

    return 0;
}

编译运行:

# mpicc comm.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Hello from rank 0 out of 4 processes in MPI_COMM_WORLD
Hello from rank 2 out of 4 processes in MPI_COMM_WORLD
Hello from rank 1 out of 4 processes in MPI_COMM_WORLD
Hello from rank 3 out of 4 processes in MPI_COMM_WORLD

点对点消息

  • MPI 负责管理通信器内的进程之间的数据交换
  • MPI 数据交换的媒介:消息
    • 源进程 rank
    • 目标进程 rank
    • 包含源、目的进程的通信器
    • 标记:区分两个进程间的一组可能的消息,用户自定

发送

MPI_Send(
    /* in */ void* message,
    int count, MPI_Datatype datatype,
    int dest, int tag, MPI_Comm comm)

发送的消息内容是:从 message 参数处开始的一个 MPI_Datatype[count] 数组

其中,count 是数据元素的数量(数组长度);MPI_Datatype 为其类型,基本就是和 C 的简单数据类型一一对应:

MPI_Datatype对应的 C 数据类型
MPI_SHORTshort int
MPI_INTint
MPI_LONGlong int
MPI_LONG_LONGlong long int
MPI_UNSIGNED_CHARunsigned char
MPI_UNSIGNED_SHORTunsigned short int
MPI_UNSIGNEDunsigned int
MPI_UNSIGNED_LONGunsigned long int
MPI_UNSIGNED_LONG_LONGunsigned long long int
MPI_FLOATfloat
MPI_DOUBLEdouble
MPI_LONG_DOUBLElong double
MPI_BYTEunsigned char

再次强调,MPI 发送的是数组。单发一个数 int a 也要将其看作 int msg[1] = &a,所以写作 MPI_Send(&a, 1, MPI_INT, ...);而如果要发送一个数组 int A[3],则不必再取地址:MPI_Send(A, 3, MPI_INT, ...)

再次强调,MPI 发送的是数组,理解这点后再去看 MPI 接口,就没那么魔幻了,很多都是「数组首地址 + 长度 + 类型」这三个配套出现,可能有多组这个三元组,例如 MPI_Scatter,后面再加上一个「源/目标进程号 + 通信器」。

接收

MPI_Recv(
    /* out */ void* message,
    int count, MPI_Datatype datatype,
    int source, int tag, MPI_Comm comm,
    MPI_Status* status)

status 就是 source + tag + 可能的 error。

例子

把之前的带 rank 的 hello 改成顺序版本:

#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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);

    if (size == 1) {
        printf("This example requires more than one process to execute.\n");
        MPI_Finalize();
        exit(1);
    }

    // 消息收发的参数
    int message[2];  // buffer
    int dst, src;
    int tag = 0;
    MPI_Status status;

    if (rank != 0) {  // 给进程 0 发消息
        message[0] = rank;
        message[1] = size;

        dst = 0;

        MPI_Send(message, 2, MPI_INT, dst, tag, MPI_COMM_WORLD);
    } else {  // 进程 0:顺序收消息
        for (src = 1; src < size; src++) {
            MPI_Recv(message, 2, MPI_INT, src, MPI_ANY_TAG, MPI_COMM_WORLD, &status);

            printf("Hello from process %d out of %d.\n",
                   message[0], message[1]);
        }
    }

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc send-recv.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Hello from process 1 out of 4.
Hello from process 2 out of 4.
Hello from process 3 out of 4.

这个程序用上 master-worker 模式了:

  • rank 为 0 的进程是 master,负责顺序收消息、打印;
  • rank 为其他值的进程是 worker,负责发一条消息给 master;
  • if-else 分化 master 和 worker 的工作。

聚合通信

聚合通信:包含通信器内的所有进程的通信模式(群消息)

同步:Barrier

Barrier:栅栏:

  • 所有人都堵在这里等;
  • 所有人都到齐了再放行。

栅(zhà)栏,居然不是读 shān,我说咋老是打不出来。。。另外原来 zhà 栏是这个字啊,从未设想过😭

MPI_Barrier(MPI_Comm comm)

e.g. 又一个 Hello World:

#include <mpi.h>
#include <stdio.h>

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

    MPI_Barrier(MPI_COMM_WORLD);

    int size, rank;
    MPI_Comm_size(MPI_COMM_WORLD, &size);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);

    int len;
    char name[MPI_MAX_PROCESSOR_NAME];
    MPI_Get_processor_name(name, &len);

    MPI_Barrier(MPI_COMM_WORLD);

    printf("Hello, world! Process %d of %d on %s\n", rank, size, name);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc barrier.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Hello, world! Process 0 of 4 on c8e9719000d7
Hello, world! Process 2 of 4 on c8e9719000d7
Hello, world! Process 3 of 4 on c8e9719000d7
Hello, world! Process 1 of 4 on c8e9719000d7

联想:OpenMP 的 barrier 指令。

广播:Bcast

MPI_Bcast(
    void *shared_data,
    int count, MPI_Datatype datatype,
    int root, MPI_Comm comm)

rootshared_data 广播(同步)给各进程的 shared_data 里。

e.g.

#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);

    int A[4];
    for (int i = 0; i < 4; i++) {
        A[i] = 0;
    }

    int root = 0;  // root process

    if (rank == root) {
        A[0] = 3;
        A[1] = 5;
        A[2] = 4;
        A[3] = 1;
    }

    MPI_Bcast(A, 4, MPI_INT, root, MPI_COMM_WORLD);

    printf("Rank %d: A = [%d, %d, %d, %d]\n", rank, A[0], A[1], A[2], A[3]);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc bcast.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Rank 0: A = [3, 5, 4, 1]
Rank 1: A = [3, 5, 4, 1]
Rank 2: A = [3, 5, 4, 1]
Rank 3: A = [3, 5, 4, 1]

分散:Scatter

MPI_Scatter(
    void *send_data, int send_count, MPI_Datatype send_type,
    void *recv_data, int recv_count, MPI_Datatype recv_type,
    int root, MPI_Comm comm);

rootsend_data 分散到各个进程的 recv_data 里,包括自己的,每个人发 send_count 个。

e.g.

#include <mpi.h>
#include <stdio.h>
#include <stdlib.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);

    if (size != 4) {
        printf("This example requires 4 processes to execute.\n");
        MPI_Finalize();
        exit(1);
    }

    int A[4], B[4];
    for (int i = 0; i < 4; i++) {
        A[i] = 0;
        B[i] = 0;
    }

    int root = 0;  // root process

    if (rank == root) {
        A[0] = 3;
        A[1] = 5;
        A[2] = 4;
        A[3] = 1;
    }

    MPI_Scatter(A, 1, MPI_INT,
                B, 1, MPI_INT,
                root, MPI_COMM_WORLD);

    printf("Rank %d: A = [%d, %d, %d, %d], B = [%d, %d, %d, %d]\n", rank, 
           A[0], A[1], A[2], A[3], 
           B[0], B[1], B[2], B[3]);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc scatter.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Rank 0: A = [3, 5, 4, 1], B = [3, 0, 0, 0]
Rank 1: A = [0, 0, 0, 0], B = [5, 0, 0, 0]
Rank 2: A = [0, 0, 0, 0], B = [4, 0, 0, 0]
Rank 3: A = [0, 0, 0, 0], B = [1, 0, 0, 0]

收集:Gather

MPI_Gather(
    void *send_data, int send_count, MPI_Datatype send_type,
    void *recv_data, int recv_count, MPI_Datatype recv_type,
    int dest, MPI_Comm comm);

MPI_Gather 就是做反向的 MPI_Scatter:把各个进程的 send_data 收集到 destrecv_data 里。

e.g.

#include <mpi.h>
#include <stdio.h>
#include <stdlib.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);

    /* assert size == 4 */

    int A[4], B[4];
    for (int i = 0; i < 4; i++) {
        A[i] = 0;
        B[i] = 0;
    }

    A[0] = rank;  // 各自进程的结果

    int dest = 0; // 收集到 dest

    MPI_Gather(A, 1, MPI_INT,
               B, 1, MPI_INT,
               dest, MPI_COMM_WORLD);

    printf("Rank %d: A = [%d, %d, %d, %d], B = [%d, %d, %d, %d]\n", rank, 
           A[0], A[1], A[2], A[3], 
           B[0], B[1], B[2], B[3]);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc gather.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Rank 0: A = [0, 0, 0, 0], B = [0, 1, 2, 3]
Rank 1: A = [1, 0, 0, 0], B = [0, 0, 0, 0]
Rank 2: A = [2, 0, 0, 0], B = [0, 0, 0, 0]
Rank 3: A = [3, 0, 0, 0], B = [0, 0, 0, 0]

全局收集:Allgather

MPI_Gather(
    void *send_data, int send_count, MPI_Datatype send_type,
    void *recv_data, int recv_count, MPI_Datatype recv_type,
    MPI_Comm comm);

类似于 MPI_Gather,但是收集的结果是广播到所有进程上的,而不是上缴到 dest(所以也就没这个参数了)。

e.g.

#include <mpi.h>
#include <stdio.h>
#include <stdlib.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);

    /* assert size == 4 */

    int A[4], B[4];
    for (int i = 0; i < 4; i++) {
        A[i] = 0;
        B[i] = 0;
    }

    A[0] = rank;  // 各自进程的结果

    MPI_Allgather(A, 1, MPI_INT,
                  B, 1, MPI_INT,
                  MPI_COMM_WORLD);

    printf("Rank %d: A = [%d, %d, %d, %d], B = [%d, %d, %d, %d]\n", rank, 
           A[0], A[1], A[2], A[3], 
           B[0], B[1], B[2], B[3]);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc allgather.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Rank 0: A = [0, 0, 0, 0], B = [0, 1, 2, 3]
Rank 1: A = [1, 0, 0, 0], B = [0, 1, 2, 3]
Rank 2: A = [2, 0, 0, 0], B = [0, 1, 2, 3]
Rank 3: A = [3, 0, 0, 0], B = [0, 1, 2, 3]

规约:Reduce

关于「规约」、「reduce」的词意以及这个过程的示意图,见 OpenMP 的 reduction 指令。

MPI_Reduce(const void *send_data, void *recv_data,
           int count, MPI_Datatype datatype, 
           MPI_Op op,
           int dest, MPI_Comm comm);

每个进程发 countdatatype 类型的本地结果 send_datadestdest 将这些结果做 op 运算,结果放到 recv_data

op 可以是:

  • MPI_MAXMPI_MIN
  • MPI_SUMMPI_PROD
  • MPI_LAND(逻辑与)、MPI_BAND(按位与),类似的还有 OR、XOR。要求 datatype 是整型
  • MPI_MAXLOC(最大值其位置)、MPI_MINLOC。要求 datatype 是对:MPI_DOUBLE_INTMPI_2INI

e.g. 计算两个向量的点积: a ⋅ b = ∑ i a i b i a \cdot b = \sum_i a_i b_i ab=iaibi

具体来说就是做这件事:

a ⋅ b =   [ 1 , ⋯   , 1 ⏟ 100个1 , 2 , ⋯   , 2 , ⋯   ] × [ 2 , ⋯   , 2 ] T   =   1 × 2 + ⋯ + 1 × 2 ⏟ 100次 +     2 × 2 + ⋯ + 2 × 2 +     ⋯ \begin{array}{rll} a \cdot b =&\ [\underbrace{1,\cdots,1}_\textrm{100个1},2,\cdots,2,\cdots] \times [2,\cdots,2]^T\\ ~ =&\ \underbrace{1 \times 2 + \cdots +1 \times 2}_\textrm{100次} +\\ ~ &\ 2 \times 2 + \cdots + 2 \times 2 +\\ ~ &\ \cdots \end{array} ab= =   [1001 1,,1,2,,2,]×[2,,2]T 100 1×2++1×2+ 2×2++2×2+ 

其中,最后一个等号右边每一行由一个进程来算,行之间加起来用 reduce 来做。

#include <mpi.h>
#include <stdio.h>
#include <stdlib.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);

    int local_vector_size = 100;
    int global_vector_size = size * local_vector_size;

    double *a, *b;
    a = (double *) malloc(local_vector_size * sizeof(double));
    b = (double *) malloc(local_vector_size * sizeof(double));
    for (int i = 0; i < local_vector_size; i++) {
        a[i] = 1.0 * rank;
        b[i] = 2.0;
    }

    // 上面都是造数据
    // 下面正式开始算 dot product,两阶段:本地累积、全局规约
    
    double partial_sum = 0.0;
    for (int i = 0; i < local_vector_size; i++) {
        partial_sum += a[i] * b[i];
    }

    int root = 0;
    double sum = 0.0;
    MPI_Reduce(&partial_sum, &sum, 
               1, MPI_DOUBLE, MPI_SUM, 
               root, MPI_COMM_WORLD);

    if (rank == root) {
        printf("The dot product is %g\n", sum);
    }

    free(a);
    free(b);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc reduce.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
The dot product is 1200

全局规约:Allreduce

类似于从 MPI_GatherMPI_AllgatherMPI_AllreduceMPI_Reduce 的运算,但是把结果广播到每一个进程(所以也就无需 dest 参数)。

MPI_Allreduce(const void *send_data, void *recv_data,
              int count, MPI_Datatype datatype, 
              MPI_Op op,
              MPI_Comm comm);

e.g.

#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);

    int input = 0;
    switch (rank) {
        case 0: input = 2; break;
        case 1: input = 7; break;
        case 2: input = 1; break;
    }

    int output;
    MPI_Allreduce(&input, &output, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);

    printf("Rank %d: result = %d.\n", rank, output);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc allreduce.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Rank 3: result = 10.
Rank 1: result = 10.
Rank 2: result = 10.
Rank 0: result = 10.

全局到全局:Alltoall

alltoall 通信模式:

  • 每个发送器也是接收器;
  • 不同的数据被发送到每个接收器:第 i 个数据分区被发送到第 j 个进程;

用每行表示一个进程,每列表示一个数据分区,则 alltoall 的效果类似于矩阵转置:

我的理解是 Alltoall = Allscatter,不知道对不对哈:

  • 作 sender:每个进程把自己的数组 A 做 Scatter 发到各个进程;
  • 作 recver:进程 i 把各个进程发来的数(A[i] from j)按 sender 的 rank j 拼成新数组 B:B[j] = A[i] form j
MPI_Alltoall(
    void *send_data, int send_count, MPI_Datatype send_type,
    void *recv_data, int recv_count, MPI_Datatype recv_type,
    MPI_Comm comm);

e.g.

#include <assert.h>
#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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);

    assert((size == 4) && "this example is designed for 4 processes.");

    int A[4], B[4];

    for (int i = 0; i < 4; i++) {
        A[i] = i + 1 + 4 * rank;
    }

    MPI_Alltoall(A, 1, MPI_INT, B, 1, MPI_INT, MPI_COMM_WORLD);

    sleep(rank);
    printf("Rank %d: A = [%2d, %2d, %2d, %2d], B = [%2d, %2d, %2d, %2d]\n", rank, 
           A[0], A[1], A[2], A[3], 
           B[0], B[1], B[2], B[3]);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc alltoall.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Rank 0: A = [ 1,  2,  3,  4], B = [ 1,  5,  9, 13]
Rank 1: A = [ 5,  6,  7,  8], B = [ 2,  6, 10, 14]
Rank 2: A = [ 9, 10, 11, 12], B = [ 3,  7, 11, 15]
Rank 3: A = [13, 14, 15, 16], B = [ 4,  8, 12, 16]

非阻塞通信

上文的 点对点消息 和 聚合通信 都是阻塞的:发/收 完成之前,函数不会返回。

MPI 还提供了非阻塞的接口:

MPI_Isend(
    void* message,
    int count, MPI_Datatype datatype,
    int dest, int tag, MPI_Comm comm,
    MPI_Request *send_request)

MPI_Irecv(
    void* message,
    int count, MPI_Datatype datatype,
    int source, int tag, MPI_Comm comm,
    MPI_Request *recv_request)

// 还有类似的 MPI_Ibarrier, MPI_Ibcast, 
// MPI_Iscatter, MPI_Igather
// MPI_Ialltoall, MPI_Iallgather
// MPI_Ireduce, ...

就是函数名 MPI_Xxx -> MPI_Ixxx,参数最后加一个 MPI_Request,用于跟踪该异步通信。这些函数在调用后立即返回。

欲知异步通信是否完成,使用 MPI_Test,把 MPI_Ixxx 的 request 传进来,检查,已完成则置 flag 的值为真:

MPI_Test(MPI_Request *request, int *flag, MPI_Status *status);

在必须完成异步通信时,使用 MPI_Wait,阻塞,等通信完成:

MPI_Wait(MPI_Request *request, MPI_Status *status);

用非阻塞通信有一个好处是,可以防呆,避免一些程序顺序瑕疵可能带来的死锁问题。考虑如下程序:

#include <assert.h>
#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);

    assert(size == 2);

    int tag = 0;
    int a = rank, b = -1;

    // work
    MPI_Send(&a, 1, MPI_INT, 1 - rank, tag, MPI_COMM_WORLD);
    MPI_Recv(&b, 1, MPI_INT, 1 - rank, tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);

    printf("Rank %d: recv value %d.\n", rank, b);

    MPI_Finalize();
    return 0;
}

send 在前,recv 在后,可以工作。但如果交换二者顺序,就直接死锁(都先 recv,但没人发啊):

// DEADLOCK!!!
MPI_Recv(&b, 1, MPI_INT, 1 - rank, tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
MPI_Send(&a, 1, MPI_INT, 1 - rank, tag, MPI_COMM_WORLD);

改成非阻塞通信:

#include <assert.h>
#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);

    assert(size == 2);

    int tag = 0;
    MPI_Status status;
    MPI_Request send_req, recv_req;

    int a = rank, b = -1;

    // 可以交换
    MPI_Isend(&a, 1, MPI_INT, 1 - rank, tag, MPI_COMM_WORLD, &send_req);
    MPI_Irecv(&b, 1, MPI_INT, 1 - rank, tag, MPI_COMM_WORLD, &recv_req);

    // 可以交换
    MPI_Wait(&send_req, &status);
    MPI_Wait(&recv_req, &status);

    printf("Rank %d: recv value %d.\n", rank, b);

    MPI_Finalize();
    return 0;
}

编译运行:

Rank 0: recv value 1.
Rank 1: recv value 0.

交换 IsendIrecv,程序也正常工作。交换两句 Wait,也正常工作。所以这个就很舒服了。

自定义数据类型

MPI_Type_create_struct(int count, 
    const int array_of_block_lengths[],
    const MPI_Aint array_of_displacements[],
    const MPI_Datatype array_of_types[],
    MPI_Datatype *newtype);

MPI_Type_commit(MPI_Datatype *newtype);

e.g.

#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int x;
    double y;
} Pair;

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);

    // 要动态创建的新 mpi 类型
    MPI_Datatype mpi_pair;

    int nitems = 2;              // # fields of Pair
    MPI_Datatype types[nitems];  // element types
    MPI_Aint offsets[nitems];    // element offsets
    int blocklengths[nitems];    // element count

    // Pair.x
    types[0] = MPI_INT;
    offsets[0] = offsetof(Pair, x);
    blocklengths[0] = 1;

    // Pair.y
    types[1] = MPI_DOUBLE;
    offsets[1] = offsetof(Pair, y);
    blocklengths[1] = 1;

    // 注册类型
    MPI_Type_create_struct(nitems, blocklengths, offsets, types, &mpi_pair);
    MPI_Type_commit(&mpi_pair);

    // 然后就可以把 Pair 结构体用 MPI 通信了

    int root = 0;

    Pair pair;
    if (rank == root) {
        pair.x = 10;
        pair.y = 3.14;
    }

    MPI_Bcast(&pair, 1, mpi_pair, root, MPI_COMM_WORLD);

    printf("Rank %d: recv Pair{x=%d, y=%g}\n", rank, pair.x, pair.y);

    MPI_Finalize();
    return 0;
}

编译运行:

$ mpicc newtype.c && mpirun -n 4 --allow-run-as-root --oversubscribe ./a.out
Rank 0: recv Pair{x=10, y=3.14}
Rank 1: recv Pair{x=10, y=3.14}
Rank 3: recv Pair{x=10, y=3.14}
Rank 2: recv Pair{x=10, y=3.14}

参考文献

  • Thomas Sterling,Matthew Anderson,Maciej Brodowicz. 高性能计算:现代系统与应用实践. 第 8 章 MPI 的基础
  • OpenMPI 官网: https://www.open-mpi.org/
  • 下载地址: https://www.open-mpi.org/software/ompi/v4.1/
  • 安装文档: https://www.open-mpi.org/faq/?category=building#easy-build

EOF

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/50904.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

移动WEB开发之流式布局--移动WEB开发之flex布局--携程网首页案例制作

案例&#xff1a;携程网移动端首页 访问地址&#xff1a;携程旅行-酒店预订,机票预订查询,旅游度假,商旅管理-携程无线官网 (ctrip.com) 1. 技术选型 方案&#xff1a;我们采取单独制作移动页面方案 技术&#xff1a;布局采取flex布局 2. 搭建相关文件夹结构 3. 设置视口标…

三、【react-redux】数据共享

文章目录1、优化项目结构2、添加一个新容器组件2.1、新项目结构2.2、CODE2.2.1、reduc/constant.js2.2.2、redux/actions/person.js2.2.3、redux/reducers/person.js2.2.4、redux/store.js2.2.5、Count.jsx2.2.6、Person.jsx2.3、Result3、总结本示例修改自 上一章 求和Demo 1、…

SVN版本控制软件

尚硅谷SVN版本控制软件教程&#xff08;一套掌握svn操作&#xff09; 学习网址&#xff1a;https://www.bilibili.com/video/BV1mW411M7yR/?spm_id_from333.999.0.0&vd_source461545ff50a35eaeaa8218ecdc5f7152 学习时长&#xff1a;1小时46分钟 未学习 5.启动服务器 6.…

维视智造明星产品推荐(一) 环外侧工业镜头

维视智造明星产品推荐&#xff08;一&#xff09;环外侧工业镜头 市场洞察 产品外观质量检测及标签检测&#xff0c;是工业制造中常见的两个质检场景。根据产品特点及产线环境&#xff0c;往往可以做多种检测方案的选择。在圆柱状产品如药瓶、瓶盖、齿轮、螺母等的生产检测中&a…

11.30排序

目录 一.排序 1.概念 1.1排序 1.2稳定性 2.七大基于比较的排序 二.插入排序 3.1 直接插入排序-原理 2.折半插入排序 3.分析 二.每日一题订正 1.选择题 2.不要二 三.希尔排序 1 原理 2.代码实现 3.分析 四.选择排序 1.原理 2.代码 3.优化版 4.分析 五.测量…

使用Cpolar内网穿透开启群晖WebDAV

文章目录1.前言2.群晖组件安装2.1.软件安装&#xff08;1&#xff09;WebDav server套件下载安装&#xff08;2&#xff09;cpolar套件下载安装&#xff08;3&#xff09;RaiDrive的下载安装2.2.群晖NAS端软件的设置2.3.Cpolar云端设置2.4.Cpolar本地设置3.访问端软件设置4.公网…

Pytorch的入门操作(三)

2.7 使用Pytorch实现手写数字识别 2.7.1 目标 知道如何使用Pytorch完成神经网络的构建知道Pytorch中激活函数的使用方法知道Pytorch中torchvision.transforms 中常见图形处理函数的使用知道如何训练模型和如何评估模型 2.7.2 思路和流程分析 流程: 准备数据&#xff0c;这…

推荐系统-召回-概述(五):一切为了业务

在前面几篇文章里&#xff0c;我们介绍了主流的召回模型和算法。但算法更多地是从个性化推荐的角度来解决问题。许多业务上的问题&#xff0c;如安全问题、商业价值、用户体验、流量扶持等种种业务需求&#xff0c;仅仅基于模型&#xff0c;是无法得到完美解决的&#xff0c;它…

成长的旅途,未知的邂逅

成长的旅途&#xff0c;未知的邂逅兰舟千帆纷繁复杂&#xff0c;斑驳陆离的岁月邂逅&#xff1f;出发&#xff01;兰舟千帆 我是兰舟千帆&#xff0c;从2020年加入csdn写博客到现在差不多就是两年了。马上也就三年了。这样的时间段&#xff0c;也同样记录着我的成长。哎嘿。我现…

构建基于 Ingress 的全链路灰度能力

作者&#xff1a;涂鸦 背景 随着云原生技术不断普及&#xff0c;越来越多的业务应用开始向云原生架构转变&#xff0c;借助容器管理平台 Kubernetes 的不可变基础设施、弹性扩缩容和高扩展性&#xff0c;助力业务迅速完成数字化转型。其中&#xff0c;集群入口流量管理方式在…

清朝盛衰的六个时间点!

清朝盛衰的整个过程经历了六个时间节点&#xff1a; 一六六一年﹙顺治十八年﹚是第一个时间节点。 正月初六夜半时分&#xff0c;顺治帝预感生命垂危&#xff0c;急命太监传呼麻勒吉与王熙两人赶赴养心殿。帝对王熙说&#xff1a;“朕出痘&#xff0c;势将不起&#xff0c;尔…

[附源码]Python计算机毕业设计Django高校流浪动物领养网站

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

PCB信号仿真之为什么DDR走线要同组同层?

作者&#xff1a;一博科技高速先生成员 刘春 随着信号速率的不断提高&#xff0c;对信号时序的要求也越来越严格。在PCB设计中&#xff0c;我们等长的最终目的都是为了等时&#xff0c;以满足信号的时序要求。因此&#xff0c;需要我们对信号在传输线上的时延有一定的了解&…

python之文件操作相关知识

python之文件操作相关知识 一、文件的打开与关闭 1、打开文件 在Python中&#xff0c;使用 open() 函数&#xff0c;可以打开一个已经存在的文件&#xff0c;或创建一个新文件 语法如下&#xff1a; open(文件名, 访问模式) 说明&#xff1a; 访问模式决定了打开文件的模式&…

【自然语言处理(NLP)】基于SQuAD的机器阅读理解

【自然语言处理&#xff08;NLP&#xff09;】基于SQuAD的机器阅读理解 作者简介&#xff1a;在校大学生一枚&#xff0c;华为云享专家&#xff0c;阿里云专家博主&#xff0c;腾云先锋&#xff08;TDP&#xff09;成员&#xff0c;云曦智划项目总负责人&#xff0c;全国高等学…

vue3和vue2组件风格对比

Vue3 组合式 API&#xff08;Composition API&#xff09; 主要用于在大型组件中提高代码逻辑的可复用性。 传统的组件随着业务复杂度越来越高&#xff0c;代码量会不断的加大&#xff0c;整个代码逻辑都不易阅读和理解。 Vue3 使用组合式 API 的地方为 setup。 在 setup 中…

无代码资讯|SAP发布低代码平台;钉钉低代码应用数破500万;轻流举办无代码城市论坛......

栏目导读&#xff1a;无代码资讯栏目从全球视角出发&#xff0c;带您了解无代码相关最新资讯。 TOP3 大事件 1、SAP 召开“SAP TechEd ”大会&#xff0c;发布低代码平台 SAP Build 11 月 15 日-16 日&#xff0c;全球企服巨头 SAP 在美国拉斯维加斯召开“2022 SAP TechEd”产…

MySQL是如何实现事务的隔离级别

MySQL是如何实现事务的隔离级别 - 游生 - 博客园 摘要 本文旨在了解MySQL InnoDB引擎如何支持事务的隔离级别。 文章主要内容分两个部分。 第一部分阐述数据库的并发问题以及为之产生的ANSI SQL 标准隔离级别。 第二部分根据 MySQL 官方文档解释 InnoDB 是如何支持这些隔离…

关于python中自带的类似postman的工具

关于python中自带的类似postman的工具 1.新建一个http 请求&#xff1a; 2.添加请求方式 2.1程序运行 验证数据的运行&#xff1a; 1.post数据添加验证

打造无证服务化:这个政务服务平台有点不一样

摘要&#xff1a;华为云携手深圳市华傲数据技术有限公司针对“数字政府建设”与“数字经济发展”两大场景&#xff0c;打造华傲可信政务区块链解决方案。本文分享自华为云社区《华为云携手华傲数据打造“无证服务”政务服务平台》&#xff0c;作者&#xff1a;灰灰哒 。 当前&…