Версия для печати

Архив документации на OpenNet.ru / Раздел "Программирование, языки" (Многостраничная версия)

MPI для начинающих.

Учебное пособие + примеры

[ Автор: Илья Евсеев ]
[ Организация: ИВВиБД ]
[ Подразделение: ЦСТ ]
[ Оригинал документа на ilya-evseev.narod.ru ]

 


Оглавление

 


Краткие характеристики.

MPI - это стандарт на программный инструментарий для обеспечения связи между ветвями параллельного приложения.

MPI расшифровывается как "Message passing interface" ("Взаимодействие через передачу сообщений"). Несколько путает дело тот факт, что этот термин уже применяется по отношению к аппаратной архитектуре ЭВМ. Программный инструментарий MPI реализован в том числе и для ЭВМ с такой архитектурой.

MPI предоставляет программисту единый механизм взаимодействия ветвей внутри параллельного приложения независимо от машинной архитектуры (однопроцессорные / многопроцессорные с общей/раздельной памятью), взаимного расположения ветвей (на одном процессоре / на разных) и API операционной системы.
( API = "applications programmers interface" = "интерфейс разработчика приложений" )

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

В настоящее время разными коллективами разработчиков написано несколько программных пакетов, удовлетворяющих спецификации MPI, в частности: MPICH, LAM, HPVM и так далее. Они выступают базовыми при переносе MPI на новые архитектуры ЭВМ. Здесь в пособии рассматриваются разновидности MPICH. Это сделано по двум причинам:

Таким образом, далее по тексту термин MPI используется не только для обозначения изложенных в спецификации сведений, но и, в определенной мере, для описания характеристик конкретной базовой реализации. Увы.

Минимально в состав MPI входят: библиотека программирования (заголовочные и библиотечные файлы для языков Си, Си++ и Фортран) и загрузчик приложений.

Дополнительно включаются: профилирующий вариант библиотеки (используется на стадии тестирования параллельного приложения для определения оптимальности распараллеливания); загрузчик с графическим и сетевым интерфейсом для X-Windows и проч.

Структура каталогов MPICH выполнена в полном соответствии с традициями Юникса: bin, include, lib, man, src, ... Минимальный набор функций прост в освоении и позволяет быстро написать надежно работающую программу. Использование же всей мощи MPI позволит получить БЫСТРО работающую программу - при сохранении надежности.

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

  1. вводно-философская часть: сравнительные характеристики MPI, его плюсы и минусы, сильные и слабые стороны, прошлое и будущее, стихи и проза, лед и пламень, ...
  2. руководство пользователя по нескольким реализациям MPI: MPICH, WinMPICH, WMPI, PowerMPI и HPMPI; ссылки на документацию, в т.ч. по MPICH
  3. руководство программиста - оставлено здесь. Рекомендуется читать их именно в таком порядке. Некоторые сведения, к сожалению, повторяются, потому что разделение первоначального текста на части было выполнено несколько поспешно.

     


    Соглашения о терминах.

    Параллельное приложение состоит из нескольких ветвей, или процессов, или задач, выполняющихся одновременно. Разные процессы могут выполняться как на разных процессорах, так и на одном и том же - для программы это роли не играет, поскольку в обоих случаях механизм обмена данными одинаков. Процессы обмениваются друг с другом данными в виде сообщений. Сообщения проходят под  идентификаторами, которые позволяют программе и библиотеке связи отличать их друг от друга. Для совместного проведения тех или иных расчетов процессы внутри приложения объединяются в группы. Каждый процесс может узнать у библиотеки связи свой номер внутри группы, и, в зависимости от номера приступает к выполнению соответствующей части расчетов.

    Термин "процесс" используется также в Юниксе, и здесь нет путаницы: в MPI ветвь запускается и работает как обычный процесс Юникса, связанный через MPI с остальными процессами, входящими в приложение. В остальном процессы следует считать изолированными друг от друга: у них разные области кода, стека и данных (короче, смотрите описание Юниксовских процессов). Говорят, что процессы имеют раздельную память (separate memory).

    Особенность MPI: понятие области связи (communication domains). При запуске приложения все процессы помещаются в создаваемую для приложения общую область связи. При необходимости они могут создавать новые области связи на базе существующих. Все области связи имеют независимую друг от друга нумерацию процессов. Программе пользователя в распоряжение предоставляется коммуникатор  - описатель области связи. Многие функции MPI имеют среди входных аргументов коммуникатор, который ограничивает сферу их действия той областью связи, к которой он прикреплен. Для одной области связи может существовать несколько коммуникаторов таким образом, что приложение будет работать с ней как с несколькими разными областями. В исходных текстах примеров для MPI часто используется идентификатор MPI_COMM_WORLD. Это название коммуникатора, создаваемого библиотекой автоматически. Он описывает стартовую область связи, объединяющую все процессы приложения.

     


    Категории функций: блокирующие, локальные, коллективные.

    В manual pages особо подчеркивается принадлежность описываемой функции к той или иной категории. При первом чтении этот раздел можно пропустить, возвращаясь к нему по мере ознакомления с документацией. Вкратце:

    Блокирующие - останавливают (блокируют) выполнение процесса до тех пор, пока производимая ими операция не будет выполнена. Неблокирующие функции возвращают управление немедленно, а выполнение операции продолжается в фоновом режиме; за завершением операции надо проследить особо. Неблокирующие функции возвращают квитанции ("requests"), которые погашаются при завершении. До погашения квитанции с переменными и массивами, которые были аргументами неблокирующей функции, НИЧЕГО ДЕЛАТЬ НЕЛЬЗЯ.

    Локальные - не инициируют пересылок данных между ветвями. Большинство информационных функций является локальными, т.к. копии системных данных уже хранятся в каждой ветви. Функция передачи MPI_Send и функция синхронизации MPI_Barrier НЕ являются локальными, поскольку производят пересылку. Следует заметить, что, к примеру, функция приема MPI_Recv (парная для MPI_Send) является локальной: она всего лишь пассивно ждет поступления данных, ничего не пытаясь сообщить другим ветвям.

    Коллективные - должны быть вызваны ВСЕМИ ветвями-абонентами того коммуникатора, который передается им в качестве аргумента. Несоблюдение для них этого правила приводит к ошибкам на стадии выполнения программы (как правило, к повисанию).

     


    Принятая в MPI нотация записи.

    Регистр букв : важен в Си, не играет роли в Фортране.

    Все идентификаторы начинаются с префикса "MPI_". Это правило без исключений. Не рекомендуется заводить пользовательские идентификаторы, начинающиеся с этой приставки, а также с приставок "MPID_", "MPIR_" и "PMPI_", которые используются в служебных целях.

    Если идентификатор сконструирован из нескольких слов, слова в нем разделяются подчерками: MPI_Get_count, MPI_Comm_rank. Иногда, однако, разделитель не используется: MPI_Sendrecv, MPI_Alltoall.

    Порядок слов в составном идентификаторе выбирается по принципу "от общего к частному": сначала префикс "MPI_", потом название категории ( Type, Comm, Group, Attr, Errhandler и т.д.), потом название операции ( MPI_Errhandler_create, MPI_Errhandler_set, ...). Наиболее часто употребляемые функции выпадают из этой схемы: они имеют "анти-методические", но короткие и стереотипные названия, например MPI_Barrier, или MPI_Unpack.

    Имена констант (и неизменяемых пользователем переменных) записываются полностью заглавными буквами: MPI_COMM_WORLD, MPI_FLOAT. В именах функций первая за префиксом буква - заглавная, остальные маленькие: MPI_Send, MPI_Comm_size.

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

     


    MPI для Фортрана.

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

    Вот пример на Фортране, взятый из MPI-UX. Надеюсь, Convex простит мне этот маленький плагиат.

     


    Обрамляющие функции. Начало и завершение.

    Существует несколько функций, которые используются в любом, даже самом коротком приложении MPI. Занимаются они не столько собственно передачей данных, сколько ее обеспечением:

    1. Инициализация библиотеки. Одна из первых инструкций в функции main (главной функции приложения):
          MPI_Init( &argc, &argv );
      Она получает адреса аргументов, стандартно получаемых самой main от операционной системы и хранящих параметры командной строки. В конец командной строки программы MPI-загрузчик mpirun добавляет ряд информационных параметров, которые требуются MPI_Init. Это показывается в примере 0.

    2. Аварийное закрытие библиотеки. Вызывается, если пользовательская программа завершается по причине ошибок времени выполнения, связанных с MPI:
          MPI_Abort( описатель области связи, код ошибки MPI ); 
      Вызов MPI_Abort из любой задачи принудительно завершает работу ВСЕХ задач, подсоединенных к заданной области связи. Если указан описатель MPI_COMM_WORLD, будет завершено все приложение (все его задачи) целиком, что, по-видимому, и является наиболее правильным решением. Используйте код ошибки MPI_ERR_OTHER, если не знаете, как охарактеризовать ошибку в классификации MPI.

    3. Нормальное закрытие библиотеки:
          MPI_Finalize();
      Настоятельно рекомендуется не забывать вписывать эту инструкцию перед возвращением из программы, то есть:
      • перед вызовом стандартной функции Си exit ;
      • перед каждым после MPI_Init оператором return в функции main ;
      • если функции main назначен тип void, и она не заканчивается оператором return, то MPI_Finalize() следует поставить в конец main.

    4. Две информационных функции: сообщают размер группы (то есть общее количество задач, подсоединенных к ее области связи) и порядковый номер вызывающей задачи:
          int size, rank;
          MPI_Comm_size( MPI_COMM_WORLD, &size );
          MPI_Comm_rank( MPI_COMM_WORLD, &rank );

    Использование MPI_Init, MPI_Finalize, MPI_Comm_size и MPI_Comm_rank демонстрирует пример 0. Использование MPI_Abort будет показано далее, в примере 1.

     


    Связь "точка-точка". Простейший набор.

    Это самый простой тип связи между задачами: одна ветвь вызывает функцию передачи данных, а другая -  функцию приема. В MPI это выглядит, например, так:

    Аргументы функций:

    1. Адрес буфера, из которого в задаче 1 берутся, а в задаче 2 помещаются данные. Помните, что наборы данных у каждой задачи свои, поэтому, например, используя одно и то же имя массива в нескольких задачах, Вы указываете не одну и ту же область памяти, а разные, никак друг с другом не связанные.

    2. Размер буфера. Задается не в байтах, а в количестве ячеек. Для MPI_Send указывает, сколько ячеек требуется передать (в примере передаются 5 чисел). В MPI_Recv означает максимальную емкость приемного буфера. Если фактическая длина пришедшего сообщения меньше - последние ячейки буфера останутся нетронутыми, если больше - произойдет ошибка времени выполнения.

    3. Тип ячейки буфера. MPI_Send и MPI_Recv оперируют массивами однотипных данных. Для описания базовых типов Си в MPI определены константы MPI_INT, MPI_CHAR, MPI_DOUBLE и так далее, имеющие тип MPI_Datatype. Их названия образуются префиксом "MPI_" и именем соответствующего типа (int, char, double, ...), записанным заглавными буквами. Пользователь может "регистрировать" в MPI свои собственные типы данных, например, структуры, после чего MPI сможет обрабатывать их наравне с базовыми. Процесс регистрации описывается в главе "Типы данных".

    4. Номер задачи, с которой происходит обмен данными. Все задачи внутри созданной MPI группы автоматически нумеруются от 0 до (размер группы-1). В примере задача 0 передает задаче 1, задача 1 принимает от задачи 0.

    5. Идентификатор сообщения. Это целое число от 0 до 32767, которое пользователь выбирает сам. Оно служит той же цели, что и, например, расширение файла - задача-приемник:
      1. по идентификатору определяет смысл принятой информации ;
      2. сообщения, пришедшие в неизвестном порядке, может извлекать из общего входного потока в нужном алгоритму порядке. Хорошим тоном является обозначение идентификаторов символьными именами посредством операторов "#define" или "const int".

    6. Описатель области связи (коммуникатор). Обязан быть одинаковым для MPI_Send и MPI_Recv.

    7. Статус завершения приема. Содержит информацию о принятом сообщении: его идентификатор, номер задачи-передатчика, код завершения и количество фактически пришедших данных.

     


    Подробнее об "MPI_Status" - статусе завершения приема.

    С одной стороны, мы передаем в MPI_Recv номер задачи, от которой ждем сообщение, и его идентификатор; а с другой - получаем их от MPI в структуре status? Это сделано потому, что MPI_Recv может быть вызвана с аргументами-джокерами ("принимай что угодно/от кого угодно"), и после такого приема данных программа узнает фактические номер/идентификатор, читая поля MPI_SOURCE и MPI_TAG из структуры status.

    Поле MPI_ERROR, как правило, проверять необязательно - обработчик ошибок, устанавливаемый MPI по умолчанию, в случае сбоя завершит выполнение программы ДО возврата из MPI_Recv. Таким образом, после возврата из MPI_Recv поле status.MPI_ERROR может быть равно только 0 (или, если угодно, MPI_SUCCESS);

    Тип MPI_Status не содержит поля, в которое записывалась бы фактическая длина пришедшего сообщения. Длину можно узнать так:

        MPI_Status status;
        int count;
        MPI_Recv( ... ,  MPI_INT, ... , &status );
        MPI_Get_count( &status, MPI_INT, &count );
        /* ... теперь count содержит количество принятых ячеек */ 
    Обратите внимание, что аргумент-описатель типа у MPI_Recv и MPI_Get_count должен быть одинаковым, иначе, в зависимости от реализации:

     


    Как узнать размер сообщения
    ДО помещения его в приемный буфер?

    Итак, по возвращении из MPI_Recv поля структуры status содержат информацию о принятом сообщении, а функция MPI_Get_count возвращает количество фактически принятых данных. Однако имеется еще одна функция, которая позволяет узнать о характеристиках сообщения ДО того, как сообщение будет помещено в приемный пользовательский буфер: MPI_Probe. За исключением адреса и размера пользовательского буфера, она имеет такие же параметры, как и MPI_Recv. Она возвращает заполненную структуру MPI_Status и после нее можно вызвать MPI_Get_count. Стандарт MPI гарантирует, что следующий за MPI_Probe вызов MPI_Recv с теми же параметрами (имеются в виду номер задачи-передатчика, идентификатор сообщения и коммуникатор) поместит в буфер пользователя именно то сообщение, которое было принято функцией MPI_Probe. MPI_Probe нужна в двух случаях:

    1. Когда задача-приемник не знает заранее длины ожидаемого сообщения. Пользовательский буфер заводится в динамической памяти:
      MPI_Probe( MPI_ANY_SOURCE, tagMessageInt, MPI_COMM_WORLD, &status );
       /* MPI_Probe вернет управление после того как примет */
       /* данные в системный буфер */
      MPI_Get_count( &status, MPI_INT, &bufElems );
      buf = malloc( sizeof(int) * bufElems );
      MPI_Recv( buf, bufElems, MPI_INT, ...
       /* ... дальше параметры у MPI_Recv такие же, как в MPI_Probe ); */
       /* MPI_Recv останется просто скопировать */
       /* данные из системного буфера в пользовательский */ 
      Вместо этого, конечно, можно просто завести на приемной стороне буфер заведомо большой, чтобы вместить в себя самое длинное из возможных сообщений, но такой стиль не является оптимальным, если длина сообщений "гуляет" в слишком широких пределах.

    2. Когда задача-приемник собирает сообщения от разных отправителей с содержимым разных типов. Без MPI_Probe порядок извлечения сообщений в буфер пользователя должен быть задан в момент компиляции:
      MPI_Recv( floatBuf, floatBufSize, MPI_FLOAT, MPI_ANY_SOURCE, tagFloatData, ... );
      MPI_Recv( intBuf,   intBufSize,   MPI_INT,   MPI_ANY_SOURCE, tagIntData,   ... );
      MPI_Recv( charBuf,  charBufSize,  MPI_CHAR,  MPI_ANY_SOURCE, tagCharData,  ... ); 
      Теперь, если в момент выполнения сообщение с идентификатором tagCharData придет раньше двух остальных, MPI будет вынужден "законсервировать" его на время выполнения первых двух вызовов MPI_Recv. Это чревато непроизводительными расходами памяти. MPI_Probe позволит задать порядок извлечения сообщений в буфер пользователя равным порядку их поступления на принимающую сторону, делая это не в момент компиляции, а непосредственно в момент выполнения:
      for( i=0; i<3; i++ ) {
          MPI_Probe( MPI_ANY_SOURCE,MPI_ANY_TAG,MPI_COMM_WORLD,&status );
          switch( status.MPI_TAG ) {
              case tagFloatData:
                  MPI_Recv( floatBuf, floatBufSize, MPI_FLOAT, ... );
                  break;
              case tagIntData:
                  MPI_Recv( intBuf, intBufSize, MPI_INT, ... );
                  break;
              case tagCharData:
                  MPI_Recv( charBuf, charBufSize, MPI_CHAR, ... );
                  break;
          } /* конец switch */
      } /* конец for */ 

    Многоточия здесь означают, что последние 4 параметра у MPI_Recv такие же, как и у предшествующей им MPI_Probe.
    Использование MPI_Probe продемонстрировано в
    примере 2.

     


    Джокеры.

    В примере 2 используются два джокера: MPI_ANY_SOURCE для номера задачи-отправителя ("принимай от кого угодно") и MPI_ANY_TAG для идентификатора получаемого сообщения ("принимай что угодно"). MPI резервирует для них какие-то отрицательные целые числа, в то время как реальные идентификаторы задач и сообщений лежат всегда в диапазоне от 0 до 32767. Пользоваться джокерами следует с осторожностью, потому что по ошибке таким вызовом MPI_Recv может быть захвачено сообщение, которое должно приниматься в другой части задачи-получателя.

    Если логика программы достаточно сложна, использовать джокеры можно ТОЛЬКО в функциях MPI_Probe и MPI_Iprobe, чтобы перед фактическим приемом узнать тип и количество данных в поступившем сообщении (вообще-то, можно принимать, и не зная количества - был бы приемный буфер достаточно вместительным, но тип для MPI_Recv надо указывать явно - а он может быть разным в сообщениях с разными идентификаторами).

    Достоинство джокеров: приходящие сообщения извлекаются по мере поступления, а не по мере вызова MPI_Recv с нужными идентификаторами задач/сообщений. Это экономит память и увеличивает скорость работы.

     


    Два в одном флаконе: MPI_Sendrecv.

    Некоторые конструкции с приемо-передачей применяются очень часто:

    Ситуация настолько распространенная, что в MPI специально введены две функции, осуществляющие одновременно посылку одних данных и прием других. Первая из них - MPI_Sendrecv. Ее прототип содержит 12 параметров: первые 5 параметров такие же, как у MPI_Send, остальные 7 параметров такие же как у MPI_Recv. Один ее вызов проделывает те же действия, для которых в первом фрагменте требуется блок IF-ELSE с четырьмя вызовами. Следует учесть, что:

    MPI_Sendrecv_replace помимо общего коммуникатора использует еще и общий для приема-передачи буфер. Не очень удобно, что параметр count получает двойное толкование: это и количество отправляемых данных, и предельная емкость входного буфера. Показания к применению:

    MPI_Sendrecv_replace так же гарантированно не вызывает клинча.

    Что такое клинч? Дальше следует краткая иллюстрация этой ошибки, очень распространенной там, где для пересылок используется разделяемая память.

    Вариант 1:

        -- Ветвь 1 --          -- Ветвь 2 --
        Recv( из ветви 2 )     Recv( из ветви 1 )
        Send( в ветвь 2 )      Send( в ветвь 1 ) 

    Вариант 1 вызовет клинч, какой бы инструментарий не использовался: функция приема не вернет управления до тех пор, пока не получит данные; поэтому функция передачи не может приступить к отправке данных; поэтому функция приема... и так до самого SIG_KILL;)

    Вариант 2:

        -- Ветвь 1 --          -- Ветвь 2 --
        Send( в ветвь 2 )      Send( в ветвь 1 )
        Recv( из ветви 2 )     Recv( из ветви 1 ) 

    Вариант 2 вызовет клинч, если функция передачи возвращает управление только после того, как данные попали в пользовательский буфер на приемной стороне. Скорее всего, именно так и возьмется реализовывать передачу через разделяемую память/семафоры программист-проблемщик.

    Однако при использовании MPI зависания во втором варианте не произойдет! MPI_Send, если на приемной стороне нет готовности (не вызван MPI_Recv), не станет ее дожидаться, а положит данные во временный буфер и вернет управление программе НЕМЕДЛЕННО. Когда MPI_Recv будет вызван, данные он получит не из пользовательского буфера напрямую, а из промежуточного системного. Буферизация - дело громоздкое - может быть, и не всегда сильно экономит время (особенно на SMP-машинах), зато повышает надежность: делает программу более устойчивой к ошибкам программиста.

    MPI_Sendrecv и MPI_Sendrecv_replace также делают программу более устойчивой: с их использованием программист лишается возможности перепутать варианты 1 и 2.

     


    Зачем MPI знать тип передаваемых данных?

    Действительно, зачем? Стандартные функции пересылки данных, например, memcpy, прекрасно обходятся без подобной информации - им требуется знать только размер в байтах. Вместо одного такого аргумента функции MPI получают два: количество элементов некоторого типа и символический описатель указанного типа (MPI_INT, и т.д.). Причин тому несколько:

    1. Пользователю MPI позволяет описывать свои собственные типы данных, которые располагаются в памяти не непрерывно, а с разрывами, или наоборот, с "налезаниями" друг на друга. Переменная такого типа характеризуется не только размером, и эти характеристики MPI хранит в описателе типа. В учебнике по MPI приведен пример конструирования сложного типового шаблона путем последовательного создания нескольких пользовательских типов. Затем производится транспонирование матрицы через вызов MPI_Sendrecv, где для передачи в качестве описателя используется указанный шаблон.

    2. Приложение MPI может работать на гетерогенном вычислительном комплексе (коллективе ЭВМ с разной архитектурой). Одни и те же типы данных на разных машинах могут иметь разное представление, например: на плавающую арифметику существует 3 разных стандарта (IEEE,IBM,Cray); тип char в терминальных приложениях Windows представлен альтернативной кодировкой ГОСТ, а в Юниксе - кодировкой KOI-8r ; ориентация байтов в многобайтовых числах на ЭВМ с процессорами Intel отличается от общепринятой (у Intel - младший байт занимает младший адрес, у всех остальных - наоборот). Если приложение работает в гетерогенной сети, через сеть задачи обмениваются данными в формате XDR (eXternal Data Representation), принятом в Internet. Перед отправкой и после приема данных задача конвертирует их в/из формата XDR. Естественно, при этом MPI должен знать не просто количество передаваемых байт, но и тип содержимого.

    3. Обязательным требованием к MPI была поддержка языка Фортран в силу его инерционной популярности. Фортрановский тип CHARACTER требует особого обращения, поскольку переменная такого типа содержит не собственно текст, а адрес текста и его длину. Функция MPI, получив адрес переменной, должна извлечь из нее адрес текста и копировать сам текст. Это и произойдет, если в поле аргумента-описателя типа стоит MPI_CHARACTER. Ошибка в указании типа приведет: при отправке - к копированию служебных данных вместо текста, при приеме - к записи текста на место служебных данных. И то, и другое приводит к ошибкам времени выполнения.

    4. Такие часто используемые в Си типы данных, как структуры, могут содержать в себе некоторое пустое пространство, чтобы все поля в переменной такого типа размещались по адресам, кратным некоторому четному числу (часто 2, 4 или 8) - это ускоряет обращение к ним. Причины тому чисто аппаратные. Выравнивание данных настраивается ключами компилятора. Разные задачи одного и того же приложения, выполняющиеся на одной и той же машине (даже на одном и том же процессоре), могут быть построены с разным выравниванием, и типы с одинаковым текстовым описанием будут иметь разное двоичное представление. MPI будет вынужден позаботиться о правильном преобразовании. Например, переменные такого типа могут занимать 9 или 16 байт:
          typedef struct {
              char    c;
              double  d;
          } CharDouble; 

    Создание собственных типов описывается дальше по тексту.

     


    Неэффективная передача разнотипных данных.

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

    Вариант 1 (настолько тупой и медленный, что никогда не применяется). Каждый элемент в разнотипном наборе данных посылается отдельно:

        #define msgTag 10
        struct {
            int   i;
            float f[4];
            char  c[8];
        } s;
    
        MPI_Send(&s.i, 1, MPI_INT,   targetRank, msgTag,   MPI_COMM_WORLD );
        MPI_Send( s.f, 4, MPI_FLOAT, targetRank, msgTag+1, MPI_COMM_WORLD );
        MPI_Send( s.c, 8, MPI_CHAR,  targetRank, msgTag+2, MPI_COMM_WORLD ); 
    ... и на приемной стороне столько же раз вызывается MPI_Recv.

    Вариант 2 ("классический"). Функция приема/передачи вызывается один раз, но до/после нее многократно вызывается функция упаковки/распаковки:

        Передача
        ========
        int bufPos = 0;
        char tempBuf[ sizeof(s) ];
        MPI_Pack(&s.i, 1, MPI_INT,   tempBuf, sizeof(tempBuf), &bufPos, MPI_COMM_WORLD );
        MPI_Pack( s.f, 4, MPI_FLOAT, tempBuf, sizeof(tempBuf), &bufPos, MPI_COMM_WORLD );
        MPI_Pack( s.c, 8, MPI_CHAR,  tempBuf, sizeof(tempBuf), &bufPos, MPI_COMM_WORLD );
        MPI_Send( tempBuf, bufPos, MPI_BYTE, targetRank, msgTag, MPI_COMM_WORLD );
    
        Прием
        =====
        int bufPos = 0;
        char tempBuf[ sizeof(s) ];
        MPI_Recv( tempBuf, sizeof(tempBuf), MPI_BYTE, sourceRank, msgTag,
            MPI_COMM_WORLD, &status );
        MPI_Unpack( tempBuf, sizeof(tempBuf), &bufPos,&s.i, 1, MPI_INT,  MPI_COMM_WORLD);
        MPI_Unpack( tempBuf, sizeof(tempBuf), &bufPos, s.f, 4, MPI_FLOAT,MPI_COMM_WORLD);
        MPI_Unpack( tempBuf, sizeof(tempBuf), &bufPos, s.c, 8, MPI_CHAR, MPI_COMM_WORLD); 

    Вариант 2 обозван здесь классическим, потому что пришел в MPI из PVM, где предлагается в качестве единственного. Он прост в понимании, за что его все и любят. Замечания по применению:

    1. MPI_BYTE - это особый описатель типа; который не описывает тип данных для конкретного языка программирования (в Си он ближе всего к unsigned char). Использование MPI_BYTE означает, что содержимое соответствующего массива НЕ ДОЛЖНО подвергаться НИКАКИМ преобразованиям - и на приемной, и на передающей стороне массив будет иметь одну и ту же длину и одинаковое ДВОИЧНОЕ представление.

    2. Зачем функциям упаковки/распаковки требуется описатель области связи? Описатель, помимо прочего, несет в себе информацию о распределении подсоединенных к области связи задач по процессорам и компьютерам. Если процессоры одинаковые, или задачи выполняются на одном и том же процессоре, данные просто копируются, иначе происходит их преобразование в/из формата XDR (eXternal Data Representation - разработан фирмой Sun Microsystems, используется в Интернете для взаимодействия разнотипных машин). Учтите, что коммуникаторы у функций упаковки/распаковки и у соответствующей функции передачи/приема должны совпадать, иначе произойдет ошибка ;

    3. По мере того как во временный буфер помещаются данные или извлекаются оттуда, MPI сохраняет текущую позицию в переменной, которая в приведенном примере названа bufPos. Не забудьте проинициализировать ее нулем перед тем как начинать упаковывать/извлекать. Естественно, что передается она не по значению, а по ссылке. Первый же аргумент - "адрес временного буфера" - во всех вызовах остается неизменным ;

    4. В примере НЕКОРРЕКТНО выбран размер временного буфера: использовалось НЕВЕРНОЕ предположение, что в XDR-формате данные займут места не больше, чем в формате используемого ветвью процессора; или что XDR-преобразование заведомо не будет применено. Правильным же решением будет для определения необходимого размера временного буфера на приемной стороне использовать связку MPI_Probe / MPI_Get_count / MPI_Recv, а на передающей - функцию MPI_Pack_size:
          int bufSize = 0;
          void *tempBuf;
          MPI_Pack_size( 1, MPI_INT,   MPI_COMM_WORLD, &bufSize );
          MPI_Pack_size( 4, MPI_FLOAT, MPI_COMM_WORLD, &bufSize );
          MPI_Pack_size( 8, MPI_CHAR,  MPI_COMM_WORLD, &bufSize );
          tempBuf = malloc( bufSize );
          /* ... теперь можем упаковывать, не опасаясь переполнения */ 
    Однако и вариант 2 замедляет работу: по сравнению с единственным вызовом memcpy на SMP-машине или одном процессоре занудная упаковка/распаковка - дело весьма небыстрое!

    Вариант 3 ("жульнический"). Если есть уверенность, что одни и те же типы данных в обеих ветвях приложения имеют одинаковое двоичное представление, то:

        Передача:  MPI_Send( &s, sizeof(s), MPI_BYTE, ... );
        Прием:     MPI_Recv( &s, sizeof(s), MPI_BYTE, ... ); 

    А все, чем чреват такой подход, подробно перечислено в предыдущей главе.

     


    Создание и использование собственных типов данных.

    Общие правила:

    При первом прочтении можете пропустить описание конструкторов MPI_Type_vector и MPI_Type_indexed, сразу перейдя к MPI_Type_struct.

    MPI_Type_contiguous : самый простой конструктор типа, он создает описание массива. В следующем примере оба вызова MPI_Send делают одно и то же.

        int a[16];
        MPI_Datatype intArray16;
        MPI_Type_contiguous( 16, MPI_INT, &intArray16 );
        MPI_Type_commit( &intArray16 );
    
        MPI_Send( a, 16, MPI_INT, ... );
        MPI_Send( a, 1, intArray16, ... );
    
        MPI_Type_free( &intArray16 ); 
    Функция MPI_Type_count вернет количество ячеек в переменной составного типа: после MPI_Type_count( intArray16, &count ) значение count станет равным 16. Как правило, прямой необходимости использовать эти функции нет, и тем не менее.

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

    MPI_Type_vector(
      int count,       /* количество элементов в новом типе */
      int blocklength, /* количество ячеек базового типа в одном элементе */
      int stride,      /* расстояние между НАЧАЛАМИ эл-тов, в числе ячеек */
      MPI_Datatype oldtype,  /* описатель базового типа, т.е. типа ячейки */
      MPI_Datatype &newtype  /* cсылка на новый описатель */
    ); 
    То есть:
    1. новый тип состоит из элементов;
    2. каждый элемент является массивом ячеек базового типа;
    3. расстояние в количестве ячеек задается между НАЧАЛАМИ элементов, а НЕ между КОНЦОМ предыдущего и НАЧАЛОМ следующего; таким образом, элементы могут и располагаться с разрывами, и "налезать" друг на друга.
    Функция MPI_Type_hvector полностью ей аналогична, за одним исключением: расстояние между элементами задается не в количестве ячеек базового типа, а в байтах. В примере показывается применение обеих этих функций.

    MPI_Type_indexed : расширение "векторного" описателя; длины массивов и расстояния между ними теперь не фиксированы, а у каждого массива свои. Соответственно, аргументы #2 и #3 здесь - не переменные, а массивы: массив длин и массив позиций.

    Пример: создание шаблона для выделения верхней правой части матрицы.

        #define  SIZE  100
        float a[ SIZE ][ SIZE ];
        int pos[ SIZE ]
        int len[ SIZE ];
        MPI_Datatype upper;
        ...
        for( i=0; i<SIZE; i++ ) {    /*   xxxxxx   */
            pos[i] = SIZE*i + i;     /*   .xxxxx   */
            len[i] = SIZE - i;       /*   ..xxxx   */
        }                            /*   ...xxx   */
    
        MPI_Type_indexed(
            SIZE,   /* количество массивов в переменной нового типа */
            len,    /* длины этих массивов */
            pos,    /* их позиции от начала переменной, */
                    /* отсчитываемые в количестве ячеек */
            MPI_FLOAT,  /* тип ячейки массива */
            &upper );
        MPI_Type_commit( &upper );
    
         /* Поступающий поток чисел типа 'float' будет
          * размещен в верхней правой части матрицы 'a'
          */
        MPI_Recv( a, 1, upper, .... ); 
    Аналогично работает функция MPI_Type_hindexed, но позиции массивов от начала переменной задаются не в количестве ячеек базового типа, а в байтах.

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

    MPI_Type_struct(
        count,               /* количество полей */
        int *len,            /* массив с длинами полей */
                             /* (на тот случай, если это массивы) */
        MPI_Aint *pos,       /* массив со смещениями полей */
                             /* от начала структуры, в байтах */
        MPI_Datatype *types, /* массив с описателями типов полей */
        MPI_Datatype *newtype ); /* ссылка на создаваемый тип */ 

    Здесь используется тип MPI_Aint: это просто скалярный тип, переменная которого имеет одинаковый с указателем размер. Введен он исключительно для единообразия с Фортраном, в котором нет типа "указатель". По этой же причине имеется и функция MPI_Address: в Си она не нужна (используются оператор вычисления адреса & и основанный на нем макрос offsetof() ); а в Фортране оператора вычисления адреса нет, и используется MPI_Address.

    Пример создания описателя типа "структура":

    #include <stddef.h>  /* подключаем макрос 'offsetof()' */
    
    typedef struct {
        int    i;
        double d[3];
        long   l[8];
        char   c;
    } AnyStruct;
    
    AnyStruct st;
    
    MPI_Datatype anyStructType;
    
    int          len[5] = { 1, 3, 8, 1, 1 };
    MPI_Aint     pos[5] = { offsetof(AnyStruct,i), offsetof(AnyStruct,d),
                            offsetof(AnyStruct,l), offsetof(AnyStruct,c),
                            sizeof(AnyStruct) };
    MPI_Datatype typ[5] = { MPI_INT,MPI_DOUBLE,MPI_LONG,MPI_CHAR,MPI_UB };
    
    MPI_Type_struct( 5, len, pos, typ, &anyStructType );
    MPI_Type_commit( &anyStructType );
      /* подготовка закончена */
    
    MPI_Send( st, 1, anyStructType, ... ); 

    Обратите внимание: структура в примере содержит 4 поля, а массивы для ее описания состоят из 5 элементов. Сделано это потому, что MPI должен знать не только смещения полей, но и размер всей структуры. Для этого и служит псевдотип MPI_UB ("upper bound").
    Адрес начала структуры и адрес ее первого поля, как правило, совпадают, но если это не так: нулевым элементом массива typ должен быть MPI_LB.

    MPI_Type_extent и MPI_Type_size : важные информационные функции. Их характеристики удобно представить в виде таблицы:

    Вид данных sizeof MPI_Type_extent MPI_type_size
    стандартный тип равносильны
    массив равносильны
    структура равносильны sizeof(поле1)+sizeof(поле2)+...
    Описатель типа MPI с перекрытиями и разрывами не определена адрес последней ячейки данных -
    адрес первой ячейки данных +
    sizeof(последней ячейки данных)
    sizeof(первой ячейки данных) +
    sizeof(второй ячейки данных) + ...

    Можно сказать, что MPI_Type_extent сообщает, сколько места переменная типа занимает при хранении в памяти, а MPI_Type_size - какой МИНИМАЛЬНЫЙ размер она будет иметь при передаче (ужатая за счет неиспользуемого пространства). MPI_Type_size отсутствует в примере, потому что закомментирована в WinMPICH - в binding.h сказано, что стандарт на нее сформулирован неправильно.
    В Фортране их придется использовать постоянно ввиду отсутствия sizeof (в Фортране чего не хватишься - того и нет).

     


    Коллективные функции.

    Под термином "коллективные" в MPI подразумеваются три группы функций:

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

    Как поступить, если требуется ограничить область действия для коллективной функции только частью присоединенных к коммуникатору задач, или наоборот - расширить область действия? Создавайте временную группу/область связи/коммуникатор на базе существующих, как это показано в разделе про коммуникаторы.

     


    Точки синхронизации, они же барьеры.

    Этим занимается всего одна функция:

        int MPI_Barrier( MPI_Comm comm ); 

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

    Это единственная в MPI функция, вызовами которой гарантированно синхронизируется во времени выполнение различных ветвей! Некоторые другие коллективные функции в зависимости от реализации могут обладать, а могут и не обладать свойством одновременно возвращать управление всем ветвям; но для них это свойство является побочным и необязательным - если Вам нужна синхронность, используйте только MPI_Barrier.

    Когда может потребоваться синхронизация? В примерах синхронизация используется перед аварийным завершением: там ветвь 0 рапортует об ошибке, и чтобы ни одна из оставшихся ветвей вызовом MPI_Abort не завершила нулевую досрочно-принудительно, перед MPI_Abort поставлен барьер.

    Это утверждение непроверено, но: АЛГОРИТМИЧЕСКОЙ необходимости в барьерах, как представляется, нет. Параллельный алгоритм для своего описания требует по сравнению с алгоритмом классическим всего лишь двух дополнительных операций - приема и передачи из ветви в ветвь. Точки синхронизации несут чисто технологическую нагрузку вроде той, что описана в предыдущем абзаце.

    Иногда случается, что ошибочно работающая программа перестает врать, если ее исходный текст хорошенько нашпиговать барьерами. Как правило, барьерами нивелируются ошибки под кодовым названием "гонки" (в англоязычной литературе используется термин "backmasking"; я НЕ уверен, что под этими терминами понимается строго одно и то же). Однако программа начнет работать медленнее, например:

                     ветвь
    Без барьеров:      0    xxxx....xxxxxxxxxxxxxxxxxxxx
                       1    xxxxxxxxxxxx....xxxxxxxxxxxx
                       2    xxxxxxxxxxxxxxxxxxxxxx....xx
    
    Воткнем барьеры:   0    xxxx....xx(xxxxxxxx(||||xxxxxxxx(||xx
                       1    xxxxxx(||||x....xxxxxxx(xxxxxxxx(||xx
                       2    xxxxxx(||||xxxxxxxx(||||..xxxxxxxx(xx
                    
                            ----------------------------- > Время
    Обозначения:
        x   нормальное выполнение
        .   ветвь простаивает - процессорное время отдано под другие цели
        (   вызван MPI_Barrier
        |   MPI_Barrier ждет своего вызова в остальных ветвях 

    Так что "задавить" ошибку барьерами хорошо только в качестве временного решения на период отладки.

     


    Функции коллективного обмена данными.

    Основные особенности и отличия от коммуникаций типа "точка-точка":

    MPI_Bcast рассылает содержимое буфера из задачи, имеющей в указанной области связи номер root, во все остальные:

        MPI_Bcast( buf, count, dataType, rootRank, communicator ); 

    Она эквивалентна по результату (но не по внутреннему устройству) следующему фрагменту:

        MPI_Comm_size( communicator, &commSize );
        MPI_Comm_rank( communicator, &myRank );
        if( myRank == rootRank )
            for( i=0; i<commSize; i++ )
                MPI_Send( buf, count, dataType, i,
                    tempMsgTag, communicator );
        MPI_Recv( buf, count, dataType, rootRank, tempMsgTag,
            communicator, &status ); 

    MPI_Gather ("совок") собирает в приемный буфер задачи root передающие буфера остальных задач. Ее аналог:

        MPI_Send( sendBuf, sendCount, sendType, rootRank, ... );
        if( myRank == rootRank ) {
            MPI_Type_extent( recvType, &elemSize );
            for( i=0; i<commSize; i++ )
                MPI_Recv( ((char*))recvBuf) + (i * recvCount * elemSize),
                    recvCount, recvType, i, ... );
        } 
    Заметьте, что а) recvType и sendType могут быть разные и, таким образом, будут задавать разную интерпретацию данных на приемной и передающей стороне; б) задача-приемник также отправляет данные в свой приемный буфер.

    Векторный вариант "совка" - MPI_Gatherv - позволяет задавать РАЗНОЕ количество отправляемых данных в разных задачах-отправителях. Соответственно, на приемной стороне задается массив позиций в приемном буфере, по которым следует размещать поступающие данные, и максимальные длины порций данных от всех задач. Оба массива содержат позиции/длины НЕ в байтах, а в количестве ячеек типа recvCount. Ее аналог:

    MPI_Send( sendBuf, sendCount, sendType, rootRank, ... );
    if( myRank == rootRank ) {
        MPI_Type_extent( recvType, &elemSize );
        for( i=0; i<commSize; i++ )
            MPI_Recv( ((char*))recvBuf) + displs[i] * recvCounts[i]
                * elemSize, recvCounts[i], recvType, i, ... );
    } 

    MPI_Scatter ("разбрызгиватель") : выполняет обратную "совку" операцию - части передающего буфера из задачи root распределяются по приемным буферам всех задач. Ее аналог:

    if( myRank == rootRank ) {
        MPI_Type_extent( recvType, &elemSize );
        for( i=0; i<commSize; i++ )
            MPI_Send( ((char*)sendBuf) + i*sendCount*elemSize,
                sendCount, sendType, i, ... );
    }
    MPI_Recv( recvBuf, recvCount, recvType, rootRank, ... ); 

    И ее векторный вариант - MPI_Scatterv, рассылающая части неодинаковой длины в приемные буфера неодинаковой длины.

    MPI_Allgather аналогична MPI_Gather, но прием осуществляется не в одной задаче, а во ВСЕХ: каждая имеет специфическое содержимое в передающем буфере, и все получают одинаковое содержимое в буфере приемном. Как и в MPI_Gather, приемный буфер последовательно заполняется данными изо всех передающих. Вариант с неодинаковым количеством данных называется MPI_Allgatherv.

    MPI_Alltoall : каждый процесс нарезает передающий буфер на куски и рассылает куски остальным процессам; каждый процесс получает куски от всех остальных и поочередно размещает их приемном буфере. Это "совок" и "разбрызгиватель" в одном флаконе. Векторный вариант называется MPI_Alltoallv.

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

    В учебнике, изданном MIT Press, есть хорошая СХЕМА для всех перечисленных в этом разделе функций. Понять, что как работает, по ней нелегко, зато вспоминать, если однажды уже разобрался, удобно.

    Помните, что коллективные функции несовместимы с "точка-точка": недопустимым, например, является вызов в одной из принимающих широковещательное сообщение задач MPI_Recv вместо MPI_Bcast.

     


    Распределенные операции.

    Идея проста: в каждой задаче имеется массив. Над нулевыми ячейками всех массивов производится некоторая операция (сложение/произведение/ поиск минимума/максимума и т.д.), над первыми ячейками производится такая же операция и т.д. Четыре функции предназначены для вызлва этих операций и отличаются способом размещения результата в задачах.

    MPI_Reduce : массив с результатами размещается в задаче с номером root:

    int vector[16];
    int resultVector[16];
    MPI_Comm_rank( MPI_COMM_WORLD, &myRank );
    for( i=0; i<16; i++ )
        vector[i] = myRank*100 + i;
    MPI_Reduce(
        vector,  /* каждая задача в коммуникаторе предоставляет вектор */
        resultVector,   /* задача номер 'root' собирает данные сюда */
        16,      /* количество ячеек в исходном и результирующем массивах */
        MPI_INT, /* и тип ячеек */
        MPI_SUM, /* описатель операции: поэлементное сложение векторов */
        0,       /* номер задачи, собирающей результаты в 'resultVector' */
        MPI_COMM_WORLD  /* описатель области связи */
    );
    if( myRank==0 )
        /* печатаем resultVector, равный сумме векторов */ 

    Предопределенных описателей операций в MPI насчитывается 12:

    Естественный вопрос: а с массивами каких типов умеют работать эти функции? Ответ приводится в виде таблицы:
    Операция Допустимый тип операндов
    MPI_MAX, MPI_MIN целые и вещественные
    MPI_SUM, MPI_PROD целые, вещественные, комплексные
    MPI_LAND, MPI_LOR, MPI_LXOR целые и логические
    MPI_LAND, MPI_LOR, MPI_LXOR целые (в т.ч. байтовые)

    Этим типам операндов могут соответствовать следующие описатели:
    Тип Описатель в Си Описатель в Фортране
    целый MPI_INT, MPI_UNSIGNED_INT, MPI_LONG, MPI_UNSIGNED_LONG, MPI_SHORT, MPI_UNSIGNED_SHORT MPI_INTEGER
    целый байтовый MPI_BYTE (нет)
    вещественный MPI_FLOAT, MPI_DOUBLE, MPI_LONG_DOUBLE MPI_REAL, MPI_DOUBLE_PRECISION
    логический (нет, пользуйтесь типом int) MPI_LOGICAL
    Комплексный (нет) MPI_COMPLEX

    Количество поддерживаемых операциями типов для ячеек векторов строго ограничено вышеперечисленными. Никакие другие встроенные или пользовательские описатели типов использоваться не могут! Обратите также внимание, что все операции являются ассоциативными ( "(a+b)+c = a+(b+c)" ) и коммутативными ( "a+b = b+a" ).

    MPI_Allreduce : результат рассылается всем задачам, параметр 'root' убран.

    MPI_Reduce_scatter : каждая задача получает не весь массив-результат, а его часть. Длины этих частей находятся в массиве-третьем параметре функции. Размер исходных массивов во всех задачах одинаков и равен сумме длин результирующих массивов.

    MPI_Scan : аналогична функции MPI_Allreduce в том отношении, что каждая задача получает результрующий массив. Главное отличие: здесь содержимое массива-результата в задаче i является результатом выполнение операции над массивами из задач с номерами от 0 до i включительно.

    В упоминавшейся уже книге распределенные операции иллюстрирует соответствующая СХЕМА

    Помимо встроенных, пользователь может вводить свои собственные операции, но механизм их создания здесь не рассматривается. Для этого служат функции MPI_Op_create и MPI_Op_free, а также тип MPI_User_function.

     


    Коммуникаторы, группы и области связи.

    Группа - это некое множество ветвей. Одна ветвь может быть членом нескольких групп. В распоряжение программиста предоставлен тип MPI_Group и набор функций, работающих с переменными и константами этого типа. Констант, собственно, две: MPI_GROUP_EMPTY может быть возвращена, если группа с запрашиваемыми характеристиками в принципе может быть создана, но пока не содержит ни одной ветви; MPI_GROUP_NULL возвращается, когда запрашиваемые характеристики противоречивы. Согласно концепции MPI, после создания группу нельзя дополнить или усечь - можно создать только новую группу под требуемый набор ветвей на базе существующей.

    Область связи ("communication domain") - это нечто абстрактное: в распоряжении программиста нет типа данных, описывающего непосредственно области связи, как нет и функций по управлению ими. Области связи автоматически создаются и уничтожаются вместе с коммуникаторами. Абонентами одной области связи являются ВСЕ задачи либо одной, либо двух групп.

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

    Зачем вообще нужны разные группы, разные области связи и разные их описатели?

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

    Важно помнить, что ВСЕ функции, создающие коммуникатор, являются КОЛЛЕКТИВНЫМИ! Именно это качество позволяет таким функциям возвращать в разные ветви ОДИН И ТОТ ЖЕ описатель. Коллективность, напомню, заключется в следующем:

     


    Создание коммуникаторов и групп

    Копирование. Самый простой способ создания коммуникатора - скопировать "один-в-один" уже имеющийся:

        MPI_Comm tempComm;
        MPI_Comm_dup( MPI_COMM_WORLD, &tempComm );
         /* ... передаем данные через tempComm ... */
        MPI_Comm_free( &tempComm ); 
    Новая группа при этом не создается - набор задач остается прежним. Новый коммуникатор наследует все свойства копируемого. См. также пример 6

    Расщепление. Соответствующая коммуникатору группа расщепляется на непересекающиеся подгруппы, для каждой из которых заводится свой коммуникатор.

      MPI_Comm_split(
        existingComm,   /* существующий описатель, например MPI_COMM_WORLD */
        indexOfNewSubComm,   /* номер подгруппы, куда надо поместить ветвь */
        rankInNewSubComm,    /* желательный номер в новой подгруппе */
        &newSubComm );       /* описатель области связи новой подгруппы */ 
    Эта функция имеет одинаковый первый параметр во всех ветвях, но разные второй и третий - и в зависимости от них разные ветви определяются в разные подгруппы; возвращаемый в четвертом параметре описатель будет принимать в разных ветвях разные значения (всего столько разных значений, сколько создано подгрупп). Если indexOfNewSubComm равен MPI_UNDEFINED, то в newSubComm вернется MPI_COMM_NULL, то есть ветвь не будет включена ни в какую из созданных групп. См. также пример 7

    Создание через группы. В предыдущих двух случаях коммуникатор создается от существующего коммуникатора напрямую, без явного создания группы: группа либо та же самая, либо создается автоматически. Самый же общий способ таков:

    1. функцией MPI_Comm_group определяется группа, на которую указывает соответствующий коммуникатор;
    2. на базе существующих групп функциями семейства MPI_Group_xxx создаются новые группы с нужным набором ветвей;
    3. для итоговой группы функцией MPI_Comm_create создается коммуникатор; не забудьте, что она должна быть вызвана во ВСЕХ ветвях-абонентах коммуникатора, передаваемого первым параметром;
    4. все описатели созданных групп очищаются вызовами функции MPI_Group_free.
    Такой механизм позволяет, в частности, не только расщеплять группы подобно MPI_Comm_split, но и объединять их. Всего в MPI определено 7 разных функций конструирования групп.

    Может ли задача обратиться к области связи, абонентом которой не является?
    Нет. Описатель области связи передается в задачу функциями MPI, которые одновременно делают эту задачу абонентом описываемой области. Таков единственный существующий способ получить описатель.
    Попытки "пиратскими" средствами обойти это препятствие (например, получить описатель, посредством MPI_Send/MPI_Recv переслать его в другую задачу, не являющуюся его абонентом, и там им воспользоваться) не приветствуются, и исход их, скорее всего, будет определяться деталями реализации.

     


    Полезная нагрузка коммуникатора: атрибуты.

    Помимо характеристик области связи, тело коммуникатора содержит в себе некие дополнительные данные (атрибуты). Механизм хранения атрибутов называется "caching". Атрибуты могут быть системные и пользовательские; в системных, в частности, хранятся:

    Атрибуты идентифицируются целыми числами, которые MPI назначает автоматически. Некоторые константы для описания системных атрибутов: MPI_TAG_UB, MPI_HOST, MPI_IO, MPI_WTIME_IS_GLOBAL. К этим атрибутам программист обращается редко, и менять их не может; а для таких часто используемых атрибутов, как обработчик ошибок или описание топологии, существуют персональные наборы функций, например, MPI_Errhandler_xxx.

    Атрибуты - удобное место хранения совместно используемой информации; помещенная в атрибут одной из ветвей, такая информация становится доступной всем использующим коммуникатор ветвям БЕЗ пересылки сообщений (вернее, на MPP-машине, к примеру, сообщения будут, но на системном уровне, т.е. скрытые от глаз программиста).

    Пользовательские атрибуты создаются и уничтожаются функциями MPI_Keyval_create и MPI_Keyval_free; модифицируются функциями MPI_Attr_put, MPI_Attr_get и MPI_Attr_delete. При создании коммуникатора на базе существующего атрибуты из последнего тем или иным образом копируются или нет в зависимости от функции копирования типа MPI_Copy_function, адрес которой является параметром функции создания атрибута.
    То же и для удаления атрибутов при уничтожении коммуникатора: задается пользовательской функцией типа MPI_Delete_function, указываемой при создании атрибута.

     


    Корректное удаление отслуживших описателей.

    Здесь имеются в виду ВСЕ типы системных данных, для которых предусмотрена функция MPI_Xxx_free (и константа MPI_XXX_NULL). В MPI-I их 7 штук ( можете посчитать сами, посмотрев в mpi.h ):

    1. коммуникаторы;
    2. группы;
    3. типы данных;
    4. распределенные операции;
    5. квитанции (request's);
    6. атрибуты коммуникаторов;
    7. обработчики ошибок (errhandler's).
    Дальше все описывается на примере коммуникаторов и групп, но изложенная схема является общей для всех типов ресурсов.

    Не играет роли, в каком порядке уничтожать взаимосвязанные описатели. Главное - не забыть вызвать функцию удаления ресурса MPI_Xxx_free вовсе. Соответствующий ресурс не будет удален немедленно, он прекратит существование только если будут выполнены два условия:

    Взаимосвязанными описателями являются описатели коммуникатора и группы (коммуникатор ссылается на группу); или описатели типов, если один создан на базе другого (порожденный ссылается на исходный).

    Пример:

        MPI_Comm subComm;
        MPI_Group subGroup;
        int rank;
        MPI_Comm_rank( MPI_COMM_WORLD, &rank );
    
        MPI_Comm_split( MPI_COMM_WORLD, rank / 3, rank % 3, &subComm );
         /* Теперь создан коммуникатор subComm, и автоматически создана
          * группа, на которую распространяется его область действия.
          * На коммуникатор заведена ссылка из программы - subComm.
          * На группу заведена системная ссылка из коммуникатора.
          */
    
        MPI_Comm_group( subComm, &subGroup );
         /* Теперь на группу имеется две ссылки - системная
          * из коммуникатора, и пользовательская subGroup.
          */
    
        MPI_Group_free( &subGroup );
         /* Пользовательская ссылка на группу уничтожена,
          * subGroup сброшен в MPI_GROUP_NULL.
          * Собственно описание группы из системных данных не удалено,
          * так как на него еще ссылается коммуникатор.
          */
    
        MPI_Comm_free( &subComm );
         /* Удалена пользовательская ссылка на коммуникатор,
          * subComm сброшен в MPI_COMM_NULL. Так как других ссылок
          * на коммуникатор нет, его описание удаляется из системных данных.
          * Вместе с коммуникатором удалена системная ссылка на группу.
          * Так как других ссылок на группу нет, ее описание удаляется
          * из системных данных.
          */
    
    Еще раз: для MPI не играет роли, в каком порядке будут вызваны завершающие вызовы MPI_Xxx_free, это дело программы.

    И не пытайтесь уничтожать константные описатели вроде MPI_COMM_WORLD или MPI_CHAR: их создание и уничтожение - дело самого MPI.

     


    Вкратце о второстепенных деталях.

    Интеркоммуникаторы и интрaкоммуникаторы: описатели областей связи соответственно над двумя группами или над одной. MPI_COMM_WORLD является интрАкоммуникатором. Интер-коммуникаторы не являются предметом первой необходимости для новичка, поэтому за пределами данного абзаца упоминаний о них нет. Все упомянутые в документе функции, оперирующие коммуникаторами, либо не различают "интра-" и "интер-" вовсе, либо явно требуют "интра-". К числу последних относятся:

    Пользовательские топологии. Внутри группы задачи пронумерованы линейно от 0 до (размер группы-1). Однако через коммуникатор можно ДОПОЛНИТЕЛЬНО навязать для них еще одну систему нумерации. Таких дополнительных систем в MPI две: картезианская n-мерная решетка (с цикличностью и без оной), а также биориентированный граф. Предоставляются функции для создания нумераций (MPI_Topo_test, MPI_Cart_xxx, MPI_Graph_xxx) и для преобразования номеров из одной системы в другую. Этот механизм не должен восприниматься как предоставляющий возможность подгонки связей между ветвями под аппаратную топологию для повышения быстродействия - он всего лишь автоматизирует перерасчет адресов, которым должны заниматься ветви, скажем, при вычислении матриц: через коммуникатор задается картезианская система координат, где координаты ветви совпадают с координатами вычисляемой ею подматрицы.

    Обработчики ошибок. По умолчанию, если при выполнеии функции MPI обнаружена ошибка, выполнение всех ветвей приложения завершается. Это сделано в расчете на неряшливого программиста, не привыкшего проверять коды завершения (malloc,open,write,...;), и пытающегося распространить такой стиль на MPI. При аварийном завершении по такому сценарию на консоль выдается очень скудная информация: в лучшем случае, там будет название функции MPI и название ошибки. Обработчик ошибок является принадлежностью коммуникатора, для управления обработчиками служат функции семейства MPI_Errhandler_xxx. Пример написания собственного обработчика, выводящего более полную диагностику, находится здесь, краткое русское описание - здесь.

    Многопоточность. Сам MPI неявно использует многопоточность очень широко, и не мешает программисту делать то же самое. Однако: разные задачи имеют с точки зрения MPI ОБЯЗАТЕЛЬНО разные номера, а разные потоки (threads) внутри одной задачи для него ничем не отличаются. Программист сам идентификаторами сообщений и коммуникаторами должен устанавливать такую дисциплину для потоков, чтобы один поток не стал, допустим, вызывая MPI_Recv, джокером перехватывать сообщения, которые должен принимать и обрабатывать другой поток той же задачи. Другим источником ошибок может быть использование разными потоками коллективных функций над одним и тем же коммуникатором: используйте MPI_Comm_dup !

    Работа с файлами. В MPI-2 средства перенаправления работы с файлами появились, в MPI-1 их нет. Все вызовы функций напрямую передаются операционной системе (Unix/Parix/NFS/...) на той машине, или на том процессорном узле MPP-компьютера, где находится вызывающая ветвь. Теоретически возможность подключения средств расширенного управления вводом/выводом в MPI-1 есть - каждый коммуникатор хранит атрибут с числовым кодом MPI_IO - это номер ветви, в которую перенаправляется ввод/вывод от всех остальных ветвей в коммуникаторе; сам MPI ничего с ним не делает и никак не использует. Для MPI существует ряд дополнительных библиотек такого рода, как для конкретных платформ, так и свободно распространяемые многоплатформенные, но я не видел ни одной.

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

        setvbuf( stdout, NULL, _IOLBF, BUFSIZ );
        setvbuf( stderr, NULL, _IOLBF, BUFSIZ ); 
    и не забудьте написать "#include <stdio.h>" в начале программы...

     


    Изменения

    1. 13 апреля 98 - первая редакция.
    2. 20 апреля 98 - вторая редакция. Готово все, кроме раздела "Распределенные операции"

    = MPI =

    программный инструмент
    для параллельных вычислений

    Краткий обзор: назначение, особенности, перспективы

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


    Содержание

     


    Место MPI в иерархии средств
    параллельного программирования

    Создание параллельной программы включает в себя две основных стадии:

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

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

    • через разделяемую память, синхронизация доступа ветвей к такой памяти происходит посредством семафоров,
    • в виде сообщений.
    Первый метод является базовым для SMP-машин, второй - для сетей всех типов. Однако в принципе одно может быть сымитировано через другое:
    • разделяемая память на не-SMP-комплексах - посредством организации единого виртуального адресного пространства на изолированных друг от друга физических ОЗУ; такая имитация аппаратными средствами нередко применяется на NUMA-машинах;
    • на SMP-машине вырожденным каналом связи для передачи сообщений служит разделяемая память.

    Следовательно, единый интерфейс программиста может быть создан, тем не менее долгое время создатели компьютеров и операционных систем шли разными путями. Например, для работы с семафорами и разделяемой памятью в Windows, в Юниксах 4.4BSD и SVR4 примерно одни и те же действия представлены разными наборами системных вызовов. В мире локальных/глобальных сетей стандарт вроде бы выработан - это TCP/IP. Однако для работы с внутримашинными сетями, где состав абонентов и маршрутизация трафика жестко фиксированы, а вероятность доставки пакетов равна 100%, универсальность и гибкость TCP/IP отрицательно сказываются на скорости, поэтому для MPP-машин интерфейсы программиста создаются индивидуально (или, вернее, создавались индивидуально до тех пор, пока не появился MPI).

    Таким образом, можно считать назревшей потребность в стандарте на интерфейс программиста, который бы сделал создание параллельных приложений таким же:

    • мобильным,
    • эффективным,
    • надежным,
    ... каким Си и Юникс сделали программирование вообще. Стандартом по выбору основных производителей ЭВМ решено сделать MPI. Ими образован MPI Forum, и в свет выпущена спецификация, которой должны удовлетворять все конкретные разработки. Головная организация проекта - Аргоннская национальная лаборатория США - распространяет пакет MPICH (MPI CHameleon), перенесенный на большинство платформ. Большая часть созданных другими разработчиками реализаций MPI базируется на MPICH. По идее, в итоге иерархия средств разработки должна стать примерно такой:

    Параллельное приложение
    Средства быстрой разработки приложений (RAD)
    Распараллеливающие препроцессоры

    M P I


    разделяемая память
    и семафоры
    индивидуальные интерфейсы
    с передачей сообщений
    TCP/IP
    SMP-машины MPP-машины сети

     

    Правила работы с MPI

    MPI расшифровывается как Message Passing Interface - Интерфейс с передачей сообщений, т.е. конкретному стандарту присвоено название всего представляемого им класса программного инструментария. В его состав входят, как правило, два обязательных компонента:
    • библиотека программирования для языков Си, Си++ и Фортран,
    • загрузчик исполняемых файлов.
    Кроме того, может присутствовать справочная система (manual pages для Юникса), командные файлы для облегчения компиляции/компоновки программ и все такое прочее. В стандарте отсутствует все лишнее, например, нет средств автоматического переноса и построения копий исполняемого файла в сети. Это нужно, если MPI-приложение предстоит выполнять сетью машин - но это можно выполнить и утилитами Юникса. В стандарте нет никаких средств автоматической декомпозиции, нет отладчика (правда, есть функции хронометража и предусмотрена возможность профилирования). То есть это система межпроцессовой связи в чистом (можно даже сказать - в голом) виде, и не более того.

    Для MPI принято писать программу, содержащую код всех ветвей сразу. MPI-загрузчиком запускается указываемое количество экземпляров программы. Каждый экземпляр определяет свой порядковый номер в запущенном коллективе, и в зависимости от этого номера и размера коллектива выполняет ту или иную ветку алгоритма. Такая модель параллелизма называется Single program/Multiple data ( SPMD ), и является частным случаем модели Multiple instruction/Multiple data ( MIMD ). Каждая ветвь имеет пространство данных, полностью изолированное от других ветвей. Обмениваются данными ветви только в виде сообщений MPI.

    Все ветви запускаются загрузчиком одновременно как процессы Юникса. Количество ветвей фиксировано - в ходе работы порождение новых ветвей невозможно. Если MPI-приложение запускается в сети, запускаемый файл приложения должен быть построен на каждой машине.

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

     


    Функции пересылки данных

    Хотя с теоретической точки зрения ветвям для организации обмена данными достаточно всего двух операций (прием и передача), на практике все обстоит гораздо сложнее. Одними только коммуникациями "точка-точка" (т.е. такими, в которых ровно один передающий процесс и ровно один принимающий) занимается порядка 40 функций. Пользуясь ими, программист имеет возможность выбрать:
    • Способ зацепления процессов - в случае неодновременного вызова двумя процессами парных функций приема и передачи могут быть произведены:
      • Автоматический выбор одного из трех нижеприведенных вариантов;
      • Буферизация на передающей стороне - функция передачи заводит временный буфер, копирует в него сообщение и возвращает управление вызвавшему процессу. Содержимое буфера будет передано в фоновом режиме;
      • Ожидание на приемной стороне,
        завершение с кодом ошибки на передающей стороне;
      • Ожидание на передающей стороне,
        завершение с кодом ошибки на приемной стороне.
    • Способ взаимодействия коммуникационного модуля MPI с вызывающим процессом:
      • Блокирующий - управление вызывающему процессу возвращается только после того, как данные приняты или переданы (или скопированы во временный буфер);
      • Неблокирующий - управление возвращается немедленно (т.е. процесс блокируется до завершения операции), и фактическая приемопередача происходит в фоне. Функция неблокирующего приема имеет дополнительный параметр типа "квитанция". Процесс не имеет права производить какие-либо действия с буфером сообщения, пока квитанция не будет "погашена";
      • Персистентный - в отдельные функции выделены:
        • создание "канала" для приема/передачи сообщения,
        • инициация приема/передачи,
        • закрытие канала.
        Такой способ эффективен, к примеру, если приемопередача происходит внутри цикла, а создание/закрытие канала вынесены за его границы.
    2 простейшие (но и самые медленные) функции - MPI_Recv и MPI_Send - выполняют блокирующую приемопередачу с автоматическим выбором зацепления (кстати сказать, все функции приема совместимы со всеми функциями передачи).

    Таким образом, MPI - весьма разветвленный инструментарий. Приведу цитату из раннего себя: "То, что в конкурирующих пакетах типа PVM реализовано одним-единственным способом, в MPI может быть сделано несколькими, про которые говорится: способ А прост в использовании, но не очень эффективен; способ Б сложнее, но эффективнее; а способ В сложнее и эффективнее при определенных условиях".

    Замечание о разветвленности относится и к коллективным коммуникациям (при которых получателей и/или отправителей несколько): в PVM эта категория представлена одной функцией, в MPI - 9 функций 5 типов:

    • broadcast: один-всем,
    • scatter: один-каждому,
    • gather: каждый-одному,
    • allgather: все-каждому,
    • alltoall: каждый-каждому.
    На упреки типа "Зачем столько всего сваливать в одну кучу? Программисту легче будет в случае необходимости самому написать такую функцию, нежели разбираться в разбухшей документации?" можно возразить, что, например, вариант-самоделка "один-всем" будет, скорее всего, выглядеть примерно так:
        if( myRank == 0 )
        {
            for( i=1; i < numLoops; i++ )
                MPI_Send( ... , i, ... );
        } else
            MPI_Recv( ... , 0, ... ); 
    ... в то время, как имеющиеся в MPI функции оптимизированы - не пользуясь функциями "точка-точка", они напрямую (на что, согласно идеологии MPI, программа пользователя права не имеет) обращаются:
    • к разделяемой памяти и семафорам на SMP-машине, при этом происходит одно копирование в разделяемую память и numLoops-1 копирований из нее просто функцией memcpy();
    • к TCP/IP при работе в сети: в качестве адреса получателя при передаче используется т.н. "широковещательный", с "-1" в поле адреса машины.
    А если такая архитектурно-зависимая оптимизация невозможна, используется оптимизация архитектурно-независимая (интересно, как много программистов захотело бы делать ее вручную?): передача производится не напрямую от одного ко всем (время передачи линейно зависит от количества ветвей-получателей), а по двоичному дереву (время передачи логарифмически зависит от количества). Как следствие, скорость работы повышается.

     


    Коллективы ветвей

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

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

    В качестве идентификатора ожидаемого сообщения функции приема может быть передан т.н. "джокер" - принять первое пришедшее сообщение независимо от его идентификатора и/или отправителя. Такой вызов может по ошибке перехватывать и те сообщения, которые должны быть приняты и обработаны в другом месте ветви. Для коммуникаторов "джокера" не существует, поэтому работа разных функций через разные коммуникаторы гарантированно предохраняет их от взаимных краж информации: коммуникаторы являются несообщающимися сосудами.

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

     


    Зачем MPI знать о типах данных

    О типах передаваемых данных MPI должен постольку-поскольку при работе в сетях на разных машинах данные могут иметь разную разрядность (например, тип int - 4 или 8 байт), ориентацию (младший байт располагается в ОЗУ первым на процессорах Intel, последним - на всех остальных), и представление (это, в первую очередь, относится к размерам мантиссы и экспоненты для вещественных чисел). Поэтому все функции приемопередачи в MPI оперируют не количеством передаваемых байт, а количеством ячеек, тип которых задается параметром функции, следующим за количеством: MPI_INTEGER, MPI_REAL и т.д. Это переменные типа MPI_Datatype (тип "описатель типов", каждая его переменная описывает для MPI один тип). Они имеются для каждого базового типа, имеющегося в используемом языке программирования.

    Однако, пользуясь базовыми описателями, можно передавать либо массивы, либо одиночные ячейки (как частный случай массива). А как передавать данные агрегатных типов, например, структуры? В MPI имеется механизм конструирования пользовательских описателей на базе уже имеющихся (как пользовательских, так и встроенных).

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

  4. не налезают друг на друга,
  5. не располагаются с разрывами (выравнивание полей в структурах не в счет).

    В MPI сняты оба этих ограничения! Это позволяет весьма причудливо "вырезать", в частности, фрагменты матриц для передачи, и размещать принимаемые данные между собственных. В спецификации MPI приведен пример создания пользовательского описателя типа, передача матрицы с использованием которого приводит к ее транспонированию.

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

     


    Чего нет в MPI, но будет в MPI-2

    В функциональности MPI есть пробелы, которые устранены в следующем проекте, MPI-2. Спецификация на MPI-2 уже выпущена в свет, а появление первых реализаций планируется в конце 1998 года. Вкратце перечислим наиболее важные нововведения:
    • Взаимодействие между приложениями. Поддержка механизма "клиент-сервер". Станет возможным писать на MPI не только расчетные математические задачи, но и системы массового обслуживания (базы данных и проч.).
       
    • Динамическое порождение ветвей. Для программирования расчетных задач это не нужно (хотя многие по привычке хотели бы иметь в MPI аналоги fork() и spawn() ).
      Действительно, не имеет смысла запускать ветвей больше чем... и нет особого вреда в том, чтобы запускать ветвей меньше, чем... реально имеется процессоров, не так ли? Процессор не станет работать в N раз быстрее, если вместо одной ветви на нем запустить N ветвей. И наоборот, ветвь, для которой не найдется работы, в "спящем" состоянии будет потреблять сущие крохи от процессорного времени.
      Однако такая возможность однозначно необходима для написания системы массового обслуживания.
       
    • Для работы с файлами создан архитектурно-независимый интерфейс. Это имеет значение, особенно если диск находится на одной ЭВМ, а ветвь, которая должна с ним работать - на другой. В отсутствие такого интерфейса пересылку данных приходится либо организовывать вручную, либо полагаться на сетевые возможности операционной системы (NFS, Parix и т.д.). По сравнению и с тем, и с другим, MPI гарантирует лучший баланс между универсальностью и быстродействием.
       
    • Сделан шаг в сторону SMP-архитектуры. Теперь разделяемая память может быть не только каналом связи между ветвями, но и местом совместного хранения данных. Для этого ветви делегируют в MPI т.н. буфера-"окна". Интерфейс выполнен так, чтобы в-принципе его можно было реализовать и через передачу сообщений на не-SMP-комплексах. MPI автоматически поддерживает идентичность содержимого всех "окон" с одинаковым идентификатором.
       
      Для этого механизма придуман термин "One-sided communications" ("односторонние коммуникации"), так как ветви-получателю не требуется явно вызывать функцию приема для получения новой информации; функция передачи в ветви-отправители осуществляет "Remote memory access" ("удаленный доступ к памяти", сокращенно RMA).
       
      Правда, возникает вопрос: раз уж передача данных становится неявной, а синхронизация ветвей при доступе к "окнам" - явной, не станут ли возможными в MPI т.н. нестабильные ошибки, вызванные неправильной синхронизацией - бич программ, взаимодействующих через разделяемую память?

     


    Сравнение с PVM

    Сравнение необходимо, так как названия MPI и PVM часто упоминаются рядом.
    Итак, общие характеристики:

    • одна и та же решаемая задача: обеспечение межпроцессовой связи,
    • методика программирования: SPMD с передачей сообщений,
    • методика использования: библиотека для Си/Фортрана + загрузчик,
    • мобильность инструмента и, как следствие, мобильность создаваемых программ.
    Отличия PVM: в PVM...
    • интерфейс программиста проще и примитивнее,
    • интерфейс пользователя сложнее и запутаннее,
    • предусмотрено динамическое размножение ветвей (есть в MPI-2),
    • возможно взаимодействие приложений, запущенных одним и тем же пользователем (есть в MPI-2),
    • загрузчик приложений, создающий собственно Виртуальную Параллельную Машину, является постоянно запущенным (полезно для исследовательских целей); в отличие от MPI, где срок жизни загрузчика строго совпадает со сроком жизни одного конкретного приложения (более простое, надежное и компактное решение),
    • PVM старше на 6 лет, но не продвигается производителями ЭВМ на роль единого стандарта.
    Следует, однако, отметить, что здесь сравниваются не реализации, а спецификации. Стандарт MPI предлагает больший спектр возможностей, нежели стандарт PVM, но есть определенные основания считать, что в настоящее время над реализацией PVM работает более квалифицированный коллектив разработчиков.

    Подробнее о PVM читайте тут.

     


    Сравнение с SHM

    Преимущества модели shared memory ( SHM ) по сравнению с MPI:

    • Модель программирования межпроцессных связей через общую память с синхронизацией семафорами - базовая для SMP- и однопроцессорных компьютеров. Функции работы с Shared memory непосредственно входят в состав каждой многозадачной операционной системы. В пределах одного компьютера все остальные средства межпроцессовой коммуникации реализуются через SHM, и потому заведомо являются менее быстрыми,
       
    • В модели с общей памятью совместно используемые данные хранятся в такой памяти в единственном экземпляре, без создания локальных копий для каждой ветви приложения. Ветви, работающие с такими данными, затрачивают незначительное время на синхронизацию доступа к данным, и в-целом соотношение скорости коммуникаций и скорости вычислений в модели с общей памятью выше, чем в модели с передачей сообщений. Следовательно, класс задач, пригодных к распараллеливанию в SHM, в-принципе шире, нежели в MPI.
       
    • SHM намного дольше используется для программирования высокопроизводительных вычислений, и многие проблемные программисты либо изучили эту методику лучше, либо вообще изучили только ее одну.

    Эти доводы нельзя было не привести здесь потому, что в реальной жизни их регулярно, и не без основания, приводят уже упомянутые проблемные программисты. Но в документе, посвященном MPI, им, конечно, будут противопоставлены определенные контрдоводы.

    Итак, почему MPI не(намного) хуже SHM:

    • в хорошо распараллеленном приложении на собственно взаимодействие между ветвями (пересылки данных и синхронизацию) тратится небольшая доля времени - несколько процентов от общего времени работы; таким образом, замедление пересылок, допустим, в два раза не означает общего падения производительности вдвое - она понизится на несколько процентов; подобное незначительное понижение производительности можно считать приемлемым,
       
    • средства, имеющиеся в MPI (асинхронные/перманентные коммуникации) и особенно в MPI-2 (удаленный доступ к памяти) предоставляют скорость, сравнимую со скоростью SHM.

    Почему MPI предпочтительнее SHM:

    • все имеющиеся реализации SHM API либо в-принципе непереносимы за пределы SMP-машин, либо недостаточно эффективны, либо недостаточно распространены;
       
    • стоимость вычислений и коммуникаций в SMP-машинах падает медленнее, чем в MPP-комплексах и сетях;
       
    • методика обмена данными в виде сообщений обладает изначально большей наглядностью для программиста,
       
    • в отличие от MPI, SHM API, как правило, реализует только самые базовые операции,

    Таким образом:

      MPI сокращает разрыв в скорости выполнения программ, и увеличивает разрыв в скорости их написания.

     


    Какое программное обеспечение существует
    между программистом и MPI

    MPI сам по себе является средством:

    • сложным: спецификация на MPI-1 содержит 300 страниц, на MPI-2 - еще 500 (причем это только отличия и добавления к MPI-1), и программисту для эффективной работы так или иначе придется с ними ознакомиться, помнить о наличии нескольких сотен функций, и о тонкостях их применения;
    • специализированным: это система связи - и все.

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

    Таким образом, программист заинтересован в инструментах, которые облегчали бы:

    • проведение декомпозиции,
    • запись ее в терминах MPI.
    То есть, в данном случае это средства, генерирующие на базе неких входных данных текст программы на стандартном Си или Фортране, обладающей явным параллелизмом, выраженным в терминах MPI; содержащий вызовы MPI-процедур, наиболее эффективные в окружающем контексте. Такие средства, в-частности, делают написание программы не только легче, но и надежнее, так как:
    • ошибки, которые MPI в-принципе не может обнаружить в момент выполнения, генератор имеет возможность обнаруживать в момент построения программы, например:
          Ветвь 1:                     Ветвь 2:
          MPI_Recv( ... , 2, ... );    MPI_Recv( ... , 1, ... );
          MPI_Send( ... , 2, ... );    MPI_Send( ... , 1, ... );
                          |                            |
                   Это номер ветви-отправителя/получателя
      Примечание: здесь произойдет блокировка - ветви не смогут завершить прием, потому что не смогут начать передачу, потому что не смогут завершить прием. Как итог, программа повиснет.

      Еще одно примечание: а вот если вызовы функций приема и передачи поменять местами, то блокировки не произойдет - MPI выберет для передачи буферизованный режим, и MPI_Send, скопировав данные во временный буфер, вернет управление сразу же, не дожидаясь, пока сообщение будет фактически принято приемной стороной. Таким образом, по сравнению с низкоуровневыми коммуникациями, MPI все-таки делает программу более устойчивой к ошибкам программиста. Правда, буферизация означает падение быстродействия тем более ощутимое, чем быстрее происходит собственно пересылка данных.
       

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

    Назовем некоторые перспективные типы такого инструментария, который лишал бы программиста необходимости вообще помнить о присутствии MPI.

    Средства автоматической декомпозиции. Идеалом является такое оптимизирующее средство, которое на входе получает исходный текст некоего последовательного алгоритма, написанный на обычном языке программирования, и выдает на выходе исходный текст этого же алгоритма на этом же языке, но уже в распараллеленном на ветви виде, с вызовами MPI. Что ж, такие средства созданы (например, в состав полнофункционального пакета Forge входит, наряду с прочим, и такой препроцессор), но до сих пор, насколько мне известно, никто не торопится раздавать их бесплатно. Кроме того, вызывает сомнение их эффективность.

    языки программирования. Это наиболее популярные на сегодняшний день средства полуавтоматической декомпозиции. В синтаксис универсального языка программирования (Си или Фортрана) вводятся дополнения для записи параллельных конструкций кода и данных. Препроцессор переводит текст в текст на стандартном языке с вызовами MPI. Примеры таких систем: mpC (massively parallel C) и HPF (High Performance Fortran).

    Общим недостатком инструментов, производящих преобразование "текст в текст", является то, что синтаксическому разбору подвергаются оба текста: и исходный (его обрабатывает распараллеливающий препроцессор), и генерируемый (его обрабатывает компилятор). Это уменьшает скорость построения программы, и, кроме того, необходимость делать синтаксический разбор усложняет написание препроцессора. Поэтому, например, те фирмы-производители, которые поставляют свои ЭВМ вместе с Фортраном, встраивают HPF прямо в компилятор машинно-зависимого кода. Для расширений языка Си аналогичное решение может быть найдено в использовании GNU C.

    Оптимизированные библиотеки для стандартных языков. В этом случае оптимизация вообще может быть скрыта от проблемного программиста. Чем больший объем работы внутри программы отводится подпрограммам такой библиотеки, тем бОльшим будет итоговый выигрыш в скорости ее (программы) работы. Собственно же программа пишется на обычном языке программирования безо всяких упоминаний об MPI, и строится стандартным компилятором. От программиста потребуется лишь указать для компоновки имя библиотечного файла MPI, и запускать полученный в итоге исполняемый не непосредственно, а через MPI-загрузчик. Популярные библиотеки обработки матриц, такие как Linpack, Lapack и ScaLapack, уже переписаны под MPI.

    Средства визуального проектирования. Действительно, почему бы не расположить на экране несколько окон с исходным текстом ветвей, и пусть пользователь легким движением мыши протягивает стрелки от точек передачи к точкам приема - а визуальный построитель генерирует полный исходный текст? Тем, кто стряпал базы данных в Access'e, такая технология покажется наиболее естественной.

    Отладчики и профайлеры. Об отладчиках мне пока нечего сказать, кроме того, что они нужны. Должна быть возможность одновременной трассировки/просмотра нескольких параллельно работающих ветвей - что-либо более конкретное мне пока сказать трудно.

     


    Рекомендуемые источники информации по MPI

    Наконец, если у Вас есть вопросы, можете написать письмо мне.

     


    Версии документа



    Использование MPI
    на компьютерах ЦСТ

    Памятка пользователя

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

     


    Оглавление

     


    Общие правила

    Здесь перечислены правила, в той или иной степени применимые к большинству реализаций MPI. Некоторые замечания относятся к особенностям Юникса, о которых не мешает помнить всегда, а работая с MPI - в особенности.

    Утилиты построения и запуска MPI-приложений

    Эти утилиты находятся в подкаталоге bin каталога MPI. Для их быстрого запуска наберите в командной строке или укажите в стартовом файле следующую команду:

           C-Shell (файл .cshrc):     setenv PATH путь_к_MPI/bin:$PATH
             bash (файл .bashrc):     export PATH=путь_к_MPI/bin:$PATH
       DOS/Win (C:\AUTOEXEC.BAT):     set PATH=путь_к_MPI\bin;%PATH%
    

    Командные файлы с именами mpicc, mpiCC, mpif77 и т.д. предназначены для компиляции и компоновки соответствующих исходных текстов. Примеры:

        mpicc mpi_ex0.c -o mpi_ex0
        mpiCC mpi_ex1.cc -o mpi_ex1
        mpif77 mpi_ex2.f -o mpi_ex2 -g -lm
    
    При этом будет вызван обычный компилятор Си/Си++/Фортрана с указанной командной строкой, дополненной директивами подключения заголовочных и библиотечных файлов MPI.

    Дополнительные ключи, не передаваемые компилятору, а обрабатываемые самим командным файлом, в спецификации не оговорены, но возможны. Например, в MPICH ключ -echo включает трассировку выполнения командного файла, полезную для поиска ошибок в инсталляции сопутствующего мат.обеспечения. У большинства утилит для вывода подсказки служит ключ -help.

    Загрузчик MPI-приложений называется mpirun. У него ключей много разных и всяких, но только один из них является обязательным (-np = number of processes):

        mpirun -np N [mpirun_args] program_file [command_line_args ...]
    Параллельное приложение будет образовано N задачами-копиями, загруженными из программного файла program_file. В момент запуска все задачи одинаковы (и имеют одинаковые параметры командной строки), но получают от MPI разные номера от 0 до N-1. Пример:
        mpirun -np 5 ./mpi_ex0 

    Примечание: текущий каталог "./" указывается здесь явно, потому что без явного указания либо в имени программы, либо в переменной окружения PATH Юникс НЕ станет искать в нем приложение. Если по умолчанию PATH не содержит ".", Вы можете сами исправить .cshrc или .bashrc в своем домашнем каталоге:

        C-Shell:    set path = ( . $path )
          bash:     export PATH=.:$PATH
    

    Документация

    Практика показывает, что на период работы с той или иной библиотекой полезно бывает распечатать и держать под рукой ее заголовочные файлы из подкаталога include. В Convex MPI такой файл один: mpi.h, в MPICH их три: mpi.h (описания констант и типов), mpi_errno.h (коды ошибок и их описания) и binding.h (прототипы функций). В случае для Windows печатать рекомендуется не оригиналы, а  копии, из которых вычищены все детали, относящиеся к реализации ("_declspec(dllimport)" и проч.).

    Manual pages - хорошее подспорье, когда они есть. Независимо от этого, документация из разных версий MPICH доступна для Вас двумя способами:

    Более детальную информацию, как-то: спецификацию, учебники и различные реализации MPI можно найти на сервере NetLib. Первоисточником для данного пособия является книга "MPI: The complete reference" издательства MIT Press - Вы найдете ее на этом сервере.

    Отладка MPI-приложений обычным отладчиком

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

    Отладка ветви, запускаемой непосредственно из mpirun (в большинстве реализаций так запускается только ветвь 0):

    • если mpirun в данной реализации не требуется:
    • если mpirun позволяет запустить ветвь 0 под отладчиком:

        нет проблем, запускайте и отлаживайте.

    • ... если ни то, ни другое:

        тяжелый случай...
        Можно попробовать поместить MPI_Init() в следующее окружение:

        /* в зависимости от реализации MPI,
         * прочие ветви в этот момент могут быть еще не запущены
         */
        MPI_Init( &argc, &argv );
        /* здесь уже все ветви заведомо запущены
         */
        MPI_Comm_size( MPI_COMM_WORLD, &commSize );
        MPI_Comm_rank( MPI_COMM_WORLD, &myRank );
        
        printf("Rank=%d, PID=0x%X\n", myRank, getpid() );
        if( myRank==0 )
        {
            puts("MPI_Init ok, press any key...");
            getch();     /* ветвь 0 остановится здесь */
        }
        MPI_Barrier( MPI_COMM_WORLD );  /*остальные ветви остановятся здесь*/
        
      Дождавшись после после запуска приложения появления указанной надписи, следует "подцепить" (attach) предварительно запущенный отладчик (отладчики) к избранной ветви (ветвям).
    Отладка ветвей, запускаемых из MPI_Init() нулевой ветви:
    • если реализация позволяет ручной запуск ведомых ветвей:
      Как правило, это означает, что вместо команды запуска ветвей будут не выполнены, а выведены на консоль. После этого пользователь должен запустить их сам, и при этом имеет возможность любую из них запустить под отладчиком.

    • ... если не позволяет:
      См. выше описание "тяжелого случая". Поскольку ветвь 0 изначально запускается под отладчиком, вместо puts+getch в приведенном примере - после MPI_Init, но перед MPI_Barrier - можно просто поставить точку останова (breakpoint, говоря по-нашему).

    Объединение в кластеры

    Администратору, выбирая реализацию MPI для установки на ту или иную машину, обязательно следует иметь представление о следующих вещах:

    • в какой момент определяется протокол для приемопередачи между ветвями приложения:
      • в момент построения MPI,
      • в момент конфигурирования MPI (если оно может быть выполнено без повторного построения),
      • в момент построения приложения,
      • автоматически на стадии выполнения приложения,
    • является ли выбираемый протокол лучшим из возможных?
    • предусмотрена ли возможность многомашинной работы,
    • ... и поддерживается ли протокол P4?

    В многомашинной конфигурации - о каждой части MPI и MPI-приложения рекомендуется в общих чертах знать:

    • на каких машинах (узлах) эта часть должна находиться,
    • в какой момент она попадает на узел:
      • при инсталляции MPI,
      • при построении MPI-приложения,
      • при запуске MPI-приложения, и т.д.
    • и каким образом она туда попадет:
      • требуется инсталляция,
      • требуется компиляция+инсталляция,
      • необходимо копировать вручную,
      • копирование производится автоматически, и т.д.
    Как правило, найти в документации ответ на эти вопросы не составляет труда, но четкая постановка самих вопросов требует некоторого времени и опыта.

     


    MPICH: базовая реализация

    MPI CHameleon - это реализация, которую разработчики стандарта MPI выпускают как средство наглядной пропаганды своих замыслов. MPICH:

    • перенесен на большое количество Unix-платформ,
    • поддерживает разные коммуникационные интерфейсы нижнего уровня (SVR4 IPC, TCP/IP, IBM MPL и несколько архитектурно-зависимых для массивно-параллельных машин - Intel NX и т.д.)
    • полностью реализует все заложенные в спецификацию возможности.
    Следует отметить, что хотя, с одной стороны, MPICH реализует все, с другой - не все он реализует предельно эффективно. Например, сконфигурированный с использованием разделяемой памяти для внутримашинных пересылок, MPIСH не может работать на кластере, а сконфигурированный с поддержкой сети, использует TCP/IP-протокол и загрузчик rsh даже для внутримашинной связи. Выбор одного из вариантов задается при инсталляции ключем -comm:
        ./configure -comm=ch_shmem
        ./configure -comm=ch_p4 -device=shared
    

    По умолчанию установка производится в каталог /usr/local/mpich. Следовательно, после установки делаем:

        export PATH=/usr/local/mpich/bin:$PATH
        export MANPATH=/usr/local/mpich/man:$MANPATH
    

    Документация и подсказки

    Присутствует все необходимое: в виде man-pages, в виде HTML-страниц, в виде PostScript- и Tex- документов - в подкаталогах man, www и doc соответственно.
    Кроме того, представляют интерес рассыпанные по подкаталогам README-файлы.

    Профилирование: PMPI, Jumpshot

    Профилирующая библиотека в MPICH идентична базовой, а функция MPI_Pcontrol ничего не делает. Таким образом, формально эта часть спецификации поддерживается, фактически - нет.

    Тем не менее, в MPICH включены средства, предназначенные облегчить генерацию и обработку статистики вручную. Это набор подпрограмм CLOG_Xxx, которыми программист может пользоваться для сохранения статистических данных, и написанное на яве средство визуализации под названием Jumpshot. Ни то, ни другое мною не испытывалось.

    Отладка

    mpirun имеет ключи для запуска приложения из-под того или иного отладчика, например, -gdb запустит GDB, -xxgdb - GDB c графическим интерфейсом для X-Window, и так далее. Поскольку GDB позволяет отлаживать только один процесс, mpirun запускает под ним нулевую ветвь. Нулевая ветвь запускается автоматически и останавливается после входа в MPI_Init(), поэтому пытаться ставить точки останова до MPI_Init() в высшей степени бессмысленно. Запуск остальных ветвей производится в MPI_Init() нулевой ветви.

    В последнее время ходит много слухов о параллельном отладчике TotalView, который якобы весьма перспективен, перенесен на многие платформы и совместим с MPI. Сам я никогда его не видел, но так говорят.

    Объединение нескольких машин через P4

    Последовательность действий следующая:

    1. при конфигурировании MPICH (перед построением) должно быть выбрано "устройство P4":
          ./configure -device=ch_p4 -comm=shared
    2. в каталоге .../mpich/util/machines, согласно находящемуся там README-файлу, должны быть созданы текстовые файлы с именами вида machines.<ARCH>, каждый из которых содержит адреса всех машин данной архитектуры.
    3. на всех ведомых машинах должен быть настроен сервис "remote shell":
      1. имя протокола для порта 514 прописано в файле /etc/services,
      2. то же самое имя вместе с именем демона rshd прописано в файле /etc/inetd.conf,
      3. имя ведущей машины прописано в файле .rhosts в домашнем каталоге пользователя, или в /etc/hosts.equiv
    4. на ведущей машине должен иметься клиент "remote shell".
    Проверка работоспособности: rsh имя_машины uname -a
    Показатель того, что rsh не работает: сообщение Permission denied.

    Программный файл приложения должен быть предварительно вручную скопирован на те машины, на которых его предполагается запускать mpirun'ом.

    Реализации, производные от MPICH

    Коммерческие реализации выпускаются производителями ЭВМ. Как правило, они напрямую основаны на MPICH, поэтому повторяют его не только вследствие стандарта, но и во многих технических мелочах. Отличия могут заключаться в:

    • оптимизации под конкретную платформу,
    • учете архитектурных особенностей,
    • дополнительных функциях и утилитах,
    • совместимости с уже наработанным для данной платформы мат.обеспечением (отладчики, профайлеры и т.д.).
    В-частности, HP-MPI, установленный на ЭВМ HP SPP-1600, расчитан на одновременное использование всех коммуникационных интерфейсов с автоматическим выбором самого быстрого: разделяемая память (в т.ч. с оптимизацией для SPP), P4 и т.д.

    Большинство Win32-реализаций MPI - так же не свободные, а коммерческие продукты. Это объясняется тем, что собственно MPICH изначально для переноса и оптимизации под Windows не предназначен и не приспособлен; ожидать, что эта работа будет проделана на безвозмездной основе, не приходится.

     


    MPI на ЭВМ SPP-1600

    Адрес компьютера: spp.csa.ru.
    MPI находится в каталоге /opt/mpi.
    Это HP MPI: коммерческая реализация, оптимизированная специально под архитектуру машин этой фирмы. Она работает на всех компьютерах (от рабочих станций до майнфреймов) с процессорами PA RISC и операционными системами HP-UX и SPP-UX.

    Запуск

    Собственно, mpirun, хотя на SPP можно обойтись и без него:

        ./mpi_ex0 -np 5
    то есть mpirun не нужен, а его ключи указываются первыми в командной строке приложения.

    Полезные дополнительные утилиты, в MPICH отсутствующие (подробности - в ManPages, или при запуске с ключом -help):

    • mpijob - выводит информацию о запущенных MPI-приложениях, запущенных как на локальной машине, так и на HPMPI-кластере. Аналогична команде Юникса jobs.

    • mpiclean - уничтожает MPI-приложение, действует наподобие команды kill. Идентификаторы приложений выдает команда mpijob. Ключ -m служит для освобождения "подвисшей" разделяемой памяти.

    • xmpi - графический загрузчик и монитор:
    XMPI sample screenshot

    Отладка

    Параллельный отладчик на SPP есть! Называется он CXdb. Фирма Convex адаптировала CXdb к процессору PA-RISC и операционной системе HP-UX фирмы Hewlett-Packard после своего превращения в Convex Division of HP. CXdb позволяет отлаживать приложение, состоящее из множества процессов, каждый из которых, в свою очередь, состоит из множества потоков. Пользователь имеет возможность указывать область действия для вводимых команд, т.е. перечислять те потоки и процессы, на которые команда распространяется. Управлять отладчиком можно как через систему меню и экранных кнопок, так и через несложный командный язык. Замечание из собственного опыта: в CXdb оказалось все необходимое для отладки писавшихся мною приложений, как параллельных, так и чисто последовательных.

    MPI application running under CXdb

    MPI-приложение под CXdb запускается так: все аргументы mpirun записываются в отдельный файл, назовем его mpi_ex0.cmd:

        -np 4 ./mpi_ex0
    Команда для запуска отладчика выглядит так: cxdb -mpi mpi_ex0.cmd. Отладчик запускает программу, прогоняет ее до MPI_Init(), приостанавливает, после чего переходит в режим ввода команд.

    2 предупреждения касательно точек останова:

    • Не ставьте точек останова до MPI_Init! К тому моменту, когда у Вас появится такая возможность, вся эта часть программы до MPI_Init включительно уже выполнена.
    • Точка останова, включаемая щелчком мыши по номеру строки в окне исходного текста, действует только в текущем потоке текущего процесса. Будьте внимательны!

    Подсказки и документация

    Manual pages по HP MPI находятся в каталоге /opt/mpi/share/man:

        C-Shell:   setenv MANPATH /opt/mpi/share/man:$MANPATH
          bash:    export MANPATH=/opt/mpi/share/man:$MANPATH
    
    Файл с полезной информацией по HP MPI: /opt/mpi/newconfig/RelNotes/.

    Справочная информация по CXdb:

    • Как видно на вышеприведенной картинке (см.в верхний правый угол обоих окон), в самом отладчике имеется оперативная помощь. Составлена она качественно.
    • man cxdb на SPP-1600 - описание запуска и ключей командной строки.
    • man csd на dragon.csa.ru или magic.csa.ru. Может пригодиться, если CXdb используется в консольном режиме, то есть с ключом -csd. При этом его собственная полноэкранная подсказка становится недоступна.

    Профилирование

    Стандартный профайлер CXpa приспособлен для работы с MPI-приложениями. я с ним не работал.

    Допустим следующий способ профилирования:

    1. mpirun запускается с ключом -t:
          mpirun -np 4 -t mystatfile program_name [program_args...]
      каждая ветвь в ходе выполнения будет писать статистику в свой файл, имена всех файлов будут иметь общий префикс mystat и расширение .tr

    2. созданный двоичный файл с расширением .tr может быть просмотрен в мониторе xmpi, либо в генераторе отчета mpitrstat:
          mpitrstat foo.tr | more
      Вот пример такого отчета:
       mpitrstat  HP MPI 1.2  - SPP-UX            Sat Nov  7 07:58:05 1998
      
      
                                Application Summary
      
                             Duration                          Average
             Trace   Procs     [secs]      User       MPI     [kbytes]
             ---------------------------------------------------------
             foo     4       21.16959   100.00%     0.00%       0.000K
      
      
                                Application Analysis
                                        foo
      
                                                  Duration            
             Process      Executable   Segments     [secs]         MPI
             ---------------------------------------------------------
             0:11971(0)   mpi_ex9      0          5.305031       0.00%
             0:11972(1)   ...          0          5.274548       0.00%
             0:11973(2)   ...          0          5.305022       0.00%
             0:11970(3)   ...          0          5.284993       0.00%
      
                                  Routine Summary
      
                                       Overhead  Blocking             
             Routine       Calls         [secs]    [secs]      Percent
             ---------------------------------------------------------
             USER_code         4       21.16959   0.00000      100.00% 
      
                                  Process Summary
      
                     Overhead Blocking     User                       
             Process   [secs]   [secs]   [secs]    Dominating Routines
             ---------------------------------------------------------
      

    3. Несколько .tr-файлов могут быть объединены в один утилитой mpitrget. Предполагается, что таким образом файлы ветвей будут объединяться в единый файл-отчет приложения.

    4. Запись статистики можно включать и отключать программно вызовами функций MPIHP_Trace_on() и MPIHP_Trace_off(). Запуск с первоначально отключенной статистикой делается так:
          mpirun -np 4 -t mystatfile:off program_name [program_args...]
      Собираемая статистика предназначена исключительно для профилирования MPI, а не пользовательской части приложения:
      • сколько вызовов MPI-подпрограмм было сделано за время трассировки,
      • сколько времени потрачено в процентном/абсолютном отношении,
      • какое количество времени ушло на ожидания при приемопередачах,
      • и какая его доля при этом не была использована приложением, и т.д.

      Почему на SPP не рекомендуется работать с MPI

      Основных причин - три:

      1. С 1996 года процессор PA-RISC 7200 успел морально устареть, и производительность SPP на фоне современных ЭВМ довольно невелика.

      2. На ЭВМ с общей памятью, какой является SPP, MPI по скорости работы проигрывает программам, использующим общую память, семафоры и многопоточность.

      3. На ЭВМ с оптимизирующими распараллеливающими компиляторами, какой опять-таки является SPP, MPI проигрывает традиционному программированию по скорости разработки приложения, поскольку требует явного ручного распараллеливания, с сомнительным выигрышем по скорости выполнения (см.пред.пункт).

      По-видимому, наилучшим стало бы такое разделение обязанностей:

      • на SPP производится отладка и (частично) профилирование; для этого на SPP имеется удобный отладчик, а кроме того, большое ОЗУ, большой жесткий диск, качественное "железо" и надежное программное обеспечение,
      • затем приложение MPI переносится на более быстрый, но менее оснащенный мат.обеспечением компьютер, где после минимальных исправлений запускается в рабочем режиме.

      К сожалению, в настоящий момент SPP сверх всякой меры перегружен именно рабочими расчетами некоторых особенно достойных деятелей, из-за чего 8-процессорный монстр работает как черепаха. Эта безответственная практика безнаказанно цветет столь пышным цветом, что какое-то более рациональное его использование стало практически невозможным.

       


      WinMPICH: устаревшая реализация для Windows

      Свои первые примеры на MPI я писал, используя пакет WinMPICH 0.92b и Microsoft Visual C++ 4.0. Все работает нормально. К примерам прилагается make-сценарий.

      WinMPICH скомпилирован в отладочном режиме, а именно...

      • На экране регулярно будут появляться окна наподобие этого:

        Debug Assertion Failed!

        Выбирайте "Ignore" ( "Пропустить" ).

      • Отладочному варианту для работы требуются две дополнительных динамических библиотеки, доступных через PATH. Скачайте их со страницы WinMPICH, поместите в его BIN-директорию, и добавьте C:\WinMPICH\bin в переменную окружения PATH.

      • WinMPICH без проблем компилируется в релиз-версию, не содержащую отладочной информации, не выводящую ненужных окошек и не требующую дополнительных библиотек.

      Недостатки

      • Последняя версия WinMPICH вышла в 1996 году. Судя по всему, проект заглох. Таким образом, эта реализация не предоставляет программисту ту часть MPI, которая была или будет стандартизована позднее.

      • WinMPICH не совместим с другими реализациями MPI, то есть не может служить для объединения разнотипных машин в MPI-кластер. Объясняется это тем, что для межмашинной связи WinMPICH использует свой собственный упрощенный протокол вместо стандартного P4, используемого в базовом MPICH.

      • В WinMPICH-кластере все машины, кроме ведущей (с которой происходит запуск mpirun) должны работать под управлением Windows NT, а не Windows'9x.

      • Поскольку все ветви запускаются загрузчиком MPIRUN.EXE, их нельзя запустить из-под отладчика. Отладчик требуется "присоединять" к уже запущенной ветви, в запланированную для начала отладки точку исходного текста которой рекомендуется вписать вызов assert(0), или, на худой конец, getch(). За более подробными разъяснениями по поводу отладки в среде Win32 милости прошу сюда.

       


      WMPI: еще одна реализация MPICH для Windows

      WMPI наиболее популярен в среде Windows в настоящий момент. Работает в '95 и в NT, с Си и Фортраном. В обоих случаях имеются в виду компиляторы от Микрософта. Путем определенных манипуляций можно заставить WMPI работать с Borland C++.

      Сайт разработчиков: http://dsg.dei.uc.pt/wmpi

      Недостатки

      • исходники не публикуются,
      • мне не удалось связать его с MPICH/Linux, несмотря на заявления авторов WMPI о возможности такого симбиоза.

      Достоинства

      • развивается в ногу с MPICH (в частности, поддерживает MPI-IO),
      • запускается и работает быстрее, чем WinMPICH,
      • пригоден для образования MPI-кластеров на базе машин с Win32.

      Документация и подсказки

      В HTML-формате, смотрите файл .../Docs/index.html. Описание MPI-IO (функций распределенного файлового ввода-вывода) отсутствует.

      Построение программ

      Kомандные файлы MPICC.BAT и MPIRUN.BAT отсутствуют, так как предполагается, что программист использует для работы интегрированную среду Microsoft Developer Studio, а не командную строку. Применительно к Developer Studio подробно указаны все изменения, которые требуется внести в проект по умолчанию.

      Вниманию фанатов командной строки предлагается пример mpicc.bat для построения консольных WMPI-приложений.

      Запуск

      Построенный EXE-файл запускается без всяких загрузчиков.

      Он требует для работы динамическую библиотеку CVWMPI.DLL (если программа консольная) или VWMPI.DLL (если она графическая). Соответственно, Вам требуется либо вписать каталог с DLL в переменную окружения PATH, либо скопировать DLL в...:

      1. The directory from which the application loaded.
      2. The current directory.
      3. Windows 95: The Windows system directory. Use the GetSystemDirectory function to get the path of this directory. Windows NT: The 32-bit Windows system directory. Use the GetSystemDirectory function to get the path of this directory. The name of this directory is SYSTEM32.
      4. Windows NT only: The 16-bit Windows system directory. There is no Win32 function that obtains the path of this directory, but it is searched. The name of this directory is SYSTEM.
      5. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
      6. The directories that are listed in the PATH environment variable.

      Так гласит описание LoadLibrary в справочнике по WinAPI.

      Количество и место запуска ветвей задается в текстовом файле, имеющим то же имя, что EXE-файла, плюс расширение .PG. Строки в нем имеют такой вид:

          # комментарий
          local Количество_ветвей
          Адрес_машины Количество_ветвей Имя_EXE_файла
      

      Когда PG-файл читается, ветвь номер 0 уже создана (в запущенном EXE-файле в этот момент выполняется функция MPI_Init), таким образом, общее количество ветвей на 1 больше указываемого в PG-файле. То есть, чтобы запустить приложение из 4 ветвей на своей машине, надо написать в PG-файле:

          local 3

      При желании, если вместо стандартного интерпретатора команд Command.com или Cmd.exe используется 4DOS или 4NT, простейший mpirun.btm может быть записан так:

      @echo off
      ::  This is mpirun.btm (4DOS/4NT batch file) sample for WMPI
      
      if "%1" ne "-np" (
         echo Usage mpirun -np NN program.exe args...
         beep
         quit 1
      )
      echo local %@dec[%2] > %@path[%3]%@name[%3].pg
      set PATH=C:\Program files\WMPI_1.2\Lib\Console;%PATH%
      %3&
      

      Чтобы получить подсказку по ключам запуска WMPI-приложения, запустите его с ключом -p4help.

      Отладка

      Параллельных отладчиков для Win32 нет. Каждая отлаживаемая ветвь должна выполняться под управлением отдельного экземпляра отладчика. Ветвь 0 отлаживается запуском EXE-файла непосредственно из интегрированной среды или автономного отладчика (WinDbg для Visual C++, Turbo Debugger для Borland C++).
      К остальным ветвям отладчик следует "цеплять". В DevStudio это делается через пункт меню "Build->Start Debug->Attach to Process...".

      Профилирование

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

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


      PowerMPI для Парситека

      PowerMPI - это MPICH, адаптированный к архитектуре ЭВМ Parsytec. В качестве коммуникационного уровня он использует API Parix. Файлы PowerMPI располагаются внутри каталога с Париксом (/export/home/parix/mpi на ЭВМ pink.csa.ru, /epx/mpi на всех остальных).

      В построенном приложении от MPI не остается уже ничего - это обычное приложение Парикса, которое и запускаться должно командой px run. Это, кстати, означает, что при использовании PowerMPI Парситеки не могут быть объединены в MPI-кластеры ни друг с другом, ни с ЭВМ других типов. Поскольку Парcитек не cтал утруждать себя созданием таких типовых утилит, как mpicc, mpif77 и mpirun, строить MPI-приложение тоже предлагается как-то так:

      px ancc mpi_sin.c -o mpi_sin.px -I/epx/mpi/include \
         -L/epx/mpi/lib/parix/ch_px -lmpi

      Предлагаемые здесь варианты mpicc/mpif77 и mpirun я написал, чтобы иметь меньше возни с: а) переносом готовых MPI-приложений на Парситек; б) пересаживанием закоренелых MPI-пользоватетелей на него же. Для размещения сценариев в каталоге с PowerMPI создается подкаталог bin, который затем должен быть подключен к переменной окружения PATH:

          setenv PATH /epx/mpi/bin:$PATH

      Замечания по mpicc:

      • переменная PX_MPI в исходном тексте mpicc (секция "Options") должна содержать правильный путь к PowerMPI,
      • драйвер к компилятору Фортрана-77 получается следующим образом: ln -s mpicc mpif77,
      • редактируйте при необходимости секцию "Check self name" для исправления имени компилятора Фортрана-77 и добавления поддержки других компиляторов (f90, etc.).
      Эти командные файлы могут быть либо не установлены на некоторых Парситеках, либо содержать ошибки и недоработки. Самые последние версии всегда находятся на этой странице.

      Отладка и профилирование

      Для этого доступны все средства, какие есть в составе Парикса. Разбираться с ними я либо не пробовал, либо пробовал, но безуспешно. Сюда относятся параллельный полноэкранный отладчик DeTop (у меня не запустился), профайлеры prof и gprof. Собственно PowerMPI в своем составе ничего ни для отладки, ни для профилирования не имеет; документация так же отсутствует.

      Что будет при попытке использовать MPICH вместо PowerMPI ?

      MPICH будет использовать для межузловой связи "P4 поверх TCP/IP поверх Ethernet" вместо значительно более быстрого "Parix поверх HighSpeedLink", поскольку ничего не знает о последнем.

      Пользователь должен самостоятельно копировать программный файл MPICH-приложения на те узлы, на которых собирается его запускать. Для этого администратор системы должен открыть локальные диски вычислительных узлов для пользовательского доступа.

      В случае, если предыдущий пункт не выполнен, или загрузчик mpirun запускается только с ключем -np, MPICH запустит приложение не на всем Парситеке, а на одном-единственном узле (надо полагать, это будет входной узел). Естественно, что в этом случае никакое распараллеливание не принесет ни малейшего выигрыша.

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

       


      Версии документа

        31 мая 1999 ........ выпущен в свет