一、个人信息
条目
信息
姓名
Thysrael
学号
xxxxxxx
学院
xxxxxx
二、题目分析
本题是让多个进程依次 接收和发送信息,最终形成一个消息传递的闭环。
在程序设计方面,采用不太明显的主从模式 ,第 0 个进程作为主进程,首先发送信息,然后接收。而对于从进程,首先接受信息,其次发送信息,最后循环时间等信息由主进程进行汇总统计。信息考虑到必须依次传递,所以在消息传递的过程中采用阻塞传递的方式,这样可以保证顺序的依次执行。
三、代码实现
3.1 多种语言
采用 CPP 和 C 实现。采用两种语言分别实现的原因是验证 “是一个跨语言的通讯协议” 的观点。最终两种程序都可以在本地和实验平台进行运行。
在本地的时候,我采用的是 mpicc, mpic++ 两款编译器,具体的编译指令如下
mpic++ -o ring ./ring.cpp mpicc -o ring ./ring.c
运行指令都是相同的,即
在计算平台上面,编译采用的编译器都是 mpiicc 可以编译两种语言。运行采用的是 mpiexe ,据我的调研,mpiexe, mpirun 没有本质区别。
后来了解到 python 也有很好的库用于实现 MPI,所以又试了试 python 版的 HelloWorld 程序,如下所示
from mpi4py import MPIcomm = MPI.COMM_WORLD print ("Hello from rank %d out of %d !" %(comm.rank, comm.size))
在终端中运行
mpirun -np 6 python hello.py
结果如图所示
3.2 计时的实现
在 ring 中的每个节点基本上是等价的,在一次循环中,每个节点从发送信息到接受信息的时间间隔是一样的,所以可以任选一个节点进行时间的测量,为了方便,选择了 0 号进程。实现如下
double start_time = MPI_Wtime ();double end_time = MPI_Wtime ();double time_gap = end_time - start_time;
为了避免偶然因素,所以进行了多轮的测量,最后累加起来求平均值,所以在多轮测量的外部,有一个变量负责记录总的时间间隔,最后计时的架构如下所示
int main (int argv, char *argc[]) { ... double total_time = 0 ; for (int i = 0 ; i < ROUND; i++) { MPI_Barrier (MPI_COMM_WORLD); if (my_id == 0 ) { double start_time = MPI_Wtime (); .... double end_time = MPI_Wtime (); double time_gap = end_time - start_time; total_time += time_gap; ... } else { .... } } if (my_id == 0 ) { printf ("total time is %lf\n" , total_time); printf ("total round is %d\n" , ROUND); printf ("average time is %lf\n" , total_time / ROUND); } ... return 0 ; }
3.3 环传递的实现
正如第二章的分析,我们需要用阻塞消息传递 来实现环形传递,所以我们要使用 MPI_Send 和 MPI_Recv 两个函数来实现。
对于主进程,需要先发出信息,然后接收信息。而对于从进程,要先接收信息,然后发送信息,具体如下所示
send_message_to_next (my_id, size, i);recive_message_from_pre (my_id, size, i);recive_message_from_pre (my_id, size, i);send_message_to_next (my_id, size, i);
在代码细节方面,用两个函数封装了 MPI_Send, MPI_Recv 操作,如下所示
void send_message_to_next (int my_id, int size, int round) { char message[MESSAGE_LEN]; sprintf (message, "\n%d say 'Hi~' to %d" , my_id, next (my_id, size)); if (!round) { printf ("%s\n" , message); } MPI_Send ( message, MESSAGE_LEN, MPI_CHAR, next (my_id, size), MESSAGE_TAG, MPI_COMM_WORLD ); } void recive_message_from_pre (int my_id, int size, int round) { char message[MESSAGE_LEN]; MPI_Status status; MPI_Recv ( message, MESSAGE_LEN, MPI_CHAR, pre (my_id, size), MESSAGE_TAG, MPI_COMM_WORLD, &status ); if (!round) { printf ("error is %x\n" , status.MPI_ERROR); printf ("%d knows %d smiled for her, however...\n" , my_id, status.MPI_SOURCE); } }
3.4 MPI_Status
MPI_Status 作为信息接收者 可以利用的一个传递信息,用于显示这次消息传递的各个参数。如果没有使用需求的话,可以直接使用 mpi.h 中定义的宏 *MPI_STATUS_IGNORE 来占位。
本着给了就要用的思想,我对于 MPI_Status 做了初步调研,其在 mpi.h 库中如下
typedef struct _MPI_Status { int count; int cancelled; int MPI_SOURCE; int MPI_TAG; int MPI_ERROR; } MPI_Status, *PMPI_Status;
其中我主要利用了 MPI_SOURCE, MPI_ERROR 两个
printf ("source is %d\n" , status.MPI_SOURCE); printf ("error is %x\n" , status.MPI_ERROR);
其中 SOURCE 正常工作,但是 ERROR 作为错误码,即使正常运行,但是也会出现随机数(可能也不那么随机),如下所示
我找不到具体原因,只找到相关文献说 STATUS_ERROR 在阻塞通信中没有意义
MPI_ERROR is the member of the MPI_Status structure that contains the error code of the corresponding receive operation. Although in practice it has no use for a blocking receive (MPI_Recv ) since the error code is returned directly by the routine, it does have uses for its non-blocking counterpart (MPI_Irecv ) where the MPI_Status cannot be obtained until the MPI_Request collected is waited on (MPI_Wait , MPI_Waitall , MPI_Waitany , MPI_Waitsome ) or tested (MPI_Test , MPI_Testall , MPI_Testany , MPI_Testsome ).
然后在 mpi4py 中实践,发现成功错误码 python 是 0
>>> from mpi4py import MPI>>> print (MPI.SUCCESS)0
后记,在我不知道改了啥之后,似乎 error_code 在成功情况下又变成了 0
四、实验成果
4.1 基本成果
条目
内容
节点数
8
CPU 数
8
运行轮数
5000
总运行时间
0.161244s
每轮运行时间
0.000032
运行截图:
4.2 冷启动和溢出猜想
如果打印每一轮的测试信息,就会发现两个有趣的现象:
前几轮的时间会明显很大。
时间在趋于平稳后会有周期性(待定)的长时间传递出现
如图所示:
从上图可以看出前几个周期明显要长很多。
从这里可以看出会出现一个 “尖刺”。如果绘图(感谢陈凝香的图表):
可以更加明显的观测到这种现象。
在于小组同学商讨后,我个人倾向于这样的一种解释:这可能与某种 “高速缓存机制(cache)” 有关,对于一开始,位于路由中或者其他关键节点的高速缓存存在常见的 “冷启动” 现象,需要填充缓存来达到增加命中率的目的,而这个过程会伴随这缺失的代价,这就导致一开始的时候的缓慢,而之后的周期性缓慢,可能是 cache 中某一缓存行被换出的过程,当一个缓存行被换出,需要发生一次缺失,才能更新 cache,这就导致了一次长的周期。
当然也可能就是普通的网速问题,具组内某同学说,这个一点也不周期,我就是在强行解释,嗯嗯,有道理。
五、附录
5.1 C 实现
#include <stdio.h> #include <string.h> #include <stdlib.h> #include "mpi.h" #define ROUND 5000 #define MESSAGE_LEN 256 #define MESSAGE_TAG 314 int next (int cur, int size) { return (cur + 1 ) % size; } int pre (int cur, int size) { return (cur + size - 1 ) % size; } void send_message_to_next (int my_id, int size, int round) { char message[MESSAGE_LEN]; sprintf (message, "\n%d say 'Hi~' to %d" , my_id, next(my_id, size)); if (!round) { printf ("%s\n" , message); } MPI_Send( message, MESSAGE_LEN, MPI_CHAR, next(my_id, size), MESSAGE_TAG, MPI_COMM_WORLD ); } void recive_message_from_pre (int my_id, int size, int round) { char message[MESSAGE_LEN]; MPI_Status status; MPI_Recv( message, MESSAGE_LEN, MPI_CHAR, pre(my_id, size), MESSAGE_TAG, MPI_COMM_WORLD, &status ); if (!round) { printf ("error is %d\n" , status.MPI_ERROR); printf ("%d knows %d smiled for her, however...\n" , my_id, status.MPI_SOURCE); } } int main (int argv, char *argc[]) { int size, my_id; MPI_Init(&argv, &argc); MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &my_id); double total_time = 0 ; for (int i = 0 ; i < ROUND; i++) { MPI_Barrier(MPI_COMM_WORLD); if (my_id == 0 ) { double start_time = MPI_Wtime(); send_message_to_next(my_id, size, i); recive_message_from_pre(my_id, size, i); double end_time = MPI_Wtime(); total_time += (end_time - start_time); } else { recive_message_from_pre(my_id, size, i); send_message_to_next(my_id, size, i); } } if (my_id == 0 ) { printf ("\n==================================\n" ); printf ("total node is %d\n" , size); printf ("total time is %lf\n" , total_time); printf ("total round is %d\n" , ROUND); printf ("average time is %lf\n" , total_time / ROUND); printf ("==================================\n\n" ); } MPI_Finalize(); return 0 ; }
5.2 CPP 实现
#include <cstdio> #include <cstring> #include <cstdlib> #include "mpi.h" using namespace std;const int ROUND = 5000 ;const int MESSAGE_LEN = 256 ;const int MESSAGE_TAG = 314 ;inline int next (int cur, int size) { return (cur + 1 ) % size; } inline int pre (int cur, int size) { return (cur + size - 1 ) % size; } void send_message_to_next (int my_id, int size, int round) { char message[MESSAGE_LEN]; sprintf (message, "\n%d say 'Hi~' to %d" , my_id, next (my_id, size)); if (!round) { printf ("%s\n" , message); } MPI_Send ( message, MESSAGE_LEN, MPI_CHAR, next (my_id, size), MESSAGE_TAG, MPI_COMM_WORLD ); } void recive_message_from_pre (int my_id, int size, int round) { char message[MESSAGE_LEN]; MPI_Status status; MPI_Recv ( message, MESSAGE_LEN, MPI_CHAR, pre (my_id, size), MESSAGE_TAG, MPI_COMM_WORLD, &status ); if (!round) { printf ("%d knows %d smiled for her, however...\n" , my_id, status.MPI_SOURCE); } } int main (int argv, char *argc[]) { int size, my_id; MPI_Init (&argv, &argc); MPI_Comm_size (MPI_COMM_WORLD, &size); MPI_Comm_rank (MPI_COMM_WORLD, &my_id); double total_time = 0 ; for (int i = 0 ; i < ROUND; i++) { MPI_Barrier (MPI_COMM_WORLD); if (my_id == 0 ) { double start_time = MPI_Wtime (); send_message_to_next (my_id, size, i); recive_message_from_pre (my_id, size, i); double end_time = MPI_Wtime (); double time_gap = end_time - start_time; total_time += time_gap; printf ("\n------------------------------------\n" ); printf ("Round %5d cycle time is %lf\n" , i, time_gap); printf ("------------------------------------\n" ); } else { recive_message_from_pre (my_id, size, i); send_message_to_next (my_id, size, i); } } if (my_id == 0 ) { printf ("\n==================================\n" ); printf ("total node is %d\n" , size); printf ("total time is %lf\n" , total_time); printf ("total round is %d\n" , ROUND); printf ("average time is %lf\n" , total_time / ROUND); printf ("==================================\n\n" ); } MPI_Finalize (); return 0 ; }
5.3 提交脚本自动化
虽然提交可以有脚本,但是提交脚本本身却需要自己编写,十分的不自动,所以我又重新自己写了一份 python 脚本,可以根据需求自动生成提交脚本,并且完成自动编译和提交工作
makefile 文件
SHELL:=/bin /bash all : module load intel/19.0 .5 .281 ; \ python3 ./auto.py clean: rm *.err *.out machinefile* script
auto.py 文件
import os import timesrc_name = input ("the Source File: " ) batch_name = input ("the Name of this Batch (default name is src file name): " ) or src_name.split('.' )[0 ] partition = input ("the Partition (default partition is cpu-quota): " ) or "cpu-quota" node_num = input ("the Number of Nodes (default num is 4): " ) or "4" core_num = input ("the Number of Core (default num is 32): " ) or "32" clear_machinefile = input ("Clear Machinefile[y/n] (default is 'y'): " ) or 'y' clear_machinefile = True if clear_machinefile == 'y' else False os.system('mpiicc -o ' + batch_name + " ./" + src_name) with open (file="script" ,mode="w" ,encoding="utf-8" ) as fb: fb.write('#!/bin/bash\n\n' ) fb.write('#SBATCH -J ' + batch_name + "\n" ) fb.write('#SBATCH -p ' + partition + "\n" ) fb.write('#SBATCH -N ' + node_num + '\n' ) fb.write('#SBATCH -n ' + core_num + '\n' ) fb.write('#SBATCH -o ' + batch_name + '.out\n' ) fb.write('#SBATCH -e ' + batch_name + '.err\n\n' ) fb.write('srun hostname | sort > machinefile.${SLURM_JOB_ID}\n' ) fb.write('NP=`cat machinefile.${SLURM_JOB_ID} | wc -l`\n' ) fb.write('module load intel/19.0.5.281\n' ) fb.write('export I_MPI_HYDRA_TOPOLIB=ipl\n' ) fb.write('mpirun -genv I_MPI_FABRICS shm:dapl -np ${NP} -f ./machinefile.${SLURM_JOB_ID} ./' + batch_name + "\n" ) os.system('sbatch script' ) while not os.path.exists('./' + batch_name + '.out' ): print ("Still in the queue..." ) time.sleep(1 ) os.system('less ' + './' + batch_name + '.out' ) if clear_machinefile: os.system("rm ./machinefile.*" )