MPICH笔记(五):聚合通信

MPICH笔记(五):聚合通信

官方文档索引:

https://www.mpich.org/static/docs/v3.2/

概述:

大体有三类聚合通信\(^{[1]}\):

1) 同步。

2)数据迁移。如:MPI_Bcast广播、MPI_Scatter散射、MPI_Gather聚合、MPI_Allgather聚合结果存到每个进程、AlltoAll数据全交换(有点像矩阵的转职)。

3)聚合运算。如归约(MPI_Reduce)、扫描(MPI_Scan)。

代码:

#include <iostream>
#include "mpi.h"

using namespace std;

int main(void) {
    MPI_Init(nullptr, nullptr);
    int rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    printf("Rank %d > Reach %d\n", rank, __LINE__);
    MPI_Barrier(MPI_COMM_WORLD);
    printf("Rank %d > Reach %d\n", rank, __LINE__);
    MPI_Finalize();
    return 0;
}

 

执行命令:

mpiexec -n 8 ./文件名

 

输出:

Rank 1 > Reach 10
Rank 2 > Reach 10
Rank 6 > Reach 10
Rank 3 > Reach 10
Rank 4 > Reach 10
Rank 7 > Reach 10
Rank 0 > Reach 10
Rank 5 > Reach 10
Rank 0 > Reach 12
Rank 1 > Reach 12
Rank 2 > Reach 12
Rank 3 > Reach 12
Rank 4 > Reach 12
Rank 5 > Reach 12
Rank 6 > Reach 12
Rank 7 > Reach 12

去掉MPI_Barrier(MPI_COMM_WORLD)之后

输出:

Rank 2 > Reach 10
Rank 2 > Reach 12
Rank 3 > Reach 10
Rank 3 > Reach 12
Rank 0 > Reach 10
Rank 0 > Reach 12
Rank 5 > Reach 10
Rank 5 > Reach 12
Rank 7 > Reach 10
Rank 7 > Reach 12
Rank 4 > Reach 10
Rank 4 > Reach 12
Rank 6 > Reach 10
Rank 6 > Reach 12
Rank 1 > Reach 10
Rank 1 > Reach 12

可以看到,由于Barrier将所有进程进行了同步,所以使用Barrier的代码所有进程保证都先执行了第10行的内容,之后才是第12行的内容,而不使用Barrier的代码自然没有这个限制。

 

参考资料:

[1]: 《并行计算的编程模型(Programming Models for Parallel Computing)》第一版,Pavan Balaji [美]、美国阿贡国家实验室编著,张云泉、李士刚、逄仁波、袁良译,机械工业出版社。

MPICH笔记(四):非阻塞通信

MPICH笔记(四):非阻塞通信

官方文档链接:

int MPI_Isend(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)

int MPI_Irecv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request)

int MPI_Wait(MPI_Request *request, MPI_Status *status)

int MPI_Waitall(int count, MPI_Request array_of_requests[],  MPI_Status array_of_statuses[])

int MPI_Request_free(MPI_Request *request)

概述:

MPI的非阻塞通信的函数的命名规则一般是在对应的阻塞式函数的第二个单词处开头添加大写I,表示这个函数时非阻塞的,并且函数的参数会有一定的调整(如MPI_Irecv()比MPI_Recv()少了输出MPI_Status类型数据的参数),并都添加上了输出MPI_Request类型数据的参数,用来在之后的程序中进行判断通信是否完成。

代码:

#include <iostream>
#include "mpi.h"

using namespace std;

typedef long long LL;

int main(void) {
    MPI_Init(nullptr, nullptr);
    int rank, size;
    int dat;
    MPI_Request *r;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);
    if (!rank) {
        r = new MPI_Request[size - 1];
        dat = 233;
        /*
        * 进行非阻塞式点对点发送
        * 与具有rank i的进程的通信状态存储在r[i - 1]中
        * 即地址r + i - 1上     
        */
        for (int i = 1; i < size; i++) {
            MPI_Isend(&dat, 1, MPI_INT, i, 0, MPI_COMM_WORLD, r + i - 1);
        }
        /*
        * 进行一个比较耗时的计算,这里使用循环来暴力计算10到100的阶乘之和
        * 对1000000007取模的值
        */
        LL mod = 1000000007ll;
        LL sum = 0, ret;
        for (int i = 10; i <= 100; i++) {
            ret = 1;
            for (int j = 1; j <= i; j++) {
                ret = (ret * i) % mod;
            }
            sum = (sum + ret) % mod;
            printf("Rank 0 > do operation %d!\n", i);
        }
        MPI_Waitall(size - 1, r, MPI_STATUSES_IGNORE);
        delete [] r;
    } else {
        r = new MPI_Request();
        MPI_Irecv(&dat, 1, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, r);
        for (int i = 0; i < 1000; i++)
            for (int j = 0; j < 1000000; j++);
        MPI_Wait(r, MPI_STATUS_IGNORE);
        printf("Rank %d > Done!\n", rank);
        delete r;
    }
    MPI_Finalize();
    return 0;
}

执行指令:

mpiexec -n 8 ./文件名

输出:

Rank 0 > do operation 10!
Rank 0 > do operation 11!
Rank 0 > do operation 12!
Rank 0 > do operation 13!
Rank 0 > do operation 14!
Rank 0 > do operation 15!
Rank 0 > do operation 16!
Rank 0 > do operation 17!
Rank 0 > do operation 18!
Rank 0 > do operation 19!
Rank 0 > do operation 20!
Rank 0 > do operation 21!
Rank 0 > do operation 22!
Rank 0 > do operation 23!
Rank 0 > do operation 24!
Rank 0 > do operation 25!
Rank 0 > do operation 26!
Rank 0 > do operation 27!
Rank 0 > do operation 28!
Rank 0 > do operation 29!
Rank 0 > do operation 30!
Rank 0 > do operation 31!
Rank 0 > do operation 32!
Rank 0 > do operation 33!
Rank 0 > do operation 34!
Rank 0 > do operation 35!
Rank 0 > do operation 36!
Rank 0 > do operation 37!
Rank 0 > do operation 38!
Rank 0 > do operation 39!
Rank 0 > do operation 40!
Rank 0 > do operation 41!
Rank 0 > do operation 42!
Rank 0 > do operation 43!
Rank 0 > do operation 44!
Rank 0 > do operation 45!
Rank 0 > do operation 46!
Rank 0 > do operation 47!
Rank 0 > do operation 48!
Rank 0 > do operation 49!
Rank 0 > do operation 50!
Rank 0 > do operation 51!
Rank 0 > do operation 52!
Rank 0 > do operation 53!
Rank 0 > do operation 54!
Rank 0 > do operation 55!
Rank 0 > do operation 56!
Rank 0 > do operation 57!
Rank 0 > do operation 58!
Rank 0 > do operation 59!
Rank 0 > do operation 60!
Rank 0 > do operation 61!
Rank 0 > do operation 62!
Rank 0 > do operation 63!
Rank 0 > do operation 64!
Rank 0 > do operation 65!
Rank 0 > do operation 66!
Rank 0 > do operation 67!
Rank 0 > do operation 68!
Rank 0 > do operation 69!
Rank 0 > do operation 70!
Rank 0 > do operation 71!
Rank 0 > do operation 72!
Rank 0 > do operation 73!
Rank 0 > do operation 74!
Rank 0 > do operation 75!
Rank 0 > do operation 76!
Rank 0 > do operation 77!
Rank 0 > do operation 78!
Rank 0 > do operation 79!
Rank 0 > do operation 80!
Rank 0 > do operation 81!
Rank 0 > do operation 82!
Rank 0 > do operation 83!
Rank 0 > do operation 84!
Rank 0 > do operation 85!
Rank 0 > do operation 86!
Rank 0 > do operation 87!
Rank 0 > do operation 88!
Rank 0 > do operation 89!
Rank 0 > do operation 90!
Rank 0 > do operation 91!
Rank 0 > do operation 92!
Rank 0 > do operation 93!
Rank 0 > do operation 94!
Rank 0 > do operation 95!
Rank 0 > do operation 96!
Rank 0 > do operation 97!
Rank 0 > do operation 98!
Rank 0 > do operation 99!
Rank 0 > do operation 100!
Rank 1 > Done!
Rank 4 > Done!
Rank 6 > Done!
Rank 3 > Done!
Rank 5 > Done!
Rank 2 > Done!
Rank 7 > Done!

非阻塞式通信通常会结合MPI_Wait()或MPI_Waitall()来使用,这样一方面可以避免阻塞式通信带来的延时,可以直接去执行通信环节之后的与前面通信结果无关的操作,又可以在需要使用到前面通信结果之前调用MPI_Wait()或MPI_Waitall()来阻塞,确保通信的完成。

代码概述:

MPI_Waitall()

这个函数的第三个参数是输出参数,是一个具有与第一个参数指定的元素个数的MPI_Status数组,因此,在不需要的时候,这里需要传递的是预定义常量MPI_STATUSES_IGNORE,而不是MPI_STATUS_IGNORE。

MPI_Request_free()

用于释放掉通常由如上类似的非阻塞函数传递的MPI_Request对象,注意,一旦释放后,将不能使用它传递给Wait函数来进行阻塞。

MPICH笔记(三):数据类型

MPICH笔记(三):数据类型

官方文档索引:

int MPI_Type_free(MPI_Datatype *datatype)

int MPI_Type_indexed(int count, const int *array_of_blocklengths, const int *array_of_displacements, MPI_Datatype oldtype, MPI_Datatype *newtype)

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

int MPI_Type_commit(MPI_Datatype *datatype)

int MPI_Type_vector(int count, int blocklength, int stride,  MPI_Datatype oldtype, MPI_Datatype *newtype)

 

代码:

#include <iostream>
#include "mpi.h"

using namespace std;

int main(void) {
    MPI_Init(nullptr, nullptr);
    int rank;
    int *dat = new int[9];
    MPI_Datatype *myType = new MPI_Datatype();
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Type_vector(3, 1, 3, MPI_INT, myType);
    MPI_Type_commit(myType);
    if (rank == 1) {
        for (int i = 0; i < 9; i++) dat[i] = 0;
        MPI_Recv(dat, 1, *myType, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        printf("Rank 1 >\n");
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (j) putchar(' ');
                printf("%d", dat[i * 3 + j]);
            }
            putchar('\n');
        }
    } else if (rank == 0) {
        for (int i = 0; i < 9; i++) {
            dat[i] = i + 1;
        }
        printf("Rank 0 >\n");
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (j) putchar(' ');
                printf("%d", dat[i * 3 + j]);
            }
            putchar('\n');
        }
        MPI_Send(dat + 1, 1, *myType, 1, 0, MPI_COMM_WORLD);
    }
    MPI_Type_free(myType);
    delete myType;
    delete [] dat;
    MPI_Finalize();
    return 0;
}

运行指令:

mpiexec -n 2 ./文件名

输出信息:

Rank 0 >
1 2 3
4 5 6
7 8 9
Rank 1 >
2 0 0
5 0 0
8 0 0

代码概述:

上面的代码只操作两个进程,进程0生成一个3 * 3的矩阵,并将其第二列发送给进程1,进程1将接收到的这一列放在其自身的第一列上。

MPI_Type_vector()

简单的用来创建新的MPI数据类型的函数,其最后一个参数MPI_Datatype *newtype是要保存新创建好的数据类型的空间的地址。它用一个四元组来描述新的数据类型,依次是新类型包含了几个旧数据类型块、新类型的每个旧数据类型块中要用几个连续的旧类型数据、旧类型数据块的大小、旧类型。

MPI_Type_commit()

用来启用新的数据类型,创建新的数据类型后,需要在使用前将新数据类型的地址传递给这个函数,这个函数会对新构造的数据类型进行分析并在使用新的数据类型进行消息通信前对非连续的数据进行通信优化\(^{[1]}\)。

MPI_Type_free()

用来清除传递给它的地址上存放的新数据类型。

 

 

参考资料:

[1]: 《并行计算的编程模型(Programming Models for Parallel Computing)》第一版,Pavan Balaji [美]、美国阿贡国家实验室编著,张云泉、李士刚、逄仁波、袁良译,机械工业出版社。

MPICH笔记(二):阻塞通信

MPICH笔记(二):阻塞通信

MPICH支持点对点通信,主要使用:

MPI_Send()MPI_Recv(),两个函数一个是发送,一个是接收,官方的文档十分详细,单击前边的这两个函数即可跳转到对应页面。

需要注意的是:MPI_Recv()是阻塞接收函数,保证在接收到消息前不会执行之后的语句;然而,根据文档的描述,MPI_Send()是阻塞发送函数,但是却可能会不发生阻塞,也就是说,函数可能在发送的消息被确认接收前返回。

另一点需要注意的是,因为MPI_Recv()会进行阻塞,所以需要考虑避免死锁现象,即同一时间所有的进程都进行MPI_Recv()操作。如下面的代码将0号进程设置为先发送再接收,而其它进程都是先接收,再发送消息,这样来避免死锁。

代码:

#include <iostream>
#include "mpi.h"

using namespace std;

int main(void) {
    int rank, size;
    int message;
    int from, to;
    MPI_Init(nullptr, nullptr);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);
    from = ((rank - 1) % size + size) % size;
    to = (rank + 1) % size;
    if (rank) {
        MPI_Recv(&message, 1, MPI_INT, from, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        printf("Rank %d > Hello from %d, and the message is %d\n", rank, from, message);
        MPI_Send(&rank, 1, MPI_INT, to, 0, MPI_COMM_WORLD);
    } else {
        MPI_Send(&rank, 1, MPI_INT, to, 0, MPI_COMM_WORLD);
        MPI_Recv(&message, 1, MPI_INT, from, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        printf("Rank %d > Hello from %d, and the message is %d\n", rank, from, message);
    }
    MPI_Finalize();
    return 0;
}

 

输出:

Rank 1 > Hello from 0, and the message is 0
Rank 2 > Hello from 1, and the message is 1
Rank 3 > Hello from 2, and the message is 2
Rank 4 > Hello from 3, and the message is 3
Rank 5 > Hello from 4, and the message is 4
Rank 6 > Hello from 5, and the message is 5
Rank 7 > Hello from 6, and the message is 6
Rank 0 > Hello from 7, and the message is 7

注:这里一共开启了8个进程。

 

代码概述:

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

前三个参数是表述消息信息的三元组,依此是存储位置、数量(在接收函数中,这个参数表示最大可能的数据长度,需要保证这个大小要不小于实际传递信息的小才可以成功接收数据)、单位大小(数据类型)。source是来源进程的rank,如果不限定来源,可以使用预定义量MPI_ANY_SOURCE。tag表示消息的编号,因为可能有多个来自同一进程的不同作用的信息,如果不限制tag值,可以使用预定义常量MPI_ANY_TAG。status参数是用来获取消息的状态信息,如果不需要关于此消息的状态信息,可以使用预定义常量MPI_STATUS_IGNORE。

关于MPI_Status的详情见:https://docs.microsoft.com/en-us/message-passing-interface/mpi-status-structure

int MPI_Send(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)

前三个参数仍然是描述信息的三元组,其中count需要是传输信息的实际数量。而后dest是接收进程在comm通信子内的rank。tag要与接收者设置的tag一致,注意,不可以使用MPI_ANY_TAG。

注:关于阻塞,两个阻塞式通信函数都会一直阻塞直到内存申请成功,关于具体细节,此处占坑,日后补充。

MPICH笔记(一):通信子

MPICH笔记:简单使用MPICH(一)

使用MPICH需要导入的头文件:

mpi.h

MPICH函数的命名规则:

MPI_Xxxx_xxx…即以MPI开头第二个单词的首字母大写,后面单词的首字母全部小写,单词之间用_隔开。

MPICH预定义常量命名规则:

MPI_XXXX_XXXX…即以MPI开头,每个字母都大写。

 

示例代码:

#include "mpi.h"
#include <cstdio>
#include <iostream>

using namespace std;

int main(int argc, char **argv) {
    int wrank, wsize, mrank;
    MPI_Init(&argc, &argv);
    /*
    * MPI_COMM_WORLD 和 MPI_COMM_SELF 是两个预定义的通信子(或者叫通信域, communicator),
    * MPI_COMM_WORLD 包含了所有进程,
    * MPI_COMM_SELF 仅包含自身进程。
    */
    MPI_Comm_rank(MPI_COMM_WORLD, &wrank);
    MPI_Comm_size(MPI_COMM_WORLD, &wsize);
    MPI_Comm_rank(MPI_COMM_SELF, &mrank);
    printf("World rank %d, world size %d, self rank %d\n", wrank, wsize, mrank);
    MPI_Finalize();
    return 0;
}

输出:

World rank 2, world size 4, self rank 0
World rank 0, world size 4, self rank 0
World rank 3, world size 4, self rank 0
World rank 1, world size 4, self rank 0

注:输出顺序可能有所不同,因为是由多个进程同时执行,执行先后会有差异。

代码简述:

通信子(通信域,communicator):

MPI的通信子包含一组进程以及一个(隐藏的)通信文本,通信文本的作用在于保证消息和库的一致性\(^{[1]}\)。

MPICH有两个预定义的communicator,分别是MPI_COMM_WORLD和MPI_COMM_SELF,前者包含所有进程,后者只包含自身进程。

MPI_Comm_rank(MPI_Comm, int *)

获得进程在通信子comm中的编号。保存在int *参数所指向的空间中。

MPI_Comm_size(MPI_Comm, int *)

获得通信子comm中进程个数,保存在int *参数所指向的空间中。

MPI_Init(int *, char **)

初始化。

MPI_Finalize()

结束。

注:大多数MPI函数都需要在MPI_Init(int *, char **)之后,且在MPI_Finalize()之前使用。

 

手动创建通信子:

#include "mpi.h"
#include <iostream>

using namespace std;

int main(void) {
    int mrank, wrank, wsize;
    MPI_Comm *evenAndOddComm = new MPI_Comm();
    MPI_Init(nullptr, nullptr);
    MPI_Comm_rank(MPI_COMM_WORLD, &wrank);
    MPI_Comm_size(MPI_COMM_WORLD, &wsize);
    /*
    * create a new commonicator
    */
    if (wrank & 1) MPI_Comm_split(MPI_COMM_WORLD, 1, wrank >> 1, evenAndOddComm);
    else MPI_Comm_split(MPI_COMM_WORLD, 2, wrank >> 1, evenAndOddComm);
    MPI_Comm_rank(*evenAndOddComm, &mrank);
    printf("Message from world rank %d -> world size %d, myColor is %d, and mrank is %d\n",
        wrank, wsize, wrank & 1 ? 1 : 2, mrank);
    MPI_Comm_free(evenAndOddComm);
    delete evenAndOddComm;
    MPI_Finalize();
    return 0;
}

代码简述:

MPI_Comm_split(MPI_Comm, int color, int key, MPI_Comm *out)

得到comm中进程的划分,具有相同color值的通信子会被划分到同一个新的通信子中,out指向的空间存放新的通信子相关的信息,key将是调用该函数的进程在新的通信子中的序号。特别地,color可以指定为MPI_UNDEFINED,但是这样调用函数后得到的out将不可用,如调用MPI_Comm_rank()函数时会产生异常。

MPI_Comm_free(MPI_Comm *)

释放通信子。

 

参考资料:

[1]: 《并行计算的编程模型(Programming Models for Parallel Computing)》第一版,Pavan Balaji [美]、美国阿贡国家实验室编著,张云泉、李士刚、逄仁波、袁良译,机械工业出版社。

【笔记】OpenMP常用内容小结

【笔记】OpenMP常用内容小结

记录比较杂乱,计划分点记录,每点之间如果没有特别说明,则没有关联。

1.使用预定义宏_OPENMP来判断编译时是否启用了OpenMP,只要使用#ifdef _OPENMP即可。

2.OpenMP默认是不支持嵌套多线程的,实际上,只在一些新版本中才提供这一机制,并且,需要手动开启,可以调用omp_set_nested()函数来开启。

关于常用并行算法范式的笔记

关于常用并行算法范式的笔记

阶段并行:同步迭代、异步迭代。

分治:将分治后的数据进行分发,之后再分治,再派发。

宏流水:流水线一样,每个节点只执行其中几个环节。

主从结构:有一个“最高”的管理节点,负责指挥、分发、汇总,其它节点之间不通信,各自完成自身任务。【性能常受限于这个“最高”结点】

工作池:多用于共享内存模型,内存就像是一个队列,每次执行完一个任务后结果会抛入池中,各节点从中拿任务,之后在放回新任务。

参考《超算竞赛导引》

关于超算发展与架构分类的笔记

关于超算发展与架构分类的笔记

历史:

  1. SIMD (Single Instruction Multiple Data) 阵列处理机。【e.g. ILLIAC-IV阵列处理机,约1】
  2. 具有流水结构的向量机。
  3. MIMD (Multiple instruction multiple data) 共享存储多处理机系统。
  4. MPP系统。【用小节点、非一致性存储访问结构(non-Uniform memory access, NUMA)、超节点或向量加超标量的方法】
  5. 集群系统。【早期大多同构,当今逐渐转向异构方式,主要由CPU+GPU或CPU+MIC (many integrated core)组成】

 

架构分类:

 

参考:《超算竞赛导引》——《超算竞赛导引》编写组

衡量计算性能的单位——FLOPS

衡量计算性能的单位——FLOPS

计算方式:

\(FLOPS = sockets \times \frac{cores}{socket} \times \frac{cycles}{second} \times \frac{FLOPs}{cycle}\)

FLOPS is the abbreviation of  “floating point operations per second”. Therefore, sometimes it also is written as FLOP/s.

The “sockets” is the number of network sockets, which are the endpoint to sending or receiving message from others in the network.

And the “cores / socket” is the number of cores per socket.

The “cycles / second”, as we all know, is clock rate.

The “FLOPs / cycle” is the times of floating point operations per clock rate.

单位:

\(1 MFLOPS = 1e6 FLOPS\),

\(1 GFLOPS = 1e9 FLOPS\),

\(1 TFLOPS = 1e12 FLOPS\),

\(1 PFLOPS = 1e15 FLOPS\),

\(1 EFLOPS = 1e18 FLOPS\).

参考了:

https://en.wikipedia.org/wiki/FLOPS

https://en.wikipedia.org/wiki/Clock_rate