Опыт переноса параллельной программы с PVM на MPI.

[ Автор: Илья Евсеев ]
[ Организация: ИВВиБД ]
[ Подразделение: ЦСТ ]

 

Содержание

 

Постановка задачи

Имеется программа Crystal95. Делает она буквально следующее: CRYSTAL95 computes the electronic structure of periodic materials within Hartree Fock, density functional or various hybrid approximations. The Bloch functions of the periodic systems are expanded as linear combinations of atom centered Gaussian functions. Powerful screening techniques are used to exploit real space locality. The code may be used to perform consistent studies of the physical, electronic and magnetic structure of molecules, polymers, surfaces and crystalline solids. То есть, ее суть - прикладные математические расчеты.

Программа имеет параллельную архитектуру, может быть собрана поверх одного из нескольких коммуникационных интерфейсов, в т.ч. PVM. В нашем распоряжении есть исходные тексты Crystal95 на языке Фортран-77, с недвусмысленным трупным запахом Фортрана-4.

Работающий вариант программы инсталлирован на компьютере compic.ptc.spbu.ru в Петербургском Университете, где его сопровождением занимается инженер-программист Павел Егорович Борисов. Он (работающий вариант, а не Павел) служил эталоном для проверки того, что написал я.

Имеется, скажем так, суперкомпьютер фирмы "Парситек", на нем инсталлированы коммуникационные библиотеки Embedded Parix и PowerMPI (реализация MPI над Embedded Parix). В природе существует так же и PowerPVM, но он не установлен, так как его нужно покупать дополнительно.

В определенных кругах зародилась, как ни странно, мЬIcль: скрестить Парситек с Кристаллом путем портирования последнего под MPI. Работа оказалась достаточно нелинейной; ниже перечислены основные проблемы и замечания по их решению: где-то конкретные, а где-то - не очень. Поскольку подразумевалось, что работа будет носить не только практический, но и методически-назидательный характер, я излагаю весь полученный опыт предельно подробно, стараясь не пренебречь ни одной мелочью.

 

Результаты работы

Все имевшиеся проблемы могут быть четко разделены на три группы по типу источника:

  1. Crystal'95 - непродуманная конструкция и ненаглядная запись. От меня потребовавалось очень глубокое разбирательство в структуре исходного модуля, чтобы разработать адекватную замену для него.

  2. Фортран - способность пропускать ошибки неопытного программиста (такого, как я), превращая их из элементарных ошибок времени компиляции в неподъемные ошибки времени выполнения.

  3. Парситек - отсутствие самого необходимого (например, отладчика); ненадежность вообще всего, что есть.

Программа, собранная поверх MPI, опробована на трех компьютерах:

На каждой из них она находится в каталоге ~il/cry95mpi

Прилагаемые файлы:

Примечание 1: если файлы на разных машинах различаются, заведомо последний и наиболее правильый вариант можно взять с этой страницы.

Примечание 2: список прочих моих рассказок про параллельное программирование находится тут.

 

Как запустить программу

Первое: из каталога, в котором находятся Ваши файлы, должен быть доступ к программам из ~il/cry95mpi. Либо добавьте этот путь в переменную окружения PATH, либо (лучше) сделайте ссылки:

    ln -s ~il/cry95mpi/{MPIintegrals,MPIscf,runmpi} .

Второе: должен быть доступ к BIN-директории MPI: отредактируйте PATH. Вот где находится MPI на трех упомянутых машинах:

Третье: создайте файл paths.lst с названиями каталогов, в которых ветви приложения будут создавать результаты. Каталоги обязательно должны иметь разные имена, поскольку каждая ветвь дает файлам одинаковые названия. Меняя количество каталогов, Вы тем самым задаете количество ветвей. Ниже приведен пример такого файла. Он задает каталоги для пяти ветвей: одной служебной и четырех расчетных.

#  paths.lst
#  =========
#  Working directories for nodes of MPI-based Crystal'95
#

/tmp/il/cry95mpi/node0
/tmp/il/cry95mpi/node1
/tmp/il/cry95mpi/node2
/tmp/il/cry95mpi/node3
/tmp/il/cry95mpi/node4

## EOF ##

Запустите runmpi, указав в его командной строке имя файла данных, например: runmpi i29. Если он отработает успешно, в Вашем каталоге появятся файлы с именами step1, step2, ... с результатами вычислений.

Что делать, если runmpi "свалится" во время выполнения? Необходимо восстановить paths.lst из временной копии

    mv __paths_.lst paths.lst
Кроме того, рекомедуется вручную удалить со всем содержимым каталоги, перечисленные в paths.lst.

На Парситеке runmpi "сваливается" всегда - таков Парситек! Вам потребуется запускать недовыполненые команды вручную, или переместить их в отдельный командный файл.

 


Замена pvm_spawn

В PVM изначально запускается одна ветвь, ветви могут запускать другие ветви - из того же программного файла или из других. Все приложения, входящие в состав Crystal95, в PVM-варианте состоят из двух программ: ведущей (master) и ведомой. Мастер запускает по одному ведомому процессу на каждом вычислительном узле. Адреса узлов мастер читает из начала текстового конфигурационного файла.

В MPI все ветви идентичны и запускаются одновременно, различаясь только порядковым номером в коллективе. Для них выбрано следующее распределение ролей: ветвь 0 занимается функциями ведущей (читает конец config-файла, рассылает его всем остальным, впоследствии ведает сервисом "Next value"), а ветви с номерами 1..N решают задачи PVM-ветвей с номерами 0..(N-1) соответственно. Каждое приложение состоит из одного-единственного файла, который запускается в количестве N+1 экземпляров.

Проблема может возникнуть, если количество ветвей для pvm_spawn() не задается пользователем в настроечном файле или с консоли, а вычисляется на начальной стадии алгоритма, т.е. уже в ходе работы программы. Общего правила, как такое поведение программы расписать средствами MPI, не существует. К счастью, ничего подобного в Crystal95 нет: количество ветвей изначально задается извне.

 

Аварийное завершение

В PVM не существует простого механизма для того, чтобы одна отдельно взятая ветвь могла завершить работу всего параллельного приложения (с точки зрения PVM такого понятия, как приложение, не существует вовсе), или группы (кстати, механизмом групп Crystal95 не пользуется). Места, в которых приложение может принять решение об аварийном завершении, в Crystal95 (в несколько в упрощенном виде) выглядят примерно так:

Ветвь-мастер Ведомые ветви
  do i=0,N-1
    send(task=i,buf=данные(i))
  enddo
  do j=0,N-1
    recv(task=j,buf=ответ)
    if(ответ.eq.ошибка) then     
      do k=0,N-1
        send(task=k,buf=ошибка)  
      enddo
      pvm_exit
      stop
    endif
  enddo
  do j=0,N-1
    send(task=j,buf=не_ошибка)
  end_do
  recv(task=master,buf=данные)
  сделать_что-то_умное(данные,iflag)  
  send(task=master,buf=iflag)
  recv(task=master,buf=iflag)
  if(iflag.eq.ошибка) then
    pvm_exit
    stop
  endif

В MPI-варианте с использованием типового подхода на базе MPI_Barrier и MPI_Abort все сильно упрощается:

if(myRank.eq.0) then
  do i=1,N
    call MPI_Send( данные(i), ... , rank=j, ... )
  enddo
else
  call MPI_Recv( данные, ... , rank=0, ... )
  сделать_что-то_умное(данные,iflag)
  if(iflag.eq.ошибка) then
    write(*,*) 'Приехали...'
    call MPI_Abort( MPI_COMM_WORLD, MPI_ERR_OTHER, ierror )
    stop
  enif
endif
call MPI_Barrier( MPI_COMM_WORLD, ierror )

 

Нумерация ветвей

Задачи внутри PVM изначально имеют нумерацию, не пригодную для распределения обязанностей внутри приложения: эта нумерация позиционирует их не внутри отдельного приложения, а в рамках Виртуальной Машины в-целом. Для объединения в коллектив со строгой нумерацией от 0 до (число_задач-1) в PVM, как правило, используются т.н. "группы".

В Crystal95 над PVM используется менее привычная схема: идентификаторы запускаемых по-одной ведомых ветвей мастер накапливает в массиве ITIDS(0:NPROC-1). Затем мастер распространяет массив всем ведомым. Каждая ведомая ветвь находит в ITIDS() свой идентификатор и запоминает его позицию в переменной IAM. Как нетрудно заметить, позиция строгим порядковым номером задачи является.

MPI-ветви последовательно нумеруются непосредственно при запуске. Поскольку ветвь 0 играет роль мастера, то в MPI_Send/MPI_Recv в качестве номера ветви вместо ITIDS(n) надо писать просто n+1. IAM и NPROC получаются посредством вызовов MPI_Comm_rank() и MPI_Comm_size() с последующим уменьшением на 1:

    call MPI_Init( ierror )
    call MPI_Comm_size( MPI_COMM_WORLD, nproc, ierror )
    call MPI_Comm_rank( MPI_COMM_WORLD, iam, ierror )

    nproc = nproc-1
    iam = iam-1

    if ( iam.lt.0 ) then
       call MasterNode
    else
       call second(ft)
    endif
Перебор по всем ведомым ветвям:
    было:    do i=0,NProc-1
    стало:   do i=1,NProc

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

 

Привязка ветвей к узлам, работа с файлами

PVM-вариант использует следующую схему: после того, как на очередном узле посредством pvm_spawn() запущена ведомая ветвь, ей передается имя каталога, в котором она должна будет размещать свои рабочие файлы. То есть: для ветви принудительно задается место создания. Поскольку рабочие файлы всех ветвей имеют одни и те же имена, каталоги обязательно должна различаться на тот случай, если несколько ветвей реально работают с одним и тем же диском, как это имеет место быть в Париксе.

В MPI официально не существует способа заставить загрузчик ветвь с конкретным номером запустить на узле с конкретным именем. Исключение составляет только нулевая ветвь - она грузится на узле, к которому подключена консоль пользователя. Необходимо, чтобы уже после запуска ветвь определила свое местонахождение посредством функции MPI_Get_processor_name, и в зависимости от него использовала соответствующий каталог. Пример здесь не приводится в силу несколько избыточного размера.

Проблема, из-за которой упомянутый пример становится ненужным, заключается в том, что и PowerPVM, и PowerMPI возвращают такое "псевдо-имя", которое с реальными сетевыми именами узлов никак не соотносится:

я выбрал самое простое решение: создал на каждом диске все возможные каталоги: один из них будет задействован выполняющейся на данном узле ветвью, а остальные останутся пустыми. Если впоследствии это решение покажется неудовлетворительным, придется мне переписать MPI_Get_processor_name() через gethostname() (кстати, на Парситеках класса CC/nK такой подход так же может оказаться не вполне корректным), а пока и так сойдет.

 

Замена pvm_pack и pvm_unpack

Упаковка (помещение разрозненных данных в передающий буфер) и распаковка (извлечение из приемного) может быть представлена в MPI одним из следующих аналогов:

  1. MPI_Pack и MPI_Unpack.
    Замечание: в PVM буфер автоматически: создается для передачи в pvm_initsend, наращивается при необходимости в pvm_pack, создается для приема в pvm_recv.
    Проблема: в MPI буфер (и указатель на текущую позицию в нем) должен объявляться вручную. При переполнении передающего буфера его размер средствами Фортрана увеличить невозможно.

  2. Можно заменить упаковку такой самодельной конструкцией: заводится массив того же типа, что и данные, данные переписыватся в ячейки массива, массив передается вызовом pvm_psend или MPI_Send. На приемной стороне все то же самое, но в обратном порядке.
    Ограничение: все данные должны быть однотипны, а их количество заранее известно.

  3. Аналогично п.2, но заводится не массив, а структура.
    Достоинство: появляется возможность передавать разнотипные данные.
    Недостаток: нет в Фортране структур...

  4. В MPI конструируется пользовательский описатель типа.
    Достоинство: данные могут быть разнотипными.
    Недостатки: громоздкий механизм создания; состав данных и их взаимное месторасположение должны быть фиксированными. Например, в Фортране параметры передаются в подпрограмму не по значению, а по ссылке; в итоге ссылки (адреса значений) в стеке всегда будут на фиксированном расстоянии друг от друга, но сами эти адреса от вызова к вызову могут быть разными.
Мне в итоге не понадобился ни один из этих вариантов: все коммуникации в программе постепенно упростились до такой степени, что передаче подлежала ровно одна ячейка (переменная или готовый массив), причем для массива всегда была известна максимальная длина. ячейка обрабатывалась непосредственным вызовом MPI_Send/MPI_Recv.

 

Замена pvm_mcast

Ни в коем случае не следует "в лоб" заменять pvm_mcast или pvm_bcast на MPI_Bcast. MPI_Bcast - коллективная функция, которая должна быть вызвана во всех ветвях: в одной - на передачу, в остальных - на прием. Она не совместима с функциями типа "точка-точка", такими, как MPI_Send и MPI_Recv.

Решение "в лоб" заключается в следующем: надо

call pvm_mcast( TasksCount, TaskIds, MsgId, ierror )
поменять на:
do i=1,TasksCount
  call MPI_Send( ... , rank=TaskIds(i), tag=MsgId, MPI_COMM_WORLD, ierror )
enddo

Замена на MPI_Bcast всех взаимосвязанных вызовов pvm_mcast и pvm_recv более элегантна, и гарантирует выигрыш по скорости работы, но требует намного больше времени, а кроме того, не всегда возможна, например, следующий текст MPI_Bcast'ом не заменить:

Мастер Ведомые
  if(все_хорошо) then
    call pvm_mcast( N,slaves,MsgOk,info)
  else
    call pvm_mcast( N,slaves,MsgFuck,info) 
  endif
 call pvm_recv(master,-1,info)

 


"Особенности" Фортрана

  1. Фортран не позволяет писать хорошо,
  2. зато создает непревзойденные возможности для того,
    чтобы писать по-настоящему плохо!
Из-за нестрогого синтаксиса многие ошибки, засекаемые в Си на стадии компиляции, в Фортран-программах пропускаются компилятором и превращаются в ошибки времени выполнения, где их порой бывает очень нелегко выловить, учитывая параллельную конструкцию программы и глючную платформу (см.следующий раздел). Эти примеры знает каждый: Ошибка, связанная с неэквивалентностью COMMON-блоков: я допустил ее в силу плохого знания Фортрана, а компоновщик не счел нужным поставить о ней в известность. Как итог, программа не работала:

А эти неожиданные эффекты я в виде отдельного примера воспроизвести не сумел (да и не пытался, откровенно-то говоря):

Недоделанность Фортрана вынуждает реализовывать недостающие функции в модулях, написанных на Си; а в Фортран-программе подпрограммы на Си, как известно, лишены возможности пользоваться отладочными выводами на консоль. Это придает отладке дополнительный динамизм.

Что касается дурного стиля написания программ, то Фортран усиленно формирует его в двух основных направлениях:

  1. хаотически-бессмысленное использование меток
    (в случае с Read их даже нечем заменить),
  2. и отсутствие выравнивания текста:
    все строки начинаются в PVM-модуле с 7 колонки и только с нее!
Завершающее обобщение можно сформулировать так: это не язык, а сплошное недоразумение. Очень хочется верить, что через несколько лет он окажется там же, где и его нынешние поклонники - то есть, на пенсии.

 

Как Фортран записывает метки в объектном файле?

Увы, по-разному. Пусть у нас имеется нечто простейшее на Фортране:

C   This is PIPA.F
    call TheSub1
    call The_Sub_2
    end
Скомпилируем его командой fc -c pipa.f и внимательно рассмотрим результат команды nm pipa.o. Мы увидим описания двух меток:

Компилятор Метка 1 Метка 2
HP Fortran на SPP-1600 thesub1_ the_sub_2_
IBM XL Fortran на Парситеке thesub1 the_sub_2
GNU Fortran thesub1_ the_sub_2__

Во-первых, такое многообразие само по себе является несколько неожиданным и требует привыкания (читай: еще день коту под хвост); а во-вторых, обращение GNU Fortran'a с метками, содержащими подчерк, оказалось несовместимым со стандартными правилами: библиотека MPI для Фортрана написана на Си и экcпортирует метки вида "mpi_init_", а объектный файл, построенный g77, импортирует "mpi_init__". Положение спас секретный ключик "-fno-second-underscore", подробнее смотрите makefile.ptc. Пользуясь случаем, выражаю благодарность Дмитрию Федорову из Новосибирского Института ядерной Физики за оперативный ответ в RU.UNIX.LINUX.

 


"Особенности" Парикса

ВСЕ программные или аппаратные компоненты, сделанные в Парситеке или по заказу Парситека, являются так или иначе бракованными: платы HighSpeedLink, операционная система Embedded Parix, отладчик DeTop, пакет PowerMPI - все, с чем мне пришлось иметь дело. Конкретно это выражается в следующем: Следует заметить, что даже при фантастическом условии устранения брака в аппаратном и программном обеспечении компьютеры Парситек останутся весьма неполноценными для своей стоимости изделиями. Поскольку этот вопрос прямого отношения к теме документа все же не имеет, его доказательную часть здесь я опускаю.

Важно, что в итоге на Парситеке готовая программа работает неправильно. Модуль MPIintegrals всегда завершается с сообщением "core dumped". Сбой происходит в инструкции STOP, то есть при попытке нормального завершения - в самой последней команде. Это заведомо не моя ошибка. Сбой может принять одну из двух форм:

 

Как повысить скорость на Парситеке/CC

ЭВМ Парситек/CC является набором из независимых материнских плат, засунутых в один корпус, и соединенных скоростной внутримашинной сетью. Каждая плата оснащена своим жестким диском, с которого производится загрузка операционной системы. Однако Парикс-приложение этим диском не пользуется: Парикс перехватывает функции работы с файлами и перенаправляет их на диск входного узла. В-принципе, если ветви Crystal95 будут писать временные файлы на локальные диски узлов, а не через сеть на диск entry-узла, скорость работы должна сильно повыситься. Что для этого нужно:

 

Как я отлаживался

На Парситеке отлаживать параллельную программу невозможно: отладчик DeTop не запускается - и точка. IBM'овским отладчиком XLdb можно отлаживать только последовательные программы (без MPI и Парикса). Поэтому с какого-то момента весь проект был перетянут на SPP, где параллельный отладчик ЕСТЬ! Называется он CXdb. После Turbo Debugger'a выглядит бледновато, но при некотором навыке вполне пригоден для отладки любой степени сложности. Из всех известных мне отладчиков CXdb - единственный, умеющий отлаживать приложения, состоящие из нескольких процессов.

То, что SPP не был немедленно выбран платформой для разработки, привело к потере действительно большого количества времени. На Парситеке единственым инструментом отладки являлись проверочные выводы, которые требовалось скурпулезно составлять и расставлять (а если отладочный вывод тоже написан с ошибкой, а?). С отладчиком же все стало намного проще - я получил возможность "взламывать" задачу, как говорится, в лоб, заставляя ее работать методом грубой силы. Там, где многократно вызываемый кусок кода сбоил предположительно на поздних итерациях, перед отладкой в CXdb я запускал программу с проверочными выводами в этом куске; но реального толку в такой простой задаче от них немного.

SPP - самая популярная наша машина, при этом далеко не самая быстрая. Причина популярности - в аппаратной и программной надежности, и в исключительно широкой номенклатуре программных средств. На практике это сводится к тому, что на SPP редко бывает меньше 5-6 считающих пользователей, и машина сильно тормозит. На Парситеке ситуация обратная - больше одного пользователя не бывает; при этом сам компьютер находится на перманентном частичном ремонте.

Вопрос: значит ли это, что при покупке новых суперкомпьютеров я рекомендую ориентироваться на машины, подобные SPP ? Ответ: нет. Нашей казенной кормушке сейчас вообще должно быть не до покупки суперкомпьютеров, ни новых, ни старых, никаких.

 


Неформальные выводы

Чтобы выполнить перевод и последующую отладку, мне пришлось разбираться в коммуникационной части исходной программы гораздо более подробно, чем мне бы хотелось (и чем ее авторы того заслуживают). Причиной тому стала ее запутанность.

PVM и MPI - это инструменты-братья, схожие по смыслу, но, как показывает опыт, весьма различные в мелочах.

Первая же серьезная работа убедительно показала, насколько ублюдочной, нежизнеспособной конструкцией являются компьютеры Парситек со всех точек зрения; насколько несоизмеримы оказались затраты на его покупку (в валюте) и сопровождение (в валюте и в рублях) с тем мизерным эффектом, который приносит (кому-то? кому? право, не знаю) его эксплуатация.

Судя по всему, адаптирование Crystal95 с PVM на MPI и Парситек наши pykoвoдящие 0рганы рассматривают как некий пилотный проект, за которым должно последовать еще 15-20 таких же блатных работенок.

я же, со своей стороны, нахожу нелепым повторять подобный перевод с эстонского на армянский больше одного раза: правильнее один раз потратить два-три месяца на перенос PVM под Parix (и пусть Genias подавится, не надо покупать PowerPVM), чем двадцать раз по две недели на очередную отдельно взятую чушь. Вот только как это объяснить руководящим?

А если совсем коротко, то - это было бессмысленное, тягостное занятие...

 


Илья Евсеев, март 1999


Хостинг от uCoz