0%

MPI并行计算——使用篇

从用户(使用者)的角度介绍消息传递模型代表——MPI。

SPMD编程风格

  • SPMD(Single Program Multiple Data)即单程序多数据,允许任务分支执行用户指定的程序片段。这已经成为分布式编程的通用范式。
  • 例如:进程号为0的进程完成A部分代码,为1的进程执行B部分代码,……,但各部分代码写在同一份程序源文件中。
  • 注意和SIMD区别:SIMD主要是描述指令集和硬件体系结构的。

MPI基础

  • 典型的MPI程序结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "mpi.h"
int main(int argc, char* argv[])
{
// do sth (serial)
// ...
int a;

// Initialize MPI environment
// should be called before any other MPI functions (except MPI_INITALIZED(flag))
MPI_Init(&argc, &argv);

// parallel computing with MPI functions
int rc = MPI_Xxx(args);

// Finalize MPI environment
MPI_Finalize();

// do sth (serial)
// ...
}
  • 注意:这是十分典型的SPMD。各进程中的变量即使有相同的名称(例如上面的a),也是完全不相干的。其中一个进程中的a变量变化不会影响其他进程。
  • 初始化也可以写成MPI_Init(NULL,NULL);
  • 通常在MPI_Init()后都要获取进程数和当前进程ID(MPI接口中称为rank)
    • 其中的MPI_COMM_WORLD是MPI预先为所有进程定义的通信域(Communicator),详细解释见后。
    • 在同一个中的进程的rank范围为0,1,...,N10,1,...,N-1,其中NN是该组的大小(注意组和通信域是不相同的两个概念)
1
2
3
4
int rank, size;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // process ID
MPI_Comm_size(MPI_COMM_WORLD, &size); // #process
  • 一定要有MPI_Finalize,否则结果是不可预知的。一种可能是某个进程错误退出:
1
2
3
--------------------------------------------------------------------------
mpirun has exited due to process rank 0 with PID 0 on node LAPTOP-XXX exiting improperly. There are three reasons this could occur:
...
  • MPI也有C/C++/Fortran多种版本
  • 进程数控制:不在程序中指定,而是通过命令行参数设置。
    • 直接调用,指定进程数为4:$ mpirun -n 4 ./example
    • 使用SLURM调度系统时则可以用类似方式指定:$ srun -n 4 ./example
    • 因此,MPI程序不能预设特定进程数,而必须假定能在任何进程数下完成计算。

注意:在默认情况下OpenMPI的slots数和计算机cpu的核心数相同。例如笔者的笔记本为4核,则mpirun -n 4可运行,进程数超过4则会报错:

1
2
3
-----------------------------------------------------------------------
There are not enough slots available in the system to satisfy the 6 slots that were requested by the application:
...

如果希望设定更高进程数,可以在mpirun后加--oversubscribe项。但除非有充分的理由和实验证据,并行单元的数量不要超过系统的物理核心数。可能的原因有:oversubscribe后缓存不命中概率上升、上下文切换浪费资源等。

MPI通信

  • 主要有两种方式:点对点通信、集合通信

点对点通信

阻塞式

  • MPI_Send(buffer,count,type,dest,tag,comm)
    • 待发送的数据存放在buffer指向的空间中,长度为count,类型为type
    • 关于type:MPI预定义了一堆类型(如MPI_INTMPI_LONG,…),建议使用这个类型而非C的原生类型
    • dest就是接受进程在comm通信域中的rank(进程ID),comm默认为MPI_COMM_WORLD
    • tag:便于区分多个消息
  • MPI_Recv(buffer,count,type,sorce,tag,comm,status)
    • 接收量(count参数,其实就是buffer的大小)可以大于等于发送的量,但小于时就会溢出报错。count可以是0。
    • status包含消息大小之类的信息。
  • 通配符:
    • sourceMPI_ANY_SOURCE
    • tagMPI_ANY_TAG
    • 注意发送时必须指定dest,没有通配符。
  • 小心死锁

非阻塞式

  • MPI_Isend,MPI_Irecv

    • MPI_Isend返回时不保证消息一定成功发送了,只表示消息可以被发送。
    • MPI_Irecv返回时不保证已经接收相应消息,只表示符合条件的消息可被接收。
    • 相比阻塞通信能提高使用效率(计算通信重叠),实际提升情况和实现有关。
    • 参数:多出一个request,返回一个句柄,不表示发送成功,之后可以通过检视该句柄MPI_Wait(request,status)来确定是否对面收到。
  • MPI_Wait

    • 一直等待非阻塞通信句柄request对应的非阻塞通信完成后返回。
    • 其实MPI_Send = MPI_Isend + MPI_Wait
  • MPI_Waitall

    • 等待给定非阻塞通信句柄表中所有通信完成后才返回。

案例:梯形法算数值积分

  • 并行化策略:
    • 任务划分到进程——每个进程计算若干梯形的面积
    • 汇总结果——0号进程MPI_Recv收取并累加其他进程的结果
  • 可以自己去写一写PPT的示例代码
  • 这个案例就是经典的Map reduce
  • 用集合通信能做更简洁的实现

集合通信

  • Map reduce的一种实现

  • 很多集合通信API内部也是用点对点实现的

  • All前缀:数据转发到所有进程

  • v后缀:每个进程处理的数据量可以不同

  • MPI_Bcast

    • 把buffer处count个数据从root向组内其他进程中的同名变量发送(或称同步)
    • 如每个进程各有一个value,则buffer处填&value即可同步所有进程value变量的值。
  • MPI_Reduce:归约,将结果汇总到同一进程中

    • op:操作(MPI_XX格式,如MPI_SUM等,也可自定义)
    • 非dest进程就不用专门alloc一块recvbuf了,只有rank=dest的进程需要真地为recvbuf分配内存空间。
  • MPI_Scan:前缀归约

    • 按照rank顺序逐个执行操作(即前缀归约)
    • 这时候各进程都要alloc一个recvbuf了
  • MPI_scatter

    • 类似split,一整段数据切分为几段,每个进程分配一段
  • MPI_Gather:scatter的反向操作

  • MPI_Allgather:gather后再bcast(因此没有dest)

  • MPI_Allreduce:reduce后再bcast

  • MPI_Alltoall:相当于n次gather:

  • Cornell Virtual Workshop: All to All

  • MPI_Barrier(comm):同步,阻塞到组内所有进程都执行到相同的barrier为止

  • 通信域(comm参数)

    • 和组关联(不过这两个概念不同),只能域内进程相互通信
    • 通常直接所有进程公用一个通信域即可

MPI-IO

  • 不用fopen而用MPI_File_open(),如果要充分利用并行文件系统(当然课程中无所谓)
  • 和文件系统配合,合并打开文件的请求,则文件只会开一次,共享句柄。各进程可以同时读写。
  • 独立I/O:和普通读写类似,各进程单独请求,适合大文件IO
  • 协调I/O
    • 机制和上面讲的一样,MPI内部会有一个buffer
    • 接口后缀_all
    • 可以利用并行文件系统优化,适合小文件IO
  • 有专门的MPI_File

MPI编译

  • 安装(Ubuntu,OpenMPI):sudo apt install openmpi-bin openmpi-doc libopenmpi-dev,如果没装全是没法编译的(doc除外)。

  • 编译:mpicc/mpicxx ...(其实是gcc/g++的wrapper)

  • 也可以用IntelMPI,但二者不能混用

多种实现

  • MPICH:官方版本,但性能差一点
  • Intel MPI之类:基本上都是基于MPICH开发
  • OpenMPI:性能也不错,用得多

欢迎关注我的其它发布渠道