Интеграл MPI

F(x, y)=sqrt((1-x^2)*(1-y^2))

x, y Î [-1; 1]

Очевидно, всё симметрично, поэтому это то же самое, что 4*integral(x, y Î [0; 1]).

Для простоты используем метод прямоугольников.

Далее как обычно, меняем x и y от 0 до 1 равномерно за N шагов, где N достаточно большое, чтобы долго считалось. Разделяем на узлы по «полосам», на каждом считаем частичную сумму значений функции в точках на «локальной» области, потом передаем результат на один узел, где всё суммируется, делится на количество точек (границы от 0 до 1, поэтому площадь области тоже единица, в общем же случае надо еще на нее умножить) и умножается на 4.

Главное – правильно поделить область между узлами, без «просветов» и «перекрытий», а из взаимодействия между процессами здесь есть только однократная передача одного числа от всех не-первых узлов первому, выполняемая после окончания расчетов.

Текст программы.

program mpi_integral

double precision aa, bb

include 'mpif. h'

F(aa, bb)=sqrt((1-aa*aa)*(1-bb*bb))

integer(4) :: messageSize

double precision :: message

integer(4) :: ierr, nodeID, firstNode, otherNode, numberOfNodes, tag

integer(4) :: statusMPI(MPI_STATUS_SIZE)

real(8) :: beginTime, endTime

integer i, j,k! loop variables etc

integer N! number of subdivisions

double precision xstart, xend, xstep

double precision ystart, yend, ystep

double precision x, y

integer nstart, nend, nrepeat

integer cc

double precision tt

integer pp

integer maxpp

double precision int_result

firstNode = 0

НЕ нашли? Не то? Что вы ищете?

otherNode = 1

tag = 100

N=100000

call MPI_INIT(ierr)

call MPI_COMM_RANK(MPI_COMM_WORLD, nodeID, ierr)

call MPI_COMM_SIZE(MPI_COMM_WORLD, numberOfNodes, ierr)

if (nodeid .eq. firstnode)then

print *, 'integral started, total nodes = ',numberofnodes

print *, 'square size = ',N,' x ',N

end if

! decide range of integration for each node

ystart=0

yend=1

ystep=(yend-ystart)/N

ystart=0.5d0 * ystep

xstep=ystep

if (nodeid. eq. 0) then

! if we are in first node, start from beginning

nstart=0

else

! if in other, start from some place

nstart=floor(nodeid*1.0d0/numberofnodes*N)+1

end if

if (nodeid. eq. (numberofnodes-1)) then

! if we are in last node, end at the end

nend=N-1

else

! if in other, end just before next node starts

nend=floor((nodeid+1.0d0)/numberofnodes*N)

end if

! now calculate the linear values

xstart=nstart*1.0d0/N + 0.5d0 * xstep

nrepeat=nend-nstart+1

print *, 'node number ',nodeid

print *, 'start value = ',nstart

print *, 'end value = ',nend

print *, 'number of repeats = ',nrepeat

messagesize = 1

tt=0

beginTime = MPI_WTIME()

if(nodeID == firstNode) then

! perform as node #1

! calculate

int_result=0

x = xstart

do i=1,nrepeat

y=ystart

do j=1,N

int_result=int_result+F(x, y)

y=y+ystep

end do

x=x+xstep

end do

! receive from others and accumulate

! skip if we are the only node

if (numberOfNodes>1)then

do i=1,numberOfNodes-1

call MPI_RECV(message, messageSize, MPI_REAL8,i, tag, MPI_COMM_WORLD, statusMPI, ierr)

int_result=int_result+message

end do

end if

else

! perform as any other node

! calculate

int_result=0

x = xstart

do i=1,nrepeat

y=ystart

do j=1,N

int_result=int_result+F(x, y)

y=y+ystep

end do

x=x+xstep

end do

! send to first node

message=int_result

call MPI_SEND(message, messageSize, MPI_REAL8,firstNode, tag, MPI_COMM_WORLD, ierr)

end if

endTime = MPI_WTIME()

tt=tt+(endtime-begintime)

if (nodeid. eq. firstnode)then

int_result=int_result/(0.25d0*N*N)

print *, 'result = ',int_result

print *, 'time = ',(endtime-begintime),' seconds'

end if

call MPI_FINALIZE(ierr)

end program mpi_integral

Результаты ее запуска с использованием script-ов и очередей, выделяющих заданное количество узлов для запуска копий программы, представлены в таблице.

Количество узлов

Время расчета, с.

Результат

Ускорение

входной узел

180.35

2.

0.92

1

166.57

2.

1

2

83.45

2.

2.00

3

55.64

2.

2.99

4

41.74

2.

3.99

5

33.39

2.

4.99

6

27.82

2.

5.99

8

20.90

2.

7.97

Все измерения, кроме входного узла, выполнялись 1 раз, ждать и усреднять было лень.

После взаимодействия с одномашинным параллельным интегралом уже было ожидаемо, что при увеличении числа точек N до значений, при которых «достаточно долго считается», вновь заявит о себе накопление ошибок округления при суммировании 100000x100000 = 10 млрд чисел. Так оно и получилось (перед «достаточно долгим» запуском были проведены исследования на меньшем числе точек, подтвердившие, что используется строго одна и та же сетка вне зависимости от количества процессов, между которыми поделено выполнение расчета). И в случае «многоузлового» расчета суммы большого числа слагаемых результат менялся, когда менялось количество промежуточных переменных, использовавшихся для его накопления. Поскольку количество сложений, приходящихся на каждую такую переменную, с увеличением числа узлов уменьшается, можно предположить (хотя не очень уверенно), что результат при этом приближается к правильному.

Интересно, что при «автоматическом» размещении программы на одном из расчетных узлов уже достигается ускорение по сравнению с запуском на входном узле.

Зависимость ускорения от числа используемых процессов представлена на графике.

(численные значения ускорения, представленные в таблице, округлены, график же строился по исходным)

Поскольку основная часть нагрузки на узлы – параллельная, а последовательная состоит лишь из суммирования частичных результатов, даже при такой нерациональной ее реализации, как в приведенной выше программе, при использованном числе процессов (от 1 до 8) ее влияние в прямом смысле незаметно. Наблюдается устойчивый рост производительности прямо пропорционально числу задействованных процессов. Это не «подогнанная» картинка, а экспериментальные данные, причем измеренные кустарным способом один раз даже без усреднения. Но всё равно верится с трудом, поэтому ниже приведены «сырые» данные.

ydata =

0.

1

1.

2.

3.

4.

5.

7.

Сначала программа была запущена тупо по-linux-овому на локальном узле, результат приведен в графе с номером «0». Затем при количестве процессов от 1 до 6 использовался script очереди, с указанием по 1 экземпляру программы на каждый компьютер. С 7 процессами так не получилось (видимо, «входной» узел не может участвовать в очереди), поэтому число 8 получено задействованием 4 компьютеров по 2 копии программы на каждом. А дальше было не столько лень увеличивать, сколько появились опасения, что результат запуска программы по несколько копий на каждом узле будет недостаточно «чистым» по сравнению с задействованием каждого узла только однократно.

Не совсем понятно, что считать «последовательной» программой, ведь при запуске вышеприведенной “mpi”-программы в одном экземпляре (не важно, через “mpiexec” или нет) алгоритм ведет себя как чисто последовательный, не использует каких-либо пересылок, обсчитывает всю область интегрирования один целиком в последовательном цикле. Так что последовательными равноправно могут считаться запуски 0 и 1, а разница во времени их выполнения, видимо, обусловлена разницей в производительности процессоров «входного» и «расчетного» узлов (было два запуска с результатами 180.36 и 180.35 секунд, так что, скорее всего, это разница именно в производительности, а не в загрузке узлов другими пользователями). По сравнению же с запуском на одном расчетном узле в последовательном режиме, параллельный запуск на большем числе процессов дает выигрыш в производительности ровно во столько раз, каково их число.