【實驗背景目的及要求】
隨着國民經濟、國防建設和高科技的快速發展,越來愈多的領域對高性能計算有強烈的需求,包括原子能、航空、航天、激光、氣象、石油、海洋、天文、地震、生物、材料、醫藥、化工等。特別是全球氣候變化和天氣預報、生物分子結構探索、湍流研究、新材料探索以及不少國防研究課題,都迫切需要高性能計算。高性能計算課程的目標就是培養從事高性能計算方面的實踐創新型人才,課堂上的理論知識能讓學生對高性能計算的各種技術有所瞭解,但要深入理解高性能計算,需要結合當前科學前沿,動手去實驗,去了解高性能計算能夠解決的問題、高性能計算的集羣環境、高性能計算的編程技術以及高性能計算算法的設計。
本實驗主要是對“基於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. 累加上述該粒子受到的力即可。