Важным моментом при написании параллельной программы является работа по сохранению данных в файлы или восcтановлению данных из них. Примерами необходимости таких действий могут служить задачи загрузки начальных данных в массивы при запуске программы и сохранение промежуточных данных и/или результатов счета.
Если говорить о загрузке начальных данных или сохранении результатов счета, то эта процедура происходит в самом начале работы программы или в самом ее конце, и никаких особенностей или отличий о того, как это может быть сделано в последовательной программе, для параллельной программы нет.
Однако, в том случае, когда речь идет о сохранении промежуточных данных, появляются некоторые нюансы, связанные со структурой параллельной программы. Дело в том, что файловые операции - очень медленные операции. Если сохранение данных происходит в каждом итерационном цикле, то такое дейтсвие может существенно снизить быстродействие программы, что кстати верно и в случае обычного последовательного алгоритма. Поэтому при проектировании параллельной программы следует придерживаться двух принципов:
- Сохранение данных должно происходить не в каждом итерационном цикле, а, скажем, в каждом десятом. В любом случае время между моментами сохранения болжно быть много больше времени, требующемся для записи данных в файл.
- Сохранение данных должно быть реальзовано с использованием неблокирующих операторов ввода-вывода, позволяющих совместить по времени файловые операции и вычисления.
Рекомендации, данные на этой странице, проверялись на компиляторе Intel Fortran. Проверьте, поддерживает ли ваш компилятор процедуры асинхронной записи. Если нет - ну что ж, вам придется отказаться от распараллеливания процедур вычисления и сохранения данных. Постарайтесь делать запись на диск как можно реже, чтобы это минимальным образом сказалось на быстродействиии вашего кластера.
Посмотрим на конкретном примере, как можно реализовать эти два принципа. Возьмем нашу программу вычисления уравнения теплопроводности, а именно - блок, описывающий итерационный цикл:
55 c Подпрограмма вычисления искомой функции 56 subroutine iter(f0,f1,xmax,ymax,df) 57 include 'mpif.h' 58 integer xmax,ymax 59 real*8 f0(xmax,ymax), f1(xmax,ymax) 60 real*8 dt,dx,dy 61 real*8 df 62 integer x,y,n,myid,np,ierr,status(MPI_STATUS_SIZE) 63 common myid,np 64 dt=0.01 65 dx=0.5 66 dy=0.5 67 c Обмениваемся границами с соседом 68 if ( myid .gt. 0 ) 69 x call MPI_SENDRECV( 70 x f0(1,2), xmax, MPI_REAL8, myid-1, 1, 71 x f0(1,1), xmax, MPI_REAL8, myid-1, 1, 72 x MPI_COMM_WORLD, status, ierr) 73 if ( myid .lt. np-1 ) 74 x call MPI_SENDRECV( 75 x f0(1,ymax-1), xmax, MPI_REAL8, myid+1, 1, 76 x f0(1,ymax), xmax, MPI_REAL8, myid+1, 1, 77 x MPI_COMM_WORLD, status, ierr) 78 c Вычисляем функцию в ячейках сетки 79 do x=2,xmax-1 80 do y=2,ymax-1 81 f1(x,y)=f0(x,y)+dt*( 82 x (f0(x+1,y)-2*f0(x,y)+f0(x-1,y))/(dx*dx) 83 x + 84 x (f0(x,y+1)-2*f0(x,y)+f0(x,y-1))/(dy*dy) 85 x ) 86 end do 87 end do 88 c Копируем границу для следующей итерации 89 do x=1,xmax 90 f1(x,1)=f0(x,1) 91 f1(x,ymax)=f0(x,ymax) 92 end do 93 do y=1,ymax 94 f1(1,y)=f0(1,y) 95 f1(xmax,y)=f0(xmax,y) 96 end do 97 c Находим максимальную дельту 98 df=0.0 99 do x=1,xmax 100 do y=1,ymax 101 if ( df .lt. abs(f0(x,y)-f1(x,y)) ) then 102 df = abs(f0(x,y)-f1(x,y)) 103 endif 104 end do 105 end do 106 return 107 end
Поскольку мы решили, что сохранение данных будет происходить на каждой 10-й итерации, то есть запись будет происходить в зависимости от номера итерации, то мы должны будем передавать в тело подпрограммы номер текущего цикла. Поэтому вначале изменим описание подпрограммы, добавив дополнительный параметр ее вызова (строка 56).
Как мы знаем, рассматриваема нами подпрограмма по неизменяемым (и это важно!) в течение цикла данным массива f0, вычисляет значения массива f1. Поэтому в ее теле мы должны решить, наступил ли десятый цикл, и если да, то инициировать неблокирующую запись массива f0 в файл. Что мы и делаем в строке 77b. Далее мы открываем файл и инициируем процесс записи в него массива данных (строки 77k и 77q).
Кроме того, надо позаботиться о том, чтобы каждый процесс параллельной программы сохранял свою часть данных в отдельный, принадлежащий только ему файл. Вычислением название файла данных мы в строке 77d.
После инициации записи управление передается циклу, в котором и происходят необходимые нам вычисления, причем процессы вычисления и записи идут параллельно. Перед возвратом управления из подпрограммы мы должны удостовериться, что процедура записи в файл закончена и закрыть файл (строки 105b и 105с).
Теперь посмотрим, как это можно реализовать в программном коде.
55 c Подпрограмма вычисления искомой функции 56 subroutine iter(f0,f1,xmax,ymax,df,n) 57 include 'mpif.h' 58 integer xmax,ymax 59 real*8 f0(xmax,ymax), f1(xmax,ymax) 60 real*8 dt,dx,dy 61 real*8 df 62 integer x,y,n,myid,np,ierr,status(MPI_STATUS_SIZE) 63 common myid,np ------------------------------------------------------------------- 63a character(LEN=20):: st,fname 63b logical ex ------------------------------------------------------------------- 64 dt=0.01 65 dx=0.5 66 dy=0.5 67 c Обмениваемся границами с соседом 68 if ( myid .gt. 0 ) 69 x call MPI_SENDRECV( 70 x f0(1,2), xmax, MPI_REAL8, myid-1, 1, 71 x f0(1,1), xmax, MPI_REAL8, myid-1, 1, 72 x MPI_COMM_WORLD, status, ierr) 73 if ( myid .lt. np-1 ) 74 x call MPI_SENDRECV( 75 x f0(1,ymax-1), xmax, MPI_REAL8, myid+1, 1, 76 x f0(1,ymax), xmax, MPI_REAL8, myid+1, 1, 77 x MPI_COMM_WORLD, status, ierr) ------------------------------------------------------------------- 77a c Если десятый цикл, то сохраняем данные 77b if ( ((n/10)*10 .EQ. n ) then 77c c Вычисляем название файла данных 77d write(fname,'(A,I2.2,A)') "node",myid,".dat" 77e c Проверяем существует ли файл для записи 77f inquire(file=fname, exist=ex) 77g c Если существует, то открываем его для перезаписи, 77h c если файл не существует - открываем его как новый 77i write(st,'(A)') 'new' 77j if ( ex ) write(st,'(A)') 'old' 77k open(unit=20, 77l $ file=fname, 77m $ asynchronous='yes', 77n $ status=st, 77o $ form='unformatted') 77p c Инициируем запись массива f0 в файл 77q write(20,ID=idvar,asynchronous='yes') f0 77r endif ------------------------------------------------------------------- 78 c Вычисляем функцию в ячейках сетки 79 do x=2,xmax-1 80 do y=2,ymax-1 81 f1(x,y)=f0(x,y)+dt*( 82 x (f0(x+1,y)-2*f0(x,y)+f0(x-1,y))/(dx*dx) 83 x + 84 x (f0(x,y+1)-2*f0(x,y)+f0(x,y-1))/(dy*dy) 85 x ) 86 end do 87 end do 88 c Копируем границу для следующей итерации 89 do x=1,xmax 90 f1(x,1)=f0(x,1) 91 f1(x,ymax)=f0(x,ymax) 92 end do 93 do y=1,ymax 94 f1(1,y)=f0(1,y) 95 f1(xmax,y)=f0(xmax,y) 96 end do 97 c Находим максимальную дельту 98 df=0.0 99 do x=1,xmax 100 do y=1,ymax 101 if ( df .lt. abs(f0(x,y)-f1(x,y)) ) then 102 df = abs(f0(x,y)-f1(x,y)) 103 endif 104 end do 105 end do ------------------------------------------------------------------- 105a c Дожидаемся окончания записи и закрываем файл 105b wait(unit=20,ID=idvar) 105c close(unit=20) ------------------------------------------------------------------- 106 return 107 endзагрузить исходник программы
Процедура записи данных, так, как она представлена на этой странице, не вполне оптимальна. Дело в том, что после выхода из подпрограммы до начала следующего цикла выполняется код, который не модифицирует записываемый массив. И код этот выполняется достаточно долго. Но для простоты восприятия мы всю процедуру сохранения данных уложили внутрь рассматриваемой подпрограммы. Выше дана ссылка на исходный код программы, в котором учтена эта недоработка.
В заключение посмотрим, как изменились временные (скоростные) характеристики нашей программы от добавления процедуры записи. В представленной ниже таблице дана длительность работы нашей программы в трех вариантах. Первый вариант - наша исходная программа без записи данных в файл. Второй вариант - программа с записью данных в синхронном режиме, то есть без распараллеливания процедур вычисления и записи. И третий вариант - программа с записью данных на диск в асинхронном режиме, то есть с распараллеливанием счета и записи в файл.
Точность вычислений в программе немного увеличена по сравнению с исходной программой, которую мы рассматривали в пердыдущих разделах, чтобы потребовалось большее число итераций для нахождения решения уравнения. Все варианты программы запускались для работы на трех процессорах. Запись данных велась на расшаренный в NFS ресурс, кроме первого процесса, где запись велась на локальный диск. Кроме того даны результаты, полученные при записи данных на локальный диск (всеми процессами параллельной задачи).
Вариант програмы | Файловая система | Время Счета |
---|---|---|
Записи данных нет | не исп. | 1 мин. 26 сек. |
Синхронная запись | NFS | 2 мин. 46 сек. |
Синхронная запись | локальная | 1 мин. 37 сек. |
Асинхронная запись | NFS | 2 мин. 37 сек. |
Асинхронная запись | локальная | 1 мин. 28 сек. |
Резюме. По результатам тестов можно сделать заключение. Использование NFS сильно замедляет процесс счета. Поэтому стоит подумать об использовании на вычислительных узлах кластера локальных файловых систем для сохранения данных. Однако в этом случае вам придется перед запуском программы и после окончания счета синхронизировать данные на всех узлах, поскольку вообще говоря заранее неизвестно, на каком узле кластера запустится процесс программы с определенным номером. Другими словами файл, созданный процессом 2 на узле node005 в следующий раз может понадобиться процессу 2 на узле node007.
Следующий вывод, который можно сделать - использование асинхронной записи позволяет увеличить общее быстродействие программы, хотя и не существенно. Однако преимущество асинхронной записи будет тем заметнее, чем больше времени у вас будет занимать вычисление одного цикла итерации.