从用户(使用者)的角度介绍消息传递模型代表——MPI。
SPMD编程风格
- SPMD(Single Program Multiple Data)即单程序多数据,允许任务分支执行用户指定的程序片段。这已经成为分布式编程的通用范式。
- 例如:进程号为0的进程完成A部分代码,为1的进程执行B部分代码,……,但各部分代码写在同一份程序源文件中。
- 注意和SIMD区别:SIMD主要是描述指令集和硬件体系结构的。
MPI基础
- 典型的MPI程序结构
1 |
|
- 注意:这是十分典型的SPMD。各进程中的变量即使有相同的名称(例如上面的
a
),也是完全不相干的。其中一个进程中的a
变量变化不会影响其他进程。 - 初始化也可以写成
MPI_Init(NULL,NULL)
; - 通常在
MPI_Init()
后都要获取进程数和当前进程ID(MPI接口中称为rank)- 其中的
MPI_COMM_WORLD
是MPI预先为所有进程定义的通信域(Communicator),详细解释见后。 - 在同一个组中的进程的rank范围为,其中是该组的大小(注意组和通信域是不相同的两个概念)
- 其中的
1 | int rank, size; |
- 一定要有
MPI_Finalize
,否则结果是不可预知的。一种可能是某个进程错误退出:
1 | -------------------------------------------------------------------------- |
- MPI也有C/C++/Fortran多种版本
- 进程数控制:不在程序中指定,而是通过命令行参数设置。
- 直接调用,指定进程数为4:
$ mpirun -n 4 ./example
- 使用
SLURM
调度系统时则可以用类似方式指定:$ srun -n 4 ./example
- 因此,MPI程序不能预设特定进程数,而必须假定能在任何进程数下完成计算。
- 直接调用,指定进程数为4:
注意:在默认情况下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_INT
,MPI_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
包含消息大小之类的信息。
- 接收量(
- 通配符:
source
:MPI_ANY_SOURCE
tag
:MPI_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分配内存空间。
- op:操作(
-
MPI_Scan
:前缀归约- 按照rank顺序逐个执行操作(即前缀归约)
- 这时候各进程都要alloc一个recvbuf了
-
MPI_scatter
- 类似split,一整段数据切分为几段,每个进程分配一段
-
MPI_Gather
:scatter的反向操作 -
MPI_Allgather
:gather后再bcast(因此没有dest) -
MPI_Allreduce
:reduce后再bcast -
MPI_Alltoall
:相当于n次gather: -
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:性能也不错,用得多