【实验背景目的及要求】
随着国民经济、国防建设和高科技的快速发展,越来愈多的领域对高性能计算有强烈的需求,包括原子能、航空、航天、激光、气象、石油、海洋、天文、地震、生物、材料、医药、化工等。特别是全球气候变化和天气预报、生物分子结构探索、湍流研究、新材料探索以及不少国防研究课题,都迫切需要高性能计算。高性能计算课程的目标就是培养从事高性能计算方面的实践创新型人才,课堂上的理论知识能让学生对高性能计算的各种技术有所了解,但要深入理解高性能计算,需要结合当前科学前沿,动手去实验,去了解高性能计算能够解决的问题、高性能计算的集群环境、高性能计算的编程技术以及高性能计算算法的设计。
本实验主要是对“基于MPI/OpenMP混合编程的大规模多体问题仿真实验”进行探索和研究,并为高性能计算课程制定探索创新性的实验内容。N-Body问题(多体问题)是天体力学和一般力学的基本问题之一,研究N个质点相互之间作用的运动规律,它切合当前科学前沿,在高性能计算领域也具有一定的代表性。MPI/OpenMP是一种分布式/共享内存层次结构,是高性能计算中两种常用并行结构的混合体,它提供结点内和结点间的两级并行,能充分利用共享存储模型和消息传递模型的优点,有效地改善系统的性能,这种编程模型能够综合地培养学生的高性能计算法方面的编程能力。基于这个基础,要求学生设计解决N-body的算法,比如PP(Particle-Particle)、PM(Partical-Mesh)、BH(Barnes-Hut)、FMM(Fast Multipole Method)等,以及基于这些算法延伸出来的算法。在实验室集群上面完成OpenMP/MPI程序,并分析使用高性能计算解决问题达到的效率,这些能够培养学生高性能计算算法设计能力及在高性能计算环境的实践能力。另外,在实验的过程中,指导学生考虑高性能计算编程中常涉及的程序可扩展性问题及负载均衡问题,培养学生精益求精的科研精神。
整个实验贴合科学前沿,能指导学生全面深入理解高性能计算,并能充分培养学生在高性能计算方面的思考、创新及实践能力。
实验主要完成对于“基于MPI/OpenMP混合编程的大规模多体问题仿真实验”的探索和研究,并完成高性能计算课程的实验内容设计。MPI是集群计算中广为流行的编程平台。但是在很多情况下,采用纯的MPI消息传递编程模式并不能在这种多处理器构成的集群上取得理想的性能。为了结合分布式内存结构和共享式内存结构两者的优势,人们提出了分布式/共享内存层次结构,MPI/OpenMP混合编程模型就是其中的一种。N-Body问题又称为多体问题,N表示任意正整数。它是天体力学和一般力学的基本问题之一,研究N个质点相互之间作用的运动规律,对其中每个质点的质量和初始位置、初始速度都不加任何限制。简单的说,N-Body问题是指找出已知初始位置、速度和质量的多个物体在经典力学情况下的后续运动,它既可以应用于宏观的天体,也可以应用于微观的分子、原子。
实验问题的创新
N-Body问题是一个经典的高性能计算应用, 广泛应用于天体物理、等离子体物理、分子动力学、流体动力学,它贴合科学前沿,并在高性能计算领域具有非常强的代表性。
实验编程模型的创新
使用MPI/OpenMP混合编程模型有许多的好处
(1)有效的改善MPI代码可扩展性
MPI代码不易进行扩展的一个重要原因就是负载均衡。它的一些不规则的应用都会存在负载不均的问题。采用混合编程模式,能够实现更好的并行粒度。MPI仅仅负责结点间的通信,实行粗粒度并行:OpenMP实现结点内部的并行,因为OpenMP不存在负载均衡问题,从而提高了性能。
(2)数据拷贝问题
数据拷贝常常受到内存的限制,而且由于全局通信其可扩展性也较差。在纯的MPI应用中,每个结点的内存被分成处理器个数大小。而混合模型可以对整个结点的内存进行处理,可以实现更加理想的问题域。
(3)MPI实现的不易扩展
在某些情况下,MPI应用实现的性能并不随处理器数量的增加而提高,而是有一个最优值。这个时候,使用混合编程模式会比较有益,因为可以用OpenMP线程来替代进程这样就可以减少所需进程数量,从而运行理想数目的MPI进程,再用OpenMP进一步分解任务,使得所有处理器高效运行。
(4)带宽和延迟限制问题
减少结点间的消息但是却增加了消息的长度。在简单的通信中,比如,在某时仅允许一个结点发送/接收一条消息,消息带宽是没有影响的,整个延时会降低因为此时消息的数量少了。在更复杂的情况下,允许消息并发的发送/接收,长消息数量的减少会产生不良影响。
(5)通信与计算的重叠
大多数MPI实现如:MPICH和LAM,都是使用单线程实现。这种单线程的实现可以避免同步和上下文转换的开销,但是它不能将通信和计算分开。因此,即使是在有多个处理器的系统上,单个的MPI进程不能同时进行通信和计算。MPI+OpenMP混合模型可以选择主线程或指定一个线程进行通信,而其它的线程执行计算的部分。
实验算法设计的创新
本实验的算法设计不是固定的,解决N-Body问题有很多可用的算法,不同的算法都有各自的优劣势,这种开放性的算法设计能让学生自己充分发挥自己的思考。
【实验环境】
OpenMP MPICH2
【实验内容】
【实验过程】
分析:
l N-body方法有几种,常用的算法设计思想比如PP(Particle-Particle)、PM(Partical-Mesh)、BH(Barnes-Hut)、FMM(Fast Multipole Method)等。其中PP算法是两层循环求粒子间的作用力产生的位置变化,其时间复杂度为O(N^2),PM、BH算法是对PP算法的改进与创新,其时间复杂度是O(NlogN),而FMM算法则更是将时间复杂度降到O(N)。
l 在上述算法中,选择PP算法对该N-body问题进行编程获得解决方案。
PP算法是两层循环求出粒子间的作用力,然后根据作用力大小,对其位置进行改动。
【实验实现】
单线程的N-body解决方案:
解决思路:
1. 对粒子进行受力分析,将空间分成x、y、z三个方向,求出两粒子间的万有引力下,其加速度在x、y、z方向的大小
2. 通过累加后的各方向的加速度,更新其速度以及空间位置。
3. 循环继续执行1、2操作。
代码结构:
Ø 初始化粒子的属性,如位置、初速度、初加速度的值
//初始化粒子的状态属性 void init_Random_Particles(ObjectParticle* &object_Particles,int pariticle_number) { srand((unsigned)time(NULL));//初始化随机数种子 for (int i = 0; i <pariticle_number; i++) //产生10个随机数 { object_Particles[i].m = rand() /double(RAND_MAX)*MAX_M; object_Particles[i].vx = rand() /double(RAND_MAX)*MAX_V * 2 - MAX_V / 2; object_Particles[i].vy = rand() /double(RAND_MAX)*MAX_V * 2 - MAX_V / 2; object_Particles[i].vz = rand() /double(RAND_MAX)*MAX_V * 2 - MAX_V / 2;
object_Particles[i].px = rand() /double(RAND_MAX)*MAX_P * 2 - MAX_P / 2; object_Particles[i].py = rand() /double(RAND_MAX)*MAX_P * 2 - MAX_P / 2; object_Particles[i].pz = rand() /double(RAND_MAX)*MAX_P * 2 - MAX_P / 2;
object_Particles[i].ax = 0;//rand() / double(RAND_MAX)*MAX_A * 2 - MAX_A / 2; object_Particles[i].ay = 0;// rand() / double(RAND_MAX)*MAX_A * 2 - MAX_A / 2; object_Particles[i].az = 0;// rand() / double(RAND_MAX)*MAX_A * 2 - MAX_A / 2;
object_Particles[i].ax_up = 0; object_Particles[i].ay_up = 0; object_Particles[i].az_up = 0; } |
Ø 分别对粒子进行循环,同时记录粒子改变的加速度
此处同时处理两个粒子,加快运行速度
本来需要N^2的时间,此处将其缩短为N(N-1)/2
//更新加速度的增量 for (int i = 0; i <particle_Number; i++) for (int j = i+1; j <particle_Number; j++) change_Particle_aup(object_Particles[i],object_Particles[j]); |
记录粒子改变的加速度
//通过万有引力修改1粒子相互作用下加速度的增量 void change_Particle_aup(ObjectParticle &object1,ObjectParticle &object2) { //距离的平方,其中还没有除于基数平3ci方的MAX_Basic_Meter^3 long double distance_between_pow3 = 1; long double dx, dy, dz; dx = object2.px - object1.px; dy = object2.py - object1.py; dz = object2.pz - object1.pz; distance_between_pow3 = dx*dx + dy*dy + dz*dz; distance_between_pow3 = pow(sqrt(distance_between_pow3), 3);//求得其三次方
Fx=GMmdx/(r^3),ax=Fx/m 此处获得G/r^3 long double tmp = G / distance_between_pow3; //修改粒子1加速度增量 long double Fa_tmp1 = tmp*object2.m; object1.ax_up += Fa_tmp1*dx; object1.ay_up += Fa_tmp1*dy; object1.az_up += Fa_tmp1*dz; //同理修改粒子2 long double Fa_tmp2 = tmp*object1.m; object2.ax_up -= Fa_tmp2*dx; object2.ay_up -= Fa_tmp2*dy; object2.az_up -= Fa_tmp2*dz; } |
Ø 通过记录的改变的加速度的值,分别对粒子进行更新
//更新粒子状态 for (int i = 0; i <particle_Number; i++) {//Acceleration velocity shift long double t_tmp = time_beats; update_velocity(object_Particles[i], t_tmp);//更新速度 update_shift(object_Particles[i], t_tmp);//更新位移 update_acceleration_up(object_Particles[i], t_tmp);//更新加速度增量 } |
运行结果:
截图为某粒子在程序运行中,在不同时刻其位置、速度的变化
OpenMP中多线程的N-body解决方案:
1. 将N个粒子划分,让各线程分别计算各粒子获得的加速度
2. 将N个粒子进行位置更新,并让粒子进行下一步循环更新位置
3. 准备对粒子进行位置更新前,需让各线程进行同步。
4. 让主线程进行输出操作以方便查看程序状态
代码结构:
omp_set_num_threads(NUM_THREADS); //设置进程数目 #pragma omp parallel private(num,tid,p) //多个并行进程开始 { tid = omp_get_thread_num(); while (true) { #pragma omp master //主线程记录执行次数 { temp_number++; printf("begin_%d_____________________________________\n", temp_number); } //PP算法 //更新加速度的增量,此处是同时更新相互作用的两个粒子 for (int i = tid; i < particle_Number; i +=NUM_THREADS) for (int j = i + 1; j < particle_Number; j++) if (i != j) //叠加粒子的加速度量 change_Particle_aup(object_Particles[i], object_Particles[j]); #pragma omp barrier //所有线程同步 { //更新粒子状态 for (int i = 0; i < particle_Number; i +=NUM_THREADS) { long double t_tmp = time_beats; update_velocity(object_Particles[i], t_tmp);//更新速度 update_shift(object_Particles[i], t_tmp);//更新位移 update_acceleration_up(object_Particles[i], t_tmp);//更新加速度增量 //输出执行的内容,方便查阅 printf("\nobject: m:%lf tid:%d\n\tpx:%lf,py%lf,pz%lf\n\tvx:%lf,vy%lf,vz%lf\n", object_Particles[i].m, tid, object_Particles[i].px, object_Particles[i].py, object_Particles[i].pz, object_Particles[i].vx, object_Particles[i].vy, object_Particles[i].vz); } } #pragma omp barrier //所有线程同步,以执行下一步循环 { #pragma omp master //主线程输出执行次数,方便查阅 printf("__end__%d________________________________________\n", temp_number); } } } |
运行结果
截图为粒子在程序运行中,在不同时刻其位置、速度的变化
OpenMP与MPI的N-body解决方案:
1. 将N个粒子划分给MPI中创建的PN个进程。
2. 每个进程中都建立TN个线程(OpenMP中创建),让每个进程类似OpenMp多线程处理N-body方法,计算其分配的N/PN个粒子。
3. 由于在第二层循环中,处理的粒子大多是其他进程的,此时需要通过接收其他进程发送过来的数据进行处理。
4. 同理,各个进程需要在循环前,将粒子广播给其他进程,以让其他进程获取粒子信息从而进行计算。
代码结构:
MPI_Status status; /*启动MPI计算*/ MPI_Init(&argc, &argv); /*MPI_COMM_WORLD是通信子*/ /*确定自己的进程标志符MyID*/ MPI_Comm_rank(MPI_COMM_WORLD, &MyID); /*组内进程数是SumID*/ MPI_Comm_size(MPI_COMM_WORLD, &SumID); int number_count_one = particle_Number / SumID + 1;//每个进程执行的数目(OpenMP) if (MyID == 0) { //分配任务并初始化数据 init_Random_Particles(object_Particles, particle_Number); } omp_set_num_threads(NUM_THREADS); //设置进程数目 #pragma omp parallel private(num,tid) //多个并行进程开始 { tid = omp_get_thread_num(); while (true) { if (MyID == 0) { temp_number++; printf("begin_%d____________________________________%d___\n", temp_number, tid); } //PP算法 //更新加速度的增量 int number_count_index = number_count_one*MyID; //当前线程执行的粒子的下标 int thread_count = number_count_one /NUM_THREADS + 1; //当前线程执行的粒子的总数 if (tid == NUM_THREADS - 1) thread_count = number_count_one - (thread_count*NUM_THREADS); int i, len;
#pragma omp barrier //所有线程同步 #pragma omp master //发送当前粒子给其他进程 MPI_Bcast(buffer, number_count_one, ObjectParticle, 0, MPI_COMM_WORLD) //先运算自己的粒子加速度增量
change_Particles_own(buffer, number_count_one);
for (int i = 0; i < SumID; i++) { if (i != MyID)//接收其他进程的数据 { #pragma omp master //再接收其他进程发送的数据进行加速度增量处理 MPI_Recv(&buffer, number_count_one, ObjectParticle, MyID, 0, MPI_COMM_WORLD); } //各个子线程进行各自的粒子加速度处理 change_Particles_other(buffer, number_count_one,i); } #pragma omp barrier //所有线程同步,进行粒子更新操作 update_own_particles_thread(MyID, tid);
#pragma omp barrier //所有线程同步,准备进入下一步循环 printf("__end__%d________________________________________\n", temp_number); //更新加速度增量后,将其发送给主进程MyID=0,让其进行粒子更新操作 MPI_Send(&object_Particles[number_count_index + tid*(number_count_one /NUM_THREADS + 1)], thread_count, sizeof(ObjectParticle), 0, tid,MPI_COMM_WORLD); } } |
运行结果
截图为粒子在程序运行中,在不同时刻其位置、速度的变化
【小结】
1. 此次实验中采用PP算法解决N-body问题。
2. 在解决过程中,一共分成三个过程,一步步解决问题最终解决了问题。在单线程运行以及OpenMP多线程运行时候,其粒子的大小达到10^9之后程序将无法正常运行。而采用MPI的多进程运行时却能正常运行,但是在小数据测试中,MPI多进程运行的速度却远远不及单线程或OpenMp单进程多线程的运行速度。
3. 实验中的粒子的初始状态数据均为随机生成,在真实环境中可能需要从文本读入,此实验没有为其做出接口,并且真实环境中,数据一般是多节点获取初始数据,此实验只是单节点生成数据。此次实验用到的是单机版大数据分析运算,如果将其迁徙到分布式,只需要对数据输入功能进行修改,而主要算法代码无需大的变动。
4. 本次实验采用PP算法,但也有其他的高效算法,如BH算法、FFM算法。其算法是基于Treecode的算法,将粒子的分布用树结构来表示,然后再计算粒子的相互作用力。其算法实现也较为简单。
l 将粒子用树的表示:
将一个空间分成八份,存在一颗八叉树中。如果分配后的粒子数量还是很大,将再进行分配直到粒子数目合适为止。
l 计算粒子受到的作用力:
1. 计算各个节点的质心。
2. 通过其他结点的质心,计算其他结点对该粒子造成的力。
3. 计算在粒子在本结点内所有其他粒子对其造成的力。
4. 累加上述该粒子受到的力即可。