Blog
Learning by doing.
MPI学习笔记
May 2, 2013
• MPI
• Tags:
MPI
FORTRAN
今天开始做一些MPI的小练习,练习取自一个不错的网站。 往下阅读前请将源程序下载好。。
Exercise 1: Hello, World!
这个练习给我们示范了三个函数的使用。
CALL MPI_INIT(ierror) ! 初始化MPI
CALL MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierror) ! 各进程探测自己的Rank
CALL MPI_FINALIZE(ierror) ! 结束MPI
ierror
这个参数接收函数的返回,主要就是表示这个函数是否正确执行。
MPI_COMM_WORLD
每个进程组都在一个world里面,类似group的概念。
rank
每个进程都有一个进程号rank。
除了示范这三个函数,这个练习还要求我们学会MPI程序的编译和执行。
我只说一下目前主流的编译和执行流程:
mpif90 hello1.f -o hello1
mpirun -np 12 hello1 # 使用12个进程运行
Exercise 2: Hello, Again!
这个练习给我们示范了三个函数的使用again!
CALL MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierror) ! 探测全局大小
CALL MPI_SEND(message, 12, MPI_CHARACTER, i, tag, MPI_COMM_WORLD, ierror) ! 发送消息
CALL MPI_RECV(message, 12, MPI_CHARACTER, i, tag, MPI_COMM_WORLD, ierror) ! 接收消息
为什么MPI_SEND()
和MPI_RECV()
参数里有个12呢?
数一下要发送的消息的字母数吧。
由于message = 'Hello World!'
包含12个字符,后边数据类型又是MPI_CHARACTER
,所以就算不知道每个参数是什么意思,组合起来一看也就猜出来了。
MPI_SEND()
参数中的i
又是什么意思?
这里循环i
,应该是具有指代进程号的作用,查一下书,知道这个位置是”dest”,也就是destination,即目标进程的rank。
MPI_SEND()
参数中tag
又是什么?
这个猜不出来,只能查书了,书上说,顾名思义,就是个标记消息的tag!
MPI_RECV()
参数中怎么是0?
自然表示从主进程中获取信息,所以这个0代表的是”source”,源头。
MPI_RECV()
参数中为什么多了个status
?
这个有点复杂,查书知道是这么回事:当接收进程指定了”source”或者”tag”的通配符时,相应的信息会存储在这个变量中以备查询。
Exercise 3: Pi Calculation
终于开始真刀真枪的计算了! 可是实际上这个练习还是不需要你编程,但我们仍然可以好好阅读一下这个经典的程序,从中学到一些新知识。
比如,这个程序冒出来了三个之前没见过的函数:
CALL MPI_GET_PROCESSOR_NAME(processor_name, namelen, ierr)
CALL MPI_BCAST(n, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr)
CALL MPI_REDUCE(mypi, pi, 1, MPI_DOUBLE_PRECISION, MPI_SUM, 0, MPI_COMM_WORLD, ierr)
MPI_GET_PROCESSOR_NAME()
真的顾名思义了,但是这里顾名思义存在一定的歧义: processor到底是“处理器”还是“进程”还是“节点”的意思呢?
我们知道也可能不知道,一个节点可能由一个或多个处理器组成。
而现在我们运行的大型机必然是一个节点由多个处理器组成的,我们实际运行一下程序就会发现,所谓的processor_name
返回的值是一样的,可见这里是“节点”名。
MPI_BCAST()
Broadcast,广播的意思,就是以“一对多”的方式,把消息发送给其他所有可接收消息的家伙!
标准定义是这样的:
MPI_BCAST(buffer, count, datatype, root, comm)
所以,这里表示从进程0广播1个整型数据n到整个world。
MPI_REDUCE()
Reduce通常是“删除”的意思,但这里是“归纳”、“还原”的意思(reduction),我不确定这个函数其实是”reduction”还是”reductor”还是”reduce”的缩写,知道的同学告诉我一声哈。 但是我知道这个函数正统的名字叫作“规约”,表示将零散的数据收集起来进行特定的操作。
参数中的0
表示进程0返回结果。
startwtime = MPI_WTIME()
不要放过任何一个新东西!
这里还冒出来了一个没见过的函数,差点漏看了。
wtime = wall time
,可见是返回墙钟时间的,用来记录程序开跑(运行这一行代码)时的时间。
另外,值得注意的一个行间函数定义方式
f(a) = 4.d0 / (1.d0 + a*a)
不愧是公式翻译器。。。
下面来研究一下这里的并行算法
我们知道也可能不知道,积分f(x) = 4/(1 + x^2)可以得到pi的值。
为什么呢?如果是不定积分,结果是4arctan(x)。 如果取定积分,x从0到1,那么就是我们想要的结果pi!
而积分的数值计算其实就是取x在0到1中尽量多的值,算出尽量多个f(x),然后加起来就是pi了。
比如pi约等于f(0.1)+f(0.2)+…+f(0.9)+f(1.0),当然,这样计算得到的结果误差比较大。
所以程序中h = 1.0d0 / n
,其中n = 10000
把积分域分割成了10000份。
并且在累加时将x点向左偏移了0.5个h。
现在,我们来整理一下程序的思路:
首先,h = 0.000001
。
进程0计算的第一个x点为0.5*0.000001 = 0.0000005
,
计算的第二个x点为12.5*0.000001 = 0.0000125
,
计算的第i个x点为(0.5 + 12*(i - 1)*0.000001)
;
进程1呢,计算的第一个x点为1.5*0.000001 = 0.0000005
,
计算的第二个x点为13.5*0.000001 = 0.0000125
,
计算的第i个x点为(1.5 + 12*(i - 1)*0.000001)
;
以此类推。。
这样,12个进程就分别负责了12组需要计算的x点,每个进程先把自己计算的那么多个结果累加,完了再规约到一起,得到接近pi真值的结果。
显然,分割地越细得到的结果应当越精确,而使用的进程越多,理论上的计算速度也应该越快(当然,这只是算法层面的,实际还受到通讯速度等影响)。
可见呢,计算pi这类问题本身是一个完美的并行算法问题。
归纳一下核心思想就是:
串行:Sum = A + B + C + D + E + F + ...
并行:Sum = (A + B) + (C + D) + (E + F) + ...
PERFECT!