УДК 518.5
РЕКУРСИВНОЕ РАСПАРАЛЛЕЛИВАНИЕ СИМВОЛЬНО-ЧИСЛЕННЫХ АЛГОРИТМОВ
© Г.И. Малашонок, Ю.Д. Валеев
Malashonok G.I., Valeev Y.D. Recursive disparallelizing of symbol-numerical algorithms. The recursive circuit of a dis-parallelizing of symbol-numerical algorithms and creation of SPMD-programs is offered. The circuit allows to disparallel-ize sequential recursive algorithms and is intended for realization on cluster SPMD computing systems. The circuit combines simplicity of parallel algorithms designing, repeated use availability of algorithms, typical design solutions, possibility of dynamic regulating of computing modules loading.
1. Введение
Большинство современных суперЭВМ составляют вычислительные системы SPMD типа. При этом одной из главных проблем теории программирования остается проблема разработки эффективных параллельных SPMD-программ. Попытки создания паралелльных систем компьютерной алгебры наталкиваются на эту же проблему, т. к. отсутствие развитых схем и технологий создания таких программ тормозит создание параллельных систем компьютерной математики.
Предлагаемая ниже рекурсивная схема распараллеливания предназначена для распараллеливания рекурсивных последовательных алгоритмов в компьютерной математике. В свою очередь эффективные параллельные алгоритмы компьютерной математики могут составить некоторый базис, который позволит перейти к созданию эффективных SPMD-программ в других областях.
Отметим некоторые из наиболее известных сегодня подходов к созданию параллельных программ.
Одним из основных подходов является использование станлапта MPT (Messaee
Passing Interface, 1993 г.) [1] и MPI-2 (1997 г.). При таком подходе программисту требуется описать все обмены сообщениями между процессами, что является довольно трудоемкой работой. Поэтому развиваются автоматизированные средства создания параллельных программ.
Например, разрабатываются специальные
компиляторы, в которых для получения параллельной программы из последовательной, добавляются специальные комментарии, которые подсказывают компилятору, как распределять какой-либо участок кода.
Наиболее известным примером служит стандарт параллельного программирования HPF [2] (High Performance Fortran, 1993 г.). Однако главной проблемой остается проблема создания компилятора с приемлемой эффективностью. Поэтому в 1997 г. появился проект стандарта HPF2, в котором существенно расширены возможности программиста по спецификации тех свойств его программы, извлечь которые на этапе компиляции очень трудно или невозможно.
Другим подходом является модель параллелизма по данным и управлению DVM [3] (1994 г.). Эта модель легла в основу языков параллельного программирования Фортран-DVM и Си-DVM.
Среди подходов, ускоряющих разработку параллельных программ, можно выделить подход, в котором осуществляется конструирование программ из типовых алгоритмических структур {ТАС) [5-8]. Созданы также системы функционального программирования SKIPPER [9], SKIL [10], SkelML [11], использующие ТАС.
Еще одним подходом является разработка параллельных программ на основе типовых проектных решений (design patterns) [12]. В этом подходе используются объектно-ориентированные механизмы, позволяющие наследовать готовые параллельные схемы, которые достаточно наполнить содержанием вызывая соответствующие вычислительные алгоритмы. Это позволяет многократно использовать их для похожих задач. Системы программирования, которые используют этот подход, - это Frame Works [13], DPnDP [14, 15], Enterprise [16], C02P3S [17].
При распараллеливании алгоритмов с разреженными данными при помощи статических вычислительных схем возникают простои процессоров из-за неравномерного и заранее неизвестного распределения данных. Предсказать зараннее объем вычислений для отдельных ветвей алгоритма в случае разреженных структур данных, как правило, невозможно. Поэтому тут требуются подходы, которые способны учитывать возникающие неоднородности данных непосредственно в процессе вычислений и "на лету" перераспределять загрузку вычислительных модулей.
Одной из известных систем, реализующих такой подход, является Т-система [4]. Эта система, используя алгоритм планирования, направляет возникающие в ходе вычислений подзадачи на свободные процессоры.
В данной работе предлагается схема распараллеливания рекурсивных алгоритмов. Она сочетает в себе такие черты, как простоту конструирования параллельных алгоритмов, доступность за счет ТАС повторного использование алгоритмов, как типовых проектных решений, возможность эффективного динамического использования
вычислительных ресурсов.
Одной из отличительных черт предлагаемой схемы является наличие планировщика (диспетчера), который следит за работой процессоров. Если некоторый процессор освобождается, то диспетчер находит один из наиболее загруженных процессоров и разрешает ему передать часть своего задания свободному процессору. За счет такого перераспределения заданий достигается равномерная нагрузка всех процессоров системы.
Другой особенностью является сравнительная простота создания новых алгоритмов. Для построения нового паралелль-ного алгоритма нужно описать его в виде графа, перестроить граф в дерево, и описать вычисления, которые выполняются в каждом узле такого дерева. Еще одной особенностью схемы является возмож-ность повторного использования уже готовых параллельных алгоритмов - они просто включаются как поддеревья в новое дерево.
Предлагаемая схема может быть реализована на базе интерфейса МР1, а следовательно, может использоваться, в частности, на всех вычислительных системах кластерного типа, поддерживающих МР1.
Во втором параграфе вводится ряд основных понятий, которые используются в дальнейшем.
2. Граф алгоритма
Пусть задан граф рекурсивного алгоритма решения некоторой вычислительной задачи. Вершинами такого графа являются вычислительные блоки, а ребрами являются потоки данных между этими блоками. Каждый вычислительный блок скрывает в себе некоторый подграф, узлы которого также являются вычислительными блоками. Такое вложение может продолжаться до тех пор, пока узлами графа не станут элементарные операции, далее не делимые, а ребрами - аргументы и результаты этих элементарных операций.
При этом, так как алгоритм рекурсивный, то один или несколько блоков содер-
жат подграф для решения этой же задачи, но для "усеченных" данных - это дочерние блоки, а остальные блоки обеспечивают "достроение" до решения задачи с "полными данными".
Например, в задаче блочного умножения матриц может быть четыре дочерних блока, а в задаче обращения матриц - один дочерний блок.
Будем ориентировать ребра графа сверху вниз. На следующем рисунке приведен пример некоторого графа рекурсивного алгоритма:
Рис. 1. Граф алгоритма Вершины этого графа обозначены фигурами, так что одинаковые фигуры обозначают одинаковые вычислительные блоки.
Рис. 2. Разбиение графа на слои
Ребра направлены от единственной входной вершины к единственной выходной, поэтому такой граф можно разделить на слои (группы), объединяя в один слой блоки, отстоящие на одинаковое число ребер от входной вершины, считая по самому длинному пути. В один слой объединяются блоки, ко-
торые могут быть вычислены параллельно.
Дерево алгоритма строится по этому графу следующим образом. Начальная вершина является корневой вершиной дерева и все ребра дерева исходят из корневой вершины. Весом ребра дерева назовем номер слоя, в который направлено это ребро в графе алгоритма. Вершины к-го слоя назовем к-той группой вершин дерева. Все вершины, входящие в одну группу, вычисляются параллельно, сначала вершины группы 1, затем группы 2 и т. д.
Рис. 3. Представление графа алгоритма в виде взвешенного дерева
Так как узлы графа - это вычислительные блоки, то их алгоритмы представляются подобными деревьями, а граф всего вычислительного алгоритма разворачивается в многоуровневое дерево.
Движение данных в таком дереве происходит от корневой вершины к листовым вершинам первой группы вершин, с возвратом результата вычислений обратно в корневую вершину, затем аналогично для второй группы вершин, и т. д. После возврата из последней группы вычисления останавливаются, а результат находится в корневой вершине.
В алгоритме имеется конечное число типов вычислительных блоков, соответственно, в полученном дереве алгоритма имеется конечное число типов поддеревьев. Типы деревьев нумеруются. Типом вершины графа является тип поддерева, для которого эта вершина является корневой. На рисунке вершины одинаковых типов обозначаются одинаковыми фигурами.
Ребра, исходящие из одной и той же вершины, нумеруются в некотором порядке, который согласован с возрастанием их весов. Этот порядок один и тот же для вершин одного типа. Таким образом, в дере-вс алгоритма пебпам ппиписываются веса и
номера, а вершинам приписываются уровни и типы.
3. Компьютерные модули
Вычислительная система конструктивно состоит из автономных компьютерных модулей, связанных высокоскоростным интерфейсом, который используется для обмена данными и управляющими сигналами в процессе вычислений, и дополнительным управляющим интерфейсом. Параллельная программа загружается с помощью управляющего интерфейса во все модули перед началом вычислений.
Развиваемая схема распараллеливания осуществляет крупно-блочное распараллеливание за счет параллельного выполнения блоков, начиная с нижних уровней вычислительного графа. Она носит название схемы распараллеливания нижнего уровня (Low Level Parallelization) или LLP-схемы. При этом считается, что корневая вершина лежит в самом низу на нулевом уровне.
Пусть имеется п компьютерных модулей (КМ). Выделим два из них. Главный модуль (нулевой) получает все задание и в него возвращается результат вычислений. Диспетчер (первый модуль) играет роль основного управляющего модуля. На каждом КМ запускается 2 процесса: счетный процесс и управляющий процесс. Счетный процесс отвечает за выполнение вычислений в соответствии с графом алгоритма, а управляющий процесс отвечает за управление счетным процессом и за обмен сообщениями с другими КМ. В начальный момент все КМ, кроме главного, находятся в режиме ожидания, т. е. их управляющие процессы ждут прихода сообщения с новым заданием. На главном КМ вызывается процедура для выполнения главного задания и вводятся входные данные.
4. Прием и отправка заданий
Под заданием понимается поддерево в дереве алгоритма, а номером задания является
номер ребра, входящего в корневую вершину этого поддерева.
В процессе вычислений любой КМ может обслуживать процесс: вычислений одновременно по многим поддеревьям, т. е. может выполнять несколько заданий. Для сохранения потока данных, связанного с отдельным поддеревом, создается в памяти КМ стек данных. Все стеки сохраняется в списке стеков.
При выполнении вычислений, соответствующих поддереву, происходит перемещение данных от корневой вершины к верхним вершинам, а затем возвращение вычисленных данных к корневой вершине. С каждой вершиной связан некоторый набор входных и выходных данных, который хранится в отдельном элементе (блоке) стека. При движении вверх по дереву блоки создаются, а при возвращении к корню - блоки стираются.
Блок содержит следующую информацию:
1. Тип вершины (переменная id).
2. Номер входящей ветви (переменная branch). Если это самый первый блок в стеке, то здесь хранится номер КМ, пославшего данное задание.
3. Промежуточные результаты, в которые входят результаты вычисления подзадапий для вершины (массив объектов results).
4. Ссылки params па входной массив данных. Массив данных находится в предыдущем блоке. Для первого блока массив данных находится во "внешнем" массиве - массиве данных для всего задания, полученном из другого КМ.
5. Индексы (массив целых чисел indexes), которые позволяют извлечь входные параметры для этого нодзадания из данных, на которые ссылаются params. (Индексы позволяют экономить память, т. к. при вычислении на одном процессоре не нужно копировать входные параметры для подзаданий.
Например, для блока матрицы индексы - это координаты левого верхнего и правого нижнего элемента блока матрицы.)
6. Флаги состояний подзаданий (массив целых чисел states). Количество флагов равно количеству подзаданий в данном задании. Возможные значения флагов: 0 - подзадание еще не обрабатывалось; 1 - подзадание послано другому КМ и еще не вычислено; 2 - подзадание послано другому КМ и вычислено; 3 - подзадание вычисляется данным КМ и еще не вычислено; 4
- подзадание уже вычислено данным КМ.
7. Номера модулей, которым были посланы подзадания. Если это подзадание вычислялось данным КМ, то значение номера модуля остается пулевым.
8. Тэги сообщений, в которых эти задания были посланы. Если это подзадание вычислялось данным КМ, то значение тэга остается нулевым.
Для блока определены следующие операции:
1. Создание блока:
BlockLLP(id, proc, params, blockLev) - создать блок для корневой вершины поддерева с типом id, параметрами params на уровне blockLev. BlockLLP(prevBlock, branch, lev)
- создать блок для вершины на уровне lev, которая является подзаданием номер branch для вершины prevBlock.
2. Проверка окончания вычислений: isBlockReady() - возвращает true, если завершилось вычисление блока. isGroupReady(int groupNum) - возвращает true, если в блоке завершилось вычисление группы номер groupNum.
3. Вычисление блока: calculateQ - вычисляет блок и воз-вращает результат.
При получении главного задания
счетный процесс нулевого КМ получает все дерево задания и вызывает параллельную процедуру
parallel procedure(dl, d2,...,dn, mod, bL).
Входные параметры процедуры: dl,d2,...,dn
- входные данные для главного задания; mod - параметры, фиксирующие тип задачи, например модуль, при вычислениях в факторкольце; bL граничный уровень (ГУ).
Счетный процесс главного КМ при получении всего задания: посылает mod и bL всем КМ, создает новое задание (create-TaskQ), вычисляет его (waitForTaskO()) и возвращает результат. Счетные процессы остальных КМ в это время вызывают процедуру waitForTaskQ и ждут прихода заданий.
При получении не главного задания происходит следующее. Управляющий процесс определяет номер КМ, от которого пришло задание и вызывает процедуру recieveTaskQ. Она принимает пакет сообщений с тэгом TASK_TAG. В первом сообщении (заголовке задания) содержится следующая информация:
1. Тип корневой вершины задания - id.
2. Уровень корня поддерева - startLev.
3. Тэг resultTa.g, с которым должно быть послано сообщение с результатом.
4. Диапазон свободных модулей. Диапазон задается двумя целыми числами, которые сохраняются в переменных ргосВ и ргосЕ.
После получения заголовка управляющий процесс вызывает процедуру impls[id].recieveTask(proc), которая по типу id корневой вершины определяет количество и тип входных данных и принимает их из последующих сообщений с тэгом TASK_TAG. Они размещаются в переменной objs.
После получения всех данных управляющий процесс создает из них новое задание с помотттыо [пч)до iivnbi createTaskf'i и
включает флаг recievedTask, который является сигналом для счетного процесса выходить из режима ожидания. После этого счетный процесс вызывает процедуру calculateTask(), которая выполняет задание.
Входными параметрами для createTask(id,startLev,objs,procB,procE,proc, resultTag) являются: id - тип корневой вершины дерева задания; startLev - уровень корневой вершины; objs - входные параметры для задания; ргосВ, ргосЕ - диапазон модулей, которым данный КМ может распределять подзадания; ргос - номер КМ, от которого получено задание; resultTag - тэг, с которым должен быть возвращен результат процессору ргос.
Процедура createTask выполняет следующее:
1. Создает стек для нового задания и записывает его в список стеков stacks.
2. Сохраняет resultTag в списке resultTag s List.
3. Создает нулевой блок в стеке с помощью BlockLLP (id, ргос, objs, startLev). Данная процедура инициализирует в блоке тип вершины id переданным значением id, в переменную params записывает ссылку на входные данные objs. Создает массив индексов indexes, которые указывают на весь массив входных данных. Номер КМ, пославшего задание ргос, записывает в переменную branch блока. Остальные поля в блоке оставляет нулевыми.
4. Инициализирует следующие глобальные переменные: текущий уровень currLev, свободный нижний уровень FLLev. Им присваиваются начальные значения, равные startLev.
При отправке подзадания номер к модуль посылает несколько сообщений. В первом сообщении содержатся следующие данные:
1. Тип подзадания - тип корня подзадания.
2. Уровень подзадания - уровень корня подзадания.
3. Тэг, с которым должен быть послан результат.
4. Диапазон свободных модулей: procBeg и procEnd.
В следующих сообщениях посылаются входные данные для к-то подзадания. Они определяются по входным параметрам params для данного задания и индексам для к-го подзадания, которые вычисляются по индексам indexes с помощью процедуры getlndexesForSubtask. При отправке подзадания флаг номер к в блоке устанавливается равным 1 (подзадание послано, но еще не вычислено).
Вычислив задание номер к, которое было получено из модуля Ь, компьютерный модуль выполняет следующие действия:
Посылает сообщение с тэгом RESULT__TAG, которое установит флаг номер к в блоке модуля b равным 2.
2. Посылает результат вычисления подзадания в сообщениях с тэгом resultTag, которые выбирается из списка resultTagsList.
3. Удаляет всю информацию об этом задании из списков с помощью процедуры destroyTask().
Движение по дереву задания
Каждый КМ не только распределяет подзадания другим КМ, но он назначает подзадания и себе.
Будем называть путем данного модуля список номеров заданий, которые пройдены этим модулем от корня полученного им дерева до текущей вершины. Номера заданий - это номера ветвей, по которым прошел КМ. Ветви нумеруются, начиная с 0.
На рис. 4 изображен путь, пройденный модулем в полученном им дереве. Текущий уровень равен п+3, а путь определяется
1.
5.
числами: 2, 1, 2. Фигурной скобкой отмечены ребра одного веса. Нумерация ребер ведется слева направо. Текущий уровень сохраняется в переменной сштЬеу.
»-й уровень
(*|+1)-й уровень-^ (н+2>-й урокень--(а* 3)-й уровень---
■—0-0-0-пкущ.и вершин»
Рис. 4. Движение по дереву
Путь и множество отданных подзаданий определяют список номеров заданий next. Если нет отданных подзаданий, то список next образуется из списка path увеличением всех чисел па 1. В приведенном примере это числа: 3, 2, 3. Если есть отданные под-задания, то их корневые вершины должны быть пропущены, а число на соответствующем уровне в списке next увеличено. Таким образом, по этому списку в каждом уровне указывается невыполненное подза-дание с наименьшим номером.
Текущим свободным нижним уровнем (СНУ) называется наименьший уровень, с которого начинаются подзадания, которые можно параллельно выполнять.
В примере, изображенном на рис. 4, СНУ равен п+2, т. к. на (п+1)-м уровне ветвь является последней в группе и не имеет подзаданий, которые можно отдать, а на (пЧ-2)-м уровне можно отдать подзадание номер 2.
При получении нового задания КМ сообщает диспетчеру свой новый СНУ, который равен уровню корневой вершины поддерева. СНУ сохраняется в переменной FLLev.
При движении КМ из вершииы А на уровне п в вершину В на уровне (п+1) по к-й ветви (см. рис. 4) происходит следующее:
1. В список next записывается номер свободной ветви fc+l.
2. В к-й флаг states блока А записывается число 3.
3. Процедура getlndexesForSubtask вычисляет индексы подзадания В по типу вершины А, индексам в блоке А и номеру ветви к. Создается новый блок для вершины В в текущем стеке stack и в него записываются полученные индексы.
4. В созданный блок для вершины В в переменную branch записывается номер ветви, по которой КМ перешел из А в В.
5. Если уровни FLLev и currLev совпадают, а КМ движется по последней ветви с данным весом, то увеличивается FLLev на 1 и диспетчеру посылается новое значение FLLev. (в управляющем режиме)
6. Текущий уровень currLev увеличивается на 1.
7. Процедура getldForSubtask определяет тип подзадания В по типу задания А и номеру ветви к . Вызывается процедура для вычисления подзадания В.
После вычисления подзадания КМ возвращается в вершину А, при этом выполняются следующие действия:
1. Из переменной branch блока В берется номер ветви к, по которой модуль перешел в вершину В.
2. В блок для вершины А в соответствующие позиции области results записываются результаты вычисления подзадания В с помощью процедуры writeResults. Позиции в области results определяются по типу вершины А и номеру ветви к.
3. Блок вершины В удаляется из текущего стека stack.
4. currLev уменьшается па 1.
Начиная с некоторого уровня в дереве, размер заданий становится настолько малым, что время на пересылку превосходит
время на вычисление. С этого момента модули не отдают свои подзадания и вычисляют их до конца с помощью однопроцессорного алгоритма. Этот уровень называется граничным уровнем (ГУ). Его значение хранится в переменной ЬЬ.
6. Начальный режим
Динамическое распараллеливание предполагает два этапа вычислений - начальный и диспетчерный.
В начальном режиме существует большой запас свободных модулей и списки свободных модулей распределяются вместе с заданиями. После того, как свободные модули исчерпываются, начальный режим сменяется диспетчерным, и распределение свободных модулей, по мере их высвобождения, берет на себя диспетчер.
В начале вычислений главный КМ получает все задание и номера всех остальных КМ: 1,..., п — 1. Главный КМ находится в нулевой вершине. Пусть из нулевой вершины исходит к ребер веса 1, п > к.
Тогда первые к — 1 подзаданий он распределяет к — 1 модулю, а к-тое задание вычисляет сам. Остальные (п — к) свободных модулей он делит на к частей, к — 1 часть посылает модулям, а одну часть оставляет себе.
Далее каждый КМ распределяет таким же образом свою часть свободных модулей. Процесс распределения продолжается до тех пор, пока выполняется неравенство п > к - 1.
Если у какого-нибудь модуля количество номеров свободных модулей стало меньше, чем подзаданий (п < к — 1), то он сообщает диспетчеру свой СНУ (в сообщении NEW_LOW_LEVEL) и продолжает двигаться по дереву, как было описано в предыдущем разделе.
На рис. 5 изображено несколько шагов распределения заданий и номеров для случая п — 8. В начальном режиме один из КМ может стать свободным. Свободным КМ становится, когда он вычислил свое поддерево или вычислил все возможные подзада-ния, и осталось лишь принять результаты
от КМ, которым он послал подзадания.
Рис. 5. Распределение заданий в начальном режиме
Случай первый. Пусть, например, на рис. 5 3-й КМ первым вычислил свое задание на 2-м уровне. Тогда он стал свободным.
Случай второй. Пусть 1-й КМ вычислил свое задание на 2-м уровне, и осталось только принять результаты от 3-го и 4-го КМ. 1-й КМ не может вычислять следующее под-задание, т. к. на него указывает ребро, которое имеет больший вес. Следовательно, он стал свободным.
Свободный КМ в начальном режиме сообщает о том, что он освободился диспетчеру с помощью сообщения FIRST_FREE. Диспетчер проверяет, было ли до этого получено сообщение NEW_LOW_LEVEL. Если такое сообщение было получено ранее, то происходит включение диспетчер-ного режима в результате рассылки диспетчером всем модулям сообщения SET_DISP_MODE. Иначе диспетчер игнорирует это сообщение.
7. Диспетчерный режим
Переключение в этот режим происходит после того, как диспетчер посылает всем компьютерным модулям сообщение SET_DISP_MODE. После получения такого сообщения управляющий процесс каждого КМ переключается в новый режим.
В диспетчериом режиме компьютерные модули не распределяют задания свободным модулям, а выполняют их сами, двигаясь по дереву задания. При этом они сообщают диспетчеру о своем свободном нижнем уровне СНУ, с которого они могли бы отдать задание.
Если некоторый КМ а освободится, то он пошлет сообщение FREE диспетчеру. Диспетчер, получив сообщение FREE, определяет номер модуля Ь, имеющий наименьший СНУ и связывает модули b и а. Для этого он посылает сообщение SEND_TASK_TO_FREE и номер модуля а модулю Ъ.
Получив такое сообщение, управляющий процесс модуля Ь, не прерывая работу счетного процесса, выполняет следующие действия:
1. Проверяется значение СНУ в переменной FLLev, т. к. некоторые подза-дания могли быть уже вычислены. Если FLLev = bL, то у КМ b нет заданий для распределения. В этом случае нужно возобновить модуль а в списке свободных модулей у диспетчера, для этого посылается сообщение FREE с номером КМ а.
2. Если же FLLev < bL, то задание taskNum отсылается модулю а,
next [FLLev] увеличивается на 1, вычисляется новое значение FLLev , которое посылается диспетчеру.
После того, как отдано последнее под-задание в текущей группе заданий, КМ проверяет, можно ли перейти к следующей группе подзаданий. Для этого проверяются флаги выполнения подзаданий. Если все подзадания текущей группы вычислены (флаги states в текущем блоке для них имеют значения 2 или 4), то можно перейти к вычислению следующей группы.
Если все группы подзаданий на текущем уровне вычислены, то можно вычислить результат текущего задания. Если текущий блок является нулевым в стеке, то послать результат модулю, пославшему это задание, как описано выше. Если текущий блок не является первым, то подняться на 1 уровень вверх.
Если же некоторые подзадания были отданы другим модулям и еще не вычислены, то работа с этим стеком прекращается.
Производятся следующие действия:
1. Оставляется информация о текущем задании в стеке.
2. Проверяется список стеков. Проверка начинается с последнего блока в последнем стеке. Если все подзадания в текущей группе этого блока уже вычислены, то:
а) задание в этом стеке становится текущим,
б) КМ продолжает выполнение задания со следующей группы, т. е. вызывается параллельная процедура для этого задания с указанием группы, с которой будет продолжено вычисление.
3. Если подзадания текущей группы еще не вычислены, то проверяется последний блок в предыдущем стеке.
4. Если все стеки проверены и ни одно
задание еще пе готово, то диспетчеру р
посылается сообщение FREE.
8. Диспетчер
Управляющий процесс одного из модулей выполняет функцию диспетчера всего вычислительного процесса. Он отвечает за равномерное распределение вычислительной нагрузки. Для этого он отслеживает по- >
явление освободившихся КМ и связывает с ними те КМ, у которых имеются еще не вычисленные поддеревья (подзадания) на самых нижних уровнях вычислительного дерева, чтобы они могли отдать их освободившимся КМ.
Диспетчер организует два списка.
Первый - это список СНУ модулей, который хранятся в виде упорядоченного по значениям СНУ списка с именем FLLevs.
Элементом такого списка является пара целых чисел: номер КМ и СНУ этого КМ. !
Второй список - это список свободных !
модулей freeMods, его образуют номера КМ, j
которые не заняты вычислениями и простаивают.
На рис. 6 изображено некоторое состояние списка FLLevs.
bwLew
Рис. 6. Список СНУ модулей
Диспетчер принимает сообщения следующих типов:
NEWTASK - так же как и другие КМ, диспетчер может принимать и вычислять задание.
NEW LOW LEVEL - в этом сообщении диспетчер принимает значение СНУ модуля р, пославшего это сообщение. После его получения диспетчер удаляет из списка FLLevs элемент, соответствующий КМ р и вставляет новый элемент со значением (р, FLLev) так, чтобы FLLevs остался упорядоченным. Если FLLev=bL, то элемент не добавляется в список, т. к. КМ р уже находится на ГУ и не может отдать часть своего подзадания.
FREE - сообщение, в котором содержится номер / свободного КМ. Получив это сообщение диспетчер выбирает из списка FLLevs 1-й элемент (т, lev). Так как это
1-й элемент, то КМ т имеет наименьший номер СНУ, т. е. поддерево имеет самую большую высоту. Далее возможны случаи:
1. Если список FLLevs пуст, то номер свободного КМ добавляется в список свободных КМ - freeMods.
2. Если lev> = 0, то диспетчер посылает КМ т сообщение
SEND_TASK_TO_FR.EE с номером / для того, чтобы КМ т отдал свое подзадание па уровне lev свободному КМ /. После этого диспетчер удаляет элемент (то, lev) из FLLevs, что означает, что КМ т отдает свое подзадание и его НУ пока не определен. После того, как КМ т отдаст свое подзадание, он пошлет диспетчеру сообщение NEW_LOW_LEVEL со своим новым СНУ. Диспетчер обновит элемент для т.
Каждый раз при поступлении сообщения NEW_LOW_LEVEL диспетчер проверяет список freeMods. Если freeMods не пуст, то диспетчер выбирает из списка номер сво-
бодного модуля и производит те же действия, что и при поступлении сообщения FREE_TAG.
Если некоторые сообщения FREE не могут быть обработаны, то они сохраняются в списке freeMods.
Если новых сообщений нет, то диспетчер останавливается на некоторое время, давая больше вычислительных ресурсов счетному процессу.
9. Реализация задания в LLP
При реализации задания в системе LLP используется объектно-ориентированный подход, т. к. именно он позволяет разделить программу распределения и программу заданий. В дальнейшем используется понятие класс, который объединяет в себе данные и функции, оперирующие над данными (методы).
Пусть имеем некоторый вычислительный алгоритм. Для того чтобы реализовать его в виде LLP задания, нужно в первую очередь построить для него граф. Граф должен обладать теми свойствами, которые были описаны ранее. Вершинами графа являются вычислительные алгоритмы, которые сами являются подграфами, либо являются элементарными операциями. Каждому типу вершин соответствует задание в LLP. Задание в LLP - это вычислительный алгоритм, который в LLP имеет свой порядковый номер и класс, определяющий его реализацию.
Таким образом, для реализации основного алгоритма нужно определить для него уникальный номер и написать класс, который определит конфигурацию его графа и взаимодействие его вершин.
Следующее, что нужно сделать - это определить количество новых типов вершин в графе. Для каждого нового типа вершин нужно реализовать задание в LLP.
В качестве примера реализации задания LLP рассмотрим блочно-рекурсивный алгоритм умножения матриц (Aij) х (Bij), в котором каждая матрица разбивается на 4 подблока: Qj = AiiBij+Ai2B2j, (i,j = 1,2).
Граф этого алгоритма изображен на рис. 7.
Рис. 7. Граф алгоритма умножения матриц
У этого графа 2 типа вершин:
Вершина 1-го типа - основное задание, которое принимает на входе 2 матрицы А и В и возвращает их произведение АВ. В вершине блоки матриц А и В распределяются на 4 вершины 2-го типа. После вычисления подзаданий, в вершине 1-го типа принимаются готовые подблоки Сц, С\2. С21, Сг, и из них собирается результат АВ.
Вершины 2-го типа вычисляют выражение АВ + СВ, где А, В, С, И - матрицы, которые подаются на вход вершине. В вершине 2-го типа 4 матрицы распределяются на 2 подзадаиия: АВ и СВ, которые являются вершинами 1-го типа. После выполнения умножений вершины 2-го типа принимают по 2 произведения, складывают их и получают А В + СВ. В данном графе вершины 2-го типа используются для вычисления выражений Су = АцВ^ + Ai2B2j, где
= 1,2-
Номера присваиваются всем заданиям. Номера могут быть произвольными целыми числами, при условии, что они все различны. Номер определяет тип вершины.
Пусть типы вершин для вершин 1-го и
2-го типа имеют номера 1 и 2.
Классы для всех заданий имеют общий абстрактный базовый класс ЬЬРТазЫтр1е-тепЬаНоп, а все задания соответственно являются его подклассами.
Методов, которые нужно реализовать в подклассах, всего 8: getInfo, зеНпс1ехезГог11оо1;,
getParamsForSubtask, getlndexesForSubtask, sendTask, calculateTasklproc, calculateBlock, calcBeforeGroup.
1) Метод get Info.
Этот метод возвращает объект, в котором содержится информация о данной вершине: ngroups - Количество групп в данной вершине.
totSubTasks - Общее количество подзаданий в данном задании. nsubTasks - Массив, г-й элемент которого равен количеству подзаданий в г-й группе, nlndxs - Количество индексов в блоке для данной вершины.
nresults - количество промежуточных результатов results, которые хранятся в блоке вершины.
ids - Массив, г-й элемент которого равен типу вершины.
resPos - Двумерный массив, resPos[i] = массив позиций в results, в которые будут записаны результаты подзаданий. resTypes - Типы результатов данной вершины.
inputTypes - Типы входных параметров данной вершины.
Например, для вершины типа 1 метод getlnfo возвращает следующую информацию: 1,4,{4},8,4, {2,2,2,2},{{0},{1},{2},{3}}, {MATRIX_TYPE}, {MATRIX_TYPE, MATRIX_TYPE}, где {...} - обозначает одномерный массив, а - двумер-
ный, a MATRIX__TYPE - обозначает матричный тип.
2) Метод setlndexesForRoot.
Этот метод вызывается системой LLP, когда данный КМ принял задание от другого КМ и создает корневую вершину для принятого подграфа. Данный КМ вызывает этот метод и передает в него ссылки на входные параметры (params) и массив индексов для корневого блока (indxs). Т. к. задание было послано по сети, то в нем не содержится лишних данных, поэтому индексы обычно устанавливаются таким образом, чтобы указать на входные параметры целиком.
Индексы для матрицы - это обычно строка и столбец верхнего левого и правого нижнего угла, используя которые можно
производить операции с подблоками матриц, не копируя их в новые объекты.
Для вершины 1-го типа данный метод возвратит: {0,0,ширина А, высота А, 0,0,ширина В, высота В}.
3,4) Методы getParamsForSubtask и getlndexesForSubtask.
Эти методы рассматриваются вместе, т. к. они очень тесно взаимосвязаны и вызываются друг за другом.
Они вызываются данным КМ, когда он движется однопроцессорно по дереву вниз в стартовом или диспетчерном режиме. Когда КМ движется по ветви вниз, он создает блок для той вершины, в которую перешел. В новом блоке он должен создать массив входных параметров pararns и массив индексов indxes. Для получения параметров params он вызывает getParamsForSubtask с параметрами: prevBlock - ссылка на преды-
ттлмттт/ттт пелгг {тг\нг f\ пглгг Ъ'птглглт-.ттл п'тттярт* ттптт-
rtj ^-IW.lv, .vu ж
задание) и taskNum - номер ветви, по которой он двигался вниз. Метод getParamsForSubtask возвращает массив ссылок на параметры для нового блока.
Для вычисления indexes для нового блока он вызывает getlndexesForSubtask с теми же параметрами, что и getParamsForSubtask.
Прежде чем перейти к реализации этих методов для умножения, рассмотрим, что из себя представляют подзадания для вершины 1-го типа.
Из графа алгоритма видно, что, например, 0-е подзадание для вершины 1-го типа
- это 4 матрицы: Ац, А\2, Вц, i?2i- Чтобы передать их на вход вершины 2-го типа внутри одного процессора, не нужно копировать эти подблоки в новые матрицы, т. к. при этом расходуется память. Вместо этого, на вход вершины 2-го типа нужно передать ссылки на матрицы А и В и 8 индексов, которые определяют объединенные подблоки (Ап и А12) и (Вц И В21).
Таким образом, метод getParamsForSubtask для вершины типа 1 будет возвращать тот же массив params. Метод getlndexesForSubtask для 0-го подзадания возвращает индексы reslndexes[i], вычисляемые следующим образом через индексы предыдущего блока indexes [ij:
(indxes[ОТ], indxes[1]) (indxes[4], indxes[5])
(indxes [2], (indxes H,
indxes [3]) indxes[7])
Рис. 8. 0-е подзадание вершины 1-го типа
reslndxes [0] =indxes [0];
reslndxes[l]=indxes[l];
reslndxes[2] = (indxes [0] +indxes [2]) /2;
reslndxcs[3] - indxes[3];
reslndxes[4]=indxes [4];
reslndxes [5]=indxes [5];
reslndxes [6] =indxes [6];
reslndxes [7]=(indxes [5] 4-indxes [7]) /2;
5) Метод sendTask.
Данный KM вызывает метод sendtask с ссылкой на блок (параметр block), из которого посылается подзадание, с номером подзадания (taskNum) и номером КМ (ргос), которому оно будет послано.
Для вершины типа 1 sendTask вызовет методы getParamsForSubtask и getlndexesForSubtask для определения подблоков, которые будут посланы КМ ргос, и вызовет процедуру send, которая отправит подблоки.
6) Метод calculateTasklproc.
Этот метод вычисляет с помощью однопроцессорного алгоритма задание в вершине.
Он вызывается, когда КМ переходит на граничный уровень (ГУ), в этом случае размер задания достаточно мал, поэтому выгоднее вычислить его однопроцессорно, чем распределять на другие КМ.
Для вершины 1-го типа данный метод умножит входные подблоки, которые содержатся внутри матриц в params, с координатами из indexes.
7) Метод calculateBlock.
Этот метод собирает результат вычисления всего задания из промежуточных результатов в массиве results в блоке. Он вызывается, когда готовы все группы у вершины.
Как было записано в методе getlnfo, в results [0] будет записан результат 0-го под-
Ац Ац
А21 А22
Вц В12
В21 В22
задания, т. е. подблок Сц, в results[l] - Си, в results[2] - С21 и в results[3] - С22- Поэтому calculateBlock для вершины типа 1 соберет матрицу результата С из подблоков Сц.
8) Метод calcBeforeGroup.
Этот метод вызывается перед вычислением каждой группы. Он может использоваться для выполнения промежуточных вычислений. В этом методе могут производится любые вычисления с помощью однопроцессорных алгоритмов для получения данных, которые потребуются для последующих под-заданий, но вычисления не должны занимать много времени, т. к. они выполняются последовательно.
10. Заключение
Описанная схема распараллеливания рекурсивных алгоритмов легла в основу системы параллельной компьютерной алгебры, которая создается в лаборатории алгебраических вычислений Тамбовского государственного университета имени Г. Р. Державина. Система создается на основе JDK-1.5, MPI и интегрированной системы ParJava ИСП РАН. Эксперименты проводятся на установленном в лаборатории 16 - процессорном Myrinet-кластере с пиковой производительностью 75 Мф. Реализованы алгоритмы умножения матриц и полиномов, а также алгоритмы обращения матриц. Эксперименты демонстрируют высокую эффективность и хорошую масштабируемость полученных параллельных программ [18].
ЛИТЕРАТУРА
1. Message-Passing Interface Forum, Document for a Standard Message-Passing Interface, 1993. Version 1.0.
http://www.unix.mcs.anl.gov/mpi/
2. High Performance Fortran Forum. High Performance Fortran Language Specification. Version 2.0, January 1997
3. Коновалов H.A., Крюков В.А., Михайлов C.H., Погребцов JT.A. Fortran-DVM - язык разработки мобильных
параллельных программ// Программирование. 1995. № 1. 17. С. 49-54.
4. Abramov S., Adamovitch A. and Kovalenko М. T-system: programming environment providing automatic dynamic parallelizing on IP-network of Unix-computers. Report on 4-th International Russian-Indian seminar and exibition, Sept. 15-25, 1997, Moscow.
5. Берзигияров П.К. Программирование на типовых алгоритмических структурах с массивным параллелизмом //
Вычислительные методы и программирование. 2001. Т.2. С. 1-16.
6. Berzigyarov Р.К. Static Pipelines for Divide-and-Conquer Functions: transformation and Analysis. Preprint IVTAN, N 8-391.
М., 1995
7. Campbell D.K.G. Towards the Classification of Algorithmic Skeletons. Technical Report
YCS 276. York, 1996. *"
8. Cole M.I. Algorithmic Skeletons: Structured Management of Parallel Computation.
Massachusetts, Cambridge. Boston: 1989.
9. Serot J., Ginhac D., Derutin J-P. Skipper:
A skeleton-based parallel programming environment for real-time image processing applications // LNCS. 1999. 1662, p.
296-305.
10. Botorog G.H., Kuchen H. Skil: an imperative language ming // Proc. of the Fifth International Symposium on Computer Society. 1996. p. 243-252.
11. BratvoldT.A. Skeleton-based parallelization of functional Engineering. Heriot-Watt University. Edinburgh, 1994.
12. Gamma E., Helm R., Johnson R., Vlissides J. Design Patterns: Elements of Reusable Object-Oriented Software. Reading-Amsterdam-Tokyo: Addison-Wesley, 1995.
13. Singh A., Schaeffer J., Szafron D. Views on template-based parallel programming // Proc. of CASCON’96. Toronto, 1996. p. 1-12.
14. Siu S. Openness and Extensibility in Design-Pattern-Based Parallel Programming Systems. Master of Applied Science Thesis. University of Waterloo. Ontario. Canada. Waterloo, 1996.
15. Sin S., De Simone M., Goswami D., Singh A. Design patterns for parallel programming // Parallel and Distributed Processing Techniques and Applications. California. Pasadena, 1996. p. 230-240.
16. MacDonald S. Design patterns in enterprise // Proc. of CASCON’96. Toronto, 1996. p. 1-10.
17. MacDonald S., Szafron D., Schaeffer J., Bromling S. Generating parallel program frameworks from parallel design patterns
// Proc. of EuroPar’2000. Berlin: Springer-Verlag, 2000. p. 95-104.
18. Малашонок Г.И., Аветисян А.И., Валеев Ю.Д., Зуев М.С. Параллельные алгоритмы компьютерной алгебры // Труды института системного программирования. 2004. Т.8. 4.2. С. 169-180.
БЛАГОДАРНОСТИ: Работа выполнена при частичной поддержке грантов РФФИ (проект 04-07-90268), Human Capital Foundation (проект 23-03-24) и программы Университеты России (проект ур 04.01.464).
Поступила в редакцию 2 сентября 2006 г.