导言
all-to-all 要让每个参与者给其他参与者发送数据,为什么还要分 group?分组可以控制同时争用有限资源的请求,让通信更接近硬件能稳定处理的速度;但它是否能“防止阻塞”,取决于分的是什么,以及阻塞发生在哪一层。本文从四个 rank 的调度例子出发,解释限流、错峰、接收许可和拓扑分层,再区分 NCCL group 的并发推进语义。目标是看懂通信实现的设计动机,并知道怎样验证收益;文中的容量与规模例子均为推导,没有集群性能实测。
虽然MPI属于OSI参考模型的第5层和更高层,但实现可以覆盖大多数层,其中在传输层中使用套接字和传输控制协议(TCP)。
MPI hardware research focuses on implementing MPI directly in hardware, for example via processor-in-memory, building MPI operations into the microcircuitry of the RAM chips in each node. By implication, this approach is independent of language, operating system, and CPU, but cannot be readily updated or removed.
MPI硬件研究的重点是直接在硬件中实现MPI,例如通过内存处理器,将MPI操作构建到每个节点中的RAM芯片的微电路中。通过暗示,这种方法独立于语言、操作系统和CPU,但是不能容易地更新或删除。
Another approach has been to add hardware acceleration to one or more parts of the operation, including hardware processing of MPI queues and using RDMA to directly transfer data between memory and the network interface controller(NIC 网卡) without CPU or OS kernel intervention.
另一种方法是将硬件加速添加到操作的一个或多个部分,包括MPI队列的硬件处理以及使用RDMA在存储器和网络接口控制器之间直接传输数据,而无需CPU或OS内核干预。
进程间通信都是Inter-process communication(IPC)的一种。常见有如下几种:
|mkfifo,具有p的文件属性线程共享存储器编程模型(如Pthreads和OpenMP)和消息传递编程(MPI/PVM)可以被认为是互补的,并且有时在具有多个大型共享存储器节点的服务器中一起使用。
后四个是MPI-2独有的
1 | #include <unistd.h> |
输出X个当前机器hostname
mpirun -np 6 -machinefile ./machinelist ./a.out 即可多节点执行。
MPI_Finalize()之后 ,MPI_Init()之前
https://www.open-mpi.org/doc/v4.0/man3/MPI_Init.3.php
不同的进程是怎么处理串行的部分的?都执行(重复执行?)。执行if(rank=num),那岂不是还要同步MPI_Barrier()。
而且写同一个文件怎么办?
MPI的两种最基本的并行程序设计模式 即对等模式和主从模式。
对等模式:各个部分地位相同,功能和代码基本一致,只不过是处理的数据或对象不同,也容易用同样的程序来实现。
主从模式:分为主进程和从进程,程序通信进程之间的一种主从或依赖关系 。MPI程序包括两套代码,主进程运行其中一套代码,从进程运行另一套代码。

圈收缩(cycle shrinking)-此变换技术一般用于依赖距离大于1的循环中,它将一个串行循环分成两个紧嵌套循环,其中外层依然串行执行,而内层则是并行执行(一般粒度较小)
https://shaojiemike.notion.site/41b9f62c4b054a2bb379316f27da5836

MPI_PACKED预定义数据类型被用来实现传输地址空间不连续的数据项 。
1 | int MPI_Pack(const void *inbuf, |

The input value of position is the first location in the output buffer to be used for packing. position is incremented by the size of the packed message,
and the output value of position is the first location in the output buffer following the locations occupied by the packed message. The comm argument is the communicator that will be subsequently used for sending the packed message.
1 | //Returns the upper bound on the amount of space needed to pack a message |

例子:
这里的A+i*j应该写成A+i*2吧???
来定义由数据类型不同且地址空间不连续的数据项组成的消息。
1 | //启用与弃用数据类型 |


1 | //成块的相同元素组成的类型,块长度和偏移由参数指定 |

1 | //由不同数据类型的元素组成的类型, 块长度和偏移(肯定也不一样)由参数指定 |

MPI_Cart_create
确定了虚拟网络每一维度的大小后,需要为这种拓扑建立通信域。组函数MPI_Cart_create可以完成此任务,其声明如下:
1 | // Makes a new communicator to which topology拓扑 information has been attached |
1 | int MPI_Comm_create(MPI_Comm comm, MPI_Group group, MPI_Comm * newcomm) |




特殊的函数
1 | int MPI_Sendrecv(const void *sendbuf, int sendcount, MPI_Datatype sendtype, |
特别适用于在进程链(环)中进行“移位”操作,而避免在通讯为阻塞方式时出现死锁。
There is also another error. The MPI standard requires that the send and the receive buffers be disjoint不相交 (i.e. they should not overlap重叠), which is not the case with your code. Your send and receive buffers not only overlap but they are one and the same buffer. If you want to perform the swap in the same buffer, MPI provides the MPI_Sendrecv_replace operation.
1 | //MPI标准阻塞通信函数,没发出去就不会结束该命令。 |

可能大家会想到这会死锁,如下图:
但是实际情况可能并不会死锁,这与调用的MPI库的底层实现有关。

MPI_Send将阻塞,直到发送方可以重用发送方缓冲区为止。当缓冲区已发送到较低的通信层时,某些实现将返回给调用方。当另一端有匹配的MPI_Recv()时,其他一些将返回到呼叫者。
但是为了避免这种情况,可以调换Send与Recv的顺序,或者**使用MPI_Isend()或MPI_Issend()**代替非阻塞发送,从而避免死锁。
1 | /* |
一个进程组中的所有进程都参加的全局通信操作。
实现三个功能:通信、聚集和同步。

1 | //将一个进程中得数据发送到所有进程中的广播函数 |
注意data_p在root 或者scr_process进程里是发送缓存也是接收缓存,但是在其余进程里是接收缓存。
MPI_Scatter?



1 | int MPI_Allgather(void * sendbuff, int sendcount, MPI_Datatype sendtype, |
number of elements received from any process (integer)
MPI聚合的功能分三步实现
MPI提供了两种类型的聚合操作: 归约和扫描。
1 | int MPI_Reduce( |



1 | int MPI_Op_create(MPI_User_function *function, int commute, MPI_Op *op) |
用户自定义函数 functiontypedef void MPI_User_function(void *invec, void *inoutvec, int *len, MPI_Datatype *datatype)
1 | for(i=0;i<*len;i++) { |
必须具备四个参数:
也可以认为invec和inoutvec 是函数中长度为len的数组, 归约的结果重写了inoutvec 的值。
1 | /* |

MPI_Group https://www.rookiehpc.com/mpi/docs/mpi_group.php
并行IO文件
1997年推出了MPI的最新版本MPI-2
MPI-2加入了许多新特性,主要包括
数据发送和收集

https://blog.csdn.net/susan_wang1/article/details/50033823
https://blog.csdn.net/u012417189/article/details/25798705
是否死锁: https://stackoverflow.com/questions/20448283/deadlock-with-mpi
https://mpitutorial.com/tutorials/
http://staff.ustc.edu.cn/~qlzheng/pp11/ 第5讲写得特别详细
1 | MPI_Init(&argc, &argv); |
StackOverflow的回答是,Init在调用过程中初始化MPI库,并且在进程间建立通讯和编号。
知乎的回答: OpenMPI会在调用MPI_Init时按照你传递给mpirun的指令新建进程,而你传递给MPI_Init的参数,会被传递给新建的进程。
这似乎在暗示,两个进程不是同时产生和运行的。
有顺序的观点是不成立的
即使有顺序 malloc的时间也没这么长。
难道是malloc的数据需要MPI_Init复制一遍?
简单将MPI_Init提前到最开始,时间也基本没变,也不对。

如果单独写一个只有MPI_Init的程序,IntelMPI还是要耗时800ms
1 | ipcc22_0029@ln121 ~/slurm/MPIInit [11:42:32] |
以IPCC2022初赛的北京超算云 AMD机器举例
| mpirun的选择 | mpi版本 | GCC或者ICC的选择版本 | 超算运行 | MPI_Init时间(ms) |
|---|---|---|---|---|
| IntelMPI | mpi/intel/2022.1 | gcc/10.2.0 | 只能sbatch,不能srun | 1282.24 ~ 1678.59 |
| OpenMPI | mpi/openmpi/4.1.1-gcc7.3.0 | 2706ms~3235ms | ||
| MPICH | mpich/3.1.4-gcc8.1.0 | 17ms | ||
| mpich/3.4.2 | gcc/10.2.0 | 107ms |
需要export I_MPI_PMI_LIBRARY=libpmi2.so
设置这个Intel mpi 1200 -> 1100
1 | export PMI_TIME=1 |
实在是弄不懂,为什么不同的实现,时间差别这么大。可能慢是因为额外的通路设置,是为了之后的快速传输??
3.1.4的安装选项也看不到
1 | > mpiexec --version |
暂无
Python代码的执行由Python虚拟机(解释器)来控制。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。所以就会出现尽管你设置了多线程的任务,但是只能跑一个的情况。
但是I/O密集的程序(爬虫)相对好一点,因为I/O操作会调用内建的操作系统C代码,所以这时会释放GIL锁,达到部分多线程的效果。
通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
IPCC Preliminary SLIC Optimization 5: MPI + OpenMP
| 技术路线 | 描述 | 总时间 | 加速比 | 备注 |
|---|---|---|---|---|
| Baseline | 串行程序 | 161.7s s | 1 | |
| more3omp | 前面都是可以证明的有效优化 omp_num=32 | 14.08s | ||
| more3omp | 前面都是可以证明的有效优化 omp_num=64 | 11.4s | ||
| deletevector | 把sz大小的3个vector,移到全局变量,但是需要提前知道sz大小/声明一个特别大的 | 10.64s | 可以看出写成全局变量也不会影响访问时间 | |
| enforce_Lscan | IPCC opt 4 | 8.49s | 19 | |
| enforce_Lscan_MPI_intel | intel icpc | 3.8s | 42.36 | |
| Baseline2-max ppm | 1.2GB ppm 10*1024*40*1024 | 928s | ||
| enforce_Lscan | Baseline2 | 43.79s | 21.2 | |
| enforce_Lscan_MPI_intel | intel icpc + 双节点两个时间 + MPI(DoRGBtoLABConversion) | 18.8s / 20s | 46.4 | |
| enforce_Lscan_intel | intel icpc + 单节点 | 15.8s | 58.74 | MPI(DoRGBtoLABConversion)负优化了2s |
| manualSIMD | 13.9s | |||
| stream | 13.6s | |||
| vec2mallocOMP | 11.0s | |||
| mmap | 10.6s | |||
| + -O3 | enforce_Lscan_intel | 16.2s | ||
| + -xHost | 结果不对 | 17.8s | ||
| -Ofast | 16.9s | |||
| -ipo | 15.9s | |||
| -O3 -ipo | 16.8s | |||
| -O3 -march=core-avx2 -fma -ftz -fomit-frame-pointer | 16.0s | |||
| g++ suggested options | -O3 -march-znver1 -mtune=znver1 -fma -mavx2 -m3dnow -fomit-frame-pointer | 18.1s | ||
| g++ suggested options2 | -O3 -march-znver2 -mtune=znver2 -fma -mavx2 -m3dnow -fomit-frame-pointer | 19.79s | ||
| g++ -Ofast | 16.9s | |||
| aocc -Ofast | 16.3s | |||
| aocc suggested options | 16.2s |
由于是打算两节点两进程MPI,虽然没有OpenMP的共享内存,但是也希望通信能少一点。
下面关于同步区域的想法是错误的:
因为中心点移动会十分不确定,所以全部同步是最好的。



用MPI_Send写,但是一开始没注意是阻塞的,但是为什么这么慢呢?
慢了10~20倍猜测:

好像是openmp没正常运行omp_num的值为 1,32,64时间都一样。感觉是混合编程的编译问题, 而且好像是假Openmp并行,哪里有锁的样子。突然想起来,Quest的混合变成cmake需要打开multthread类似的支持,但是这里并没用。
好像也不是mpi_init_thread的问题

果然有奇效。(结果是对的,后面我没截图了)。看到这里,可能你会觉得这个问题是OpenMPI有地方不支持openmp。但是后面有神奇的事情,如果NODELIST是fa,而不是fb就不能跑,会直接卡住。😰
首先没找到官方手册说明不同,然后研究一下这两个分区的不同。好吧从IB,cpu,内存都没区别。
限制nodelist再跑一遍。
加上打印时间,用fb分区
这个问题又没有了,但是fa分区由于经常跑可能会热一些。
由于时间已经进5s了。所以我们需要更大的例子,再讨论2节点的开销收益,之前的例子是256034000。
这里生成了1024040960的ppm.再大ppm程序的数组都申请不到栈空间了,需要重新数据结构。
重跑当前最快的enforce_Lscan
icpc + enforce_Lscan_MPI(DoRGBtoLABConversion)
icpc + enforce_Lscan
g++ suggested options
icpc + manualSIMD + lessLscan
icpc + manualSIMD + LscanSimple
icpc + manualSIMD + LscanSimple + stream
icpc + manualSIMD + LscanSimple + stream + mallocOMPinit
icpc + manualSIMD + LscanSimple + stream + mallocOMPinit + mmap
icpc + manualSIMD + LscanSimple + stream + mallocOMPinit + mmap + unrollLoop
https://www.bilibili.com/video/BV1a44y1q782 58mins-58min50s
暂无
无
需要:
网上的实现c++ : https://zhuanlan.zhihu.com/p/95819747


不知道 #pragma omp parallel for num_threads(ndata) schedule(dynamic)行不行
这个动态调度,和openmp的线程池的概念,让我感觉应该是有线程动态调度池的概念的,因为只要有个for子句加任务的api。但是for指令在进行并行执行之前,就需要”静态“的知道任务该如何划分。
for和sections指令的”缺陷“:无法根据运行时的环境动态的进行任务划分,必须是预先能知道的任务划分的情况。
所以OpenMP3.0提供task指令,主要适用于不规则的循环迭代和递归的函数调用。OpenMP遇到了task之后,就会使用当前的线程或者延迟一会后使用其他的线程来执行task定义的任务。
1 | #pragma omp parallel num_threads(2) |
另一个例子,DoSomething(),导致p.n可能会增加。taskwait是为了防止某个task导致p.n增加了,但是for循环已经结束的情况。
1 | #pragma omp single |
对于问题的修改(还没测试)
1 | int count(1); |
但是中间的if判断以及内部入队列,需要原子操作(xvec写入x时,别的线程count++了)。这就属于串行BFS的局限性了,导致并行不起来。
python的多进程里有动态进程管理
1 | from mpi4py import MPI |
我感觉,意义在于对于完全不相关的,或者没有顺序关系的任务,可以用池调度来并行。
实现每个线程执行完全不同的任务
1 | #include <iostream> |
当然可以根据 num_threads 和 omp_get_thread_num()实现不同线程执行完全不同类型任务
1 | #pragma omp parallel num_threads(2) |
也可以来实现二分线程池,来执行两个任务
1 | void do_long(int threads) { |
openmp 对不同的子句的关系种类没弄清。
暂无
对于for循环次数增加的情况,这么处理呢。
OpenMP由于是fork/join结构,fork的线程数可以一开始设置,但是for循环任务总数是一开始固定的吗?还是可以中途增加,