|
Исследования › shmem › Руководство программиста.
Вычислительная система МВС – экспресс.
Базовая коммуникационная библиотека shmem-экспресс.
Руководство программиста.
Дбар С. А., Лацис А. О., Храмцов М. Ю.
Введение .
Описываемая ниже библиотека shmem-экспресс реализует диалект коммуникационной системы shmem, хорошо известной пользователям
суперкомпьютеров Cray и Silicon Graphics, а также кластеров на основе сети
Quadrics.
Система shmem не стандартизована, ее реализации для
различного оборудования отличаются в незначительных деталях. Далее в тексте
будут специально отмечены функции shmem-экспресс, которые либо отсутствуют в
других реализациях, либо отличаются набором аргументов.
Библиотека shmem-экспресс пока реализована в минимальном,
базовом варианте. В дальнейшем планируется расширение реализации путем
добавления крупных разделов функциональности, отсутствующих в настоящее время.
Модель программирования shmem подразумевает взаимодействие
независимых процессов, каждый – со своей локальной памятью, занумерованных по
порядку от нуля, и в этом она похожа на модель программирования MPI. Отличие от
MPI состоит в том, что обмены данными между процессами являются односторонними. Чтобы данные были переданы из процесса А в процесс Б, требуется не
согласованная активность обоих процессов, как в MPI, а лишь «желание» одного из
участников. Например, процесс А может «насильно» послать данные процессу Б, без
какой-либо ответной активности с его стороны. Процесс Б, в свою очередь, может
«насильно» прочитать данные из процесса А. В англоязычных описаниях этой модели
ее принято называть «модель put/get», в отличие от принятой в
MPI «модели send/receive».
Таким образом, в рассматриваемой модели каждый процесс
работает с памятью любого другого процесса примерно так же, как с файлом на
диске, то есть может в любое время самостоятельно обмениваться с ней данными
без ведома и участия процесса - «хозяина». Фактически это означает, что
пространства памяти всех процессов образуют единую, доступную всем разделяемую
память – shared memory (отсюда – название).
Работа в дисциплине единой разделяемой памяти ставит перед
программистом две проблемы, которых нет при использовании, например, MPI.
Во-первых, передача данных в «чужую» область памяти
перестает сопровождаться принудительной синхронизацией с процессом – «хозяином»
этой области памяти. Следовательно, отдельные предписания, предназначенные
специально для синхронизации процессов, становятся совершенно необходимыми. В
модели программирования shmem основным видом синхронизации является барьер. В
отличие от большинства реализаций shmem, shmem-экспресс позволяет выполнить
барьер по совершенно произвольному подмножеству процессов.
Во-вторых, выполнение одностороннего обмена подразумевает,
что процессу – инициатору обмена известны значения адресов, по которым
интересующие его данные расположены в «чужой» памяти. В shmem для решения этой
проблемы применяется частный, но очень простой, и потому – эффективный, прием. Прежде
всего, предполагается, что двоичный исполняемый файл для всех процессов – один
и тот же. Это автоматически означает, что для многих категорий переменных и
массивов адреса одноименных программных объектов в разных процессах совпадают. В
частности, в программах на C таковыми являются все переменные и массивы,
объявленные статически. Для таких переменных и массивов односторонние обмены
можно задавать в стиле: «мой массив A положить в массив B процесса С». Зная адрес
своего массива B, процесс – инициатор обмена, тем самым, знает и его адрес в
любом другом процессе, и может пользоваться своим адресом в качестве чужого. С
другой стороны, к переменным и массивам, объявленным в теле функции без
квалификатора «static», операции
односторонних обменов данными в стиле shmem просто не применимы. Адреса этих
объектов не только не совпадают в разных процессах, но и не определены
статически. Адреса областей памяти, возвращаемые при обращениях к malloc(), занимают
промежуточное положение: они определены статически, начиная с момента обращения
к malloc(), но в разных процессах, вообще говоря, отличаются. В большинстве
реализаций shmem предусмотрена специальная коллективная операция, выделяющая
такие области памяти во всех процессах с гарантированно одинаковыми адресами. В
shmem-экспресс используется другой подход,
основанный на понятии распределенных
массивов (co-arrays). В рамках этого подхода, адреса массивов, которые программисту удобно
считать «одним и тем же массивом в разных процессах», не делаются принудительно
одинаковыми при выделении памяти, а просто рассылаются всем процессам для
общего сведения.
В последующих разделах документа функции shmem-экспресс
описываются подробно. Введем несколько важных терминов.
Переменные и
массивы, адреса которых определены статически, причем совпадают в разных
процессах, будем называть симметричными.
Будем называть
удаленными
с точки зрения некоторого процесса «чужие», то есть расположенные в другом
процессе, данные. В этом тексте мы всегда будем понимать под удаленными
«чужие», не являющиеся локальными, данные, и никогда не будем использовать
слово «удаленный» в смысле «ликвидированный» или «уничтоженный». «Удаленный
файл» - это файл на диске другой машины, а не файл, который предварительно
удалили. Данные, находящиеся в некотором процессе, то есть «свои» для этого
процесса, будем называть локальными.
Хотя стандарта
shmem, как мы уже говорили выше, и не существует, большинство функций в
большинстве реализаций совпадают. В качестве реализации, на которую
shmem-экспресс стремится быть похожей, была выбрана реализация shmem для сети
Quadrics. Если функция shmem-экспресс совпадает по имени с некоторой функцией Quadrics shmem, но отличается по
набору аргументов, будем в описании отмечать такую функцию как нестандартную.
Функции, имеющиеся только в shmem-экспресс, отмечены в описании как дополнительные.
Прототипы
описанных ниже функций находятся в файле заголовков shmem.h.
1. Функции инициализации, синхронизации и общего
управления.
void shmem_init( int *argc, char ***argv ) – инициализировать библиотеку. Нестандартная.
void shmem_finalize( void ) – выполнить общий барьер и терминировать библиотеку.
Дополнительная.
int shmem_my_pe( void )
int my_pe( void ) – узнать собственный номер процесса.
int shmem_n_pes( void )
int num_pes( void ) – узнать общее число процессов.
void shmem_barrier_all( void ) – выполнить общий барьер,
предварительно дождавшись фактического завершения всех выполненных ранее
операций «put».
void shmem_quiet( void)
void shmem_fence( void ) – дождаться фактического выполнения всех операций
«put», выполненных ранее данным процессом, в памяти соответствующих удаленных
процессов («протолкнуть буфера записи»).
double shmem_time( void ) – узнать локальное системное время в секундах.
Дополнительная.
Далее
описываются функции, позволяющие выполнять барьер не по всем процессам задачи,
а лишь по некоторому набору процессов
(избирательный барьер).
В имеющихся на сегодня реализациях shmem в качестве
набора процессов, выполняющих барьер, допускается лишь набор процессов с
номерами, следующими друг за другом с шагом, равным степени двойки. Это
довольно неудобно. В shmem-экспресс решено задавать набор процессов,
выполняющих барьер, просто в виде произвольного списка номеров процессов. Кроме
того, в имеющихся реализациях при запросе избирательного барьера каждый процесс
– участник обязан предоставить некий рабочий буфер, симметричный и заранее
обнуленный. В shmem-экспресс для реализации избирательного барьера также
требуется некий вспомогательный буфер, обладающий целым рядом нетривиальных
свойств. Однако, произвольный массив из адресного пространства процесса, даже
симметричный, в этом качестве не годится. Пригодные для организации
избирательных барьеров буфера скрыты внутри shmem, программе пользователя доступны лишь их
номера. Всего таких буферов около 100 штук. Чтобы получить возможность
выполнить избирательный барьер, его будущие участники должны «договориться» о
номере используемого при этом буфера (номер должен быть задан одинаковым у всех
участников барьера). Получение номера очередного свободного буфера – операция,
коллективная по ВСЕМ процессам (внутри нее выполняется общий барьер),
возвращаемые во всех процессах номера гарантированно совпадают:
int shmem_barrier_alloc( void ) – получить
свободный номер вспомогательного буфера для выполнения избирательного барьера. Дополнительная.
Буфера для
выполнения избирательного барьера следует получать с таким расчетом, чтобы
никакие два разных избирательных барьера, использующие один и тот же буфер, не
могли выполняться одновременно.
Возвращаемые
функцией при успешном срабатывании номера буферов положительны. Если буфера
закончились, функция возвращает 0.
void shmem_barrier( int nodes, int *barlist, int barbuf ) – выполнить избирательный барьер,
предварительно дождавшись фактического завершения всех выполненных ранее
операций «put». Нестандартная.
Список
участников барьера – в массиве barlist длиной nodes, barbuf – номер
вспомогательного буфера, полученный ранее обращением к shmem_barrier_alloc().
void *emalloc( long size ) – запросить массив памяти. Дополнительная.
По
техническим причинам, при реализации shmem-экспресс часть оперативной памяти
материнской платы узла может выводиться из-под управления ОС. Получить и
использовать в программе эту память можно только при помощи обращения к
специальной функции. Чтобы эта память не пропадала, рекомендуется вместо
функции malloc() использовать emalloc(), которая сначала пытается
воспользоваться упомянутой «неудобной» памятью, и лишь при неудаче обращается к
malloc(). Функции освобождения
выделенной таким образом памяти не предусмотрено.
Далее
описываются функции, позволяющие использовать операции односторонних обменов в
отношении данных, не являющихся симметричными. Как уже говорилось выше, для
этого процесс – инициатор обмена должен знать численное значение локального
адреса интересующих его данных в процессе – хозяине данных.
В
shmem-экспресс предусмотрена специальная коллективная операция – операция
создания распределенного массива. Каждый из участников этой операции сообщает
всем остальным значение локального адреса некоторого своего массива, чтобы
остальные могли, при необходимости, осуществлять доступ к этому массиву
односторонними обменами.
void shmem_coarray_all( void *section, long sectionsize, void *capointer[] ) –
организовать распределенный массив во всех процессах. Дополнительная.
Выполнение
этой коллективной операции подразумевает общий барьер. В результате выполнения
этой операции массив указателей capointer
в каждом процессе будет заполнен значениями локальных адресов массивов section соответствующих процессоров:
capointer[0] будет содержать значение section процесса 0,
capointer[1] будет содержать значение section процесса 1,
и так далее.
Таким
образом, каждый из процессов сможет обмениваться с массивом section любого другого процесса, хотя
массив этот симметричным не является (значения section во всех процессах разные).
Если
процесс укажет значение section
равным 0, то массив длиной sectionsize
байтов будет предварительно выделен обращением к emalloc(). После завершения операции процесс сможет узнать адрес
собственного массива, обратившись к элементу capointer с собственным номером.
void shmem_coarray( void *section, long sectionsize,
void *capointer[], int nodes, int *barlist, int barbuf ) – организовать
распределенный массив в указанном списке процессов. Дополнительная.
Выполнение
этой операции подразумевает избирательный барьер по списку процессов,
задаваемому тремя последними аргументами, в полной аналогии с функцией shmem_barrier(). Значения элементов capointer с номерами тех процессов,
которые в операции не участвуют (в barlist
не указаны), после операции не определено.
2. Функции односторонних обменов.
2.1. Односторонняя запись.
2.1.1. Одностороння запись отдельных значений.
void shmem_long_p( long *addr, long value, int pe ) –
выполнить запись значения value по
адресу addr в процесс pe.
void shmem_int_p( int *addr, int value, int pe ) – выполнить запись значения value по адресу addr в процесс pe.
void shmem_short_p( short *addr, short value, int pe ) –
выполнить запись значения value по
адресу addr в процесс pe.
void shmem_double_p( double *addr, double value, int pe ) –
выполнить запись значения value по
адресу addr в процесс pe.
void shmem_float_p( float *addr, float value, int pe ) –
выполнить запись значения value по адресу
addr в процесс pe.
2.1.2. Односторонняя запись массивов.
void shmem_put( void *target, void *source, long length, int pe )
void shmem_putmem( void *target, void *source, long length, int pe ) – выполнить
запись локального массива source длиной
length байтов по адресу target процесса pe.
Функции:
shmem_long_put,
shmem_int_put, shmem_short_put, shmem_double_put, shmem_float_put отличаются от shmem_put только типами передаваемых массивов и единицами измерения их длины length.
2.2. Одностороннее чтение.
void shmem_get( void *target, void *source, long length, int pe )
void shmem_getmem( void *target, void *source, long length, int pe ) – выполнить чтение удаленного массива source длиной length байтов из процесса pe по локальному адресу
target.
Функции:
shmem_long_get,
shmem_int_get, shmem_short_get, shmem_double_get, shmem_float_get отличаются от shmem_get только типами передаваемых массивов и единицами измерения их длины length.
2.3. Атомарное приращение.
void shmem_int_add( int *target, int value, int pe ) –
выполнить атомарное прибавление значения value
типа int к
значению с адресом target типа int в процессе номер pe.
void shmem_long_add( long *target, long value, int pe ) –
выполнить атомарное прибавление значения value
типа long к значению с адресом target типа long в процессе номер pe.
Атомарность
обеспечивается в случае, когда модификация локальных данных происходит путем
обращения к этим же функциям:
value += 5; - неправильно,
shmem_int_add( value, 5, shmem_my_pe() ); - правильно.
3. Прямая адресация удаленных данных.
Все впервые определяемые в данном разделе функции относятся к категории дополнительных.
Описанные выше возможности позволяют процессу параллельной программы получать доступ к любым удаленным данным, но только при помощи специальных функций доступа (семейства «get», семейства «put» и атомарных приращений). Также имеется возможность получить указатель для непосредственного доступа к удаленным данным, без использования специальных функций. Эта возможность реализована не для любых данных, а только для небольшой области памяти в каждом процессе, доступной непосредственно (по простому машинному указателю) из других процессов. Для краткости будем далее называть эту область памяти привилегированной. Совокупность привилегированных областей памяти всех процессов параллельной программы образует общую память.
Программа может запросить массив в привилегированной области памяти, обратившись к функции
void *shmem_cm_alloc( long n )
которая возвращает указатель на выделенный в привилегированной области данного процесса массив длиной n байтов или 0, если выделить такой массив не удалось.
Чтобы узнать, сколько байтов осталось в привилегированной области памяти данного процесса, следует обратиться к функции
long shmem_cm_left( void )
При использовании общей памяти очень важно понимать, что каждая из составляющих ее привилегированных областей имеет разные адреса «с точки зрения» разных процессов. Значение указателя на привилегированную область процесса 0, пригодное для использования в процессе 0, вовсе не обязано (и, как правило, не будет) совпадать со значением указателя на нее же, пригодное для использования в процессе 1 или 5.
Функция shmem_cm_alloc() возвращает указатель, по которому выделенный в привилегированной области данного процесса массив доступен из данного процесса. Для доступа к этому массиву из других процессов, значения указателей в этих других процессах должны быть, вообще говоря, совсем другими. Чтобы преодолеть возникающие по этой причине очевидные неудобства для программиста, используется механизм распределенных массивов в общей памяти. Этот механизм очень похож на аппарат распределенных массивов для работы при помощи функций доступа с не симметричными данными, описанный выше, в Разделе 1.
Функция
void shmem_cm_coarray_all( void *section, long sectionsize, void *capointer[] )
позволяет организовать распределенный массив в общей памяти, пригодный для непосредственного доступа к его секциям по обычному указателю.
В качестве первого аргумента ей передается значение, полученное из shmem_cm_alloc(), или 0, если требуется, чтобы функция сама вызвала shmem_cm_alloc() для выделения локальной секции распределенного массива. Второй аргумент задает требуемый размер секции. Третий аргумент – массив указателей, который функция должна заполнить указателями на секции создаваемого распределенного массива. Значения указателей имеют смысл при доступе к секциям распределенного массива из данного процесса.
Таким образом, указатель capointer[n] после обращения к этой функции указывает в каждом из процессов на одну и ту же – n-ю – секцию созданного распределенного массива. При этом числовые значения capointer[n] для одного и того же n в разных процессах будут разными.
Если какой-либо из процессов передал в качестве первого аргумента (адрес секции) значение 0, а выделить в этом процессе секцию требуемого размера в привилегированной области памяти не удалось, то во всех процессах соответствующий элемент массива capointer после обращения к функции станет равным 0. Этим можно (и нужно) пользоваться для проверки успешности создания распределенного массива в общей памяти.
В остальном данная функция вполне аналогична описанной выше, в Разделе 1, функции shmem_coarray_all(). В частности, она также реализует коллективную операцию, то есть выполняет внутри себя глобальный барьер.
Также имеется функция shmem_cm_coarray(), аналогичная в рассмотренном только что смысле функции shmem_coarray() из Раздела 1.
Распределенные массивы для работы при помощи функций доступа из Раздела 1 и распределенные массивы в общей памяти, описанные в настоящем разделе, не взаимозаменяемы. Возвращаемые в каждом из этих двух случаев значения элементов массива capointer должны использоваться только по назначению, например, если где-то в программе написано:
shmem_coarray(... , cap_f );
……
shmem_cm_coarray(... , cap_d);
……
то:
shmem_int_p( cap_f[n]+5, 10, n ); // правильно,
cap_d[n][5] = 10; // правильно,
но:
shmem_int_p( cap_d[n]+5, 10, n ); // НЕПРАВИЛЬНО,
cap_f[n][5] = 10; // НЕПРАВИЛЬНО.
4. Замечания по применению.
Технология
shmem была разработана и успешно применяется для коммуникационных сетей с очень
низкими показателями латентности и задержки записи. Коммуникационная сеть
МВС-экспресс является именно такой сетью. Использование для этих сетей именно
программистской модели shmem (а не MPI) не случайно. Оно продиктовано
необходимостью дать программисту возможности ускорения и упрощения параллельных
программ, которые характерны именно для этого оборудования.
В традиционных
коммуникационных сетях, на которые ориентирована модель программирования MPI, запуск
обмена, даже совсем короткого, стоит очень дорого. Он эквивалентен времени
передачи (в процессе выполнения уже запущенного обмена), как минимум, тысяч
байт данных. В сетях, использующих shmem, это не так. Среднее время передачи
отдельного слова удаленному процессу сопоставимо по порядку величины с временем
локального присваивания. Это позволяет передавать сложно организованные,
разбросанные по памяти мелкими порциями, данные не путем запаковки в одно большое
сообщение, как в MPI, а путем серии из большого количества отдельных
односторонних обменов, за которыми следует один акт общей синхронизации. Важно
понимать, что это касается только односторонней записи, но не чтения: запуск
одностороннего чтения стоит примерно столько же, сколько в сетях, традиционно
использующих MPI. Именно этим различием
продиктован тот факт, что в описанном выше стартовом подмножестве
shmem-экспресс имеются операции односторонней записи отдельных значений, но
отсутствуют такие же операции одностороннего чтения.
|
|