Дипломная работа студента 5-го курса кафедры вычислительной механики механико-математического факультета МГУ им М. В. Ломоносова Оленина Михаила Андреевича Научный руководитель: к. ф-м. н. Илюшин А. И. Построение системы программирования для МВС на основе понятий «Пространство-время». Введение В настоящее время все большую важность приобретает создание алгоритмов параллельной обработки данных и вычислительных моделей, предназначенных для счета на многопроцессорных вычислительных системах. Задача эта является как весьма актуальной, так и достаточно сложной. Если рассмотреть историю развития микропроцессоров, то хорошо видно, что до недавнего времени основной вклад в увеличение их производительности вносило повышение тактовой частоты. Однако с преодолением рубежа в 3Ггц дальнейшее увеличение частоты оказывается невозможным без существенных затрат. Даже переход на новый техпроцесс (с 0.65нм на 0.32нм) не дает значимого повышения по частоте. Поэтому наращивание мощности стало происходить в другой плоскости, путем увеличения количества ядер на одном кристалле, что способствовало усилению интереса к проблеме параллельных вычислений. На протяжении последних лет стремительно развивается область распределенных вычислений и параллельного программирования. С каждым днем увеличивается количество статей, научных трудов, программ и продуктов по этой тематике. Это обусловлено тем, что приемлемый (а порой и единственный) способ увеличения вычислительных мощностей достигается за счет распределения задачи на многие процессоры. Основным моментом в контексте данной работы является то, что программирование для таких сложных систем оказалось не простым делом и столкнулось с множеством проблем. Одна из них, в частности, - это синхронизация объектов и процессов, выполняющихся на разных процессорах и связанная с ней проблема взаимных координационный блокировок механизм для и тупиков; такой необходимо распределенной вводить системы. некий Если не ограничиваться распараллеливанием вычислений в пределах одной ЭВМ с многоядерным процессором, возникает еще и проблема сетевого взаимодействия компонентов системы. После долгого (по компьютерным меркам), примерно сорокалетнего, развития параллельное программирование из экзотического занятия ученых довольно быстро превращается в массовую и популярную профессию. Невероятно быстрое развитие электронных технологий обеспечило появление на рынке большого числа 2 коммерческих суперкомпьютеров. Они успешно применяются для решения многих задач в науке (математическое моделирование) и промышленности (управление сложными техническими устройствами). Множество проблем параллельного программирования до сих пор не нашли хорошего решения в современных технологиях, языках и системах параллельного программирования. По этой причине параллельная реализация алгоритмов наталкивается на значительные трудности. Разработка, отладка и сопровождение параллельных программ оказываются весьма сложным делом. Нередко встречаются ситуации, когда долго и правильно работающая программа вдруг выдает ошибочные результаты. Показателен пример с параллельной программой, разработанной Э. Дейкстрой для следующей задачи: «Даны два множества натуральных чисел S и T. Сохраняя мощности этих множеств, необходимо собрать в S наименьшие элементы множества SUT, а в T - наибольшие». Программа состоит из полутора десятков операторов. Ее частная корректность была доказана разными авторами. Но в 1996 г. Ю. Г. Карпов (Санкт-Петербургский технологический университет) доказал отсутствие свойства ее тотальной корректности. (Программа называется корректной, если при установке она вырабатывает правильный результат. Программа называется тотально корректной, если она всегда останавливается и всегда вырабатывает правильный результат.) И построенные им простые тестовые примеры показали, что программа работает, в общем неправильно. Такие случаи известны и в последовательном программировании, однако в параллельном дополнительной программировании необходимостью подобные правильно проблемы (содержательно усугубляются и во времени) запрограммировать межпроцессные коммуникации, что очень трудно сделать человеку (как и всякие действия во времени). Кроме того, параллельные программы часто должны обладать многими необычными динамическими свойствами. Хотя, конечно, есть немало задач, сравнительно просто программируемых для исполнения на МВС, все же многие задачи трудно распараллелить и запрограммировать. Все это делает параллельное программирование занятием для хорошо образованных в математике и хорошо обученных нужным технологиям людей. 3 В работе представлены способы, упрощающие решение этой задачи в части декомпозиции модели на локально программируемые объекты, автоматизации распределения этих объектов по процессорам и координации их счета. Оказывается, что использование новых средств программирования, основанных на понятиях «пространство-время», позволяет продвинуться в решении озвученных выше проблем. Желаемый результат: локальность программирования для прикладного программиста; прикладной программист получает средства для создания и хранения вычислительных моделей в виде множества программных объектов на дисках; система управления автоматически распределяет объекты по процессорам, входящим в состав МВС; система управления обеспечивает синхронизацию взаимодействия объектов по их локальным временам; система управления определяет связи между объектами по их локальным координатам; система управления обеспечивает сохранение состояния модели в процессе счета с возможностью прерывания/возобновления счета в том числе и на другой МВС. План выполнения дипломной работы В рамках дипломной работы были поставлены следующие задачи: 1. Реализация основной программной части системы OST 2. Написание документации для прикладного программиста 3. Тестирование системы OST на конкретных прикладных задачах 4. Запуск системы OST на МВС 5. Реализация задачи M2DGD об обтекании крыла. (реализация производилась в паре с Павлухиным П.В.) 4 Основные проблемы параллельных вычислений Целью системы OST (Objects-Space-Time) является существенное упрощение программирования задач и повышение эффективности их счета при работе на многопроцессорных вычислительных системах. Подход, предлагаемый для построения параллельных программных моделей в рамках проекта OST, отличается от распространенных в настоящее время подходов (в качестве примера можно указать проект DVM) тем, что в нем делается попытка «спрямить» путь от физической модели к программной реализации. Этот подход состоит в следующем. Пусть имеется физическая модель (область), которую здесь мы будем рассматривать в виде множества физических объектов, эволюционирующих и взаимодействующих друг с другом. В любом случае нам нужно отобразить это множество физических объектов на множество программно-аппаратных объектов. Традиционный путь состоит в том, что строится математическая модель для этой области и вычислительная схема, позволяющая получить численные решения для некоторого множества начальных и граничных условий. Более конкретно вычислительная модель чаще всего представляется в виде некоторого набора матриц, векторов, чисел и некоторого последовательного алгоритма обработки этих объектов для получения искомых решений. Далее строится параллельная программно-аппаратная модель (здесь под ней понимается множество программ, работающих на разных процессорах и взаимодействующих друг с другом). В случае прямого использования низкоуровневых средств типа MPI разбиение данных и последовательного алгоритма на относительно независимые части делается вручную, а в случае, например, DVM некоторые этапы такого разбиения делаются автоматически. Подчеркнем главное – при таком подходе изначально параллельная физическая модель сначала отображается на последовательную вычислительную модель, а затем решается обратная задача – последовательная вычислительная модель отображается на параллельную программно-аппаратную модель. Так как при переходе от параллельной физической модели к последовательному алгоритму вычислений теряется информация об изначальном параллелизме физической модели, то, основываясь только на анализе последовательного алгоритма, вообще говоря, невозможно восстановить исходный параллелизм для построения параллельной программно-аппаратной модели. Оказывается невозможным понять, почему, например, два конкретных действия выполняются последовательно. 5 Возможно, порядок этих действий определяется сутью физической модели, а возможно это техническое решение автора последовательного вычислительного алгоритма. Именно поэтому попытки автоматического распараллеливания программ, которые делаются на протяжении последних тридцати-сорока лет, ощутимых результатов не принесли. В идеологии проекта OST естественно строить математические, вычислительные и программные модели независимо для частей физической модели, а затем с помощью средств OST , более или менее автоматически получать композицию из программных частей, которая будет описывать физическую модель в целом. Тем не менее, подчеркнем, что для средств OST несущественен источник получения частей вычислительной модели. последовательной Это могут вычислительной быть модели. и вручную Однако выделенные естественным части будет «фотографическое» отображение частей физической модели и связей между ними на множество программно-аппаратных объектов, минуя стадию построения математической и вычислительной моделей для всей области в целом. Имеется в виду, что математическая и вычислительная модели должны описывать именно части физической модели и взаимодействие на границах (реальных или виртуальных) между ними, а не всю физическую модель в целом. Предлагается интеграция частей в стиле натурного моделирования на уровне программноаппаратных объектов, которые в данном случае играют роль виртуальных частей «натурной» модели. Основные идеи Решение вычислительных задач на многопроцессорных вычислительных системах (МВС) связано с проблемами программирования, которые не были столь серьезны в случае однопроцессорных систем. Первая проблема – это частичное упорядочение множества вычислительных действий, которые выполняются в параллельном режиме на множестве процессоров, входящих в систему. Вторая проблема – это организация связей между частями прикладной программной системы (прикладной задачи), распределенной по множеству процессоров. Хорошо известно, что в текстах прикладных программ, написанных для решения вычислительных задач с использованием, например, функций MPI (а такие программы по разным оценкам 6 составляют от 70 до 90 процентов от общего числа программ для МВС), соотношение собственно целевого кода, описывающего сами вычисления, и кода, который решает две сформулированные выше проблемы, составляет примерно один к четырем. То-есть, примерно 80% усилий каждый прикладной программист должен затрачивать на решение по сути однотипных организационных проблем, косвенно связанных с его основной задачей. В основу решения указанных выше проблем в системе OST положены две идеи: 1. «ВРЕМЯ» практически во всех областях деятельности человека всегда использовалось и используется для упорядочения действий. «Время» в нашем случае – это разметка числами всех рассматриваемых действий. Единственное условие синхронизации - взаимодействие пары объектов разрешается только при «равенстве» их локальных времен. 2. «ПРОСТРАНСТВО», в котором расположено рассматриваемое множество объектов, является столь же широко и эффективно используемым понятием. В нашем случае для организации связей между взаимодействующими объектами используется понятие «близости» пары объектов в пространстве конкретного типа. Например, это может быть трехмерная решетка (шесть соседей) или кольцо (два соседа). Могут взаимодействовать (вызывать операции друг в друге) только «соседи» («близкие» объекты). Отметим, что любая система состоит из некоторого множества частей с некоторой конкретной топологией связей. Это означает, что практически все прикладные программисты будут программировать примерно одинаковые действия. Аналогичная ситуация с арифметическими выражениями существовала до появления Фортрана. Каждый программист программировал в Ассемблере арифметические выражения. Вынесение «за скобки» в системную часть повторяемых всеми действий – это естественный выход из такого рода ситуаций. Практически то же самое можно сказать и об упорядочении действий (событий) в параллельно эволюционирующих частях любой системы с помощью разметки этих событий «временем». Использование программных механизмов, основанных на этих идеях, позволяет «автоматизировать» синхронизацию и управление связями между объектами. Такая 7 автоматизация дает два основных результата: 1. Локальность программирования для прикладного программиста. Он описывает алгоритм функционирования каждой подобласти локально, а функционированием модели всей области управляет система OST. 2. Система OST получает возможность автоматически распределять части вычислительной модели (объекты) по процессорам в стиле «подкачек» страниц в операционных системах. Результат – возможность оптимизации загрузки для всей МВС и упрощения для прикладного программиста. Общая структура системы. Прикладной программист строит вычислительную модель прикладной области в виде множества объектов, взаимодействующих путем вызова операций друг в друге. Он создает описания классов объектов и программу “раскрутки” модели. Программа “раскрутки” модели, используя ранее созданные описания классов и специальное «объектохранилище» (в дальнейшем будем называть его файлом объектов, класс «файл объектов» входит в состав системы OST), создает все объекты, входящие в состав программной модели. Эти объекты хранятся в файле объектов в «сериализованном» виде (см., например, операцию serialize в Java). Естественно, что в дальнейшем в момент счета прикладной задачи «сериализованные» объекты будут «подкачиваться» из файла в оперативную память, «десериализоваться» (см., например, операцию deserialize в Java), и «считаться». Предполагается, что все объекты, входящие в состав программной модели, могут, вообще говоря, считаться одновременно. Технически в системе OST это означает, что каждый объект должен иметь операцию RUN, которую и будет вызывать монитор OST после «десериализации» объекта и помещения его в оперативную память конкретного процессора из состава МВС. Операция RUN вызывается как в самом начале счета, так и после «выталкивания/подкачки» объекта возможно в другой процессор. Это означает, что из-за технических трудностей в настоящий момент в системе OST используется схема останова/пуска счета, аналогичная схеме “nonpreemptive scheduling”, не обеспечивающая «невидимого» для прикладного 8 программиста прерывания/возобновления счета в произвольном месте программы. После того как в некотором множестве процессоров появилось множество объектов прикладной модели и во всех этих объектах вызвана операция RUN естественно возникает два вопроса: - как они свяжутся друг с другом, а конкретно как в них появятся ссылки друг на друга, необходимые для вызова операций? - как будет происходить синхронизация моментов вызова? Ведь если один объект запрашивает результаты счета у другого объекта, то в момент вызова эти результаты должны быть готовы. Обе эти проблемы решаются путем введения механизма формализующего понятие «окружение» для программного объекта в виде списка «формальных» соседей. Это понятие можно рассматривать как существенное обобщение понятия списка формальных параметров для подпрограммы. Прикладной программист программирует свой прикладной класс полностью локально и использует при этом окружение в виде списка ссылок на соседей, считая, что в момент счета система подставит ему в список формальных соседей ссылки на фактических соседей в соответствии с текущими координатами и текущим временем конкретного объекта рассматриваемого класса и всех других объектов программной модели. А именно, объекты становятся фактическими соседями, между которыми возможен вызов операции, в соответствии с их координатами в пространстве конкретной топологии и в тот момент, когда их локальные времена совпадают. Организация связей между объектами на основе топологии конкретного пространства. Прикладной программист, использующий систему OST, создает конкретный прикладной класс, исходя только из локальных соображений, относящихся только к объектам описываемого типа. А точнее к конкретной подобласти, входящей в состав некоторой прикладной области, которая отображается на создаваемую программную модель. Конкретно локальность здесь означает, что программист вполне традиционным образом описывает внутреннее функционирование объекта данного типа, а «окружение» определяет в виде списка «формальных соседей» (этот список аналогичен списку формальных параметров подпрограммы) и весь «внешний мир» 9 для программируемого класса ограничивается этим списком. Элементы списка – это ссылки на объекты- “соседи” и координаты этих соседей относительно текущего объекта. Описание типов ссылок определяет интерфейсы, через которые текущий объект взаимодействует с окружающими его объектами-соседями. Монитор OST заполняет этот список фактическими ссылками на конкретные объекты, размещенные в конкретных процессорах, на основе абсолютных координат рассматриваемого объекта и координат всех других объектов, входящих в состав вычислительной модели. Использование этих ссылок монитор OST разрешает только в моменты равенства локальных времен соседей. Реализуется это путем приостановки операции вызова до того момента, когда локальные времена вызываемого и вызывающего объектов уравняются. Естественно должен возникнуть вопрос - в каком соотношении находится топология физической области и топология пространства объектов в программной модели? Так как число объектов в программной модели конечно (от нескольких единиц до нескольких миллионов при имеющихся в настоящее время возможностях вычислительной техники), то можно просто перенумеровать их от 1 до N, а любой граф связей можно задать таблицей из пар номеров. Однако такой способ, вообще говоря, приводит к ненужным трудозатратам. В то же время сохранение, конечно, в упрощенном виде топологии исходной физической области позволяет написать простые программы задания координат соседей. Например, в простейшем случае двумерной плоскости с целочисленными координатами и четырьмя соседями для каждой точки (объекта) одна координата соседа будет совпадать с координатой текущего объекта, а другая отличаться на единицу. Естественно программа, формирующая координаты соседей для всех экземпляров объектов будет тривиальной. На практике программировании оказалось, конкретных что использование задач всегда этого механизма показывает при уменьшение «организационной» части прикладной программы в разы, так как вся работа по организации связей между объектами «спрятана» в системе OST, а на прикладного программиста возложено только определение относительных координат для соседних объектов. Синхронизация вызовов на основе локального 10 времени объектов. Синхронизация вызовов осуществляется монитором OST на основе единственного правила – вызов разрешается, если локальные времена объектов равны. Прикладной программист обязан продвигать локальное время объекта. Это либо физическое время для моделируемого прикладного объекта или искусственное время, которое вводится лишь для упорядочения выполняемых действий. Например, это может быть номер итерации. Вызов операции из одного объекта в другом объекте всегда происходит через ссылку и при неравенстве локальных времен вызывающего и вызываемого объекта откладывается до совпадения этих времен. Ссылка на объект в системе OST представляет собой ссылку на специальный объект связи, который с помощью монитора OST обеспечивает локальный вызов (в случае нахождения вызывающего и вызываемого объектов в одном адресном пространстве) или удаленный вызов (в случае нахождения вызывающего и вызываемого объектов на разных процессорах) c соблюдением сформулированного выше условия синхронизации. В настоящее время известно много разных алгоритмов синхронизации. Один крайний случай – это ослабление условия разрешения вызова до требования, чтобы время вызывающего объекта было не меньше времени вызываемого. В этом случае разрешается много параллельных вычислений, но могут возникать ситуации, когда время в однажды вызванном объекте в результате этого вызова ушло вперед, а затем пришел вызов из другого объекта, локальное время которого оказалось меньше времени вызываемого объекта. В этом случае необходим откат системы к более раннему состоянию (возможно каскадный). Другой крайний случай – это алгоритм, глобально упорядочивающий все локальные времена и допускающий вызов только в том случае, если он помечен глобально минимальным временем. В этом случае параллелизм счета будет отсутствовать. В системе OST реализован алгоритм без откатов, который является промежуточным между этими двумя крайностями. Средства реализации поставленной задачи В качестве языковой платформы изначально (в рамках курсовой работы за 4-ый курс) был выбран язык Java, а для удалённого вызова была выбрана система Java 11 RMI (Remote Method Invocation), которая является частью стандарта языка Java. Язык Java был выбран по нескольким причинам. Во-первых, это язык кроссплатформенный, а значит код, написанный на нём, не будет требовать модификаций при переносе с одной программно-аппаратной платформы на другую. Во-вторых, язык Java хорошо стандартизирован, то есть, на разных платформах и интерпретаторах код, написанный на нём, будет интерпретироваться одинаково. И, в-третьих, язык Java обладает удобными встроенными возможностями отладки, что сокращает время на поиск ошибок, а следовательно, делает программирование на нём более эффективным. Отдельно, также, хочется отметить, что программы, требуемые для разработки на языке Java бесплатны и кросс-платформенны, что также является несомненным плюсом. Это выражается не только в отсутствии необходимости платить за покупку необходимых программ, но и позволяет писать код под разными операционными системами, без необходимости привыкать к новым средствам разработки. Конечно, существуют и платные проприетарные программы для разработки на языке Java, но они обычно специализированы для некоторых задач специального типа, которые в рамках данной работы решать необходимости нет. Изначально для сетевого взаимодействия компонент при удалённом вызове была выбрана система CORBA (Common Object Request Broker Architecture). Но в последствии было решено отказаться от её использования по нескольким причинам. Универсальность, заложенная в системе CORBA рассчитана на использование многими языками программирования, что, безусловно, является плюсом, но у такой универсальности есть и обратная сторона. Необходимо создавать специальные «заглушки», описывающие методы, доступ к которым может производиться удалённо. Эти «заглушки» нужно описывать на специальном языке idl. Но наша система написана на языке Java, причём её архитектура рассчитана на то, что прикладные объекты тоже написаны на Java (по крайней мере их оболочки, хотя сами вычислительные алгоритмы могут быть написаны и на другом языке), поэтому необходимость в связи с другими языками на уровне удалённого вызова отпадает. Сейчас CORBA уже практически перестала развиваться, что может в будущем отрицательно сказаться на совместимости с новыми версиями языков программирования. Помимо этого CORBA сложнее в использовании и требует более основательного изучения. Встроенный в Java механизм RMI не требует ни написания ни генерации заглушек, проще в освоении, не требует установки дополнительных 12 программных компонент и является частью стандарта языка, что означает его полную поддержку в будущий версиях Java. Язык Java является интерпретируемым и высокоуровневым, что, возможно замедляет выполнение написанных на нём программ. Но, во-первых, в реализации Java для всех платформ присутствует быстрый JIT (Just In Time) компилятор, компилирующий код по мере выполнения программы, что делает программы на Java самыми быстрыми среди языков такого типа. Во-вторых, Java допускает непосредственно компиляцию в машинный код. А в-третьих, как уже отмечалось выше, алгоритмы, непосредственно выполняющие вычисления, могут быть написаны и на другом языке, тогда Java будет играть роль только управляющей программы, и производительность системы в целом почти не изменится. В рамках курсовой работы за 4ый курс и дальнейших исследованиях было установлено, что Java накладывает много ограничений. Во первых, Java компилируемый язык программирования, поэтому на стадии компиляции должно быть точно определены все возможные описания возможных функций и переменных у экземпляров классов, используемых. Поэтому приходится делать интерфейсы (описания функций класса) в качестве промежуточных объектов, на место которых в дальнейшем JVM подставит экземпляр класса. Данное ограничение не дает возможности сделать более-менее универсальные подставленния удаленных соседей в объекте; Так же требуется для самого объекта описать интерфейс функций, которые могут в данном объекте вызываеться. В ходе практической реализации так же было установлено, что JNI, используемый для связи с другими языками программирования, сложен в реализации и последующей отладке, поэтому поиск и исправление простейших ошибок требует много времени, которое при этом не дает гарантий устранения ошибки. Рассмотрев плюсы и минусы реализации системы OST на языке Java было решено использовать динамический язык программирования. В качестве такого языка программирования был выбран python — один из самых молодых и быстро эволюционирующих языков программирования. Python — это динамический язык программирования, который день ото дня используется все большим количеством людей. Несмотря на свою простоту и не типичность синтаксиса по сравнению с такими общепринятыми языками программирования как C/C++/Java, он становится популярным в научной среде. Для Python написано много различных бибилиотек на все случаи жизни, позволяющих 13 сделать программирование на нем сложных конструкций быстрым и понятным делом. Особенности синтаксиса заставляют программиста писать простой, понятный и «читабельный» код, который достаточно просто может быть потом исправлен/дополнен другими программистами, без долгого периода понимания. Для удаленных вызовов в Python было решено использовать библиотеку PyRo, которая позволяет реализовать удаленный вызов в считанных строках. Если сравнивать с другими системами RPC (Remoute procedure call) такими как RMI (в Java), Corbo, Ice и другими, реализация PyRo проста, прозрачна и понятна. Объединяя интерпретируесть Python и PyRo получаем инструмент, позволяющий на ходу создать и подключать объекты для удаленного вызова. Python следит за памятью, используемой пользователем, поэтому в нем доступно использование многих типов объектов, без утомительных и сложных проверок на доступность и последующую очистку. Данный плюс Python дает возможность связывать прикладные объекты между собой простыми конструкциями. 14 Реализации системы OST Основные подходы к реализации Система OST проектировалась на основе понятий «пространство-время». Первая задача, которая требовала решения, была задача построения архитектуры всей системы в целом. Основные критерии были следующие 1. Простота – под данным критерием подразумевается, что система должна максимально использовать все плюсы объектно-ориентированного подхода. Данный подход позволяет разделить на различные уровни абстракции компоненты системы. Таким образом, получается простой, наглядный и понятный исходный код, который затем можно эффективно дорабатывать. 2. Разделимость – под данным критерием подразумевается разделение системы OST на отдельные компоненты, которые взаимодействуют друг с другом. Как уже сказано в системе используется уровни абстракции для компонент системы. Поэтому должна быть разделимость – чтобы каждая часть не завесила от реализации другой. В случае необходимости можно было бы персонализировать под конкретную задачу, тип исследования или МВС какие-то отдельные компоненты так, чтобы остальные изменять не пришлось. 3. Масштабируемость – то есть способность системы увеличивать свою производительность при добавлении ресурсов, увеличении количества узлов в МВС и прочем. При этом код системы не должен требовать больших изменений для увеличения масштабов вычислительных систем. 4. Универсальность – подходы к разработке архитектуры должен быть универсальным, по возможности требуется не использовать частности некого выбранного языка программирования для реализации. Прогресс не стоит на месте и может потребоваться переделать систему на более быстрый язык программирования. Или, наоборот, производительность будет замечательна, но будет не хватать гибкости, для этого будет выбран какой-то другой язык программирования, например, динамически-исполняемый. Основная архитектура была разработана в рамках курсовых работ за третий и четвертый курс. В дипломной работе, по сравнению с 4-ым курсом был изменен основной язык программирования. Вместо java стал использоваться python, 15 который позволил сделать систему гибкой и удобной в использовании. Структура системы OST Структура системы состоит из 4 компонент, которые определяют основные части системы: OST_Main – Основная часть системы. Отвечает за создание вычислительной модели, и дальнейшее управление счетом в целом. Данная часть является ядром всей системы. Данный объект создается в единственном экземпляре, который находится на управляющем узле, компьютере или ядре процессора. В случае счета в пределах одного компьютера, выделения отдельных ресурсов требоваться не будет. В таком случае общие ресурсы будут делиться по необходимости, но и производительность, очевидно, пострадает. OST_Local – Локальный представитель монитона OST на узле. Так как в МВС может быть несколько узлов или компьютеров, то на каждом используется локальный представитель, который обеспечивает связь с OST_Main. OST_Local так же организует инфраструктуру для запуска конкретных прикладных объектов. OST_Control – Объект-связка для прикладного объекта. Обеспечивает полностью все взаимодействие прикладного объекта с монитором OST. К этим взаимодействии 1. Определение ссылок на соседей - на основе координат прикладного объекта и координат из описания соседей подставляет необходимые ссылки. А так же при изменении координат автоматически обновляют все необходимые ссылки, как у прикладного объекта, так и у ссылающихся. 2. Синхронизации по времени – реализуются средствами объекта-связки, который по запросу из прикладному объекту производит синхронизацию со всеми заинтересованными в синхронизации с этим объектом других объектов. 3. Организация контрольных точек – реализация контрольной точки для прикладного объекта должна реализоваться извне прикладного объекта. Данные взаимодействия описываются далее по тексту, в разделе описывающем пользовательские интерфейсы. 16 OST_Object – общий класс прикладного объекта. Конкретные реализации прикладных объектов наследуются от OST_Object или от потомков этого прикладного объекта. Связь с монитором OST обеспечивается через функции, определенные в OST_Object, которые доступны прикладному объекту при наследовании. На приведенной схеме показывается взаимодействие приведенных объектов друг с другом. 17 Построение и функционирование распределенной программной системы Уточним последовательность действий прикладного программиста при создании распределенной прикладной системы с помощью средств системы OST. Рекомендуемой (но не обязательной) является следующая последовательность действий: 1. Проектирование топологии системы в виде множества составляющих ее частей (программных объектов) и связей между ними. Должна быть определена топология пространства объектов, понимаемая в обычном математическом смысле и система координат в выбранной топологии. Например, можно использовать двумерную плоскость с целочисленными координатами, в которой каждый объект будет иметь четырех соседей. Естественно, можно использовать гораздо более сложные топологии связей между объектами. Заметим, что проектирование топологии чаще всего сводится просто к «фотографическому» отображению топологии моделируемой прикладной области. 2. Проектирование разметки временем всех действий, выполняемых объектами. Например, можно строить значения переменной времени из двух частей. В качестве старшей части можно взять модельное представление физического времени, а в качестве младшей части – номер итерации внутри шага по времени. Заметим, что проектирование разметки действий временем чаще всего сводится просто к отражению переменной времени естественного процесса эволюции моделируемой прикладной области. 3. Проектирование интерфейсов для каждого объекта (здесь и далее под объектом практически всегда, если не оговорено противное, понимаются программный объект в смысле одного из объектно-ориентированных языков программировании – С++, Java и т.п.). Для каждого объекта должен быть спроектирован, во-первых, интерфейс в традиционном смысле в виде набора операций, которые в нем можно вызвать, и наборов операндов для этих операций. Во-вторых, должно быть спроектировано его «окружение» в виде списка формальных соседей и определены интерфейсы для этих соседей. 18 Заметим, что набор операций, описанный в интерфейсе формального соседа, должен быть подмножеством набора операций, определенного в описании фактического соседа. 4. Написание программных классов, реализующих поведение объектов. 5. Написание программы начала работы – «раскрутки» программной системы (модели). В этой программе должен быть определен файл, в котором будет храниться вся модель. Записями в этом файле объектов будут являться сериализованные представления объектов, из которых состоит модель. Программа раскрутки должна создать в файле объектов все объекты модели и вызвать в каждом из них операцию инициирования, передав в качестве параметров все данные, необходимые для начала работы. В частности, можно передать начальное время и начальные координаты для созданного экземпляра объекта. И, наконец, программа раскрутки должна вызвать операцию начала работы модели в мониторе OST. Монитор OST распределит объекты из файла объектов по процессорам и в каждом из них вызывает функцию run(), которая начинает счет в конкретном прикладном объекте. Объекты также могут создаваться и уничтожаться, а их координаты меняться во время основного счета. 6. Обсчет модели может быть разбит на несколько этапов. Этап счета завершается при возврате из функции run() значения завершения счета. После каждого этапа в файле объектов остается полученное на данный момент счета состояние объектов. Возобновить счет с использованием текущего состояния файла объектов можно повторным выполнением программы начала работы. В процессе счета объекты могут динамически уничтожаться и создаваться, а их координаты меняться. Модель уничтожается просто уничтожением файла объектов. Ниже приводятся фрагменты программного кода на языке python, которые в обобщенном виде иллюстрируют способы написания программ, в соответствии с приведенной выше методологией. В приложении 1 приведен законченный, исполняемый пример. 19 Во фрагментах программ, приводимых ниже в качестве примеров, используются следующие обозначения: - шрифт «курсив» указывает на то, что данное имя является абстрактным и в конкретной прикладной программе должно быть заменено программистом на конкретное. - шрифт «серый» указывает на то, что данная часть примера является частной для данного примера и в конкретной прикладной программе может быть не использована или заменена прикладным программистом на другую реализацию. - шрифт «жирный» указывает на то, что данное имя является частью системы OST. Это может быть имя класса, переменные, функции или другое. Используемые термины python: Список – структура, реализующая упорядочный контейнер. Словарь – структура, реализующая ассоциированный массив, то есть массив пар “<текстовый ключ>” => <значение> 20 Программирование класса для прикладного объекта Из изложенного выше следует, что в классе прикладного объекта кроме методов, реализующих целевые прикладные функции, должно быть запрограммировано взаимодействие с монитором OST, описано окружение в виде списка соседей, задана топология и координаты в этой топологии и, наконец, задано локальное время объекта. Взаимодействие с монитором OST Основным инструментом для обеспечения взаимодействия «прикладной объект – монитор OST» служит класс OST_Object, который содержит, во-первых, служебные методы, необходимые для работы системы OST, и, во-вторых, абстрактные методы, которые должны быть доопределены прикладным программистом. Реализация класса прикладного объекта производится через наследование от класса OST_Object. Общие переменные для монитора OST и прикладного объекта Существенным компонентом при взаимодействии монитора OST и прикладного объекта являются переменные, доступные как монитору, так и прикладному объекту. Синтаксически они являются public переменными прикладного объекта и получаются либо через наследование от OST_Object, либо через наследование от объектов производных от OST_Object, а также могут быть определены непосредственно в прикладном классе. Способы их использования описаны ниже. В данном месте ограничимся их перечислением: - self.point – структура, описывающая данный объект и его соседей. В ней должны быть заданы следующие поля: a. self.time - локальное время объекта b. self.coord – список координат. Это поле предназначено для хранения абсолютных координат конкретного экземпляра объекта. 21 c. self.neighbours – список полей, каждое из которых описывает конкретного соседа. Описание каждого соседа является структурой, в которой состоит из списка относительных координат и формального параметра, куда монитор OST подставит реальную ссылку на соседа. - self.stop определяется только в OST_Object. В системе OST предусмотрена возможность приостановки счета объекта, с целью, например, освободить ресурсы для счета другого объекта (например, в случае, когда много объектов ждут продвижения по времени) или создать контрольную точку. Для остановки счета конкретного объекта монитор OST устанавливает в нем флаг self.stop. В процессе своей работы объект должен проверять состояние этой переменной. Обнаружив, что флаг установлен, объект должен освободить ресурсы и завершить свой счет, осуществив возврат из функции run() . Для возобновления счета монитор OST восстановит объект по состоянию на момент последнего выхода из функции run() и снова вызовет в нем функцию run(). Запоминаются и восстанавливаются все переменные класса для прикладного объекта. - self.Obj_Main – указывает на «главный» объект модели (см. раздел 6.2), в котором сосредоточены переменные и функции, общие для всей модели в целом. Служебные методы класса OST_Object - self.setXYZT() - функция setXYZT должна вызываться из прикладной программы в тот момент, когда эта программа хочет известить монитор OST об изменении, сделанном этой программой, либо координат данного экземпляра объекта, либо его локального времени, либо и того и другого. В случае изменения координат результатом вызова могут быть изменения, которые возможно сделает монитор OST в списке фактических соседей. В случае продвижения вперед локального времени объекта монитор OST возможно разрешит выполнение вызова операции (который будет сделан из данного объекта в другом объекте либо в будущем, либо уже сделан ранее в рамках другого параллельного процесса), если локальные времена этих двух объектов уравняются. - self.setFinish() (будет реализовано в версии x.y.z) – данная функция должна вызываться в тот момент, когда некоторый прикладной объект хочет известить монитор OST о полном окончании своего счета. Монитор OST, после 22 возврата из функции run() объекта, у которого была вызвана эта функция, завершит этот объект. Завершение подразумевает под собой: сохранение последнего состояния прикладного объекта, удаление его как элемента вычислительной модели, а так же удаление всех ссылок на него. О том, как завершить счет модели в целом описывается в разделах 6.2.2.3 и 6.2.2.2. Пример: def run(self): N=10 for i in xrange(1,N): # <...> self.point.time += 1 self.setXYZT() self.setFinish() return 1 Абстрактные методы класса OST_Object - def run(self) - Функция счета прикладного объекта. Вызывается монитором OST для счета объекта (self в скобках – это часть синтаксиса python). Функция run() выполняет прикладной счет, продвигает локальное время объекта, возможно, изменяет координаты объекта (будет реализовано в версии x.y.z) и вызывает функцию монитора OST self.setXYZT(). Функция self.setXYZT() должна вызываться в тот момент, когда прикладной объект хочет известить монитор OST об изменении либо координат (будет реализовано в версии x.y.z) данного экземпляра объекта, либо его локального времени, либо и того и другого. Функция run() должна периодически проверять состояние переменной self.stop (будет реализовано в версии x.y.z). Обнаружив, что монитор OST установил флаг self.stop, функция run() должна подготовить прерывание своего счета и выполнить возврат 0 в вызвавшую её программу (а именно вернуть 23 управление в вызвавшей ее ранее монитор OST). Монитор OST спланирует возобновление счета данного объекта. В будущем, восстановит объект по состоянию на момент последнего возврата из функции run() и продолжит делать вызовы функции run(). Для завершения счета данного объекта функция run() должна вызвать функцию монитора OST self.setFinish(), которая уведомляет монитор о том, что после возврата из функции run() монитор приступит к завершению счета (подробнее в пункте 6.1.1.2). После этого функция run() должна вернуть 1, возвращая управление монитору OST для завершения прикладного объекта. Пример фрагмента класса для прикладного объекта: class exampleObject( OST_Object ): # <...> def run(self): N=10 for i in xrange(1,N): # <...> # далее вызов self.point.neighbors[0]["obj"].fun1() # <...> self.point.time += 1 self.setXYZT() if self.stop: return 0 self.setFinish() return 1 # <...> Определение топологии прикладной области и задание соседей для прикладных объектов Выше говорилось, что класс прикладного объекта программируется только из локальных представлений о функционировании реального объекта в прикладной области. Все взаимодействие с другими прикладными объектами идет через «окружение», представляемое в виде «формальных соседей». «Формальные соседи» - переменные, которые используются прикладным программистом в 24 алгоритме, как обычные локальные переменные. В эти переменные монитор OST кладет ссылки на «фактических соседей» - прикладные объекты, из фактического «окружения». Для задания топологии в системе OST в классе прикладного объекта необходимо определить структуру которая self.point, описывает список координат самого объекта, а так же задает его формальных соседей. Структура point Структура self.point в Python задается в виде экземпляра некого класса, в дальнейшем будем называть этот класс «класс топологии». Экземпляр этого класса топологии описывается в классе прикладного объекта в переменной self.point. Класс тополгии состоит из следующих полей: self.time – переменная времени прикладного объекта из понятий «ПРОСТРАНСТВО-ВРЕМЯ», лежащих в основе системы OST. Эта переменная используется монитором параллельно OST функционирующих для синхронизации прикладных объектов. взаимодействия Взаимодействие соседних прикладных объектов допускается только в случае равенства локальных времен в каждом из них. Прикладной объект должен регулярно продвигать вперед значение локального времени в зависимости от этапа вычислений self.coord – список координат, определяющих точку в пространстве из понятий «ПРОСТРАНСТВО-ВРЕМЯ», лежащих в основе системы OST. Это поле предназначено для хранения абсолютных координат конкретного экземпляра прикладного объекта. self.neighbors – Список полей, каждое из которых описывает конкретного соседа. Каждое поле в свою очередь является структурой (словарем). Для примера, пусть nei такая структура, тогда она состоит из двух полей: 1. nei[“obj”] – Формальная переменная соседа. Во время счета, монитор OST подставит актуальную ссылку на фактического соседа. 2. nei[“coord”] – это список целочисленных координат. Этот список определяет объекта, относительные позволяя координаты монитору OST соседа данного устанавливать экземпляра соседство между объектами. 25 Прикладной объект может не писать самостоятельно класс топологии. Для этого он может использовать стандартный класс топологии по умолчанию OST_Point_Default а все необходимые параметры задать непостредствено при иницилизации объекта Пример задания структуры point при иницилизации прикладного объекта class OST_Main_Example( OST_Main_Class ): def init(self): '''Функция иницилизации счетной модели''' for numObj in xrange( 0, self.N): obj = self.createObject(OST_Object_Example) # Сделаем экземпляр класса топологии по умолчанию obj.point = OST_Point_Deault() # зададим время obj.point.time = 0 # зададим координаты obj.point.coord = [coord_obj1,…,coord_objK] # зададим сосдей obj.point.neigbors.append( {"coord":[nei1_coord_1,…,nei_coord_M_1],"obj":None}) <…> obj.point.neigbors.append( {"coord":[neiL_coord_1,…,nei_coord_M_L],"obj":None}) obj.init(<…>) Пример задания своего класса топологии для структуры self.point в общем виде: class OST_Abstract_Point: '''Класс абстрактной топологии''' def __init__(self, <параметры> ): # зададим координаты экземпляра прикладного объекта self.coord = [x1,x2,…,xN] # зададим начальное время экземпляра прикладного объекта self.time = t0 # определим сначала пустой список соседей # и будем по одному добавлять, пример добавления далее self.neighbors = [] # следующий код необходимо повторить нужное число раз 26 # добавим описание соседа экземпляра прикладного объекта self.neighbors.append( {"coord":[nei_x1,…,nei_xN],"obj":None }) В прикладном объекте достаточно, например, при инициализации в переменную self.point положить экземпляр конкретного класса топологии. Главный объект модели При создании конкретной модели первым должен быть создан «главный» объект модели. Он может быть создан, например, некоторой main программой, которая после его создания, вызова функции инициирования в этом объекте и возврата из нее закончит свою работу. При создании экземпляра в качестве параметра указывается имя файла, который будет использоваться как файл объектов. Пример: def main(): ostmain = OST_Main_Example(filename) ostmain.init() ostmain.start() Функция инициирования модели создает вычислительную модель в виде множества объектов, сохраняемых в файле объектов в сериализованном виде, вызывает функции инициирования во всех прикладных объектах, передавая в качестве параметров все необходимые начальные данные, и наконец, запускает на счет всю модель в целом, передавая файл объектов монитору OST на исполнение. Главный объект существует в течение всего времени счета модели, его сериализованное представление хранится в файле объектов вместе с прикладными объектами и после временной остановки счета главный объект восстанавливается в памяти некоторого процессора для продолжения счета. Для написания класса главного объекта модели в системе OST предусмотрен класс OST_Main. Прикладной программист, создавая свой класс для главного объекта должен наследовать его от OST_Main. В главном объекте сосредоточены переменные и функции, имеющие смысл для всей модели в целом. Перечислим эти переменные и методы. 27 Общие переменные модели this.toutStop – максимальное процессорное время для всех процессов, в рамках которых выполняются функции run(), за которое они должны проверить флаг this.stop и выйти из функции run(). По истечении этого таймаута монитор OST принудительно завершит процесс, просрочивший время. Значение таймаута по умолчанию равно 1 секунде процессорного времени. Это значение может быть переустановлено в функции this.init() (см. раздел 6.2.3). Служебные методы главного объекта, наследуемые из OST_Main Создание прикладного объекта Для создания объекта вычислительной модели необходимо вызвать функцию создания экземпляра прикладного объекта в вычислительной модели. Пример: obj = self.createObject(OST_Object_Example) В качестве аргумента функции передается ссылка на класс, создаваемого прикладного объекта. В результате получаем очередной объект obj вычислительной модели, в котором необходимо вызвать функцию инициирования для заполнения актуальными данными, необходимыми для его счета. Функция создания объекта может быть вызвана и из любого прикладного объект. Пример: obj = self.OST_Main.createObject(OST_Object_Example) Запуск модели на счет ostmain.start() #Вызывается из main программы 28 После вызова функции start() монитор OST создает новый процесс и начинает в нем запуск модели на счет. В рамках старого процесса монитор возвращает управление из функции start(). Для того, чтобы запустить счет модели монитор OST выбирает из файла все прикладные объекты, загружает их в подходящие процессоры и запускает счет всех объектов модели, вызывая в них функцию run(). После окончания счета всех объектов модели монитор OST вызывает в главном объекте функцию Obj_Main.finish(), определенную в классе OST_Main. Завершение очередного этапа счета модели Этап счета завершается вызовом функции stop(), определенной в классе OST_Main. self.stop(); #при вызове из функции init главного объекта модели и в некоторый момент после возврата из функции start() или self.Obj_Main.stop(); /* при вызове из прикладного объекта */ Создание контрольной точки модели В дополнение к стандартному сохранению состояния объекта в файле объектов после возврата из функции run() прикладной программист может сохранить текущее состояние модели, вызвав функцию CheckPoint(). self.CheckPoint(); //при вызове из функции init главного объекта модели и в некоторый момент после возврата из функции start() или self.Obj_Main.CheckPoint (); /* при вызове из прикладного объекта */ Монитор OST установит флаг STOP во всех объектах модели. Все объекты должны будут осуществить выход из функции run(). Монитор OST сохранит сериализованные представления объектов в файле объектов и повторно вызовет функцию run() во 29 всех объектах. Если некоторый объект не выйдет из функции run() в течение времени self.toutStop, монитор OST принудительно остановит его. Завершение счета модели полностью с уничтожением фйала объектов Полное завершение счета с уничтожением всей модели (файла объектов) осуществляется вызовом функции finish(), определенной в классе OST_Main. self.finish(); //при вызове из функции init главного объекта модели и в некоторый момент после возврата из функции start() или self.Obj_Main.finish(); /* при вызове из прикладного объекта */ Функция инициирования Функцию инициирования пишет прикладной программист. Функция инициирования модели создает вычислительную модель в виде множества объектов, которые потом будет сохранены в файле объектов в сериализованном виде, вызывает функции инициирования во всех прикладных объектах, передавая в качестве параметров все необходимые начальные данные. Монитор OST распределит объекты из файла объектов по процессорам и в каждом из них вызовет функцию run(). Пример: class OST_Main_Example( OST_Main_Class_Abstract ): def init(self): '''Функция иницилизации счетной модели''' for numObj in xrange( 0, self.N): obj = self.createObject(OST_Object_Example) debug("OST_Main_Matrix: init object coord = "+str(numObj)) 30 obj.point = OST_Point_Example() obj.point.time = 0 obj.point.coord = [coord_obj1,...,coord_objK] obj.point.neigbors.append( { "coord":[nei1_coord_1,... nei_coord_M_1], "obj":None }) <...> obj.point.neigbors.append( { "coord":[neiL_coord_1,... nei_coord_M_L], "obj":None }) obj.init(<...>) После начала счета модели возможен как возврат из функции init, так и продолжение счета с целью выполнения управляющих действий для модели в целом. Для этого, например можно использовать средства стандартных операционных систем. Можно, например, организовать цикл: Останов процесса на N секунд – анализ хода счета – управляющие воздействия на модель 31 Практические исследования: умножение матриц В рамках дипломной работы реализовывались несколько задач. В качестве макетной задачи реализована задача умножения матриц. Она демонстрирует производительность. В дипломной работе эта задача использовалась в качестве отладочной. Алгоритм представляет собой итерационную процедуру. На каждой итерации алгоритма каждая прикладной объект содержит по несколько строк матрицы А и такое же количество столбцов матрицы В. При выполнении итерации проводится скалярное умножение содержащихся в прикладных объектов строк и столбцов, что приводит к получению соответствующих элементов результирующей матрицы С. По завершении вычислений в конце каждой итерации столбцы матрицы В должны быть переданы между прикладными объектами по топологии однонаправленное кольцо с тем, чтобы в каждой подзадаче оказались новые столбцы матрицы В и могли быть вычислены новые элементы матрицы C. При этом данная передача столбцов между прикладными объектами должна быть организована таким образом, чтобы после завершения итераций алгоритма в каждой подзадаче последовательно оказались все столбцы матрицы В. Схема организации необходимой последовательности передач столбцов матрицы В между прикладными объектами состоит в представлении топологии связей в виде кольцевой, однонаправленной структуры. В этом случае на каждой итерации прикладной объект с координатой i, 0 i<n, будет передавать свои столбцы матрицы В прикладному объекту с номером i+1 (в соответствии с кольцевой структурой прикладному объекту с координатой n-1 передает свои данные прикладному объекту с координатой 0) – см. схему. После выполнения всех итераций алгоритма необходимое условие будет обеспечено – в каждой прикладном объекте поочередно окажутся все столбцы матрицы В. На схеме представлены итерации алгоритма матричного умножения для случая, когда матрицы состоят из четырех строк и четырех столбцов (n=4). В начале вычислений в каждой прикладном объекте i, 0 i<n, располагаются i-я строка матрицы A и i-й столбец матрицы B. В результате их перемножения прикладной объект получает элемент cii результирующей матрицы С. Далее прикладные 32 объекты осуществляют обмены столбцами, в ходе которого каждая прикладной объект передает свой столбец матрицы B следующей прикладному объекту в соответствии с кольцевой структурой связей. Далее выполнение описанных действий повторяется до завершения всех итераций параллельного алгоритма. Результаты практических исследований на rsc4.kiam.ru, МВС с 64 узлами, на каждом из которых 2 процессора. Умножались матрицы одинакового размера 1024х1024, делящиеся на разное количество частей, в соответствие с алгоритмом, который описан выше. Кол-во узлов время эффективность 1 1377,28 c 100 % 2 689,39 c 99 % 4 344,81 c 99 % 8 172,62 c 99 % 16 87,48 c 98 % 32 50,72 с 84 % 64 31,86 с 68 % Данные результаты показывают, что система OST на макетной задаче показывает уменьшение времени счета, вплоть до 64 процессоров, то есть насыщения счета не 33 происходит. При этом эффективность остается все еще очень высокой. Если смотреть реальные прикладные задачи, то насыщение происходит на 16-32 узлах. В приложении 1 находится полный пример реализации задачи умножения матриц. Практические исследования: задача M2DGD Другой задачей, для которой была представлена реализация в системе OST – была реальная прикладная задача M2DGD. Данная задача реализовывалась Павлухиным П.В. «Последовательный комплекс программ для решения 2D нестационарных задач газовой динамики в областях сложной формы.» Его основные характеристики: - применим как к структурированным, так и неструктурированным сеткам, - второй порядок точности по времени и пространству, - высокая надежность: абсолютно устойчив по отношению к выбору шага по времени. В основе M2DGD: - дискретизация по пространству методом конечного объема (finite volume method), - кусочно-линейное восполнение сеточных функций в счетных ячейках (для достижения второго порядка), - метод С. К. Годунова вычисления потоков на гранях ячеек, - явно-неяная абсолютно устойчивая схема интегрирования по времени Для проверки корректности было произведено сравнение результатов работы последовательного и параллельного алгоритмов на сетке 450х135 с числом шагов 500 и 5000, расчет параллельной версии проводился на 16 ядрах на МВС RSC-4 в ИПМ им.М.В.Келдыша РАН (128 ядер). В следующей таблице дается сравнение подходов OST и MPI с точки зрения прикладного программиста: OST MPI объектно-ориентированный подход для модель построения параллельной параллельно выполняющихся модели; процессов, взаимодействие между ними обращение к удаленным объектам так – через рассылку сообщений 34 же, как к локальным с непосредственным вызовом методов автоматический алгоритм синхронизации, синхронизация полностью организуется предоставляемый средой автоматическое назначение самим прикладным программистом связей определение связей между процессами между объектами по заданной топологии организуется самим прикладным программистом OST «берет на себя» организацию всей низкоуровневой (системной) части программирования, предоставляя прикладному программисту высокоуровневые средства для написания параллельных программ. Для оценки эффективности двух реализаций был проведен счет тестовой задачи на трех различных разрешениях сеточного разбиения c 10 шагами по времени (запуск выполнялся на RSC-4). 35 При счете области 450х135 на 2 и 4 ядрах обе реализации показывают высокую эффективность, с дальнейшим увеличением числа ядер эффективность MPI кода остается столь же высокой; для OST эффективность, начиная с 8 ядер, начинает падать и уже на 32 ядрах возникает насыщение, когда увеличение доступных ядер в 2 раза не дает никакого уменьшения времени счета. Это связано с неэффективной работой потоков (threads) в Python, когда вычисления в каждом блоке на одной итерации по времени становятся сравнимы с временем, затрачиваемым на обслуживание потоков синхронизации времени и передачи ячеек. 36 На области 900х270 наблюдается похожая картина, высокая эффективность OST версии сохраняется уже вплоть до 16 ядер, при дальнейшем увеличении числа ядер она значительно падает. Улучшение эффективности в сравнении с областью 450х135 связано с тем, что отношение времени счета в объекте к издержкам, связанным с обслуживанием потоков, увеличилось (каждый объект содержит в 4 раза больше ячеек). На области 1800х540 особенности работы потоков в Python практически не 37 сказываются на эффективности во всех протестированных конфигурациях, и результаты OST оказываются близкими к MPI даже на 64 и 100 ядрах. 38 Выводы В рамках дипломной задачи ставилась цель разработать систему OST на основе понятий «пространство-время», которая была полностью выполнена. Реализация системы OST продемонстрировала свою эффективность, как в макетной задаче параллельного умножения матриц, так и в реальной задаче M2DGD, имеющей практический смысл. К основными достоинствами получившейся системы OST можно отнести: Простоту реализации в целом. Для того, чтобы реализовать прикладную задачу, прикладному программисту не обязательно знать и понимать всю внутреннею реализацию и подстраиваться под нее, так как это бывает в других системах параллельного программирования. Сведение параллельного программирования к последовательному. Данный подход позволяет прикладному программисту сосредоточиться на реализации алгоритма, а не делать много однообразных, плохо-автоматизируемых алгоритмов, обеспечивающих синхронизации между вычислительными узлами. Автоматическая организация связей. В случае MPI требуется вручную организовывать определение связей, которое зачастую получается неочевидным и не дает достаточной гибкости при изменении топологии связей между вычислительными узлами. В системе OST на основе понятия «пространства» - эта проблема становится легкоразрешимой. В целом результаты реализации конкретных прикладных задач, показывают сравнимость результатов системы OST с общепринятой системой MPI, разработка которой ведется уже много лет. 39 Приложение 1. Полный пример умножения матриц #-*- coding: utf-8 -*import sys from ctypes import * from copy import copy from random import * from multiprocessing import freeze_support import threading import time import Pyro.core from ost_config import * from from from from from ost_main import * ost_local import * ost_control import * ost_object import * ost_util import * def createMatrix( lenght, height ): row = [0 for i in xrange(0,lenght)] matrix = [copy(row) for i in xrange(0,height)] return matrix class loggerTime(object): def __init__(self): self.time_mult = 0 class OST_Object_Matrix( OST_Object_Abstract ): '''Класс объекта уможения матриц''' def __init__(self): '''Конструктор объекта, объявляющая переменные''' OST_Object_Abstract.__init__(self) # Размеры матрицы A self.matrixA_MAX_X = None self.matrixA_MAX_Y = None # Размер матрицы B (дополнительный размер матрицы, не определенный в А) self.matrixB_MAX_X = None # Полоса матрицы A принадлежащий экземпляру объекта 40 self.matrix = None self.matrix_MAX_X = None self.matrix_MAX_Y = None # Полоса матрицы B и матрица-буфер под полосу для передачи соседу self.matrixMult = None self.matrixMultBuf = None self.matrixMult_MAX_X = None self.matrixMult_MAX_Y = None # Полоса матрицы-результата, отвечающая даному объекту self.matrixResult = None self.matrixResult_MAX_X = None self.matrixResult_MAX_Y = None # Количество полос, на которые разбили матрицу A self.N = None self.iteration = None debug("OST_Object_Matrix: create object") def init(self, matrixA_MAX_X, matrixA_MAX_Y, matrixB_MAX_X, x, N): '''Функция иницилизации прикладного объекта (задание размеров матриц и их иницилизация в памяти)''' debug("OST_Object_Matrix: init object coord = "+str(x)) self.N = N self.iteration = 0 # Иницилизация полосы матрицы A для данного прикладного объекта self.matrix_MAX_X = matrixA_MAX_X self.matrix_MAX_Y = matrixA_MAX_Y/N #self.matrix = ( c_double * self.matrix_MAX_X * self.matrix_MAX_X )() self.matrix = createMatrix(self.matrix_MAX_X, self.matrix_MAX_Y) # Иницилизация полосы матрицы B self.matrixMult_MAX_X = matrixB_MAX_X/N self.matrixMult_MAX_Y = matrixA_MAX_X #self.matrixMult = ( c_double * self.matrixMult_MAX_X 41 * self.matrixMult_MAX_Y)() self.matrixMult = createMatrix(self.matrixMult_MAX_X, self.matrixMult_MAX_Y) self.matrixMultBuf = createMatrix(self.matrixMult_MAX_X, self.matrixMult_MAX_Y) self.numMult = x self.numMultBuf = x # Иницилизация полосы матрицы результата self.matrixResult_MAX_X = matrixB_MAX_X self.matrixResult_MAX_Y = matrixA_MAX_Y/N #self.matrixResult = ( c_double * self.matrixResult_MAX_X * self.matrixResult_MAX_Y)() self.matrixResult = createMatrix(self.matrixResult_MAX_X, self.matrixResult_MAX_Y) # Задание точки в топологии self.point = OST_Ring_Point( x, N ) self.logger = OST_time_logger() def iter(self): '''Функция итерации прикладного объекта''' # Итерация умножения матриц self.matrix и self.matrixMult if( self.point.time % 2 == 0 ): self.logger.startExp("OST_Object_Matrix("+str(self.id)+"): Iteration multMatrixMult") debug("OST_Object_Matrix("+str(self.id)+"): Iteration multMatrixMult") self.multMatrixMult() # Итерация взятия следующей полосы матрицы B у соседа по топологии else: self.logger.startExp("OST_Object_Matrix("+str(self.id)+"): Iteration getMatrixMult") debug("OST_Object_Matrix("+str(self.id)+"):Iteration getMatrixMult") self.getMatrixMult() # Продвижение по времени 42 self.point.addTime() self.logger.startExp("setXYZT "+str(self.point.time)) # Синхронизация вычислений self.setXYZT() # Окончен ли счет объекта? if (self.point.time == ( self.N * 2 - 1 ) ): return 1 else: return 0 def multMatrixMult(self): '''Функция отвечающая за итерацию умножения матриц self.matrix и self.matrixMult''' # Копирование полосы матрицы B в буфер for y in xrange(0, self.matrixMult_MAX_Y): for x in xrange(0, self.matrixMult_MAX_X): self.matrixMultBuf[y][x] = self.matrixMult[y][x] self.numMultBuf = self.numMult debug("OST_Object_Matrix("+str(self.id)+"): Mult matrix") for y in xrange(0,self.matrix_MAX_Y): for x in xrange(0,self.matrixMult_MAX_X): real_x = x + self.numMult*self.matrixMult_MAX_X real_y = y self.matrixResult[real_y][real_x] = 0 for i in xrange(0,self.matrix_MAX_X): self.matrixResult[real_y][real_x] += self.matrix[y][i] * self.matrixMult[i][x] def getMatrixMult(self): '''Функция отвечающая за взятие матрицы-буфера у соседа, в которой находится очередная полоса матрицы B''' debug("OST_Object_Matrix("+str(self.id)+"): Get matrix mult buf - request") # При наличии соседа, берется у соседа матрица-буфер if ( self.point.neighbors[0]["obj"] is not None): self.numMult,self.matrixMult = copy(self.point.neighbors[0]["obj"].getMatrixMultBuf()) 43 def getMatrixMultBuf(self): '''Функция, отвечающая за то, чтобы отдать матрицу буфер объекту, для которого текущий является соседом''' debug("OST_Object_Matrix("+str(self.id)+"): Get matrix mult buf - answer") return (self.numMultBuf, self.matrixMultBuf) def finish(self): debug("finish"+"-"*40+"\n") self.logger.finishExp() self.logger.printResults() class OST_Main_Matrix( OST_Main_Class_Abstract ): '''Класс раскрутки задачи умножения матриц''' # Задание, количества потоков N = 4 # Задание параметров матриц matrixA = None matrixA_MAX_X = 16 matrixA_MAX_Y = 16 matrixB = None matrixB_MAX_X = 16 def __init__(self): '''Функция-конструктор объекта раскрутки''' print "OST_MAIN_MATRIX: __init__" OST_Main_Class_Abstract.__init__(self) def initSize(self, A_MAX_X, A_MAX_Y, B_MAX_X): print "OST_MAIN_MATRIX: initSize" self.matrixA_MAX_X = A_MAX_X self.matrixA_MAX_Y = A_MAX_Y self.matrixB_MAX_X = B_MAX_X def initMatrix(self): print "OST_MAIN_MATRIX: initMatrix" '''Функция иницилизациии матриц''' debug("OST_Main_Matrix: init matrix") 44 #self.matrixA = ( c_double * self.matrixA_MAX_X * self.matrixA_MAX_Y )() self.matrixA = createMatrix(self.matrixA_MAX_X, self.matrixA_MAX_Y) for j in xrange(0,self.matrixA_MAX_Y): for i in xrange(0,self.matrixA_MAX_X): self.matrixA[j][i] = random() #self.matrixB = ( c_double * self.matrixB_MAX_X * self.matrixA_MAX_X )() self.matrixB = createMatrix(self.matrixB_MAX_X, self.matrixA_MAX_X) for j in xrange( 0, self.matrixA_MAX_X ): for i in xrange( 0, self.matrixB_MAX_X ): self.matrixB[j][i] = random() def init(self): '''Функция иницилизации счетной модели''' print "OST_MAIN_MATRIX: init" debug("OST_Main: init objects") for numObj in xrange( 0, self.N): obj = self.createObject(OST_Object_Matrix) debug("OST_Main_Matrix: init object coord = "+str(numObj)) obj.init(self.matrixA_MAX_X, self.matrixA_MAX_Y, self.matrixB_MAX_X, numObj, self.N) for j in xrange( 0, self.matrixA_MAX_Y/self.N ): for i in xrange( 0, self.matrixA_MAX_X ): obj.matrix[j][i] = self.matrixA[ j + numObj*self.matrixA_MAX_Y/self.N ][i] for j in xrange( 0, self.matrixA_MAX_X ): for i in xrange( 0, self.matrixB_MAX_X/self.N ): obj.matrixMult[j][i] = self.matrixB[ j ][ i + (numObj) * self.matrixB_MAX_X/self.N ] 45