Вы здесь

ИНСТРУКЦИЯ ПРИКЛАДНОМУ ПРОГРАММИСТУ ПО КОММУНИКАЦИОННОЙ СРЕДЕ TCP Router ДЛЯ МВС-1000/16

ИПМ им. М.В. Келдыша РАН.

Отдел ИВСиЛС, сектор эксплуатации МВС.

А.О. Лацис

 

          TCP Router очень похожа на среду Router для МВС-100 и на Router+  для  МВС-1000/200. Программы для МВС-100 должны работать без изменений. Программы для МВС-1000/200 потребуют изменений в части, касающейся возможности рассогласования длин сообщений на передающем и приемном концах.

 

1.      Модель вычислений.

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

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

Широковещание отсутствует.

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

Для любой пары процессов последовательность передач и приемов сообщений совпадает: если процесс 5 послал процессу 7 сначала сообщение А, а затем сообщение Б, то процесс 7 не получит сообщение Б, пока не прочитает А. В этом заключается главное отличие Router от MPI, p4, nx и ряда других популярных пакетов, в которых использование аппарата тегов и коммуникаторов позволяет принимать сообщения не в том порядке, в каком они были посланы, или, что то же самое, принимать сообщения выборочно, откладывая прием некоторых из них на потом. 

Асинхронность обменов – подлинная: вне зависимости от длин сообщений, конкретных обстоятельств загрузки внутренних буферов и т. п. передача сообщения начинает происходить, как только обмен запущен  на обоих концах, и ее скорость в астрономическом времени никак не зависит от того, проверяют ли участники обмена факт его завершения. Это позволяет совмещать во времени обмены со счетом без дополнительных усилий по «проталкиванию» обменов со стороны прикладной программы.

 

2.      Подмножества возможностей.

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

Основной набор практически совпадает с возможностями Router для МВС-100. Дополнительные возможности в каждой реализации свои (в TCP Router они не такие, как в Router+ для МВС-1000/200, хотя частично и перекрываются с ними). Как и всякие дополнительные возможности, предназначены они в основном для более или менее тонких оптимизаций производительности под конкретную архитектуру машины. Пользуясь ими в большей или в меньшей степени, Вы фактически идете на компромисс между производительностью и переносимостью программ между разными машинами семейства МВС.

 

3.      Основные возможности.

Если Вы уже сталкивались с машинами семейства МВС и с библиотекой  Router, Вам имеет смысл вместо чтения настоящего раздела просто заглянуть в директорию /common/router.example на Вашей МВС-1000/16. Если нет, раздел следует внимательно прочитать. Ниже приводится список функций Router c объяснениями, как они работают в режиме основных возможностей. Дополнительные возможности описаны далее.

Программа на C, использующая TCP Router, должна содержать

#include <routelib.h>

Для программ на Фортране никаких специальных директив не требуется.

Служебные функции.

Для  того,  чтобы  инициализировать    Router  и одновременно  узнать  собственный  номер  в наборе процессов, а также общее число процессов  в  наборе,  следует  обратиться  к функции

rf_create()

      extern int rf_create( void )

     ....

     n = rf_create();

     if ( !n )

      {

       fprintf( stderr, "rf_create error\n" );

       return...

      }

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

     extern int sysProcNumGet( void ), sysProcTotalGet( void );

     ....

     my_number  = sysProcNumGet();

     n_of_nodes = sysProcTotalGet();

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

        n = node_number()

c n теперь равно номеру процессора

        nn = n_of_nodes()

c nn теперь равно числу процессоров в конфигурации.

Вызывать перед этим rf_create в  программе  на  Фортране нет необходимости – обращение к любой из этих функций инициализирует Router.

Инициализация обязательна до обращения к любым другим функциям Router.

Для измерения времени в программах на C можно использовать функцию

_cputime()

Возвращаемое значение – целочисленное локальное время (с некоторого фиксированного момента) в миллисекундах, аргументов нет. В программах на Фортране для тех же целей применяется целочисленная подпрограмма – функция

node_time()

Функции обмена сообщениями.

     Имеется  всего  6  функций  обмена   сообщениями   между процессами:

     - запустить  посылку  указанного  массива  в  указанный процесс,

     - запустить  прием  в  указанный  массив  из  указанного процесса,

     - ждать конца посылки,

     - ждать конца приема,

     - проверить кончилась ли посылка,

     - проверить, кончился ли прием.

     Формат обращения:

        extern int r_write( int /*proc*/,

                          void* /*buffer*/, int /*length*/ );

     - запустить посылку. Первый аргумент - номер процесса, в который послать, второй - адрес передаваемого массива, третий - его длина. Нулевой длины не бывает!!!

     Возвращаемое значение:

      0           (RUN_OK) - обмен запущен,

     -1  (RUN_NO_PROC) - несуществующий номер процесса,

     -2 (RUN_BUSY) - слишком много предыдущих посылок в указанный процесс не завершено.

        extern int r_read( int /*proc*/,

                         void* /*buffer*/, int /*length*/ );

     - запустить прием. Смысл аргументов  тот  же,  что  и  в r_write.

     Возвращаемое значение:

      0 (RUN_OK) - обмен запущен,

     -1 (RUN_NO_PROC) - несуществующий номер процесса,

     -2  (RUN_BUSY) -  слишком  много  предыдущих  приемов из указанного процесса не завершено.

        extern int w_write( int /*proc*/ );

     -  ждать конца посылки. Аргумент - номер процесса, конца последней посылки которому ждать, или  специальное  значение, если ждать не последней посылки (см. ниже).

     Возвращаемое значение:

      0 - несуществующий номер процесса,

      1 - конец посылки.

     Попытка повторно подождать заведомо завершенного обмена немедленно завершается с кодом 1.

        extern int w_read( int /*proc*/ );

     -  ждать конца приема.  Аргумент - номер процесса, конца последнего   приема   от   которого  ждать,  или  специальное значение, если ждать не последнего приема (см. ниже).

     Возвращаемое значение:

      0 - несуществующий номер процесса,

      1 - конец приема.

     Попытка повторно подождать заведомо завершенного обмена немедленно завершается с кодом 1.

        extern int t_write( int /*proc*/ );

     - проверить, кончилась ли  посылка.  Аргумент  -  как  в w_write.

     Возвращаемое значение:

     0  -  несуществующий  номер  процесса,  или  посылка  не завершена,

      1 - конец посылки.

        extern int t_read( int /*proc*/ );

     - проверить, кончился ли прием. Аргумент - как в w_read.

     Возвращаемое значение:

      0 - несуществующий номер процесса, или прием не завершен,

      1 - конец приема.

     Для  каждой из описанных выше шести функций обмена сообщениями имеется Фортранный эквивалент, с  тем  же  именем,  но с  одним  дополнительным параметром СПЕРЕДИ - целочисленным кодом ответа, например:

     CALL W_WRITE( IRET, N )

     ...

Очередность запуска обменов

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

Семантика блокировок

          Ниже описывается семантика блокировок и дисциплина использования пересылаемых массивов в наиболее «жестком» варианте. Если Вы будете писать программу в рамках перечисленных здесь ограничений, она гарантированно будет работать на любом варианте библиотеки Router. В действительности конкретная реализация - TCP Router для МВС –1000/16 – налагает гораздо более слабые ограничения. Об этом – ниже, в описании дополнительных возможностей.

          Под блокировкой будем понимать невозможность возврата из обращения к функции Router без наступления некоторого внешнего события. Так, если обращение к функции r_write() не способно завершиться, пока на противоположном конце не будет вызвана r_read(), то это – блокировка. Подчеркнем, что блокировка не имеет никакого отношения к скорости срабатывания или времени наступления события, а означает только обусловленность: нас не волнует, через миллисекунду или через час после выполнения r_read() на партнерском конце наш r_write() завершится. Нас волнует тот факт, что если на партнерском конце r_read() запущен вообще не будет, то наш r_write() не завершится никогда. Очевидно, понимание логики блокировок критически важно для разработки параллельных программ.

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

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

          При этих допущениях гарантируется не более чем следующий набор блокировок:

          r_write() (запуск посылки) может блокироваться запуском приема (r_read()) на противоположном конце,

          r_read() (запуск приема) не блокируется никогда,

          t_read(), t_write() (проверки завершенности обмена) не блокируются никогда,

          w_write() (ожидание завершения посылки) может блокироваться запуском приема (r_read()) на противоположном конце, только если соответствующая посылка действительно была запущена,

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

          Несколько слов об использовании массивов, участвующих в обменах.

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

          С другой стороны, факт завершения передачи НЕ означает, что сообщение принято или даже что прием на противоположном конце запущен: сообщение может быть «запомнено по дороге» от передающего конца к приемному. Завершение передачи означает, что содержимое переданного массива можно изменять по своему усмотрению, но оно (завершение) не может служить индикатором действий приемной стороны.

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

 

4.      Дополнительные возможности.

Дополнительные возможности касаются ослабления ограничений, изложенных выше в разделах «Очередность запуска обменов» и «Семантика блокировок». Напомним еще раз, что дополнительные возможности в разных реализациях Router разные. Ниже излагается вариант TCP Router для МВС – 1000/16.

Число обменов с данным партнером в данном направлении (приемов или передач), которые можно запустить, не дожидаясь конца первого из них, на уровне основных возможностей равно 1 (нельзя запускать следующий, пока предыдущий не завершился). В TCP Router это число в действительности равно константе, которая хранится в определяемой в #include-файле routelib.h внешней целочисленной переменной request_buffer_size (сейчас там находится значение 16, изменять это значение программа пользователя не должна). Отметим особо, что при постановке таким образом обменов в очередь запоминаются не сообщения, а запросы на обмен. Если Вам удалось поставить в очередь 8 запросов на передачу, это не означает, что предыдущие 7 сообщений уже переданы. Это означает только, что все 8 запросов запомнены и будут выполнены по мере запуска соответствующих приемов, строго в том порядке, в каком запросы ставились в очередь. То же верно и в отношении постановки в очередь нескольких запросов на прием.

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

Короткие и длинные сообщения

          В упоминавшемся выше #include-файле routelib.h определена внешняя целочисленная константа small_message_size. Программа пользователя не должна изменять ее значения (которое в данный момент равно sizeof( double )). Это значение задает предельную длину так называемых коротких сообщений (все сообщения большей длины, напротив, считаются длинными). Разграничение сообщений на короткие и длинные вызвано тем, что для коротких сообщений удается улучшить, по сравнению с длинными, показатель латентности (времени запуска обмена). С точки зрения дополнительных возможностей короткие и длинные сообщения обладают разными свойствами.

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

          Правила работы с несовпадающими длинами таковы:

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

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

-         если обе длины находятся в диапазоне длинных сообщений, то действует «потоковая» дисциплина: все посылаемые данному партнеру сообщения «склеиваются» в порядке посылки, а на приемном конце получившийся сплошной поток заново «нарезается» теми частями, как заказано при приеме. При этом, очевидно, правило «один прием – одна посылка» перестает работать: можно послать 40, 29 и 31 байт (три посылки), а принять два раза по 50 или один раз 100.

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

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

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

Проверка завершения не последнего из запущенных обменов.

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

          Для этого необходимо обратиться к одной из функций синхронизации (t_write(), w_write(), t_read(), w_read()), передав ей в качестве аргумента не номер процесса, а дескриптор запущенного обмена. Дескриптор только что запущенного обмена непосредственно при срабатывании r_write() или r_read помещается во внешнюю целочисленную переменную started_handle, описанную в #include-файле routelib.h. Его можно взять оттуда и впоследствии использовать, передав в качестве аргумента в одну из функций синхронизации. При этом важно понимать следующее:

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

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

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

 

5.      Краткий разбор примера использования.

В директории /common/router.example находится исходный текст и командный файл для сборки программы tnet – простейшего теста работоспособности библиотеки Router. Программа представлена в двух вариантах – на C и на Фортране. Используется режим основных возможностей. Программа рассчитана на произвольное число процессов, запускается командой routerrun.

Делает она следующее:

-         запускает прием от всех процессов целочисленного массива заданной длины,

-         рассылает всем процессам такой же массив, заполненный счетчиком, сдвинутым на собственный номер,

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

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

Длина пересылаемого счетчика и число повторений описанного выше цикла рассылки  задаются непосредственно в исходном тексте программы.

Яндекс.Метрика