Министерство образования и науки Российской Федерации Федеральное государственное автономное образовательное учреждение высшего профессионального образования «ЮЖНЫЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ» М. Э. Абрамян ПРАКТИКУМ ПО ПАРАЛЛЕЛЬНОМУ ПРОГРАММИРОВАНИЮ С ИСПОЛЬЗОВАНИЕМ ТЕХНОЛОГИИ MPI Ростов-на-Дону 2011 2 Рецензент: к. ф.-м. н., доцент С. С. Михалкович Аннотация Учебное пособие «Практикум по параллельному программированию с использованием технологии MPI» состоит из пяти модулей. Модуль № 1 посвящен описанию процесса разработки и выполнения параллельных программ, модуль № 2 — пересылке сообщений между двумя процессами, в модуле № 3 рассматриваются операции редукции и особенности использования в параллельных программах составных типов данных, модуль № 4 посвящен коллективным операциям и способам создания новых коммуникаторов, а модуль № 5 — виртуальным топологиям. Изложение материала проводится на примерах решения типовых задач. Кроме того, пособие содержит формулировки 100 учебных заданий, выполнение которых позволит закрепить изученный материал. Автор: М. Э. Абрамян. © М. Э. Абрамян, 2011 3 Предисловие Пособие является практическим введением в параллельное программирование на основе технологии MPI — интерфейса передачи сообщений (Message Passing Interface). В настоящее время данная технология является одной из основных технологий параллельного программирования для кластерных систем и компьютеров с распределенной памятью (см. [1–5]). В пособии рассматривается стандарт MPI версии 1.1 [6], используемый в большинстве современных программных реализаций. Данный стандарт ориентирован на языки C и Фортран, однако легко может быть перенесен на другие процедурные языки (имеется также ряд объектно-ориентированных реализаций стандарта MPI). В пособии используется стандартный интерфейс MPI для языков С/С++, а также его адаптация для языка Паскаль, выполненная автором. В соответствии с модульной технологией организации учебного материала пособие разбито на 5 модулей, которые включают как основное содержание, так и контрольные разделы (проектное задание и тесты рубежного контроля), позволяющие определить уровень усвоения материала. Модуль № 1 посвящен описанию процесса разработки и выполнения параллельных программ, модуль № 2 — пересылке сообщений между двумя процессами, в модуле № 3 рассматриваются операции редукции и особенности использования в параллельных программах составных типов данных, модуль № 4 посвящен коллективным операциям и способам создания новых коммуникаторов, а модуль № 5 — виртуальным топологиям. Изложение материала проводится на примерах решения типовых задач. Кроме того, в приложении приводятся формулировки 100 учебных заданий, выполнение которых позволит закрепить изученный материал. Наличие большого количества учебных задач позволило представить проектные задания к каждому модулю в виде набора из 24 вариантов, что дает возможность преподавателю снабдить каждого учащегося отдельным вариантом проектного задания. Для изучения основных компонентов MPI достаточно использовать локальный компьютер, имитируя на нем параллельное выполнение процессов. Однако даже в этом простейшем варианте учащийся неизбежно сталкивается с дополнительными трудностями при разработке параллельных программ, обусловленными сложностью организации ввода-вывода данных для различных процессов параллельной программы и невозможностью применения для параллельных программ стандартных отладочных средств, предусмотренных в интегрированных средах. Для того чтобы облегчить освоение технологии MPI, автор разработал специализированную учебную систему — электронный задачник по параллельному MPI-программированию Programming Taskbook for MPI (PT for MPI). 4 Задачник PT for MPI включает большое число учебных заданий, связанных с различными разделами стандарта MPI-1.1. Задания по параллельному MPIпрограммированию могут выполняться на языках Паскаль и С++ в следующих программных средах: Borland Delphi 7.0 и Turbo Delphi 2006; Free Pascal Lazarus 0.9; PascalABC.NET, Microsoft Visual C++ 6.0; Microsoft Visual Studio .NET 2003, 2005 и 2008. В качестве библиотеки MPI задачник использует динамическую библиотеку mpich.dll, входящую в состав комплекса MPICH for Windows 1.2.5 — широко распространенной бесплатной реализации интерфейса MPI-1.1 для операционной системы Windows. Для программ на С++ доступ к библиотеке обеспечивается с помощью файла mpich.lib и набора заголовочных файлов, также входящих в комплекс MPICH, а для программ на Паскале — с помощью модуля MPI.pas, специально разработанного для задачника PT for MPI. Задачник PT for MPI предоставляет при выполнении заданий те же возможности, что и базовый задачник Programming Taskbook; в частности, он передает программе учащегося исходные данные, проверяет правильность результатов, полученных программой, и сохраняет сведения о каждом тестовом испытании программы в специальном файле результатов. Кроме того, в задачнике PT for MPI предусмотрены дополнительные возможности, упрощающие и ускоряющие разработку и тестирование параллельных программ. Предполагается, что при изучении библиотеки MPI настоящее пособие будет использоваться совместно с задачником PT for MPI. 5 1 Модуль № 1. Выполнение программ в параллельном режиме 1.1 Комплексная цель Познакомиться с основными понятиями, связанными с технологией параллельного программирования на основе MPI. Изучить основные этапы разработки параллельных программ. 1.2 Содержание модуля 1.2.1 Выбор языка и среды программирования Стандарт библиотеки MPI определен для двух языков: Фортрана и C (заметим, что вариант библиотеки MPI для языка С может быть использован без каких-либо изменений и в программах на языке C++). В то же время, поскольку данная библиотека является обычной процедурной библиотекой и состоит из набора функций, ее можно использовать в любом процедурном языке; требуется лишь, чтобы этот язык допускал работу с указателями. Поэтому нет никаких препятствий к тому, чтобы использовать средства библиотеки MPI в программах на языке Паскаль, широко применяемом в настоящее время при обучении программированию. При этом знания, полученные при изучении технологии MPI с применением языка Паскаль, можно будет в дальнейшем использовать и при разработке параллельных программ на других языках (учитывая лишь особенности этих языков, связанные с передачей параметров). Поскольку параллельные программы, разрабатываемые на языках C/С++ и Паскаль, имеют, по существу, одни и те же особенности, связанные со спецификой технологии MPI, мы будем описывать выполнение заданий по параллельному программированию сразу для двух языков: Паскаля и C++. Так как настоящее пособие ориентировано на применение электронного задачника Programming Taskbook for MPI, необходимо использовать программную среду, для которой имеется реализация данного задачника. Для языка Паскаль такими средами являются Borland Delphi 7, Turbo Delphi 2006 for Win32, Free Pascal Lazarus 0.9 for Win32 и PascalABC.NET, а для языка C++ — Microsoft Visual C++ 6.0 и Microsoft Visual Studio .NET 2003, 2005 и 2008. Будем считать для определенности, что при выполнении заданий на Паскале используется среда Free Pascal Lazarus 0.9, а при выполнении заданий на C++ — среда Microsoft Visual Studio .NET 2008. 1.2.2 Основные понятия MPI-программирования Знакомство с параллельным программированием начнем с рассмотрения следующей простой задачи. 6 MPIBegin1. В каждом из процессов, входящих в коммуникатор MPI_COMM_WORLD, прочесть одно целое число и вывести его удвоенное значение. Кроме того, для главного процесса (процесса ранга 0) вывести количество процессов, входящих в коммуникатор MPI_COMM_WORLD. Прежде всего разъясним термины параллельного MPI-программирования. При параллельном выполнении программы запускается несколько экземпляров этой программы. Каждый запущенный экземпляр представляет собой отдельный процесс (англ. process), который может взаимодействовать с другими процессами, обмениваясь сообщениями (messages). MPI-функции предоставляют разнообразные средства для реализации такого взаимодействия (аббревиатура MPI расшифровывается как «Message Passing Interface» — интерфейс передачи сообщений). Для идентификации каждого процесса в группе процессов используется понятие ранга (rank). Ранг процесса — это порядковый номер процесса в группе процессов, отсчитываемый от нуля (таким образом, первый процесс имеет ранг 0, а последний процесс — ранг K – 1, где K — количество процессов в группе). При этом группа процессов может включать лишь часть всех запущенных процессов параллельного приложения. С группой процессов связывается особая сущность библиотеки MPI, называемая коммуникатором (communicator). Любое взаимодействие процессов возможно только в рамках того или иного коммуникатора. Стандартный коммуникатор, содержащий все процессы, запущенные при параллельном выполнении программы, имеет имя MPI_COMM_WORLD. «Пустой» коммуникатор, не содержащий ни одного процесса, имеет имя MPI_COMM_NULL. Процесс ранга 0 часто называют главным процессом (master process), а остальные процессы — подчиненными (slave processes). Как правило, главный процесс играет особую роль по отношению к подчиненным процессам, передавая им свои данные или получая данные от всех (или некоторых) подчиненных процессов. В рассматриваемом задании MPIBegin1 все процессы должны выполнить одно и то же действие — прочесть одно целое число и вывести его удвоенное значение, а главный процесс, кроме этого, должен выполнить дополнительное действие — вывести количество всех запущенных процессов (иными словами, количество всех процессов, входящих в коммуникатор MPI_COMM_WORLD). Обратите внимание на то, что в этом простом задании процессам не требуется обмениваться сообщениями друг с другом (таковы все задания начального уровня, входящие в подгруппу «Процессы и их ранги» — см. п. 6.1). 1.2.3 Создание заготовки для параллельной программы Процесс выполнения задания с применением задачника Programming Taskbook обычно начинается с создания проекта-заготовки для выбранного задания. Особенно удобно использовать такую заготовку для заданий по параллельному программированию, поскольку в нее уже будут входить важные фрагменты кода, необходимые при выполнении любой параллельной программы. Для созда- 7 ния заготовки воспользуемся программным модулем PT4Load, входящим в состав задачника. Вызвать этот модуль можно с помощью ярлыка Load.lnk, который автоматически создается в рабочем каталоге учащегося (в среде PascalABC.NET для вызова модуля PT4Load предназначена команда меню «Модули | Создать шаблон программы»; можно также использовать кнопку на панели инструментов). При этом на экране появится окно модуля PT4Load (рис. 1). Рис. 1. Окно модуля PT4Load Так выглядит окно, если текущей программной средой задачника является среда Free Pascal Lazarus. Для изменения текущей среды достаточно выполнить в окне щелчок правой кнопкой мыши и выбрать из появившегося контекстного меню новую среду (например, «Microsoft Visual C++ 2008»; при этом в заголовке окна появится текст «[VCNET3]»). Обратите внимание на группы MPIBegin и MPIDebug, указанные в списке доступных групп заданий. Их наличие означает, что к базовому варианту задачника Programming Taskbook подключено его расширение: задачник по параллельному программированию Programming Taskbook for MPI. Заметим, что если выбрать программную среду, не связанную с Паскалем или C++ (например, Microsoft Visual Basic любой версии), то группы MPIBegin и MPIDebug в списке будут отсутствовать. Определимся с выбором среды программирования, после чего введем в поле «Задание» текст MPIBegin1. В результате кнопка «Загрузка» станет доступной и, нажав ее (или клавишу [Enter]), мы создадим заготовку для указанного задания, которая будет немедленно загружена в выбранную программную среду. В случае использования Паскаля для среды Lazarus в нее будет загружен файл MPIBegin1.lpr, содержащий следующий текст: [Pascal] program MPIBegin1; uses PT4, MPI; var flag, size, rank: integer; begin Task('MPIBegin1'); MPI_Initialized(flag); if flag = 0 then exit; 8 MPI_Comm_size(MPI_COMM_WORLD, size); MPI_Comm_rank(MPI_COMM_WORLD, rank); end. Файл с таким же содержанием будет создан и при использовании среды Borland Delphi или PascalABC.NET; другим будет только расширение файла: dpr для среды Delphi, pas для среды PascalABC.NET. Если же используется язык C++ для среды Visual Studio, то будет создан и загружен в эту среду файл MPIBegin1.cpp, начинающийся со следующего текста (завершающая часть файла, не показанная здесь, является служебной и не требует редактирования): [C++] #include <windows.h> #pragma hdrstop #include "pt4.h" #include "mpi.h" void Solve() { Task("MPIBegin1"); int flag; MPI_Initialized(&flag); if (flag == 0) return; int rank, size; MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); } Программа-заготовка для заданий по параллельному программированию содержит дополнительные операторы, отсутствующие в заготовках для «непараллельных» заданий. Эти операторы должны использоваться практически в любой параллельной MPI-программе, поэтому, чтобы учащемуся не требовалось набирать их каждый раз заново, они автоматически добавляются к программе при ее создании. Обсудим операторы программы-заготовки подробнее. Первым оператором является оператор вызова процедуры Task, инициализирующей требуемое задание. Этот оператор имеется в программах-заготовках для всех заданий, в том числе и не связанных с параллельным программированием. Заметим, что процедура Task реализована в ядре задачника Programming Taskbook (динамической библиотеке) и доступна из программы учащегося благодаря подключенному к ней модулю PT4.pas (для Паскаля) или заголовочному файлу pt4.h (для C++). Помимо заголовочного файла pt4.h в рабочем каталоге учащегося должен находиться файл pt4.cpp, содержащий определения функций, объявленных в файле pt4.h. 9 Оставшиеся операторы связаны с библиотекой MPI. Задачник использует библиотеку MPI, входящую в систему MPICH — широко распространенную бесплатную программную реализацию стандарта MPI для различных операционных систем, в том числе и для Windows. Функции и константы библиотеки MPI доступны программе благодаря подключенному к ней модулю MPI.pas (для Паскаля) или заголовочному файлу mpi.h (для C++). Отметим, что реализация функций из файла mpi.h содержится в файле mpich.lib, который требуется явным образом подключить к любому проекту на языках С/C++, использующему библиотеку MPI. Однако в нашем случае это подключение уже выполнено в ходе создания проекта-заготовки, поэтому дополнительных действий, связанных с этим подключением, выполнять не требуется. Примечание. Для подключения к проекту дополнительного lib-файла в среде Visual Studio .NET надо вызвать окно свойств проекта (команда «Project | <имя проекта> Properties…»), перейти в этом окне в раздел «Configuration Properties | Linker | Input» и указать имя подключаемого файла в поле ввода «Additional Dependencies». Аналогичные действия надо проделать и в среде Visual C++ 6.0; в ней окно свойств вызывается командой «Project | Settings», а список библиотек, в который надо добавить имя подключаемого файла, отображается на вкладке «Link» в поле ввода «Object/library modules». Вызов функции MPI_Initialized позволяет определить, инициализирован для программы параллельный режим или нет. Если режим инициализирован, то выходной параметр функции принимает значение, отличное от нуля; в противном случае параметр полагается равным нулю. Следует отметить, что инициализация параллельного режима выполняется функцией MPI_Init, которая в приведенном коде отсутствует. Это объясняется тем, что за инициализацию отвечает сам задачник, и выполняется она перед тем, как программа переходит к выполнению кода учащегося. Однако такая инициализация выполняется задачником не всегда. Например, если программа запущена в демо-режиме (для этого достаточно при вызове процедуры Task дополнить имя задания символом «?»: Task("MPIBegin1?")), задачник не выполняет инициализацию параллельного режима, поскольку в нем нет необходимости. В этой ситуации вызов в коде учащегося функций MPI (отличных от MPI_Initialized) может привести к некорректной работе программы. Вызов функции MPI_Initialized и следующий за ним условный оператор позволяют «пропустить» при выполнении программы все операторы, введенные учащимся, если программа запущена не в параллельном режиме. Два последних оператора программы позволяют определить две характеристики, необходимые для нормальной работы любого процесса любой содержательной параллельной программы: общее количество процессов (функция MPI_Comm_size) и ранг текущего процесса (функция MPI_Comm_rank). Текущим считается процесс, вызвавший данную функцию. Требуемая характеристика возвращается во втором (выходном) параметре соответствующей функции; первым параметром является коммуникатор, задающий набор процессов. 10 Благодаря вызову этих функций мы можем сразу использовать в нашей программе значения size (общее число процессов в коммуникаторе MPI_COMM_WORLD) и rank (ранг текущего процесса в коммуникаторе MPI_COMM_WORLD; значение ранга обязательно лежит в диапазоне от 0 до size – 1). Обратите внимание на то, что в варианте для языка C++ выходные параметры являются указателями на соответствующие переменные, тогда как в варианте для Паскаля эти параметры являются самими переменными, передаваемыми по ссылке (так называемые var-параметры). Примечание. Любая функция MPI возвращает информацию об успешности своего выполнения. В частности, при успешном завершении функция возвращает значение MPI_SUCCESS. Однако, как правило, возвращаемые значения функций MPI не анализируются, а эти функции обычно вызываются как процедуры, поскольку информацию об ошибках, связанных с функциями MPI, можно получить более удобным способом — с помощью так называемого обработчика ошибок (error handler). При выполнении заданий по параллельному программированию с применением задачника PT for MPI используется специальный обработчик ошибок, который определен в задачнике и обеспечивает вывод информации об ошибках в особом разделе окна задачника. Функции MPI, связанные с обработкой ошибок, в настоящем пособии не рассматриваются (см. [3, гл. 8]; [5, гл. 11]). 1.2.4 Запуск программы в параллельном режиме Теперь выясним, каким образом данный проект можно запустить в параллельном режиме. В самом деле, при компиляции и запуске обычной программы из интегрированной среды она будет запущена в единственном экземпляре. В единственном экземпляре она будет запущена и в случае, если мы выйдем из интегрированной среды и запустим на выполнение откомпилированный exeфайл данной программы. Для запуска программы в параллельном режиме необходима «управляющая» программа, которая, во-первых, обеспечивает запуск нужного количества экземпляров исходной программы и, во-вторых, перехватывает сообщения, отправленные этими экземплярами (процессами) и пересылает их по назначению. Следует заметить, что экземпляры «настоящих» параллельных программ обычно запускаются на разных компьютерах, объединенных в сеть (кластер), или на суперкомпьютерах, снабженных большим числом процессоров. Именно в ситуации, когда каждый процесс выполняется на своем собственном процессоре, и обеспечивается максимальная эффективность параллельных программ. Разумеется, для проверки правильности наших учебных программ все их экземпляры достаточно запускать на одном локальном компьютере. Однако управляющая программа необходима и в этом случае. В качестве управляющей программы для параллельных программ задачник PT for MPI использует приложение MPIRun.exe, входящее в систему MPICH. Для запуска исполняемого файла в параллельном режиме достаточно запустить 11 программу MPIRun.exe, передав ей полное имя файла, требуемое количество процессов (т. е. запущенных экземпляров программы) и некоторые дополнительные параметры. Поскольку при тестировании программы такие запуски придется осуществлять многократно, удобно создать пакетный файл (bat-файл), содержащий вызов MPIRun.exe со всеми необходимыми параметрами. Однако и в этом случае процесс тестирования параллельной программы будет не слишком удобным: каждый раз после внесения необходимых исправлений в программу ее придется перекомпилировать, после чего, покинув интегрированную среду, запускать bat-файл. Проанализировав результаты работы программы, потребуется опять вернуться в интегрированную среду для внесения в нее очередных изменений, и т. д. Для того чтобы действия по запуску параллельной программы не отвлекали от решения задачи, задачник PT for MPI выполняет их самостоятельно. Напомним, что в настоящее время мы имеем проект-заготовку для выполнения задания MPIBegin1, который уже готов к запуску. Нажмем клавишу [F9] (в среде Lazarus, Delphi или PascalABC.NET) или [F5] (в среде Visual Studio); в результате будет выполнена компиляция программы и, в случае ее успешного завершения, программа будет запущена на выполнение. Поскольку мы не вносили в заготовку никаких изменений, компиляция должна завершиться успешно. При запуске программы на экране появится консольное окно, подобное приведенному на рисунке 2. Рис. 2. Консольное окно с информацией о запуске программы в параллельном режиме После трех строк информационного сообщения в этом окне отображается командная строка, которая обеспечивает запуск программы MPIBegin1.exe (или ptprg.exe в случае использования языка C++) в параллельном режиме под управлением MPIRun.exe. Число «3», указанное перед полным именем exeфайла, означает, что соответствующий процесс будет запущен в трех экземплярах. Параметр –nopopup_debug отключает вывод сообщений об ошибках в отдельном окне (поскольку эти сообщения в конечном итоге будут выведены в окне задачника), параметр –localonly обеспечивает запуск всех экземпляров процесса на локальном компьютере. 12 Сразу после появления консольного окна, если ранее параллельная программа с именем MPIBegin1.exe (или ptprg.exe) не запускалась, на экране может появиться еще одно окно (рис. 3). Рис. 3. Окно с запросом о блокировке запущенной параллельной программы Поскольку мы не собираемся связываться с другими компьютерами, в данном окне можно выбрать любой вариант: как «Блокировать», так и «Разблокировать». На выполнение локальных экземпляров программы это не окажет никакого влияния. Наконец, на экране появится окно задачника (рис. 4). Рис. 4. Ознакомительный запуск задания MPIBegin1 Внешне это окно ничем не отличается от окна, возникающего при выполнении обычной, «непараллельной» программы. Однако отличие имеется: в данном случае информация о том, что не была выполнена ни одна из операций ввода-вывода, относится ко всем процессам, запущенным в параллельном режиме. Для завершения работы программы надо, как обычно, закрыть окно задач- 13 ника (например, щелкнув мышью на кнопке «Выход (Esc)» или нажав клавишу [Esc]). После закрытия окна задачника немедленно закроется и консольное окно, и мы вернемся в интегрированную среду, из которой была запущена наша программа. Таким образом, откомпилировав и запустив программу из интегрированной среды, мы смогли сразу обеспечить ее выполнение в параллельном режиме. Это происходит благодаря достаточно сложному механизму, который реализован в ядре задачника Programming Taskbook. Для того чтобы успешно выполнять учебные задания, не требуется детального понимания этого механизма, поэтому дадим здесь лишь его краткое описание. На самом деле, программа, запущенная из интегрированной среды, не пытается решить задачу и выполняется в обычном, «непараллельном» режиме. Обнаружив, что задача относится к группе заданий по параллельному программированию, она лишь создает пакетный файл $pt_run$.bat, записывая в него три строки комментария и командную строку, обеспечивающую вызов программы MPIRun.exe с необходимыми параметрами, после чего запускает этот пакетный файл на выполнение и переходит в режим ожидания завершения работы пакетного файла. Запущенная с помощью пакетного файла программа MPIRun.exe запускает, в свою очередь, нужное количество экземпляров программы (процессов) в параллельном режиме, и эти процессы действительно пытаются решить задачу. В частности, задачник предлагает каждому процессу его набор исходных данных и ожидает от него набор результатов. Поскольку в нашем случае ни в одном процессе не была указана ни одна операция ввода-вывода, данный запуск параллельной программы был признан ознакомительным, о чем и было сообщено в информационном разделе окна задачника. Отметим, что данное окно отображается главным процессом параллельной программы, в то время как все подчиненные процессы (а также самый первый экземпляр программы, обеспечивший создание и запуск пакетного файла) работают в «невидимом» режиме. При закрытии окна задачника происходит завершение всех процессов параллельной программы, после этого завершается выполнение пакетного файла и, наконец, обнаружив, что пакетный файл успешно завершил работу, завершает работу и тот экземпляр нашей программы, который был запущен из интегрированной среды. Примечание. «Стартовый» экземпляр программы обеспечивает выполнение еще одного действия: он автоматически выгружает из памяти все процессы параллельной программы, если в результате неправильного программирования происходит их «зависание». Если при выполнении параллельной программы в течение 15–20 с окно задачника не появляется, следовательно, она зависла (иногда зависание программы проявляется в том, что после закрытия окна задачника не происходит немедленного закрытия консольного окна, т. е. завершения работы пакетного файла). В этой ситуации надо закрыть консольное окно, следуя приведенным в нем указаниям — нажав несколько раз комбинацию клавиш [Ctrl]+[C] или 14 [Ctrl]+[Break]. Если стартовый экземпляр программы обнаружит, что пакетный файл завершил свою работу, а в памяти остались зависшие процессы параллельной программы, то он автоматически выгрузит из памяти все эти процессы. Это действие является важным, так как, пока в памяти остаются процессы, связанные с исполняемым файлом, этот файл нельзя изменить (в частности, удалить или заменить на новый откомпилированный вариант). 1.2.5 Выполнение задания MPIBegin1 Перейдем к выполнению задания. Теперь, когда мы подробно познакомились с механизмом работы программы в параллельном режиме, решение этой простой задачи не будет представлять для нас особых проблем. Начнем с ввода исходных данных. По условию в каждом процессе дано по одному целому числу. Перейдем на пустую строку, расположенную ниже вызова функции MPI_Comm_rank. Если при выполнении программы будет достигнут данный участок кода, следовательно, программа была запущена как один из процессов параллельного приложения (в противном случае был бы выполнен оператор выхода, указанный в условном операторе). Значит, в этом месте программы можно ввести элемент исходных данных, предварительно описав его (здесь и далее в варианте для языка С++ будем приводить для краткости только функцию Solve): [C++] void Solve() { Task("MPIBegin1"); int flag; MPI_Initialized(&flag); if (flag == 0) return; int rank, size; MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); int n; pt >> n; } [Pascal] program MPIBegin1; uses PT4, MPI; var flag, size, rank: integer; n: integer; begin Task('MPIBegin1'); 15 MPI_Initialized(flag); if flag = 0 then exit; MPI_Comm_size(MPI_COMM_WORLD, size); MPI_Comm_rank(MPI_COMM_WORLD, rank); GetN(n); end. Для ввода исходных данных мы используем стандартные для задачника Programming Taskbook операции: процедуру GetN на Паскале и поток ввода pt на C++. Запустив полученную программу, мы увидим на экране окно задачника (см. рис. 5). Задачник обнаружил, что ввод данных выполнен, и, таким образом, программа приступила к решению задания. Однако ни один результирующий элемент данных не был выведен, поэтому в информационном разделе окна задачника появилось сообщение об ошибке «Выведены не все результирующие данные. Ошибка произошла в процессах 1–4». Главный процесс (процесс ранга 0) в сообщении не упоминается, так как в случае, если ошибки ввода-вывода обнаружены в одном или нескольких подчиненных процессах, задачник не анализирует состояние главного процесса. Рис. 5. Окно задачника с информацией об ошибках в подчиненных процессах Если ошибки обнаружены в подчиненных процессах, то в окне задачника отображается дополнительный раздел отладки, в котором для каждого подчи- 16 ненного процесса выводится более подробная информация об ошибке. Определить, с каким процессом связано то или иное сообщение, выведенное в разделе отладки, можно по номеру, указываемому в левой части строки (перед символом «|»). Все строки, связанные с определенным процессом, нумеруются независимо от остальных строк; их номера указываются после номера процесса и отделяются от текста сообщения символом «>». Для того чтобы отобразить в разделе отладки только сообщения, связанные с каким-либо одним процессом, достаточно щелкнуть мышью на ярлычке с номером (рангом) этого процесса или нажать соответствующую цифровую клавишу. Для отображения сводной информации по всем процессам надо выбрать ярлычок с символом «*» или ввести этот символ с клавиатуры (отметим, что перебирать ярлычки можно также с помощью клавиш со стрелками [ ] и [ ]). Если строка сообщения в разделе отладки начинается с символа «!», то это означает, что данное сообщение является сообщением об ошибке и добавлено в раздел отладки самим задачником. Программа учащегося может выводить в раздел отладки свои собственные сообщения; об этой возможности будет подробно рассказано далее. Итак, ни в одном процессе не выведены результирующие данные (в этом можно убедиться и по виду области результатов: кроме комментариев в ней ничего не указано). Добавим после оператора ввода соответствующий оператор вывода, в котором выведем удвоенное значение исходного значения n: [C++] pt << 2 * n; [Pascal] PutN(2 * n); Запуск исправленного варианта приведет к появлению окна с другим сообщением об ошибке (рис. 6). Рис. 6. Окно задачника с информацией об ошибке в главном процессе 17 Теперь во всех подчиненных процессах выведены требуемые результаты. Кроме того, удвоенное число выведено и в главном процессе. Однако в главном процессе требовалось также вывести количество процессов, входящих в коммуникатор, а это сделано не было. Поэтому в данном случае в информационном разделе указано, что ошибка произошла в главном процессе (процессе ранга 0). Количество процессов хранится в переменной size. Попытаемся вывести ее значение в конце нашей программы: [C++] pt << size; [Pascal] PutN(size); Окно задачника примет вид, приведенный на рисунке 7. Рис. 7. Окно задачника с информацией о попытке вывода лишних данных Можно убедиться в том, что все результирующие данные выведены. Однако решение по-прежнему считается ошибочным, поскольку теперь мы попытались вывести лишние данные (а именно значение size) в подчиненных процессах. Как обычно, при обнаружении ошибок в подчиненных процессах дополнительная информация об этих ошибках выводится в разделе отладки. Для того чтобы значение size было выведено только в главном процессе, необходимо перед выполнением этого действия убедиться, что ранг текущего 18 процесса равен 0. Добавив соответствующую проверку, мы получим, наконец, правильное решение: [C++] void Solve() { Task("MPIBegin1"); int flag; MPI_Initialized(&flag); if (flag == 0) return; int rank, size; MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); int n; pt >> n; pt << 2 * n; if (rank == 0) pt << size; } [Pascal] program MPIBegin1; uses PT4, MPI; var flag, size, rank: integer; n: integer; begin Task('MPIBegin1'); MPI_Initialized(flag); if flag = 0 then exit; MPI_Comm_size(MPI_COMM_WORLD, size); MPI_Comm_rank(MPI_COMM_WORLD, rank); GetN(n); PutN(2 * n); if rank = 0 then PutN(size); end. При запуске этого варианта решения в информационном разделе окна задачника будет выведен текст «Верное решение. Тест 1 (из 5)», а после пяти запусков — текст «Задание выполнено!» (рис. 8). Обратите внимание на то, что при каждом тестовом запуске может изменяться количество процессов параллельной программы. 19 Рис. 8. Успешное выполнение задания MPIBegin1 Примечание. В библиотеке MPI предусмотрена функция MPI_Finalize, завершающая параллельную часть программы (после вызова этой функции нельзя использовать остальные функции библиотеки MPI). Однако в той части программы, которая разрабатывается учащимся, вызывать эту функцию нельзя, так как после выполнения данной части задачник должен «собрать» все результаты, полученные в подчиненных процессах (чтобы проанализировать их и отобразить в окне главного процесса), а для этого программа должна находиться в параллельном режиме. Поэтому задачник берет на себя обязанность не только инициализировать параллельный режим (вызовом функции MPI_Init в начале выполнения программы), но и завершить его (вызовом функции MPI_Finalize в конце программы). 1.3 Проектное задание Выполните учебные задания группы MPIBegin, указанные в вашем варианте проектного задания (формулировки заданий приведены в приложении). Если вы не получили вариант проектного задания, то выполните задания из первого варианта. ВАРИАНТ 1 Процессы и их ранги: 3, 5 ВАРИАНТ 2 Процессы и их ранги: 3, 5 ВАРИАНТ 3 Процессы и их ранги: 4, 6 ВАРИАНТ 4 Процессы и их ранги: 4, 5 ВАРИАНТ 5 Процессы и их ранги: 4, 6 ВАРИАНТ 6 Процессы и их ранги: 3, 5 20 ВАРИАНТ 7 Процессы и их ранги: 3, 6 ВАРИАНТ 8 Процессы и их ранги: 4, 6 ВАРИАНТ 9 Процессы и их ранги: 3, 6 ВАРИАНТ 10 Процессы и их ранги: 4, 5 ВАРИАНТ 11 Процессы и их ранги: 3, 5 ВАРИАНТ 12 Процессы и их ранги: 4, 6 ВАРИАНТ 13 Процессы и их ранги: 3, 6 ВАРИАНТ 14 Процессы и их ранги: 3, 5 ВАРИАНТ 15 Процессы и их ранги: 4, 6 ВАРИАНТ 16 Процессы и их ранги: 4, 6 ВАРИАНТ 17 Процессы и их ранги: 3, 6 ВАРИАНТ 18 Процессы и их ранги: 4, 6 ВАРИАНТ 19 Процессы и их ранги: 4, 5 ВАРИАНТ 20 Процессы и их ранги: 3, 5 ВАРИАНТ 21 Процессы и их ранги: 3, 5 ВАРИАНТ 22 Процессы и их ранги: 4, 5 ВАРИАНТ 23 Процессы и их ранги: 3, 5 ВАРИАНТ 24 Процессы и их ранги: 4, 6 1.4 Тест рубежного контроля Тест содержит 6 заданий, на выполнение которых отводится 3 минуты. Выберите правильный, по вашему мнению, вариант ответа и отметьте его любым значком в бланке ответов. 1. Для какого из перечисленных языков не определен стандарт библиотеки MPI? (1) C (2) C++ (3) Fortran (4) Basic 2. Укажите имя стандартного коммуникатора, содержащего все процессы, запущенные при параллельном выполнении программы. 21 (1) MPI_COMM_ALL (2) MPI_COMM_TOTAL (3) MPI_COMM_WORLD (4) MPI_COMM_ALL_PROC 3. Какая функция MPI позволяет проверить, запущена ли программа в параллельном режиме? (1) MPI_Comm_size (2) MPI_Initialized (3) MPI_Comm_rank (4) MPI_Init 4. Как называется программа из комплекса MPICH, которая обеспечивает запуск всех экземпляров параллельной программы и управление ими? (1) MPIRun.exe (2) MPIExec.exe (3) MPICH.exe (4) MPIInit.exe 5. Укажите действие, которое не выполняет экземпляр программы с решением задачи, запущенный непосредственно из интегрированной среды. (1) Создает файл $pt_run$.bat (2) Запускает файл $pt_run$.bat (3) Выполняет операторы, необ(4) Выгружает из памяти все проходимые для решения задачи цессы, если произошло их зависание 6. Сколько экземпляров программы будет запущено в результате запуска программы из интегрированной среды, если при этом запуске задачник предложит для тестирования вариант исходных данных для 5 процессов? (1) Ровно 5 (2) Ровно 6 (3) 5 или более (4) 6 или более 2 Модуль № 2. Пересылка сообщений между двумя процессами 2.1 Комплексная цель Изучить механизмы пересылки сообщений между различными процессами параллельном программы и познакомиться с особенностями используемых для этого функций MPI. 2.2 Содержание модуля Рассмотрим задание, связанное с пересылкой сообщений между различными процессами параллельной программы. MPIBegin17. В каждом процессе дано вещественное число. Переслать число из главного процесса во все подчиненные процессы, а все числа из подчиненных процессов — в главный, и вывести в каждом процессе полученные числа (в главном процессе числа выводить в порядке возрастания рангов переславших их процессов). Создадим проект-заготовку для выполнения этого задания и запустим полученную программу. Появившееся на экране окно задачника будет иметь вид, 22 приведенный на рисунке 9. Для чтения исходных данных нам будет достаточно использовать единственную переменную вещественного типа, поскольку в каждом процессе дано только одно вещественное число. Исходные данные надо переслать в другие процессы параллельной программы. Для этого в библиотеке MPI имеется много различных функций, однако чаще всего для пересылки данных между двумя процессами используется пара функций MPI_Send и MPI_Recv. Первая из указанных функций вызывается передающим процессом и определяет, какому процессу и какие данные он собирается переслать, а вторая функция вызывается принимающим процессом; в ней указываются процесс-отправитель и переменная-буфер, в которую будут записаны полученные от него данные. Рис. 9. Ознакомительный запуск задания MPIBegin17 Вначале займемся приемом и пересылкой данных для подчиненных процессов, не реализуя пока действия, которые надо выполнить в главном процессе. Добавим в программу следующий фрагмент кода (в программе на Паскале надо дополнительно описать переменную a вещественного типа и переменную s типа MPI_Status): [C++] double a; MPI_Status s; if (rank > 0) { MPI_Recv(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &s); pt << a; pt >> a; MPI_Send(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); 23 } [Pascal] if rank > 0 then begin MPI_Recv(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, s); PutR(a); GetR(a); MPI_Send(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); end; Обратите внимание на то, что в качестве первого параметра как в случае С++, так и в случае Паскаля указывается адрес той переменной, которая содержит (или должна принять) пересылаемые данные. Мы намеренно расположили вызов MPI-функций в «неестественном» порядке (вначале чтение данных, полученных из процесса 0, а затем отправка этому процессу своих данных). Это позволит нам познакомиться с одной из распространенных ошибок, возникающих при выполнении параллельных программ. Запустим нашу программу. Через 5–10 с после появления консольного окна с информацией о том, что программа запущена в параллельном режиме, на экране появится окно задачника с сообщением об ошибке в подчиненных процессах (рис. 10). Рис. 10. Окно задачника с информацией о блокировке подчиненных процессов Сообщение об ошибке вида «MPI error. Процессы 1–4 не отвечают» означает, что главный процесс нашей параллельной программы не смог в течение определенного времени (5–10 с) «связаться» с подчиненными процессами с целью получить от них информацию о введенных и выведенных в них данных. Причем, как следует из второй строки сообщения, ошибка возникла при попыт- 24 ке связаться со всеми подчиненными процессами (которых при данном запуске программы было четыре). Причина ошибки — в функции MPI_Recv. Вызов этой функции приведет к тому, что процесс-получатель перейдет в режим ожидания данных от процессаотправителя (в нашем случае — процесса ранга 0), и не продолжит выполнение программы до тех пор, пока данные не будут им получены (так работает блокирующий режим получения сообщения, который реализует функция MPI_Recv). А поскольку в нашей программе не предусмотрено (пока) посылки сообщения от процесса ранга 0, ожидание в каждом из подчиненных процессов будет длиться вечно (точнее, пока их выполнение не будет прекращено «насильственным образом»). Это пример зависания параллельной программы, возникающего обычно из-за того, что один или несколько процессов блокируются в ожидании данных, которые им не посланы. Заметим, что при закрытии окна задачника консольное окно останется на экране. В самом деле, консольное окно управляется программой MPIRun, которая завершает работу только при завершении всех процессов запущенной параллельной программы, а в данном случае завершился только главный процесс (подчиненные процессы остаются заблокированными). Для завершения программы MPIRun и закрытия консольного окна необходимо нажать несколько раз комбинацию клавиш [Ctrl]+[C] или [Ctrl]+[Break]. Примечание. При «аварийном» завершении программы MPIRun в памяти могут остаться запущенные (и зависшие) процессы параллельной программы. Это в дальнейшем будет препятствовать перекомпиляции нашей программы, поскольку, пока процесс находится в памяти, связанный в ним exe-файл недоступен для изменения. Однако при выполнении заданий с использованием задачника PT for MPI такой проблемы не возникает: вспомним о том, что программу MPIRun запустила наша программа (которая сама была запущена из интегрированной среды). Этот «непараллельный» экземпляр нашей программы остается в памяти, пока программа MPIRun не завершит работу, после чего он выгружает из памяти все зависшие процессы. Если бы эта полезная работа не выполнялась задачником, то выгружать каждый из зависших процессов пришлось бы вручную, используя диспетчер задач Windows. Итак, мы познакомились с ситуацией, когда один или несколько подчиненных процессов оказываются заблокированными. Такая же «неприятность» может произойти и с главным процессом. Дополним нашу программу фрагментом, связанным с главным процессом, причем в этом фрагменте также организуем вызов MPI-функций в неестественном порядке — вначале прием, затем отправка данных (в программе на Паскале надо дополнительно описать переменную i целого типа и удалить символ «;» в конце предыдущего фрагмента): [C++] else { for (int i = 1; i < size; ++i) 25 { MPI_Recv(&a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD, &s); pt << a; } pt >> a; for (int i = 1; i < size; ++i) MPI_Send(&a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD); } [Pascal] else begin for i := 1 to size – 1 do begin MPI_Recv(@a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD, s); PutR(a); end; GetR(a); for i := 1 to size – 1 do MPI_Send(@a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD); end; Если запустить эту программу, то после появления консольного окна можно ожидать сколько угодно, однако окно задачника на экране не появится. Это связано с тем, что заблокированным оказался главный процесс нашей параллельной программы: перед отображением окна задачника главный процесс должен выполнить тот фрагмент программы, который разработан для него учащимся, а в нашем случае этот фрагмент привел к блокировке. Поэтому главный процесс просто не дошел до того места программы, в котором выполняется вывод окна задачника на экран. Если в течение 15–20 с окно задачника не появилось, то можно считать, что произошло зависание главного процесса. В такой ситуации, как и в ситуации, описанной ранее, необходимо явным образом прервать выполнение параллельной программы, нажав несколько раз [Ctrl]+[C] или [Ctrl]+[Break]. Примечание. Любой из описанных выше «аварийных» способов завершения программы фиксируется задачником в файле результатов. Однако в случае если произошло зависание только подчиненных процессов (и на экране появилось окно задачника), в файл результатов будет записан текст «MPI error», тогда как в случае зависания главного процесса текст будет другим: «Выполнение задания прервано». Для исправления нашей программы достаточно хотя бы в одном из приведенных выше двух фрагментов изменить порядок вызова процедур MPI_Send и MPI_Recv. Например, это можно сделать в первом фрагменте: [C++] if (rank > 0) 26 { pt >> a; MPI_Send(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); MPI_Recv(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &s); pt << a; } [Pascal] if rank > 0 then begin GetR(a); MPI_Send(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); MPI_Recv(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, s); PutR(a); end При запуске программы будет выведено сообщение о верном решении, а после пяти запусков — о том, что задание выполнено (рис. 11). Рис. 11. Успешное выполнение задания MPIBegin17 Полученную программу можно упростить, если в разделе else воспользоваться вспомогательной вещественной переменной b для получения данных из подчиненных процессов. Это позволит разместить оператор ввода (pt >> a для C++, GetR(a) для Паскаля) перед последним условным оператором, а также даст возможность выполнить все действия в разделе else в единственном цикле. Приведем соответствующий вариант решения: [C++] void Solve() { Task("MPIBegin17"); int flag; 27 MPI_Initialized(&flag); if (flag == 0) return; int rank, size; MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); double a; MPI_Status s; pt >> a; if (rank > 0) { MPI_Send(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); MPI_Recv(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &s); pt << a; } else for (int i = 1; i < size; ++i) { double b; MPI_Recv(&b, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD, &s); pt << b; MPI_Send(&a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD); } } [Pascal] program MPIBegin17; uses PT4, MPI; var flag, size, rank: integer; a, b: real; i: integer; s: MPI_Status; begin Task('MPIBegin17'); MPI_Initialized(flag); if flag = 0 then exit; MPI_Comm_size(MPI_COMM_WORLD, MPI_Comm_rank(MPI_COMM_WORLD, GetR(a); if rank > 0 then begin MPI_Send(@a, 1, MPI_DOUBLE, MPI_Recv(@a, 1, MPI_DOUBLE, size); rank); 0, 0, MPI_COMM_WORLD); 0, 0, MPI_COMM_WORLD, s); 28 PutR(a); end else for i := 1 to size – 1 do begin MPI_Recv(@b, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD, s); PutR(b); MPI_Send(@a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD); end; end. Заметим, что более эффективное решение этого задания можно получить, используя коллективные операции пересылки данных (см. модуль № 4). 2.3 Проектное задание Выполните учебные задания группы MPIBegin, указанные в вашем варианте проектного задания (формулировки заданий приведены в приложении). Если вы не получили вариант проектного задания, то выполните задания из первого варианта. ВАРИАНТ 1 Обмен сообщениями между отдельными процессами: 7, 12, 15, 19, 22, 24, 26, 29 ВАРИАНТ 2 Обмен сообщениями между отдельными процессами: 9, 13, 15, 20, 21, 25, 26, 28 ВАРИАНТ 3 Обмен сообщениями между отдельными процессами: 9, 11, 15, 18, 23, 24, 27, 29 ВАРИАНТ 4 Обмен сообщениями между отдельными процессами: 7, 12, 14, 20, 21, 24, 27, 29 ВАРИАНТ 5 Обмен сообщениями между отдельными процессами: 8, 11, 14, 18, 21, 25, 27, 28 ВАРИАНТ 6 Обмен сообщениями между отдельными процессами: 10, 13, 16, 18, 23, 24, 27, 29 ВАРИАНТ 7 Обмен сообщениями между отдельными процессами: 10, 12, 14, 20, 22, 25, 26, 28 ВАРИАНТ 8 Обмен сообщениями между отдельными процессами: 7, 13, 16, 20, 23, 25, 26, 28 ВАРИАНТ 9 Обмен сообщениями между ВАРИАНТ 10 Обмен сообщениями между 29 отдельными процессами: 8, 12, 15, 19, 22, 25, 27, 28 отдельными процессами: 8, 13, 16, 19, 23, 24, 27, 28 ВАРИАНТ 11 Обмен сообщениями между отдельными процессами: 10, 11, 16, 18, 21, 24, 26, 29 ВАРИАНТ 12 Обмен сообщениями между отдельными процессами: 9, 11, 14, 19, 22, 25, 26, 29 ВАРИАНТ 13 Обмен сообщениями между отдельными процессами: 9, 13, 15, 20, 22, 25, 26, 29 ВАРИАНТ 14 Обмен сообщениями между отдельными процессами: 10, 12, 14, 20, 23, 24, 27, 28 ВАРИАНТ 15 Обмен сообщениями между отдельными процессами: 7, 11, 16, 18, 23, 24, 27, 28 ВАРИАНТ 16 Обмен сообщениями между отдельными процессами: 8, 12, 14, 19, 21, 25, 27, 29 ВАРИАНТ 17 Обмен сообщениями между отдельными процессами: 8, 12, 16, 19, 22, 24, 26, 29 ВАРИАНТ 18 Обмен сообщениями между отдельными процессами: 10, 13, 16, 19, 21, 25, 27, 28 ВАРИАНТ 19 Обмен сообщениями между отдельными процессами: 7, 11, 15, 20, 21, 25, 26, 29 ВАРИАНТ 20 Обмен сообщениями между отдельными процессами: 8, 12, 14, 18, 22, 25, 26, 29 ВАРИАНТ 21 Обмен сообщениями между отдельными процессами: 7, 13, 15, 18, 23, 24, 26, 28 ВАРИАНТ 22 Обмен сообщениями между отдельными процессами: 9, 13, 15, 19, 23, 24, 26, 28 ВАРИАНТ 23 Обмен сообщениями между отдельными процессами: 9, 11, 14, 18, 21, 24, 27, 28 ВАРИАНТ 24 Обмен сообщениями между отдельными процессами: 10, 11, 16, 20, 22, 25, 27, 29 30 2.4 Тест рубежного контроля Тест содержит 6 заданий, на выполнение которых отводится 3 минуты. Выберите правильный, по вашему мнению, вариант ответа и отметьте его любым значком в бланке ответов. 1. В каком случае в файл результатов будет записан текст «MPI error»? (1) При зависании хотя бы одного (2) При зависании главного проподчиненного процесса цесса (3) При ошибочном вводе-выводе (4) При неверном решении задачи 2. В каком случае в файл результатов будет записан текст «Выполнение задания прервано»? (1) При зависании хотя бы одного (2) При зависании главного проподчиненного процесса цесса (3) При ошибочном вводе-выводе (4) При неверном решении задачи 3. Какое количество параметров имеет функция MPI_Send? (1) 5 (2) 6 (3) 7 (4) 8 4. Какое количество параметров имеет функция MPI_Recv? (1) 5 (2) 6 (3) 7 (4) 8 5. Какой символ в языке Паскаль используется для получения адреса переменной? (1) # (2) & (3) @ (4) $ 6. Какой символ в языке C++ используется для получения адреса переменной? (1) # (2) & (3) @ (4) $ 3 Модуль № 3. Операции редукции и составные типы данных 3.1 Комплексная цель Познакомиться с коллективными операциями редукции и особенностями использования в MPI-программах составных типов данных. 3.2 Содержание модуля Рассмотрим следующее задание, связанное с коллективными операциями редукции. 31 MPIBegin52. В каждом процессе дан набор из K + 5 чисел, где K — количество процессов. Используя функцию MPI_Allreduce для операции MPI_MINLOC, найти минимальное значение среди элементов данных наборов с одним и тем же порядковым номером и ранг процесса, содержащего минимальное значение. Вывести в главном процессе минимумы, а в остальных процессах — ранги процессов, содержащих эти минимумы. Большая группа функций MPI предназначена для организации коллективного взаимодействия процессов. «Коллективные» MPI-функции, в отличие от ранее рассмотренных функций MPI_Send и MPI_Recv, позволяют организовать обмен сообщениями не между двумя отдельными процессами (отправителем и получателем), а между всеми процессами, входящими в некоторый коммуникатор. В частности, при использовании коммуникатора MPI_COMM_WORLD можно организовать коллективный обмен сообщениями между всеми запущенными процессами параллельной программы. Среди коллективных MPI-функций выделяют группу функций, обеспечивающих выполнение коллективных операций редукции, т. е. операций, связанных с пересылкой не исходных данных, а результатов их обработки некоторой групповой операцией: нахождением суммы MPI_SUM, произведения MPI_PROD, максимального MPI_MAX или минимального MPI_MIN значения и т. д. — всего в стандарте MPI предусмотрено 12 операций редукции, кроме того, программист может определять и свои собственные операции. Среди операций редукции особое место занимают операции MPI_MAXLOC и MPI_MINLOC, позволяющие найти не только максимальный или минимальный элемент среди элементов, предоставленных каждым процессом, но и его номер (в качестве номера обычно используется ранг процесса, предоставившего этот экстремальный элемент). При запуске программы-заготовки, созданной для выполнения задания MPIBegin52, мы увидим на экране окно задачника, подобное приведенному на рисунке 12. Рис. 12. Ознакомительный запуск задания MPIBegin52 32 Коллективная операция редукции может применяться одновременно к нескольким наборам данных. Если каждый процесс предоставляет массив чисел (одного и того же размера), то операция редукции применяется по отдельности к элементам предоставленных массивов с одним и тем же индексом; в результате будет получен массив того же размера, каждый элемент которого будет являться результатом применения операции редукции к элементам исходных массивов с этим же индексом. В зависимости от используемой MPI-функции полученный массив может быть переслан какому-либо конкретному процессу (функция MPI_Reduce) или всем процессам данного коммуникатора (функция MPI_Allreduce). При использовании операций MPI_MAXLOC и MPI_MINLOC исходные наборы данных должны содержать пары чисел: собственно число, которое надо обработать, и его номер. Поэтому в программе необходимо определить вспомогательный тип данных (запись в Паскале, структуру в C++) для хранения таких пар. В нашем случае должны обрабатываться вещественные числа, поэтому первый элемент пары будет вещественным, а второй — целым: [C++] struct MINLOC_Data { double a; int n; }; [Pascal] type MINLOC_Data = record a: real; n: integer; end; Для хранения исходных данных в каждом процессе должен быть выделен массив элементов типа MINLOC_Data, и такой же массив должен использоваться для хранения результатов выполнения операции редукции. Размер набора данных, который придется хранить в этих массивах, заранее неизвестен, так как он связан с количеством процессов параллельной программы. Поэтому можно либо выделять память для массивов динамически (после того как программе станет известно число процессов size), либо использовать статические массивы, размер которых окажется достаточным для любых наборов исходных данных. При выполнении задания MPIBegin52 мы будем использовать статические массивы (особенности, связанные с использованием динамических массивов, будут рассмотрены в следующем модуле). Запустив созданную программузаготовку несколько раз, мы можем убедиться в том, что для данного задания количество процессов может меняться в диапазоне от 3 до 5. Таким образом, учитывая, что размер наборов исходных данных равен K + 5, где K — количество процессов, нам достаточно описать массивы размера 10: 33 [C++] MINLOC_Data d[10], res[10]; [Pascal] var d, res: array[0..9] of MINLOC_Data; Для большего единообразия программ мы будем использовать в Паскале индексацию массивов от 0. Заметим, что именно такая индексация используется в языках Delphi Pascal, Free Pascal и PascalABC.NET для динамических массивов, которые в дальнейшем мы также будем применять в наших программах. Инициализация исходного массива d должна выполняться в каждом процессе параллельной программы (в программе на Паскале необходимо дополнительно описать переменную i целого типа): [C++] for (int i = 0; i < size + 5; ++i) { pt >> d[i].a; d[i].n = rank; } [Pascal] for i := 0 to size + 4 do begin GetR(d[i].a); d[i].n := rank; end; Рис. 13. Окно задачника с информацией об ошибках ввода-вывода 34 Выполнив запуск этого варианта программы, мы получим сообщение об ошибке «Выведены не все результирующие данные» (рис. 13). Действительно, выполнив ввод всех исходных данных, мы не вывели результаты ни в одном процессе. Напомним, что при обнаружении ошибок в подчиненных процессах задачник не анализирует состояние главного процесса, поэтому в списке ошибочных процессов отсутствует процесс 0. Перед выводом результатов необходимо выполнить соответствующую коллективную операцию редукции. Она должна быть выполнена во всех процессах, после чего в главном процессе (ранга 0) надо вывести поле a каждого элемента результирующего массива res (т. е. минимальное значение, выбранное из всех элементов исходных массивов с данным индексом), а в остальных (подчиненных) процессах — поле n (т. е. ранг процесса с этим минимальным значением): [C++] MPI_Allreduce(d, res, size + 5, MPI_DOUBLE_INT, MPI_MINLOC, MPI_COMM_WORLD); for (int i = 0; i < size + 5; ++i) if (rank == 0) pt << res[i].a; else pt << res[i].n; [Pascal] MPI_Allreduce(@d[0], @res[0], size + 5, MPI_DOUBLE_INT, MPI_MINLOC, MPI_COMM_WORLD); for i := 0 to size + 4 do if rank = 0 then PutR(res[i].a) else PutN(res[i].n); Обратите внимание на два важных момента. Во-первых, массивы исходных и результирующих данных передаются в MPI-функции как указатели на их начальный элемент, поэтому в варианте для Паскаля используется операция @ взятия адреса элемента массива с индексом 0 (заметим, что в средах Delphi и Lazarus можно использовать более простые выражения: @d и @res). Во-вторых, имя типа, указываемое в качестве четвертого параметра, должно соответствовать типу элементов обрабатываемых массивов (в данном случае надо указать имя MPI_DOUBLE_INT, соответствующее структуре из двух полей — вещественного и целого). При запуске полученной программы в среде C++ будет выведено сообщение о верном решении, а после пяти запусков — сообщение о том, что задание выполнено. Однако при запуске варианта программы для языка Паскаль в среде Lazarus будет выведено сообщение об ошибочном решении (рис. 14). 35 Рис. 14. Окно задачника с информацией об ошибочном решении Если проанализировать полученные результирующие данные, то можно заметить, что минимальное значение и его номер были правильно определены для набора из первых элементов исходных массивов, тогда как для других элементов были получены либо неверные результаты, либо нулевые значения, не имеющие отношения к исходным данным. Причина ошибочного поведения связана с тем, что при стандартных настройках компилятора Free Pascal, используемого в среде Lazarus, не выполняется выравнивание составных данных, в то время как в функциях библиотеки MPI предполагается, что выравнивание производится. Чтобы убедиться в этом, достаточно вызвать функцию MPI_Type_extent, которая позволяет определить размер памяти (в байтах), используемый для любого типа библиотеки MPI. Размер возвращается во втором параметре этой функции, имеющем специальный тип библиотеки MPI — MPI_Aint. Для вывода полученного значения воспользуемся новой возможностью задачника Programming Taskbook, появившейся в его версии 4.9, — функцией Show, которая позволяет выводить информацию в раздел отладки окна задачника. Опишем в программе переменную k типа MPI_Aint и добавим в конец программы следующий фрагмент: [Pascal] MPI_Type_extent(MPI_DOUBLE_INT, k); Show('k = ', k); При запуске этого варианта программы в окне задачника появится раздел отладки, и в нем будет выведено значение переменной k с предваряющим комментарием (рис. 15). Обратите внимание на то, что значение k выводится несколько раз, так как процедура Show выполняется в каждом процессе параллельного приложения. 36 Рис. 15. Окно задачника с отладочной информацией Таким образом, в библиотеке MPI предполагается, что для типа MPI_DOUBLE_INT отводится 16 байт, т. е. для полей этой записи выполняется выравнивание по ширине 8 байт. Если же указанное выравнивание не проводить, то запись, состоящая из одного вещественного числа двойной точности (8 байт) и одного целого числа (4 байта), будет занимать только 12 байт, и в результате все элементы массива таких записей, начиная со второго элемента, будут располагаться в памяти не в тех позициях, которые предполагает функция MPI_Allreduce. Этим и объясняется тот факт, что первые элементы исходных массивов были обработаны правильно, а остальные нет. В случае использования среды Visual Studio для языка C++, а также сред Delphi и PascalABC.NET для языка Паскаль, ошибки не возникает, так как компиляторы этих сред по умолчанию выполняют выравнивание данных по ширине 8 байт. После того как причина ошибки выяснена, исправить ее для среды Lazarus не представляет труда. Достаточно добавить в запись MINLOC_Data еще одно, «фиктивное» поле, имеющее размер 4 байта и дополняющее, таким образом, размер записи до требуемых 16 байт: [Pascal] type MINLOC_Data = record a: real; n, not_used: integer; 37 end; Заметим, что вместо добавления фиктивного поля можно было бы установить для записи MINLOC_Data режим выравнивания по ширине 8 байт, указав перед ее описанием директиву компилятора Free Pascal {$PackRecords 8}. В заключение приведем полный текст решения задания MPIBegin52 на языках C++ и Паскаль: [C++] struct MINLOC_Data { double a; int n; }; void Solve() { Task("MPIBegin52"); int flag; MPI_Initialized(&flag); if (flag == 0) return; int rank, size; MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MINLOC_Data d[10], res[10]; for (int i = 0; i < size + 5; ++i) { pt >> d[i].a; d[i].n = rank; } MPI_Allreduce(d, res, size + 5, MPI_DOUBLE_INT, MPI_MINLOC, MPI_COMM_WORLD); for (int i = 0; i < size + 5; ++i) if (rank == 0) pt << res[i].a; else pt << res[i].n; } [Pascal] program MPIBegin52; uses PT4, MPI; type MINLOC_Data = record a: real; 38 n, not_used: integer; end; var flag, size, rank: integer; d, res: array[0..9] of MINLOC_Data; i: integer; begin Task('MPIBegin52'); MPI_Initialized(flag); if flag = 0 then exit; MPI_Comm_size(MPI_COMM_WORLD, size); MPI_Comm_rank(MPI_COMM_WORLD, rank); for i := 0 to size + 4 do begin GetR(d[i].a); d[i].n := rank; end; MPI_Allreduce(@d[0], @res[0], size + 5, MPI_DOUBLE_INT, MPI_MINLOC, MPI_COMM_WORLD); for i := 0 to size + 4 do if rank = 0 then PutR(res[i].a) else PutN(res[i].n); end. 3.3 Проектное задание Выполните учебные задания группы MPIBegin, указанные в вашем варианте проектного задания (формулировки заданий приведены в приложении). Если вы не получили вариант проектного задания, то выполните задания из первого варианта. ВАРИАНТ 1 Коллективные операции редукции: 48, 50, 54, 56 Производные типы и упаковка данных: 58, 62, 65, 68, 70 ВАРИАНТ 2 Коллективные операции редукции: 49, 51, 54, 57 Производные типы и упаковка данных: 58, 63, 65, 66, 70 ВАРИАНТ 3 Коллективные операции редукции: 48, 50, 55, 57 Производные типы и упаковка ВАРИАНТ 4 Коллективные операции редукции: 49, 50, 54, 56 Производные типы и упаковка 39 данных: 59, 63, 65, 67, 69 данных: 60, 62, 65, 67, 69 ВАРИАНТ 5 Коллективные операции редукции: 48, 51, 53, 57 Производные типы и упаковка данных: 60, 61, 64, 66, 70 ВАРИАНТ 6 Коллективные операции редукции: 49, 50, 55, 57 Производные типы и упаковка данных: 58, 63, 65, 66, 70 ВАРИАНТ 7 Коллективные операции редукции: 49, 50, 55, 57 Производные типы и упаковка данных: 60, 61, 65, 66, 69 ВАРИАНТ 8 Коллективные операции редукции: 48, 50, 55, 56 Производные типы и упаковка данных: 59, 61, 64, 68, 70 ВАРИАНТ 9 Коллективные операции редукции: 49, 51, 53, 56 Производные типы и упаковка данных: 58, 62, 64, 68, 70 ВАРИАНТ 10 Коллективные операции редукции: 49, 51, 53, 56 Производные типы и упаковка данных: 59, 61, 65, 67, 70 ВАРИАНТ 11 Коллективные операции редукции: 48, 51, 54, 57 Производные типы и упаковка данных: 59, 62, 64, 67, 69 ВАРИАНТ 12 Коллективные операции редукции: 48, 51, 53, 56 Производные типы и упаковка данных: 60, 63, 65, 68, 70 ВАРИАНТ 13 Коллективные операции редукции: 48, 50, 54, 56 Производные типы и упаковка данных: 59, 61, 65, 67, 70 ВАРИАНТ 14 Коллективные операции редукции: 49, 51, 55, 56 Производные типы и упаковка данных: 60, 61, 64, 67, 69 ВАРИАНТ 15 Коллективные операции редукции: 48, 51, 55, 56 Производные типы и упаковка данных: 58, 62, 65, 67, 69 ВАРИАНТ 16 Коллективные операции редукции: 49, 51, 53, 57 Производные типы и упаковка данных: 60, 61, 64, 67, 70 ВАРИАНТ 17 ВАРИАНТ 18 40 Коллективные операции редукции: 48, 50, 53, 56 Производные типы и упаковка данных: 59, 62, 65, 66, 70 Коллективные операции редукции: 48, 50, 54, 57 Производные типы и упаковка данных: 58, 62, 65, 66, 70 ВАРИАНТ 19 Коллективные операции редукции: 49, 50, 54, 57 Производные типы и упаковка данных: 60, 63, 65, 66, 70 ВАРИАНТ 20 Коллективные операции редукции: 49, 50, 55, 56 Производные типы и упаковка данных: 59, 63, 65, 68, 70 ВАРИАНТ 21 Коллективные операции редукции: 48, 50, 54, 57 Производные типы и упаковка данных: 58, 62, 65, 66, 69 ВАРИАНТ 22 Коллективные операции редукции: 48, 51, 55, 57 Производные типы и упаковка данных: 58, 63, 65, 68, 69 ВАРИАНТ 23 Коллективные операции редукции: 49, 51, 53, 56 Производные типы и упаковка данных: 59, 61, 64, 68, 70 ВАРИАНТ 24 Коллективные операции редукции: 49, 51, 53, 57 Производные типы и упаковка данных: 60, 63, 64, 68, 70 3.4 Тест рубежного контроля Тест содержит 6 заданий, на выполнение которых отводится 3 минуты. Выберите правильный, по вашему мнению, вариант ответа и отметьте его любым значком в бланке ответов. 1. Как обозначается операция, позволяющая определить не только максимальный элемент среди элементов, предоставленных каждым процессом, но и его номер? (1) MPI_MAX (2) MPI_MAXNUM (3) MPI_MAXIND (4) MPI_MAXLOC 2. Сколько различных операций редукции предусмотрено в стандарте MPI? (1) 8 (2) 10 (3) 12 (4) 16 3. Какое количество параметров имеет функция MPI_Allreduce? (1) 5 (2) 6 (3) 7 (4) 8 41 4. Какой результат вернет функция MPI_Type_extent для типа MPI_DOUBLE_INT? (1) 8 (2) 12 (3) 16 (4) 32 5. Для какой из перечисленных сред не выполняется автоматическое выравнивание полей в сложных структурах данных? (1) Visual Studio для C++ (2) Delphi (3) Lazarus (4) PascalABC.NET 6. Укажите директиву, обеспечивающую режим выравнивания полей в сложных структурах данных для той среды, в которой выравнивание по умолчанию не производится (см. предыдущий тест). (1) AlignRec (2) AlignRecords (3) PackRec (4) PackRecords 4 Модуль № 4. Коллективные операции и создание новых коммуникаторов 4.1 Комплексная цель Познакомиться с коллективными операциями и возможностями, связанными с созданием вспомогательных коммуникаторов. 4.2 Содержание модуля Часто для эффективной реализации пересылки данных бывает удобно воспользоваться вспомогательными коммуникаторами, которые включают не все процессы параллельного приложения, а только требуемую их часть. Задания на применение вспомогательных коммуникаторов собраны в двух последних подгруппах группы MPIBegin: «Группы процессов и коммуникаторы» (п. 6.6) и «Виртуальные топологии» (п. 6.7). Рассмотрим одно из заданий, входящих в первую из этих подгрупп (задания, связанные с виртуальными топологиями, рассматриваются в следующем модуле). MPIBegin73. В каждом процессе, ранг которого делится на 3 (включая главный процесс), даны три целых числа. С помощью функции MPI_Comm_split создать новый коммуникатор, включающий процессы, ранг которых делится на 3. Используя одну коллективную операцию пересылки данных для созданного коммуникатора, переслать исходные числа в главный процесс и вывести эти числа в порядке возрастания рангов переславших их процессов (включая числа, полученные из главного процесса). Приведем окно задачника, появившееся на экране при ознакомительном запуске программы-заготовки для этого задания (рис. 16). 42 Рис. 16. Ознакомительный запуск задания MPIBegin73 При этом в консольном окне был выведен текст, показывающий, что в параллельной программе было запущено десять процессов: c:\PT4Work>"C:\Program Files\MPICH\mpd\bin\MPIRun.exe" -nopopup_debug –localonly 10 "c:\PT4Work\Debug\ptprj.exe" Таким образом, в задании надо использовать лишь часть имеющихся процессов. Разумеется, мы можем воспользоваться функциями MPI, обеспечивающими обмен данными между двумя процессами (как в решении задачи MPIBegin17, приведенном в модуле № 2), но более эффективным будет вариант, использующий подходящую коллективную операцию пересылки данных (заметим, что именно такой вариант решения указан в формулировке задания). Однако коллективные операции выполняются для всех процессов, входящих в некоторый коммуникатор, поэтому в программе необходимо предварительно создать коммуникатор, включающий только процессы, ранг которых делится на 3. Сделать это можно несколькими способами; один из них связан с использованием функции MPI_Comm_split, упомянутой в формулировке задания. Функция MPI_Comm_split позволяет «расщепить» исходный коммуникатор на набор коммуникаторов, каждый из которых связан с некоторой частью процессов, входящих в исходный коммуникатор. Следует учитывать, что массив новых коммуникаторов в программе не возникает; вместо этого каждому из процессов программы функция MPI_Comm_split предоставляет именно тот коммуникатор из созданного набора, в который входит данный процесс. Предусмотрена также ситуация, когда некоторые процессы не будут включены ни в один из созданных коммуникаторов; для таких процессов функция MPI_Comm_split возвращает «пустой» коммуникатор MPI_COMM_NULL. Для разбиения процессов на новые группы в функции MPI_Comm_split используется параметр color («цвет»). Все процессы одного цвета включаются в один и тот же новый коммуникатор; при этом любой цвет представляет собой обычное целое число. Предусмотрен также «неопределенный цвет» 43 MPI_UNDEFINED; его надо указывать для процесса, который не следует включать ни в один из новых коммуникаторов. Второй характеристикой, используемой в функции MPI_Comm_split при создании нового набора коммуникаторов, является параметр key («ключ»). Он определяет порядок, в котором будут располагаться процессы в каждом из новых коммуникаторов: процессы в каждом коммуникаторе упорядочиваются по возрастанию их ключей (если некоторые процессы имеют одинаковые ключи, то их порядок определяется средой MPI, которая управляет параллельной программой). Для сохранения в каждом из вновь созданных коммуникаторов исходного порядка следования процессов достаточно в качестве параметра key для каждого процесса указать ранг этого процесса в исходном коммуникаторе. Учитывая эти особенности функции MPI_Comm_split, создадим с ее помощью коммуникатор, в который будут входить только процессы ранга, кратного трем (в программе на Паскале надо дополнительно описать переменную comm типа MPI_Comm и переменную color целого типа): [C++] MPI_Comm comm; int color = rank % 3 == 0 ? 0 : MPI_UNDEFINED; MPI_Comm_split(MPI_COMM_WORLD, color, rank, &comm); if (comm == MPI_COMM_NULL) return; [Pascal] if rank mod 3 = 0 then color := 0 else color := MPI_UNDEFINED; MPI_Comm_split(MPI_COMM_WORLD, color, rank, comm); if comm = MPI_COMM_NULL then exit; Последний условный оператор обеспечивает немедленный выход из процесса, если с ним оказался связан «нулевой» коммуникатор comm (разумеется, в нашем случае в условии выхода можно было бы анализировать остаток от деления rank на 3, однако использованный вариант является более универсальным). Во всех остальных процессах осталось ввести три целых числа, переслать все введенные числа в главный процесс, используя коллективную функцию MPI_Gather, и вывести полученные числа. Для ввода исходных чисел в каждом процессе достаточно описать массив data из трех элементов. Размер результирующего массива res, который будет получен в главном процессе, зависит от количества процессов параллельного приложения. При выполнении задания MPIBegin52 мы уже отмечали, что в такой ситуации можно использовать либо статический массив достаточно большого размера, либо динамический массив, размер которого будет определен после того, как станет известно количество процессов. В модуле № 3, выполняя задание MPIBegin52, мы использовали ста- 44 тический массив. Теперь в качестве результирующего массива res будем использовать динамический массив. Опишем в программе на Паскале следующие переменные (для динамического массива res диапазон индексов не указывается): [Pascal] data: array[0..2] of integer; res: array of integer; i: integer; Дополним программу новым фрагментом кода: [C++] int data[3]; for (int i = 0; i < 3 ; ++i) pt >> data[i]; MPI_Comm_size(comm, &size); int* res = new int[3 * size]; MPI_Gather(data, 3, MPI_INT, res, 3, MPI_INT, 0, comm); if (rank == 0) for (int i = 0; i < 3 * size; ++i) pt << res[i]; delete[] res; [Pascal] for i := 0 to 2 do GetN(data[i]); MPI_Comm_size(comm, size); SetLength(res, 3 * size); MPI_Gather(@data[0], 3, MPI_INT, @res[0], 3, MPI_INT, 0, comm); if rank = 0 then for i := 0 to 3 * size - 1 do PutN(res[i]); Для нахождения общего числа полученных элементов мы предварительно определили количество процессов в коммуникаторе comm (с помощью функции MPI_Comm_size), записав его в переменную size. Поскольку каждый процесс коммуникатора comm пересылает в главный процесс три элемента, размер динамического массива res полагается равным size * 3 (в программе на Паскале для этого используется процедура SetLength, в программе на С++ — операция new). Затем вызывается функция MPI_Gather. Обратите внимание на то, что в функции MPI_Gather в качестве пятого параметра указывается не размер массива res, а количество элементов, принимаемых от каждого процесса. Заметим также, что функция MPI_Gather получает данные от всех процессов коммуникатора comm, в том числе и от того процесса, который служит получателем всех данных. При указании процесса-получателя мы учли, что процесс ранга 0 45 в коммуникаторе MPI_COMM_WORLD является одновременно и процессом ранга 0 в коммуникаторе comm. Обратите внимание на способ передачи указателя на массивы data и res в программе на Паскале: @data[0] и @res[0]. Заметим, что для динамического массива data этот способ необходимо использовать во всех рассматриваемых программных средах (Delphi, Lazarus, PascalABC.NET), тогда как для статического массива в средах Delphi и Lazarus допустимо использовать более краткий вариант: @data. Запустив полученную программу, мы получим сообщение о верном решении, а после пяти запусков — сообщение о том, что задание выполнено. Приведем полный текст полученного решения: [C++] void Solve() { Task("MPIBegin73"); int flag; MPI_Initialized(&flag); if (flag == 0) return; int rank, size; MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm comm; int color = rank % 3 == 0 ? 0 : MPI_UNDEFINED; MPI_Comm_split(MPI_COMM_WORLD, color, rank, &comm); if (comm == MPI_COMM_NULL) return; int data[3]; for (int i = 0; i < 3 ; ++i) pt >> data[i]; MPI_Comm_size(comm, &size); int* res = new int[3 * size]; MPI_Gather(data, 3, MPI_INT, res, 3, MPI_INT, 0, comm); if (rank == 0) for (int i = 0; i < 3 * size; ++i) pt << res[i]; delete[] res; } [Pascal] program MPIBegin73; uses PT4, MPI; var flag, size, rank: integer; 46 comm: MPI_Comm; color: integer; data: array[0..2] of integer; res: array of integer; i: integer; begin Task('MPIBegin73'); MPI_Initialized(flag); if flag = 0 then exit; MPI_Comm_size(MPI_COMM_WORLD, size); MPI_Comm_rank(MPI_COMM_WORLD, rank); if rank mod 3 = 0 then color := 0 else color := MPI_UNDEFINED; MPI_Comm_split(MPI_COMM_WORLD, color, rank, comm); if comm = MPI_COMM_NULL then exit; for i := 0 to 2 do GetN(data[i]); MPI_Comm_size(comm, size); SetLength(res, 3 * size); MPI_Gather(@data[0], 3, MPI_INT, @res[0], 3, MPI_INT, 0, comm); if rank = 0 then for i := 0 to 3 * size - 1 do PutN(res[i]); end. 4.3 Проектное задание Выполните учебные задания группы MPIBegin, указанные в вашем варианте проектного задания (формулировки заданий приведены в приложении). Если вы не получили вариант проектного задания, то выполните задания из первого варианта. ВАРИАНТ 1 Коллективная пересылка данных: 31, 33, 38, 42, 44, 45 Группы процессов и коммуникаторы: 72, 74, 77, 78, 82 ВАРИАНТ 2 Коллективная пересылка данных: 32, 33, 39, 41, 44, 46 Группы процессов и коммуникаторы: 72, 74, 77, 79, 81 ВАРИАНТ 3 Коллективная пересылка данных: 32, 33, 37, 40, 44, 47 ВАРИАНТ 4 Коллективная пересылка данных: 32, 35, 39, 40, 44, 45 47 Группы процессов и коммуникаторы: 72, 75, 76, 78, 81 Группы процессов и коммуникаторы: 72, 74, 77, 79, 80 ВАРИАНТ 5 Коллективная пересылка данных: 30, 35, 38, 42, 43, 47 Группы процессов и коммуникаторы: 71, 75, 76, 79, 82 ВАРИАНТ 6 Коллективная пересылка данных: 31, 36, 38, 42, 43, 47 Группы процессов и коммуникаторы: 71, 74, 76, 78, 80 ВАРИАНТ 7 Коллективная пересылка данных: 32, 35, 37, 41, 43, 46 Группы процессов и коммуникаторы: 71, 75, 76, 79, 80 ВАРИАНТ 8 Коллективная пересылка данных: 30, 34, 39, 41, 44, 46 Группы процессов и коммуникаторы: 72, 75, 77, 78, 81 ВАРИАНТ 9 Коллективная пересылка данных: 31, 36, 38, 42, 43, 45 Группы процессов и коммуникаторы: 72, 75, 77, 79, 82 ВАРИАНТ 10 Коллективная пересылка данных: 31, 36, 39, 41, 43, 47 Группы процессов и коммуникаторы: 71, 74, 76, 78, 82 ВАРИАНТ 11 Коллективная пересылка данных: 30, 34, 37, 40, 43, 45 Группы процессов и коммуникаторы: 71, 75, 77, 79, 80 ВАРИАНТ 12 Коллективная пересылка данных: 30, 34, 37, 40, 44, 46 Группы процессов и коммуникаторы: 71, 74, 76, 78, 81 ВАРИАНТ 13 Коллективная пересылка данных: 30, 34, 37, 41, 43, 47 Группы процессов и коммуникаторы: 72, 75, 77, 78, 81 ВАРИАНТ 14 Коллективная пересылка данных: 31, 35, 39, 42, 44, 45 Группы процессов и коммуникаторы: 71, 74, 77, 78, 80 ВАРИАНТ 15 Коллективная пересылка данных: 31, 34, 39, 40, 43, 46 Группы процессов и коммуникаторы: 71, 74, 77, 79, 81 ВАРИАНТ 16 Коллективная пересылка данных: 31, 33, 38, 40, 44, 45 Группы процессов и коммуникаторы: 71, 75, 77, 79, 80 48 ВАРИАНТ 17 Коллективная пересылка данных: 32, 36, 38, 40, 43, 47 Группы процессов и коммуникаторы: 71, 74, 76, 78, 80 ВАРИАНТ 18 Коллективная пересылка данных: 31, 36, 39, 41, 44, 46 Группы процессов и коммуникаторы: 71, 74, 77, 78, 82 ВАРИАНТ 19 Коллективная пересылка данных: 32, 35, 39, 41, 43, 47 Группы процессов и коммуникаторы: 71, 74, 76, 79, 81 ВАРИАНТ 20 Коллективная пересылка данных: 30, 35, 37, 40, 43, 46 Группы процессов и коммуникаторы: 72, 75, 76, 79, 82 ВАРИАНТ 21 Коллективная пересылка данных: 32, 33, 37, 42, 44, 46 Группы процессов и коммуникаторы: 72, 75, 76, 79, 80 ВАРИАНТ 22 Коллективная пересылка данных: 32, 33, 38, 42, 44, 47 Группы процессов и коммуникаторы: 72, 75, 76, 79, 81 ВАРИАНТ 23 Коллективная пересылка данных: 30, 34, 37, 41, 43, 45 Группы процессов и коммуникаторы: 72, 74, 76, 78, 82 ВАРИАНТ 24 Коллективная пересылка данных: 30, 36, 38, 42, 44, 45 Группы процессов и коммуникаторы: 72, 75, 77, 78, 82 4.4 Тест рубежного контроля Тест содержит 6 заданий, на выполнение которых отводится 3 минуты. Выберите правильный, по вашему мнению, вариант ответа и отметьте его любым значком в бланке ответов. 1. Как обозначается пустой коммуникатор? (1) MPI_COMM_NULL (2) MPI_COMM_NUL (3) MPI_COMM_NIL (4) MPI_COMM_EMPTY 2. Сколько параметров содержит функция MPI_Comm_split? (1) 3 (2) 4 (3) 5 (4) 6 3. Укажите, неверное продолжение фразы: «С помощью функции MPI_Comm_split можно создать новый коммуникатор, который будет содержать… (1) … 0 процессов» (2) … больше процессов, чем исходный коммуникатор» 49 (3) … все процессы, входящие в (4) … часть процессов, входящих исходный коммуникатор» в исходный коммуникатор» 4. Какую константу надо указать в качестве параметра color функции MPI_Comm_split, чтобы текущий процесс не был включен ни в один из новых коммуникаторов? (1) MPI_COLOR_EMPTY (2) MPI_NONE (3) MPI_UNDEFINED (4) MPI_EMPTY 5. Какое действие выполняет функция MPI_Gather? (1) Собирает данные со всех про- (2) Собирает данные со всех процессов группы в буфере процессов группы в буфере процесса-приемника (от каждого цесса-приемника (от каждого процесса принимается одинапроцесса может приниматься ковое число элементов данразличное число элементов ных) данных) (3) Собирает данные со всех про- (4) Собирает данные со всех процессов группы и размещает их цессов группы и размещает их в буфере приема каждого пров буфере приема каждого процесса (каждый процесс посыцесса (каждый процесс может лает одинаковое число элеменпосылать различное число тов данных) элементов данных) 6. Как передать в подпрограмму на Паскале (в средах Delphi, Lazarus или PascalABC.NET) указатель на динамический массив data? (1) &data (2) @data (3) &data[0] (4) @data[0] 5 Модуль № 5. Использование виртуальных топологий 5.1 Комплексная цель Познакомиться с возможностями библиотеки MPI, связанными с виртуальными топологиями: декартовой топологией и топологией графа. 5.2 Содержание модуля При выполнении параллельной программы каждый процесс может обмениваться данными с любым другим процессом посредством стандартного коммуникатора MPI_COMM_WORLD. Если требуется выделить какую-либо часть имеющихся процессов для организации взаимодействия между ними (например, для коллективного обмена данными в пределах только этой части процессов), то необходимо определить для нужных процессов новый коммуникатор. В предыдущем пункте для создания новых коммуникаторов мы использовали функцию MPI_Comm_split. В библиотеке MPI предусмотрен еще один способ, 50 позволяющий объединить нужные процессы; он состоит в определении для процессов параллельного приложения виртуальной топологии (virtual topology). Виртуальная топология задает на множестве процессов некоторую структуру, определенным образом упорядочивающую эти процессы. Имеются два вида виртуальной топологии: декартова топология и топология графа. В случае декартовой топологии (Cartesian topology) все процессы интерпретируются как узлы некоторой n-мерной решетки размера k1 k2 … kn (если n = 2, то процессы можно рассматривать как элементы прямоугольной матрицы размера k1 k2). В случае топологии графа (graph topology) процессы интерпретируются как вершины некоторого графа; при этом связи между процессами определяются посредством задания набора ребер (дуг) для этого графа. Если коммуникатор снабжен виртуальной топологией, то из входящих в него процессов можно выделять различные подмножества регулярной структуры. В частности, из n-мерных решеток, задаваемых с помощью декартовой топологии, можно выделять различные сечения размерности m, где m может меняться от 1 до n – 1 (например, из двумерных матриц можно выделять одномерные строки или столбцы). Воспользуемся механизмом виртуальных топологий для выполнения следующего задания. MPIBegin87. Число процессов К кратно трем: K = 3N, N > 1. В процессах 0, N и 2N дано по N целых чисел. Определить для всех процессов декартову топологию в виде матрицы размера 3 × N, после чего, используя функцию MPI_Cart_sub, расщепить матрицу процессов на три одномерные строки (при этом процессы 0, N и 2N будут главными процессами в полученных строках). Используя одну коллективную операцию пересылки данных, переслать по одному исходному числу из главного процесса каждой строки во все процессы этой же строки и вывести полученное число в каждом процессе (включая процессы 0, N и 2N). Рис. 17. Ознакомительный запуск задания MPIBegin87 51 При ознакомительном запуске этого задания окно задачника примет вид, примерно соответствующий приведенному на рисунке 17. Вариант, приведенный в окне, соответствует случаю N = 4: имеется 12 процессов, которые следует интерпретировать как элементы матрицы размера 3 4. При этом в процессах, являющихся начальными элементами строк (иначе говоря, в процессах, входящих в первый столбец матрицы), дано по четыре числа, каждое из которых надо переслать в соответствующий процесс этой же строки матрицы процессов. На первом этапе решения задачи необходимо определить требуемую декартову топологию. Для этого надо использовать функцию MPI_Cart_create, которая имеет следующие параметры: исходный коммуникатор, для процессов которого определяется декартова топология (в нашем случае MPI_COMM_WORLD); число размерностей создаваемой декартовой решетки (в нашем случае 2); целочисленный массив, каждый элемент которого определяет размер по каждому измерению (в нашем случае массив должен состоять из двух элементов со значениями 3 и size / 3); целочисленный массив флагов, определяющих периодичность каждого измерения (в нашем случае достаточно использовать массив из двух нулевых элементов); целочисленный флаг, определяющий, можно ли среде MPI автоматически менять порядок нумерации процессов (в нашем случае необходимо положить этот параметр равным 0); результирующий коммуникатор с декартовой топологией (единственный выходной параметр). Периодичность для некоторых измерений декартовой решетки удобно использовать, например, при выполнении циклического переноса данных между процессами, входящими в эти измерения (см. описание функции MPI_Cart_shift); в этом случае соответствующий элемент в массиве флагов надо задать отличным от 0. Автоматическая перенумерация процессов при определении декартовой топологии позволяет учесть физическую конфигурацию компьютерной системы, на которой выполняется параллельная программа, и тем самым повысить эффективность ее выполнения. Однако в учебных программах, выполняемых под управлением задачника PT for MPI, порядок процессов в созданных декартовых топологиях должен оставаться неизменным, поэтому перенумерацию процессов следует запретить. Приведем фрагмент программы, определяющий декартову топологию и связывающий ее с новым коммуникатором comm (этот фрагмент надо поместить после операторов, уже имеющихся в тексте заготовки; в программе на языке Паскаль надо дополнительно описать массивы dims и periods типа array of integer и переменную comm типа MPI_Comm): 52 [C++] MPI_Comm comm; int dims[] = {3, size / 3}, periods[] = {0, 0}; MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 0, &comm); [Pascal] SetLength(dims, 2); dims[0] := 3; dims[1] := size div 3; SetLength(periods, 2); periods[0] := 0; periods[1] := 0; MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 0, comm); Заметим, что в программе на языке Паскаль можно обойтись без переменных dims и periods, создавая требуемые массивы «на лету» и сразу передавая их в качестве соответствующих параметров процедуры MPI_Cart_create. Однако используемый при этом синтаксис будет различным для разных программных сред. В Delphi и Lazarus достаточно указать в качестве параметра-массива список элементов этого массива, заключенный в квадратные скобки: [Delphi, Lazarus] MPI_Cart_create(MPI_COMM_WORLD, 2, [3, size div 3], [0, 0], 0, comm); В среде PascalABC.NET следует вызвать конструктор динамического массива, указав после него список элементов в круглых скобках: [PascalABC.NET] MPI_Cart_create(MPI_COMM_WORLD, 2, new integer[2](3, size div 3), new integer[2](0, 0), 0, comm); Коммуникатор comm, созданный в результате выполнения функции MPI_Cart_create, будет содержать те же процессы, что и исходный коммуникатор MPI_COMM_WORLD, причем в том же порядке. Однако эти коммуникаторы являются различными. Это проявляется прежде всего в том, что операции пересылки данных, осуществляемые с помощью коммуникаторов MPI_COMM_WORLD и comm, выполняются независимо и не влияют друг на друга (различные коммуникаторы, содержащие один и тот же одинаково упорядоченный набор процессов, называются конгруэнтными; при их сравнении функцией MPI_Comm_compare возвращается результат, равный значению константы MPI_CONGRUENT). Кроме того, с коммуникатором comm дополнительно связана виртуальная топология, а коммуникатор MPI_COMM_WORLD никакой виртуальной топологией не обладает (проверить, связана ли с коммуникатором виртуальная топология определенного типа, можно с помощью функции MPI_Topo_test). Благодаря наличию декартовой топологии, с каждым процессом коммуникатора comm связывается не только порядковый номер (ранг процесса), но и 53 набор целых чисел, определяющих координаты этого процесса в соответствующей декартовой решетке. Координаты нумеруются от 0. Координаты процесса в декартовой топологии можно определить по его рангу с помощью функции MPI_Cart_coords (заметим, что функция MPI_Cart_rank позволяет решить обратную задачу). Для выполнения нашего задания использовать функцию MPI_Cart_coords не требуется, однако в ряде случаев (в частности, при отладке параллельных программ) она может оказаться полезной. Поэтому приведем пример ее использования, выведя в раздел отладки окна задачника координаты всех процессов, включенных в декартову решетку. Для этого дополним текст программы следующими операторами (в программе на Паскале надо дополнительно описать массив coords типа array of integer): [C++] int coords[2]; MPI_Cart_coords(comm, rank, 2, coords); Show(coords[0]); Show(coords[1]); [Pascal] SetLength(coords, 2); MPI_Cart_coords(comm, rank, 2, coords); Show(coords[0]); Show(coords[1]); Рис. 18. Вывод декартовых координат процессов в разделе отладки 54 При запуске дополненной программы окно задачника примет вид, соответствующий приведенному на рисунке 18. Напомним, что первое число в каждой строке раздела отладки (перед символом «|») обозначает ранг процесса, в котором были выведены данные, указанные в этой строке. Второе число (после которого указывается символ «>») обозначает порядковый номер строки вывода для данного процесса. В нашем случае каждый процесс вывел по одной строке, содержащей два числа: свои координаты в декартовой топологии. Мы видим, что процесс ранга 0 имеет координаты (0, 0), т. е. является первым элементом первой строки матрицы, а процесс ранга 11 имеет координаты (2, 3), т. е. является последним (четвертым по счету) элементом последней (третьей по счету) строки. Кроме того, в данном случае в первую строку матрицы входят процессы рангов 0, 1, 2, 3, а в первый столбец — процессы рангов 0, 4 и 8. Вернемся к нашей задаче. Для ее решения необходимо вначале разбить полученную матрицу процессов на отдельные строки, связав с каждой строкой новый коммуникатор. После этого следует выполнить для всех процессов, входящих в одну строку, коллективную операцию MPI_Scatter, рассылающую фрагменты набора данных из одного процесса во все процессы, входящие в коммуникатор. Расщепление декартовой решетки на набор подрешеток меньшей размерности (в частности, разбиение матрицы на набор строк или столбцов) и связывание с каждой полученной подрешеткой нового коммуникатора выполняется с помощью функции MPI_Cart_sub. В качестве ее первого параметра следует указать исходный коммуникатор с декартовой топологией, в качестве второго — массив флагов, определяющий номера тех измерений, которые должны остаться в подрешетках: если соответствующее измерение должно входить в каждую подрешетку, то на его месте в массиве указывается ненулевой флаг, если же по данному измерению выполняется расщепление исходной решетки, то значение связанного с этим измерением флага должно быть нулевым. В результате выполнения функции MPI_Cart_sub создается набор новых коммуникаторов, каждый из которых связывается с одной из полученных подрешеток (все созданные коммуникаторы автоматически снабжаются декартовой топологией). Однако возвращается данной функцией (в качестве третьего, выходного параметра) только один из созданных коммуникаторов — тот, в который входит процесс, вызывавший эту функцию. Заметим, что аналогичным образом ведет себя и функция MPI_Comm_split, рассмотренная в предыдущем пункте. Для разбиения исходной матрицы процессов на набор строк надо в качестве второго параметра функции MPI_Cart_sub указать массив из двух целочисленных элементов, первый из которых равен 0, а второй является ненулевым (например, равен 1). В этом случае все элементы матрицы с одинаковым значением первой (удаляемой) координаты будут объединены в новом коммуникаторе (назовем его comm_sub). 55 Первый процесс каждой строки (тот, который должен по условию задачи переслать свои данные во все остальные процессы этой же строки) будет иметь в полученном коммуникаторе comm_sub ранг, равный 0. Для определения ранга следует воспользоваться функцией MPI_Comm_rank. После этого, если ранг равен 0, надо прочесть исходные данные и переслать по одному элементу данных каждому процессу этого же коммуникатора, используя функцию MPI_Scatter. В конце останется вывести элемент, полученный каждым процессом. Приведем завершающую часть программы (в программе на Паскале надо дополнительно описать переменные i, b типа integer, переменную comm_sub типа MPI_Comm и динамические массивы remain_dims и a типа array of integer): [C++] MPI_Comm comm_sub; int remain_dims[] = {0, 1}; MPI_Cart_sub(comm, remain_dims, &comm_sub); MPI_Comm_size(comm_sub, &size); MPI_Comm_rank(comm_sub, &rank); int b, *a = new int[size]; if (rank == 0) for (int i = 0; i < size; ++i) pt >> a[i]; MPI_Scatter(a, 1, MPI_INT, &b, 1, MPI_INT, 0, comm_sub); pt << b; delete[] a; [Pascal] SetLength(remain_dims, 2); remain_dims[0] := 0; remain_dims[1] := 1; MPI_Cart_sub(comm, remain_dims, comm_sub); MPI_Comm_size(comm_sub, size); MPI_Comm_rank(comm_sub, rank); SetLength(a, size); if rank = 0 then for i := 0 to size - 1 do GetN(a[i]); MPI_Scatter(@a[0], 1, MPI_INT, @b, 1, MPI_INT, 0, comm_sub); PutN(b); Ранее мы отмечали, что в программе на Паскале можно обойтись без использования переменных dims и periods, создав требуемые массивы «на лету». Аналогичным образом можно обойтись и без переменной remain_dims, передав созданный «на лету» массив в качестве второго параметра процедуры MPI_Cart_sub: [Delphi, Lazarus] MPI_Cart_sub(comm, [0, 1], comm_sub); 56 [PascalABC.NET] MPI_Cart_sub(comm, new integer[2](0, 1), comm_sub); Запустив полученную программу, мы получим сообщение о верном решении, а после пяти запусков — сообщение о том, что задание выполнено. Приведем полный текст полученного решения: [C++] void Solve() { Task("MPIBegin87"); int flag; MPI_Initialized(&flag); if (flag == 0) return; int rank, size; MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm comm; int dims[] = {3, size / 3}, periods[] = {0, 0}; MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 0, &comm); MPI_Comm comm_sub; int remain_dims[] = {0, 1}; MPI_Cart_sub(comm, remain_dims, &comm_sub); MPI_Comm_size(comm_sub, &size); MPI_Comm_rank(comm_sub, &rank); int b, *a = new int[size]; if (rank == 0) for (int i = 0; i < size; ++i) pt >> a[i]; MPI_Scatter(a, 1, MPI_INT, &b, 1, MPI_INT, 0, comm_sub); pt << b; delete[] a; } [Pascal] program MPIBegin87; uses PT4, MPI; var flag, size, rank: integer; comm, comm_sub: MPI_Comm; periods, dims, remain_dims, a: array of integer; i, b: integer; begin Task('MPIBegin87'); 57 MPI_Initialized(flag); if flag = 0 then exit; MPI_Comm_size(MPI_COMM_WORLD, size); MPI_Comm_rank(MPI_COMM_WORLD, rank); SetLength(dims, 2); dims[0] := 3; dims[1] := size div 3; SetLength(periods, 2); periods[0] := 0; periods[1] := 0; MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 0, comm); SetLength(remain_dims, 2); remain_dims[0] := 0; remain_dims[1] := 1; MPI_Cart_sub(comm, remain_dims, comm_sub); MPI_Comm_size(comm_sub, size); MPI_Comm_rank(comm_sub, rank); SetLength(a, size); if rank = 0 then for i := 0 to size - 1 do GetN(a[i]); MPI_Scatter(@a[0], 1, MPI_INT, @b, 1, MPI_INT, 0, comm_sub); PutN(b); end. Примечание. Распространенной ошибкой, связанной с использованием функции MPI_Cart_sub, является неправильное указание ее второго параметра — массива флагов. Если, например, в приведенной выше программе поменять местами элементы со значениями 0 и 1 в массиве remain_dims, то при запуске программы в окне задачника будут выведены сообщения об ошибках, подобные приведенным на рисунке 19. Проанализируем эти сообщения. Из-за неверного указания массива флагов функция MPI_Cart_sub выполнила разбиение исходной матрицы не на строки, а на столбцы; в результате были созданы 4 новых коммуникатора, каждый из которых содержит 3 процесса, входящих в один и тот же столбец матрицы. При этом процесс, являющийся первым в столбце, считается процессом ранга 0 для соответствующего коммуникатора. Поэтому условие в последнем операторе if будет истинным для процессов 0, 1, 2 и 3, и именно для них будут выполняться операторы ввода исходных данных. Однако в процессах 1, 2 и 3 исходных данных не предусмотрено, поэтому при выполнении программы для этих процессов выводится сообщение об ошибке «Попытка ввести лишние исходные данные». С другой стороны, процессы 4 и 8 (являющиеся начальными процессами во второй и третьей строке матрицы) в новых коммуникаторах имеют ненулевой ранг, и по- 58 этому для них ввод данных не выполняется, что и отмечено в сообщении об ошибке для этих процессов: «Введены не все требуемые исходные данные. Количество прочитанных данных: 0 (из 4)». Обратите также внимание на то, что процесс 0 переслал свои исходные данные не в процессы, входящие в первую строку матрицы (как требовалось по условию задачи), а в процессы, входящие в первый столбец. Так как в процессах 1, 2 и 3 исходные данные отсутствовали, в остальные процессы соответствующих столбцов были пересланы нули. Заметим, что в самих процессах 1, 2 и 3 полученные нули не были выведены, так как ранее в каждом из этих процессов задачник выявил ошибку ввода и поэтому заблокировал все последующие операции ввода-вывода для этих процессов. Таким образом, приведенной в окне задачника информации вполне достаточно, чтобы распознать причину ошибки и внести в программу необходимые исправления. Рис. 19. Окно задачника при ошибочном выполнении задания MPIBegin87 Теперь рассмотрим другой вид виртуальных топологий — топологию графа. Следует отметить, что для работы с топологиями графа библиотека MPI предоставляет существенно меньше средств, чем для работы с декартовыми топологиями. Напомним, что для процессов, входящих в декартову топологию, можно определить декартовы координаты по их рангам (и ранги по декартовым координатам); кроме того, предусмотрена возможность выделения подрешеток меньшей размерности (причем каждая подрешетка автоматически связывается с 59 новым коммуникатором). Также имеется специальная функция MPI_Cart_shift, позволяющая определить «соседей» процесса вдоль одной из декартовых координат (использование этой функции упрощает пересылку сообщений вдоль указанной координаты). Что же касается топологии графа, то для нее предусмотрена лишь возможность, аналогичная возможности функции MPI_Cart_Shift, а именно определение количества и рангов всех процессовсоседей (neighbors) некоторого процесса в графе, определяемом данной топологией (соседями считаются процессы, связанные ребрами с данным процессом). Познакомимся с этой возможностью на практике, выполнив следующее задание. MPIBegin99. Число процессов K является четным: K = 2N (1 < N < 6); в каждом процессе дано целое число A. Используя функцию MPI_Graph_create, определить для всех процессов топологию графа, в которой все процессы четного ранга (включая главный процесс) связаны в цепочку: 0 — 2 — 4 — 6 — … — (2N − 2), а каждый процесс нечетного ранга R (1, 3, …, 2N − 1) связан с процессом ранга R − 1 (в результате каждый процесс нечетного ранга будет иметь единственного соседа, первый и последний процессы четного ранга будут иметь по два соседа, а остальные — «внутренние» — процессы четного ранга будут иметь по три соседа). Переслать число A из каждого процесса всем процессам-соседям. Для определения количества процессовсоседей и их рангов использовать функции MPI_Graph_neighbors_count и MPI_Graph_neighbors, пересылку выполнять с помощью функции MPI_Sendrecv. Во всех процессах вывести полученные числа в порядке возрастания рангов переславших их процессов. При ознакомительном запуске этого задания окно задачника примет вид, примерно соответствующий приведенному на рисунке 20. Рис. 20. Ознакомительный запуск задания MPIBegin99 60 Для большей наглядности изобразим процессы вместе с их исходными данными в виде графа той структуры, которая описана в формулировке задания (рис. 21). Процесс 0 80 Процесс 2 99 Процесс 4 84 Процесс 6 11 Процесс 8 62 Процесс 1 28 Процесс 3 25 Процесс 5 54 Процесс 7 39 Процесс 9 98 Рис. 21. Топология графа, описанная в задании MPIBegin99 Поскольку процесс ранга 0 имеет двух соседей (процессы ранга 1 и 2), он должен отправить им число 80 и получить от них числа 28 и 99. Процесс ранга 1 имеет только одного соседа (процесс ранга 0), поэтому он должен отправить ему число 28 и получить от него число 80. Процесс ранга 2, имеющий три соседа, должен отправить им число 99 и получить от них числа 80, 25 и 84, и т. д. Если бы каждый процесс имел информацию о количестве своих соседей, а также об их рангах (в порядке возрастания), то это позволило бы единообразно запрограммировать действия по пересылке данных для любого процесса, независимо от того, сколько соседей он имеет. Требуемая информация о соседях может быть легко получена, если на множестве всех процессов будет определена соответствующая топология графа, а для этого необходимо воспользоваться функцией MPI_Graph_create. Данная функция имеет следующие параметры: исходный коммуникатор, для процессов которого определяется топология графа; число вершин графа; целочисленный массив степеней вершин, i-й элемент которого равен суммарному количеству соседей для первых i вершин графа; целочисленный массив ребер, содержащий упорядоченный список ребер для всех вершин (вершины нумеруются от 0); целочисленный флаг, определяющий, можно ли среде MPI автоматически менять порядок нумерации процессов; результирующий коммуникатор с топологией графа (единственный выходной параметр). Как и при выполнении предыдущего задания, следует запретить перенумерацию процессов, положив соответствующей флаг равным 0. Чтобы лучше понять смысл параметров-массивов, задающих характеристики определяемого графа, перечислим их элементы для графа, приведенного на рисунке 21. Первая вершина графа (процесс ранга 0) имеет двух соседей, поэтому первый элемент массива степеней вершин будет равен 2. Вторая вершина графа (процесс ранга 1) имеет одного соседа, поэтому второй элемент массива степеней вершин будет равен 3 (к значению предыдущего элемента прибавляется 1). Третья вершина (процесс ранга 2) имеет трех соседей, поэтому третий 61 элемент массива степеней вершин будет равен 6, и т. д. Получаем следующий набор значений: 2, 3, 6, 7, 10, 11, 14, 15, 17, 18 (предпоследний элемент массива равен 17, так как процесс ранга 8, как и процесс ранга 0, имеет двух соседей). Заметим, что значение последнего элемента массива степеней вершин всегда будет в два раза больше общего количества ребер графа. В массиве ребер необходимо указать ранги всех соседей для каждой вершины (для большей наглядности будем выделять группы соседей каждой вершины дополнительными пробелами, а сверху в скобках указывать ранг вершины, соседи которой перечислены ниже): (0) (1) (2) (3) (4) (5) (6) (7) (8) (9) 1 2 0 0 3 4 2 2 5 6 4 4 7 8 6 6 9 8 Размер полученного массива ребер равен значению последнего элемента массива степеней вершин. Если число процессов равно size, то массив степеней вершин должен содержать size элементов. Размер массива ребер зависит от структуры графа; в нашем случае размер массива ребер равен 2·(size – 1), где size — число процессов. При заполнении массивов удобно отдельно обработать фрагменты, соответствующие двум первым (ранга 0 и 1) и двум последним (ранга size – 2 и size – 1) вершинам графа, а остальные вершины перебирать в цикле, обрабатывая по две вершины (ранга 2 и 3, 4 и 5, …, size – 4 и size – 3) на каждой итерации. Следуя описанию функции MPI_Graph_create в стандарте MPI [6], будем использовать для массива степеней вершин и массива ребер названия index и edges соответственно. При заполнении этих массивов удобно использовать вспомогательную переменную n, равную половине количества процессов. Приведем фрагмент программы, заполняющей массивы index и edges (в программе на Паскале надо дополнительно описать переменные целого типа n, i, j, а также два динамических массива index и edges типа array of integer): [C++] int n = size / 2; int *index = new int[size], *edges = new int[2 * (size – 1)]; index[0] = 2; index[1] = 3; edges[0] = 1; edges[1] = 2; edges[2] = 0; int j = 3; for (int i = 1; i <= n - 2; ++i) { index[2 * i] = index[2 * i - 1] + 3; edges[j] = 2 * i - 2; edges[j + 1] = 2 * i + 1; 62 edges[j + 2] = 2 * i + 2; index[2 * i + 1] = index[2 * i] + 1; edges[j + 3] = 2 * i; j += 4; } index[2 * n - 2] index[2 * n - 1] edges[j] = 2 * n edges[j + 1] = 2 edges[j + 2] = 2 = = * * index[2 * n - 3] + 2; index[2 * n - 2] + 1; 4; n - 1; n - 2; [Pascal] n := size div 2; SetLength(index, size); SetLength(edges, 2 * (size – 1)); index[0] := 2; index[1] := 3; edges[0] := 1; edges[1] := 2; edges[2] := 0; j := 3; for i := 1 to n - 2 do begin index[2 * i] := index[2 * i – 1] + edges[j] := 2 * i - 2; edges[j + 1] := 2 * i + 1; edges[j + 2] := 2 * i + 2; index[2 * i + 1] := index[2 * i] + edges[j + 3] := 2 * i; j := j + 4; end; index[2 * n - 2] := index[2 * n - 3] index[2 * n - 1] := index[2 * n - 2] edges[j] := 2 * n - 4; edges[j + 1] := 2 * n - 1; edges[j + 2] := 2 * n - 2; 3; 1; + 2; + 1; Для проверки правильности данной части алгоритма выведем в раздел отладки окна задачника значения элементов полученных массивов: [C++] for (int i = 0; i < size; ++i) Show(index[i]); ShowLine(); for (int i = 0; i < j + 3; ++i) Show(edges[i]); 63 [Pascal] for i := 0 to size - 1 do Show(index[i]); ShowLine; for i := 0 to j + 2 do Show(edges[i]); Если при очередном запуске программы число процессов будет равно 10, то в разделе отладки мы увидим наборы значений, совпадающие с теми, которые были нами получены ранее (рис. 22). Для того чтобы отобразить эти наборы в единственном экземпляре, в разделе отладки следует выбрать ярлычок, соответствующий одному из процессов (на рисунке выбран процесс ранга 0). Рис. 22. Отладочный вывод элементов массива степеней вершин и массива ребер Убедившись, что массивы сформированы правильно, создадим топологию графа, вызвав функцию MPI_Graph_create в каждом процессе параллельного приложения (в программе на Паскале надо дополнительно описать переменную g_comm типа MPI_Comm): [C++] MPI_Comm g_comm; MPI_Graph_create(MPI_COMM_WORLD, size, index, edges, 0, &g_comm); 64 [Pascal] MPI_Graph_create(MPI_COMM_WORLD, size, index, edges, 0, g_comm); Осталось реализовать завершающую часть алгоритма, непосредственно связанную с пересылкой данных. В этой части для текущего процесса (процесса ранга rank) следует определить количество count его соседей и массив neighbors их рангов в текущей топологии графа (используя функции MPI_Graph_neighbors_count и MPI_Graph_neighbors), после чего организовать обмен данными между текущим процессом и каждым из его соседей (согласно формулировке задания для этого надо использовать функцию MPI_Sendrecv, которая обеспечивает как прием сообщения от некоторого процесса, так и отправку ему другого сообщения). Приведем завершающую часть программы (в программе на Паскале надо дополнительно описать переменные count, a, b целого типа, массив neighbors типа array of integer и переменную s типа MPI_Status): [C++] int count; MPI_Graph_neighbors_count(g_comm, rank, &count); int *neighbors = new int[count]; MPI_Graph_neighbors(g_comm, rank, count, neighbors); int a, b; MPI_Status s; pt >> a; for (int i = 0; i < count; ++i) { MPI_Sendrecv(&a, 1, MPI_INT, neighbors[i], 0, &b, 1, MPI_INT, neighbors[i], 0, g_comm, &s); pt << b; } delete[] index; delete[] edges; delete[] neighbors; [Pascal] MPI_Graph_neighbors_count(g_comm, rank, count); SetLength(neighbors, count); MPI_Graph_neighbors(g_comm, rank, count, neighbors); GetN(a); for i := 0 to count – 1 do begin MPI_Sendrecv(@a, 1, MPI_INT, neighbors[i], 0, @b, 1, MPI_INT, neighbors[i], 0, g_comm, s); PutN(b); end; 65 5.3 Проектное задание Выполните учебные задания группы MPIBegin, указанные в вашем варианте проектного задания (формулировки заданий приведены в приложении). Если вы не получили вариант проектного задания, то выполните задания из первого варианта. ВАРИАНТ 1 Виртуальные топологии: 83, 86, 90, 93, 95, 98 ВАРИАНТ 2 Виртуальные топологии: 83, 86, 89, 94, 97, 100 ВАРИАНТ 3 Виртуальные топологии: 84, 86, 90, 93, 97, 100 ВАРИАНТ 4 Виртуальные топологии: 83, 88, 91, 93, 97, 100 ВАРИАНТ 5 Виртуальные топологии: 84, 85, 89, 93, 96, 98 ВАРИАНТ 6 Виртуальные топологии: 83, 85, 90, 93, 95, 98 ВАРИАНТ 7 Виртуальные топологии: 84, 88, 92, 93, 97, 100 ВАРИАНТ 8 Виртуальные топологии: 83, 88, 91, 94, 95, 98 ВАРИАНТ 9 Виртуальные топологии: 84, 85, 92, 94, 96, 98 ВАРИАНТ 10 Виртуальные топологии: 84, 88, 89, 94, 95, 100 ВАРИАНТ 11 Виртуальные топологии: 83, 85, 92, 94, 96, 100 ВАРИАНТ 12 Виртуальные топологии: 84, 86, 91, 94, 96, 98 ВАРИАНТ 13 Виртуальные топологии: 83, 85, 91, 94, 95, 98 ВАРИАНТ 14 Виртуальные топологии: 84, 88, 90, 94, 96, 100 ВАРИАНТ 15 Виртуальные топологии: 84, 88, 89, 94, 97, 98 ВАРИАНТ 16 Виртуальные топологии: 84, 85, 89, 93, 96, 100 ВАРИАНТ 17 ВАРИАНТ 18 66 Виртуальные топологии: 83, 86, 91, 94, 97, 98 Виртуальные топологии: 83, 85, 90, 93, 97, 100 ВАРИАНТ 19 Виртуальные топологии: 83, 88, 92, 93, 97, 98 ВАРИАНТ 20 Виртуальные топологии: 84, 88, 92, 94, 95, 98 ВАРИАНТ 21 Виртуальные топологии: 83, 85, 91, 93, 96, 98 ВАРИАНТ 22 Виртуальные топологии: 83, 86, 92, 93, 96, 100 ВАРИАНТ 23 Виртуальные топологии: 84, 86, 89, 93, 95, 100 ВАРИАНТ 24 Виртуальные топологии: 84, 86, 90, 94, 95, 100 5.4 Тест рубежного контроля Тест содержит 6 заданий, на выполнение которых отводится 3 минуты. Выберите правильный, по вашему мнению, вариант ответа и отметьте его любым значком в бланке ответов. 1. Сколько параметров содержит функция MPI_Cart_create? (1) 4 (2) 5 (3) 6 (4) 7 2. Какие действия выполняет функция MPI_Scatter? (1) Обеспечивает рассылку каж- (2) Обеспечивает рассылку каждым процессом различных дым процессом различных данных всем другим процесданных всем другим процессам (каждый процесс принисам (каждый процесс может мает одинаковое число элепринимать различное число ментов данных от разных проэлементов данных от разных цессов) процессов). (3) Рассылает данные от процесса- (4) Рассылает данные от процессаисточника во все процессы источника во все процессы группы (каждый процесс пригруппы (каждый процесс монимает одинаковое число элежет принимать различное чисментов данных) ло элементов данных) 3. Как в PascalABC.NET передать в качестве параметра массив из двух целочисленных нулевых значений, создав его «на лету»? (1) (0, 0) (2) [0, 0] (3) new integer[2](0, 0) (4) new integer[](0, 0) 67 4. Декартова топология для 24 процессов имеет вид трехмерной решетки размера 3 х 4 х 2, которая интерпретируется как 3 матрицы размера 4 х 2. Какие элементы должен содержать второй параметр функции MPI_Cart_sub (массив флагов), чтобы данная функция расщепила каждую матрицу на 4 одномерные строки? (1) 1, 0, 1 (2) 0, 1, 0 (3) 1, 1, 0 (4) 0, 0, 1 5. Пусть граф, содержащий 5 процессов, имеет следующую структуру: процесс ранга 0 связан со всеми остальными процессами (от 1 до 4); кроме того, процессы 1–4 объединены в «кольцо»: процесс 1 связан с 2 и 4, процесс 2 — с 1 и 3 и т. д. Какими должны быть в этом случае элементы, определяющие массив степеней вершин данного графа? (1) 4, 3, 3, 3, 3 (2) 3, 3, 3, 3, 4 (3) 4, 7, 10, 13, 16 (4) 3, 6, 9, 12, 16 6. В графе, содержащем 4 процесса, каждый процесс связан с остальными. Какими должны быть в этом случае элементы, определяющие массив ребер данного графа? (1) 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2 (2) 0, 1, 2, 0, 2, 3, 0, 1, 3, 1, 2, 3 (3) 1, 2, 3, 0, 1, 2, 0, 1, 3, 0, 2, 3 (4) 1, 2, 3, 0, 1, 3, 0, 1, 2, 0, 2, 3 6 Приложение. Формулировки учебных заданий Если количество процессов в задании не определено, то можно считать, что это количество не превосходит 12. Под главным процессом всюду в формулировках заданий понимается процесс ранга 0 для коммуникатора MPI_COMM_WORLD. Для всех процессов ненулевого ранга в заданиях используется общее наименование подчиненных процессов. Если в задании не указан тип обрабатываемых чисел, то числа считаются вещественными. Если в задании не определяется максимальный размер набора чисел, то его следует считать равным 10. Символом помечены задания, решения которых приводятся в основном содержании модулей № 1–5. 6.1 Процессы и их ранги MPIBegin1 . В каждом из процессов, входящих в коммуникатор MPI_COMM_WORLD, прочесть одно целое число и вывести его удвоенное значение. Кроме того, для главного процесса (процесса ранга 0) вывести количество процессов, входящих в коммуникатор MPI_COMM_WORLD. MPIBegin2. В главном процессе прочесть вещественное число и вывести его противоположное значение, в каждом из остальных процессов (подчиненных процессов, ранг которых больше 0) вывести его ранг. 68 MPIBegin3. В процессах четного ранга (включая главный) ввести целое число, в процессах нечетного ранга ввести вещественное число. В каждом процессе вывести удвоенное значение введенного числа. MPIBegin4. В подчиненных процессах четного ранга ввести целое число, в процессах нечетного ранга ввести вещественное число. В каждом подчиненном процессе вывести удвоенное значение введенного числа. В главном процессе не выполнять никаких действий. MPIBegin5. В каждом процессе дано целое число N (> 0) и набор из N чисел. В процессах четного ранга (включая главный) вывести сумму чисел из данного набора, в процессах нечетного ранга вывести среднее арифметическое чисел из данного набора. MPIBegin6. В каждом процессе дано целое число N (> 0) и набор из N чисел. В подчиненных процессах четного ранга вывести сумму чисел из данного набора, в процессах нечетного ранга вывести среднее арифметическое чисел из данного набора, в главном процессе вывести произведение чисел из данного набора. 6.2 Обмен сообщениями между отдельными процессами MPIBegin7. В каждом подчиненном процессе дано целое число. Переслать эти числа в главный процесс, используя функции MPI_Send и MPI_Recv (стандартные блокирующие функции для передачи и приема сообщения), и вывести их в главном процессе. Полученные числа выводить в порядке возрастания рангов переславших их процессов. MPIBegin8. В каждом подчиненном процессе дано вещественное число. Переслать эти числа в главный процесс, используя функции MPI_Bsend (посылка сообщения с буферизацией) и MPI_Recv, и вывести их в главном процессе. Полученные числа выводить в порядке убывания рангов переславших их процессов. Для задания буфера использовать функцию MPI_Buffer_attach. MPIBegin9. В каждом подчиненном процессе даны четыре целых числа. Переслать эти числа в главный процесс, используя по одному вызову функции MPI_Send для каждого передающего процесса, и вывести их в главном процессе. Полученные числа выводить в порядке возрастания рангов переславших их процессов. MPIBegin10. В каждом подчиненном процессе дано целое число N (0 < N < 5) и набор из N целых чисел. Переслать данные наборы в главный процесс, используя по одному вызову функции MPI_Bsend для каждого передающего процесса, и вывести наборы в главном процессе в порядке возрастания рангов переславших их процессов. Для определения размера пересланного набора использовать функцию MPI_Get_count. MPIBegin11. В главном процессе дан набор вещественных чисел; количество чисел равно количеству подчиненных процессов. С помощью функции 69 MPI_Send переслать по одному числу в каждый из подчиненных процессов (первое число в процесс 1, второе — в процесс 2, и т. д.) и вывести в подчиненных процессах полученные числа. MPIBegin12. В главном процессе дан набор вещественных чисел; количество чисел равно количеству подчиненных процессов. С помощью функции MPI_Bsend переслать по одному числу в каждый из подчиненных процессов, перебирая процессы в обратном порядке (первое число в последний процесс, второе — в предпоследний процесс, и т. д.), и вывести в подчиненных процессах полученные числа. MPIBegin13. В главном процессе дано целое число N и набор из N чисел; K − 1 ≤ N < 10, где K — количество процессов. С помощью функции MPI_Send переслать по одному числу их данного набора в процессы 1, 2, …, K − 2, а оставшиеся числа — в процесс K − 1, и вывести полученные числа. В процессе K − 1 для определения количества полученных чисел использовать функцию MPI_Get_count. MPIBegin14. В каждом подчиненном процессе дано целое число, причем только для одного процесса это число отлично от нуля. Переслать ненулевое число в главный процесс и вывести в главном процессе полученное число и ранг процесса, переславшего это число. Для приема сообщения в главном процессе использовать функцию MPI_Recv с параметром MPI_ANY_SOURCE. MPIBegin15. В каждом подчиненном процессе дано целое число N, причем для одного процесса это число больше нуля, а для остальных равно нулю. В процессе с ненулевым N дан также набор из N чисел. Переслать данный набор чисел в главный процесс и вывести в главном процессе полученные числа и ранг процесса, переславшего этот набор. При приеме сообщения использовать параметр MPI_ANY_SOURCE. MPIBegin16. В каждом подчиненном процессе дано целое число N, в главном процессе дано целое число K (> 0), равное количеству тех подчиненных процессов, в которых даны положительные числа N. Переслать все положительные числа N в главный процесс и вывести в нем сумму полученных чисел. Для приема сообщений в главном процессе использовать функцию MPI_Recv с параметром MPI_ANY_SOURCE. MPIBegin17 . В каждом процессе дано вещественное число. Переслать число из главного процесса во все подчиненные процессы, а все числа из подчиненных процессов — в главный, и вывести в каждом процессе полученные числа (в главном процессе числа выводить в порядке возрастания рангов переславших их процессов). MPIBegin18. В каждом процессе дано целое число. С помощью функций MPI_Send и MPI_Recv осуществить для всех процессов циклический сдвиг данных с шагом 1, переслав число из процесса 0 в процесс 1, из процесса 1 в процесс 2, …, из последнего процесса в процесс 0. В каждом процессе вывести полученное число. 70 MPIBegin19. В каждом процессе дано целое число. С помощью функций MPI_Send и MPI_Recv осуществить для всех процессов циклический сдвиг данных с шагом −1, переслав число из процесса 1 в процесс 0, из процесса 2 в процесс 1, …, из процесса 0 в последний процесс. В каждом процессе вывести полученное число. MPIBegin20. В каждом процессе даны два целых числа. С помощью функций MPI_Send и MPI_Recv переслать первое число в предыдущий процесс, а второе — в последующий процесс (для процесса 0 считать предыдущим последний процесс, а для последнего процесса считать последующим процесс 0). В каждом процессе вывести числа, полученные от предыдущего и последующего процесса (в указанном порядке). MPIBegin21. В каждом процессе даны два числа: вещественное A и целое N, причем набор чисел N содержит все значения от 0 до K − 1, где K — количество процессов. Используя функции MPI_Send и MPI_Recv (с параметром MPI_ANY_SOURCE), выполнить в каждом процессе пересылку числа A в процесс N и вывести полученное число, а также ранг процесса, из которого число было получено. MPIBegin22. В каждом процессе дано целое число N, причем для одного процесса значение N равно 1, а для остальных равно 0. В процессе с N = 1 дан также набор из K − 1 числа, где K — количество процессов. Переслать из этого процесса по одному из чисел данного набора в остальные процессы, перебирая ранги получателей в возрастающем порядке, и вывести в каждом из них полученное число. MPIBegin23. В каждом процессе дан набор из K − 1 целого числа, где K — количество процессов. Для каждого процесса переслать по одному из данных в нем чисел в остальные процессы, перебирая ранги процессовполучателей в возрастающем порядке, и вывести полученные числа в порядке возрастания рангов переславших их процессов. MPIBegin24. Количество процессов — четное число. В каждом процессе дано целое число N (0 < N < 5) и набор из N чисел. С помощью функции MPI_Sendrecv выполнить обмен исходными наборами между парами процессов 0 и 1, 2 и 3, и т. д. В каждом процессе вывести полученный набор чисел. MPIBegin25. В каждом процессе дано вещественное число. С помощью функции MPI_Sendrecv_replace поменять порядок исходных чисел на обратный (число из процесса 0 должно быть передано в последний процесс, число из процесса 1 — в предпоследний процесс, …, число из последнего процесса — в процесс 0). В каждом процессе вывести полученное число. MPIBegin26. В каждом подчиненном процессе дано вещественное число A и его порядковый номер N (целое число); набор всех номеров N содержит все целые числа от 1 до K − 1, где K — количество процессов. Переслать числа A в главный процесс и вывести их в порядке, соответствующем возрас- 71 танию их номеров N. Для передачи номера N указывать его в качестве параметра msgtag функции MPI_Send. MPIBegin27. В каждом подчиненном процессе дано целое число L (≥ 0) и набор из L пар чисел (A, N), где A — вещественное, а N — его порядковый номер. Все числа L в сумме равны 2K, где K — количество процессов; набор номеров N, данных во всех процессах, содержит все целые числа от 1 до 2K. Переслать числа A в главный процесс и вывести их в порядке, соответствующем возрастанию их номеров N. Для передачи номера N указывать его в качестве параметра msgtag функции MPI_Send. MPIBegin28. В главном процессе дан набор пар чисел (T, A); количество пар равно числу подчиненных процессов. Число T — целое, равное 0 или 1. Число A — целое, если T = 0, и вещественное, если T = 1. Переслать по одному числу A в каждый из подчиненных процессов (первое число в процесс 1, второе — в процесс 2, и т. д.) и вывести полученные числа. Для передачи информации о типе пересланного числа указывать число T в качестве параметра msgtag функции MPI_Send, для получения этой информации использовать функцию MPI_Probe с параметром MPI_ANY_TAG. MPIBegin29. В каждом подчиненном процессе даны два целых числа T, N и набор из N чисел. Число T равно 0 или 1. Набор содержит целые числа, если T = 0, и вещественные числа, если T = 1. Переслать исходные наборы в главный процесс и вывести полученные числа в порядке возрастания рангов переславших их процессов. Для передачи информации о типе пересланного числа указывать число T в качестве параметра msgtag функции MPI_Send, для получения этой информации использовать функцию MPI_Probe с параметром MPI_ANY_TAG. 6.3 Коллективная пересылка данных MPIBegin30. В главном процессе дано целое число. Используя функцию MPI_Bcast, переслать это число во все подчиненные процессы и вывести в них полученное число. MPIBegin31. В главном процессе дан набор из 5 чисел. Используя функцию MPI_Bcast, переслать этот набор во все подчиненные процессы и вывести в них полученные числа в том же порядке. MPIBegin32. В каждом процессе дано вещественное число. Используя функцию MPI_Gather, переслать эти числа в главный процесс и вывести их в порядке возрастания рангов переславших их процессов (первым вывести число, данное в главном процессе). MPIBegin33. В каждом процессе дан набор из 5 целых чисел. Используя функцию MPI_Gather, переслать эти наборы в главный процесс и вывести их в порядке возрастания рангов переславших их процессов (первым вывести набор чисел, данный в главном процессе). 72 MPIBegin34. В каждом процессе дан набор из R + 2 целых чисел, где число R равно рангу процесса (в процессе 0 даны 2 числа, в процессе 1 даны 3 числа, и т. д.). Используя функцию MPI_Gatherv, переслать эти наборы в главный процесс и вывести полученные наборы в порядке возрастания рангов переславших их процессов (первым вывести набор, данный в главном процессе). MPIBegin35. В главном процессе дан набор из K чисел, где K — количество процессов. Используя функцию MPI_Scatter, переслать по одному числу в каждый процесс (включая главный) и вывести в каждом процессе полученное число. MPIBegin36. В главном процессе дан набор из 3K чисел, где K — количество процессов. Используя функцию MPI_Scatter, переслать по 3 числа в каждый процесс (включая главный) и вывести в каждом процессе полученные числа. MPIBegin37. В главном процессе дан набор из K чисел, где K — количество процессов. Не меняя порядок расположения чисел в исходном наборе и используя функцию MPI_Scatterv, переслать по одному числу в каждый процесс; при этом первое число надо переслать в процесс K − 1, второе число — в процесс K − 2, …, последнее число — в процесс 0. Вывести в каждом процессе полученное число. MPIBegin38. В главном процессе дан набор из K(K + 3)/2 целых чисел, где K — количество процессов. Используя функцию MPI_Scatterv, переслать в каждый процесс часть чисел из данного набора; при этом в процесс ранга R надо переслать R + 2 очередных числа (в процесс 0 — первые два числа, в процесс 1 — следующие три числа, и т. д.). В каждом процессе вывести полученные числа. MPIBegin39. В главном процессе дан набор из K + 2 чисел, где K — количество процессов. Используя функцию MPI_Scatterv, переслать в каждый процесс три числа из данного набора; при этом в процесс ранга R должны быть пересланы числа с номерами от R + 1 до R + 3 (в процесс 0 — первые три числа, в процесс 1 — числа со второго по четвертое, и т. д.). В каждом процессе вывести полученные числа. MPIBegin40. В каждом процессе дано вещественное число. Используя функцию MPI_Allgather, переслать эти числа во все процессы и вывести их в каждом процессе в порядке возрастания рангов переславших их процессов (включая число, полученное из этого же процесса). MPIBegin41. В каждом процессе даны четыре целых числа. Используя функцию MPI_Allgather, переслать эти числа во все процессы и вывести их в каждом процессе в порядке возрастания рангов переславших их процессов (включая числа, полученные из этого же процесса). MPIBegin42. В каждом процессе дан набор из R + 2 целых чисел, где число R равно рангу процесса (в процессе 0 даны 2 числа, в процессе 1 даны 3 чис- 73 ла, и т. д.). Используя функцию MPI_Allgatherv, переслать эти наборы во все процессы и вывести их в порядке возрастания рангов переславших их процессов (включая числа, полученные из этого же процесса). MPIBegin43. В каждом процессе дан набор из K чисел, где K — количество процессов. Используя функцию MPI_Alltoall, переслать в каждый процесс по одному числу из всех наборов: в процесс 0 — первые числа из наборов, в процесс 1 — вторые числа, и т. д. В каждом процессе вывести числа в порядке возрастания рангов переславших их процессов (включая число, полученное из этого же процесса). MPIBegin44. В каждом процессе дан набор из 3K целых чисел, где K — количество процессов. Используя функцию MPI_Alltoall, переслать в каждый процесс три очередных числа из каждого набора (в процесс 0 — первые три числа, в процесс 1 — следующие три числа, и т. д.). В каждом процессе вывести числа в порядке возрастания рангов переславших их процессов (включая числа, полученные из этого же процесса). MPIBegin45. В каждом процессе дан набор из K(K + 1)/2 целых чисел, где K — количество процессов. Используя функцию MPI_Alltoallv, переслать в каждый процесс часть чисел из каждого набора; при этом в процесс R должно быть переслано R + 1 очередное число (в процесс 0 — первое число каждого набора, в процесс 1 — следующие два числа, и т. д.). В каждом процессе вывести полученные числа. MPIBegin46. В каждом процессе дан набор из K + 1 числа, где K — количество процессов. Используя функцию MPI_Alltoallv, переслать в каждый процесс два числа из каждого набора; при этом в процесс 0 надо переслать первые два числа, в процесс 1 — второе и третье число, …, в последний процесс — последние два числа каждого набора. В каждом процессе вывести полученные числа. MPIBegin47. В каждом процессе дан набор из K + 1 числа, где K — количество процессов. Используя функцию MPI_Alltoallv, переслать в каждый процесс два числа из каждого набора; при этом в процесс 0 надо переслать последние два числа, в процесс 1 — два числа, предшествующих последнему, …, в последний процесс — первые два числа каждого набора. В каждом процессе вывести полученные числа. 6.4 Коллективные операции редукции MPIBegin48. В каждом процессе дан набор из K + 5 целых чисел, где K — количество процессов. Используя функцию MPI_Reduce для операции MPI_SUM, просуммировать элементы данных наборов с одним и тем же порядковым номером и вывести полученные суммы в главном процессе. MPIBegin49. В каждом процессе дан набор из K + 5 чисел, где K — количество процессов. Используя функцию MPI_Reduce для операции MPI_MIN, найти минимальное значение среди элементов данных наборов с одним и тем 74 же порядковым номером и вывести полученные минимумы в главном процессе. MPIBegin50. В каждом процессе дан набор из K + 5 целых чисел, где K — количество процессов. Используя функцию MPI_Reduce для операции MPI_MAXLOC, найти максимальное значение среди элементов данных наборов с одним и тем же порядковым номером и ранг процесса, содержащего это максимальное значение. Вывести в главном процессе вначале все максимумы, а затем — ранги содержащих их процессов. MPIBegin51. В каждом процессе дан набор из K + 5 чисел, где K — количество процессов. Используя функцию MPI_Allreduce для операции MPI_PROD, перемножить элементы данных наборов с одним и тем же порядковым номером и вывести полученные произведения во всех процессах. MPIBegin52 . В каждом процессе дан набор из K + 5 чисел, где K — количество процессов. Используя функцию MPI_Allreduce для операции MPI_MINLOC, найти минимальное значение среди элементов данных наборов с одним и тем же порядковым номером и ранг процесса, содержащего минимальное значение. Вывести в главном процессе минимумы, а в остальных процессах — ранги процессов, содержащих эти минимумы. MPIBegin53. В каждом процессе дан набор из K целых чисел, где K — количество процессов. Используя функцию MPI_Reduce_scatter, просуммировать элементы данных наборов с одним и тем же порядковым номером, переслать по одной из полученных сумм в каждый процесс (первую сумму — в процесс 0, вторую — в процесс 1, и т. д.) и вывести в каждом процессе полученную сумму. MPIBegin54. В каждом процессе дан набор из 2K чисел, где K — количество процессов. Используя функцию MPI_Reduce_scatter, найти максимумы среди элементов этих наборов с одним и тем же порядковым номером, переслать по два найденных максимума в каждый процесс (первые два максимума — в процесс 0, следующие два — в процесс 1, и т. д.) и вывести в каждом процессе полученные данные. MPIBegin55. В каждом процессе дан набор из K(K + 3)/2 целых чисел, где K — количество процессов. Используя функцию MPI_Reduce_scatter, найти минимальные значения среди элементов этих наборов с одним и тем же порядковым номером и переслать первые два минимума в процесс 0, следующие три — в процесс 1, …, последние K + 1 минимумов — в процесс K − 1. Вывести в каждом процессе полученные данные. MPIBegin56. В каждом процессе дан набор из K + 5 чисел, где K — количество процессов. Используя функцию MPI_Scan, найти в процессе ранга R (R = 0, 1, …, K − 1) произведения элементов с одним и тем же порядковым номером для наборов, данных в процессах с рангами от 0 до R, и вывести найденные произведения (при этом в процессе K − 1 будут выведены произведения элементов из всех наборов). 75 MPIBegin57. В каждом процессе дан набор из K + 5 целых чисел, где K — количество процессов. Используя функцию MPI_Scan, найти в процессе ранга R (R = 0, …, K − 1) максимальные значения среди элементов с одним и тем же порядковым номером для наборов, данных в процессах с рангами от 0 до R, и вывести в каждом процессе найденные максимумы. 6.5 Производные типы и упаковка данных MPIBegin58. В главном процессе дана K − 1 тройка целых чисел, где K — количество процессов. Используя производный тип, содержащий три целых числа, и одну коллективную операцию пересылки данных, переслать все данные из главного процесса в подчиненные и вывести их в подчиненных процессах в том же порядке. MPIBegin59. В главном процессе дана K − 1 тройка целых чисел, где K — количество процессов. Используя производный тип, содержащий три целых числа, и одну коллективную операцию пересылки данных, переслать по одной тройке чисел в каждый из подчиненных процессов и вывести их в подчиненных процессах в том же порядке. MPIBegin60. В каждом подчиненном процессе дана тройка целых чисел. Используя производный тип, содержащий три целых числа, и одну коллективную операцию пересылки данных, переслать числа из подчиненных процессов в главный и вывести полученные числа в порядке возрастания рангов переславших их процессов. MPIBegin61. В главном процессе дана K − 1 тройка чисел, где K — количество процессов, причем первые два числа каждой тройки являются целыми, а третье число — вещественным. Используя производный тип, содержащий три числа (два целых и одно вещественное), переслать числа из главного процесса в подчиненные и вывести их в подчиненных процессах в том же порядке. MPIBegin62. В главном процессе дана K − 1 тройка чисел, где K — количество процессов, причем первое и третье число каждой тройки являются целыми, а второе число — вещественным. Используя производный тип, содержащий три числа (два целых и одно вещественное), переслать по одной тройке чисел в каждый из подчиненных процессов и вывести их в подчиненных процессах в том же порядке. MPIBegin63. В каждом подчиненном процессе даны три числа: одно вещественное и два целых. Используя производный тип, содержащий три числа (два целых и одно вещественное), переслать числа из подчиненных процессов в главный и вывести полученные числа в порядке возрастания рангов переславших их процессов. MPIBegin64. В каждом процессе даны три числа: первое и третье являются целыми, а второе — вещественным. Используя производный тип, содержащий три числа (два целых и одно вещественное), переслать данные из каждого процесса во все процессы и вывести в каждом процессе полученные 76 числа в порядке возрастания рангов переславших их процессов (включая числа, полученные из этого же процесса). MPIBegin65. В каждом подчиненном процессе даны R троек чисел, где R — ранг процесса. Два первых числа в каждой тройке являются целыми, а последнее — вещественным. Используя производный тип, содержащий три числа (два целых и одно вещественное), переслать числа из подчиненных процессов в главный и вывести полученные числа в порядке возрастания рангов переславших их процессов. MPIBegin66. В главном процессе даны два набора: первый содержит K целых, а второй K вещественных чисел, где K — количество процессов. Используя функции упаковки MPI_Pack и MPI_Unpack и одну коллективную операцию пересылки данных, переслать все данные из главного процесса в подчиненные и вывести их в подчиненных процессах в том же порядке. MPIBegin67. В главном процессе дана K − 1 тройка чисел, где K — количество процессов, причем первое и третье число каждой тройки является целым, а второе число — вещественным. Используя функции упаковки и одну коллективную операцию пересылки данных, переслать по одной тройке чисел в подчиненные процессы и вывести их в подчиненных процессах в том же порядке. MPIBegin68. В главном процессе дана K − 1 тройка чисел, где K — количество процессов, причем первые два числа каждой тройки являются целыми, а третье число — вещественным. Используя функции упаковки и одну коллективную операцию пересылки данных, переслать все данные из главного процесса в подчиненные и вывести их в подчиненных процессах в том же порядке. MPIBegin69. В каждом подчиненном процессе даны три числа: два целых и одно вещественное. Используя функции упаковки и одну коллективную операцию пересылки данных, переслать числа из подчиненных процессов в главный и вывести полученные числа в порядке возрастания рангов переславших их процессов. MPIBegin70. В каждом подчиненном процессе дан набор из одного вещественного и R целых чисел, где значение R равно рангу процесса (в процессе 1 дано одно целое число, в процессе 2 — два целых числа, и т. д.). Используя функции упаковки и одну функцию передачи и приема, переслать все данные в главный процесс и вывести эти данные в порядке возрастания рангов переславших их процессов. 6.6 Группы процессов и коммуникаторы MPIBegin71. В главном процессе дан набор из K целых чисел, где K — количество процессов четного ранга (0, 2, …). С помощью функций MPI_Comm_group, MPI_Group_incl и MPI_Comm_create создать новый коммуникатор, включающий процессы четного ранга. Используя одну коллективную операцию пересылки данных для созданного коммуникатора, 77 переслать по одному исходному числу в каждый процесс четного ранга (включая главный) и вывести полученные числа. MPIBegin72. В каждом процессе нечетного ранга (1, 3, …) даны два вещественных числа. С помощью функций MPI_Comm_group, MPI_Group_excl и MPI_Comm_create создать новый коммуникатор, включающий процессы нечетного ранга. Используя одну коллективную операцию пересылки данных для созданного коммуникатора, переслать исходные числа во все процессы нечетного ранга и вывести эти числа в порядке возрастания рангов переславших их процессов (включая числа, полученные из этого же процесса). MPIBegin73 . В каждом процессе, ранг которого делится на 3 (включая главный процесс), даны три целых числа. С помощью функции MPI_Comm_split создать новый коммуникатор, включающий процессы, ранг которых делится на 3. Используя одну коллективную операцию пересылки данных для созданного коммуникатора, переслать исходные числа в главный процесс и вывести эти числа в порядке возрастания рангов переславших их процессов (включая числа, полученные из главного процесса). MPIBegin74. В каждом процессе четного ранга (включая главный процесс) дан набор из трех элементов — вещественных чисел. Используя новый коммуникатор и одну коллективную операцию редукции, найти минимальные значения среди элементов исходных наборов с одним и тем же порядковым номером и вывести найденные минимумы в главном процессе. MPIBegin75. В каждом процессе дано вещественное число. Используя функцию MPI_Comm_split и одну коллективную операцию редукции, найти максимальное из чисел, данных в процессах с четным рангом (включая главный процесс), и минимальное из чисел, данных в процессах с нечетным рангом. Найденный максимум вывести в процессе 0, а найденный минимум — в процессе 1. MPIBegin76. В главном процессе дано целое число K и набор из K вещественных чисел, в каждом подчиненном процессе дано целое число N, которое может принимать два значения: 0 и 1 (количество подчиненных процессов с N = 1 равно K). Используя функцию MPI_Comm_split и одну коллективную операцию пересылки, переслать по одному вещественному числу из главного процесса в каждый подчиненный процесс с N = 1 и вывести в этих подчиненных процессах полученные числа. MPIBegin77. В каждом процессе дано целое число N, которое может принимать два значения: 0 и 1 (имеется хотя бы один процесс с N = 1). Кроме того, в каждом процессе с N = 1 дано вещественное число A. Используя функцию MPI_Comm_split и одну коллективную операцию пересылки данных, переслать числа A в первый из процессов с N = 1 и вывести их в порядке возрастания рангов переславших их процессов (включая число, полученное из этого же процесса). 78 MPIBegin78. В каждом процессе дано целое число N, которое может принимать два значения: 0 и 1 (имеется хотя бы один процесс с N = 1). Кроме того, в каждом процессе с N = 1 дано вещественное число A. Используя функцию MPI_Comm_split и одну коллективную операцию пересылки данных, переслать числа A в последний из процессов с N = 1 и вывести их в порядке возрастания рангов переславших их процессов (включая число, полученное из этого же процесса). MPIBegin79. В каждом процессе дано целое число N, которое может принимать два значения: 0 и 1 (имеется хотя бы один процесс с N = 1). Кроме того, в каждом процессе с N = 1 дано вещественное число A. Используя функцию MPI_Comm_split и одну коллективную операцию пересылки данных, переслать числа A во все процессы с N = 1 и вывести их в порядке возрастания рангов переславших их процессов (включая число, полученное из этого же процесса). MPIBegin80. В каждом процессе дано целое число N, которое может принимать два значения: 1 и 2 (имеется хотя бы один процесс с каждым из возможных значений). Кроме того, в каждом процессе дано целое число A. Используя функцию MPI_Comm_split и одну коллективную операцию пересылки данных, переслать числа A, данные в процессах с N = 1, во все процессы с N = 1, а числа A, данные в процессах с N = 2, во все процессы с N = 2. Во всех процессах вывести полученные числа в порядке возрастания рангов переславших их процессов (включая число, полученное из этого же процесса). MPIBegin81. В каждом процессе дано целое число N, которое может принимать два значения: 0 и 1 (имеется хотя бы один процесс с N = 1). Кроме того, в каждом процессе с N = 1 дано вещественное число A. Используя функцию MPI_Comm_split и одну коллективную операцию редукции, найти сумму всех исходных чисел A и вывести ее во всех процессах с N = 1. MPIBegin82. В каждом процессе дано целое число N, которое может принимать два значения: 1 и 2 (имеется хотя бы один процесс с каждым из возможных значений). Кроме того, в каждом процессе дано вещественное число A. Используя функцию MPI_Comm_split и одну коллективную операцию редукции, найти минимальное значение среди чисел A, которые даны в процессах с N = 1, и максимальное значение среди чисел A, которые даны в процессах с N = 2. Найденный минимум вывести в процессах с N = 1, а найденный максимум — в процессах с N = 2. 6.7 Виртуальные топологии MPIBegin83. В главном процессе дано целое число N (> 1), причем известно, что количество процессов K делится на N. Переслать число N во все процессы, после чего, используя функцию MPI_Cart_create, определить для всех процессов декартову топологию в виде двумерной решетки — матрицы размера N × K/N (порядок нумерации процессов оставить прежним). 79 Используя функцию MPI_Cart_coords, вывести для каждого процесса его координаты в созданной топологии. MPIBegin84. В главном процессе дано целое число N (> 1), не превосходящее количества процессов K. Переслать число K во все процессы, после чего определить декартову топологию для начальной части процессов в виде двумерной решетки — матрицы размера N × K/N (символ «/» обозначает операцию деления нацело, порядок нумерации процессов следует оставить прежним). Для каждого процесса, включенного в декартову топологию, вывести его координаты в этой топологии. MPIBegin85. Число процессов К является четным: K = 2N, N > 1. В процессах 0 и N дано по одному вещественному числу A. Определить для всех процессов декартову топологию в виде матрицы размера 2 × N, после чего, используя функцию MPI_Cart_sub, расщепить матрицу процессов на две одномерные строки (при этом процессы 0 и N будут главными процессами в полученных строках). Используя одну коллективную операцию пересылки данных, переслать число A из главного процесса каждой строки во все процессы этой же строки и вывести полученное число в каждом процессе (включая процессы 0 и N). MPIBegin86. Число процессов К является четным: K = 2N, N > 1. В процессах 0 и 1 дано по одному вещественному числу A. Определить для всех процессов декартову топологию в виде матрицы размера N × 2, после чего, используя функцию MPI_Cart_sub, расщепить матрицу процессов на два одномерных столбца (при этом процессы 0 и 1 будут главными процессами в полученных столбцах). Используя одну коллективную операцию пересылки данных, переслать число A из главного процесса каждого столбца во все процессы этого же столбца и вывести полученное число в каждом процессе (включая процессы 0 и 1). MPIBegin87 . Число процессов К кратно трем: K = 3N, N > 1. В процессах 0, N и 2N дано по N целых чисел. Определить для всех процессов декартову топологию в виде матрицы размера 3 × N, после чего, используя функцию MPI_Cart_sub, расщепить матрицу процессов на три одномерные строки (при этом процессы 0, N и 2N будут главными процессами в полученных строках). Используя одну коллективную операцию пересылки данных, переслать по одному исходному числу из главного процесса каждой строки во все процессы этой же строки и вывести полученное число в каждом процессе (включая процессы 0, N и 2N). MPIBegin88. Число процессов К кратно трем: K = 3N, N > 1. В процессах 0, 1 и 2 дано по N целых чисел. Определить для всех процессов декартову топологию в виде матрицы размера N × 3, после чего, используя функцию MPI_Cart_sub, расщепить матрицу процессов на три одномерных столбца (при этом процессы 0, 1 и 2 будут главными процессами в полученных столбцах). Используя одну коллективную операцию пересылки данных, переслать по одному исходному числу из главного процесса каждого 80 столбца во все процессы этого же столбца и вывести полученное число в каждом процессе (включая процессы 0, 1 и 2). MPIBegin89. Количество процессов K равно 8 или 12, в каждом процессе дано целое число. Определить для всех процессов декартову топологию в виде трехмерной решетки размера 2 × 2 × K/4 (порядок нумерации процессов оставить прежним). Интерпретируя эту решетку как две матрицы размера 2 × K/4 (в одну матрицу входят процессы с одинаковой первой координатой), расщепить каждую матрицу процессов на две одномерные строки. Используя одну коллективную операцию пересылки данных, переслать в главный процесс каждой строки исходные числа из процессов этой же строки и вывести полученные наборы чисел (включая числа, полученные из главных процессов строк). MPIBegin90. Количество процессов K равно 8 или 12, в каждом процессе дано целое число. Определить для всех процессов декартову топологию в виде трехмерной решетки размера 2 × 2 × K/4 (порядок нумерации процессов оставить прежним). Интерпретируя полученную решетку как K/4 матриц размера 2 × 2 (в одну матрицу входят процессы с одинаковой третьей координатой), расщепить эту решетку на K/4 указанных матриц. Используя одну коллективную операцию пересылки данных, переслать в главный процесс каждой из полученных матриц исходные числа из процессов этой же матрицы и вывести полученные наборы чисел (включая числа, полученные из главных процессов матриц). MPIBegin91. Количество процессов K равно 8 или 12, в каждом процессе дано вещественное число. Определить для всех процессов декартову топологию в виде трехмерной решетки размера 2 × 2 × K/4 (порядок нумерации процессов оставить прежним). Интерпретируя эту решетку как две матрицы размера 2 × K/4 (в одну матрицу входят процессы с одинаковой первой координатой), расщепить каждую матрицу процессов на K/4 одномерных столбцов. Используя одну коллективную операцию редукции, для каждого столбца процессов найти произведение исходных чисел и вывести найденные произведения в главных процессах каждого столбца. MPIBegin92. Количество процессов K равно 8 или 12, в каждом процессе дано вещественное число. Определить для всех процессов декартову топологию в виде трехмерной решетки размера 2 × 2 × K/4 (порядок нумерации процессов оставить прежним). Интерпретируя полученную решетку как K/4 матриц размера 2 × 2 (в одну матрицу входят процессы с одинаковой третьей координатой), расщепить эту решетку на K/4 указанных матриц. Используя одну коллективную операцию редукции, для каждой из полученных матриц найти сумму исходных чисел и вывести найденные суммы в каждом процессе соответствующей матрицы. MPIBegin93. В главном процессе даны положительные целые числа M и N, произведение которых не превосходит количества процессов; кроме того, в процессах с рангами от 0 до M·N − 1 даны целые числа X и Y. Переслать 81 числа M и N во все процессы, после чего определить для начальных M·N процессов декартову топологию в виде двумерной решетки размера M × N, являющейся периодической по первому измерению (порядок нумерации процессов оставить прежним). В каждом процессе, входящем в созданную топологию, вывести ранг процесса с координатами X, Y (с учетом периодичности), используя для этого функцию MPI_Cart_rank. В случае недопустимых координат вывести −1. MPIBegin94. В главном процессе даны положительные целые числа M и N, произведение которых не превосходит количества процессов; кроме того, в процессах с рангами от 0 до M·N − 1 даны целые числа X и Y. Переслать числа M и N во все процессы, после чего определить для начальных M·N процессов декартову топологию в виде двумерной решетки размера M × N, являющейся периодической по второму измерению (порядок нумерации процессов оставить прежним). В каждом процессе, входящем в созданную топологию, вывести ранг процесса с координатами X, Y (с учетом периодичности), используя для этого функцию MPI_Cart_rank. В случае недопустимых координат вывести −1. MPIBegin95. В каждом подчиненном процессе дано вещественное число. Определить для всех процессов декартову топологию в виде одномерной решетки и осуществить простой сдвиг исходных данных с шагом −1 (число из каждого подчиненного процесса пересылается в предыдущий процесс). Для определения рангов посылающих и принимающих процессов использовать функцию MPI_Cart_shift, пересылку выполнять с помощью функций MPI_Send и MPI_Recv. Во всех процессах, получивших данные, вывести эти данные. MPIBegin96. Число процессов К является четным: K = 2N, N > 1; в каждом процессе дано вещественное число A. Определить для всех процессов декартову топологию в виде матрицы размера 2 × N (порядок нумерации процессов оставить прежним) и для каждой строки матрицы осуществить циклический сдвиг исходных данных с шагом 1 (число A из каждого процесса, кроме последнего в строке, пересылается в следующий процесс этой же строки, а из последнего процесса — в главный процесс этой строки). Для определения рангов посылающих и принимающих процессов использовать функцию MPI_Cart_shift, пересылку выполнять с помощью функции MPI_Sendrecv. Во всех процессах вывести полученные данные. MPIBegin97. Количество процессов K равно 8 или 12, в каждом процессе дано вещественное число. Определить для всех процессов декартову топологию в виде трехмерной решетки размера 2 × 2 × K/4 (порядок нумерации процессов оставить прежним). Интерпретируя полученную решетку как K/4 матриц размера 2 × 2 (в одну матрицу входят процессы с одинаковой третьей координатой, матрицы упорядочены по возрастанию третьей координаты), осуществить циклический сдвиг исходных данных из процессов каждой матрицы в соответствующие процессы следующей матрицы (из 82 процессов последней матрицы данные перемещаются в первую матрицу). Для определения рангов посылающих и принимающих процессов использовать функцию MPI_Cart_shift, пересылку выполнять с помощью функции MPI_Sendrecv_replace. Во всех процессах вывести полученные данные. MPIBegin98. Число процессов K является нечетным: K = 2N + 1 (1 < N < 5); в каждом процессе дано целое число A. Используя функцию MPI_Graph_create, определить для всех процессов топологию графа, в которой главный процесс связан ребрами со всеми процессами нечетного ранга (1, 3, …, 2N − 1), а каждый процесс четного положительного ранга R (2, 4, …, 2N) связан ребром с процессом ранга R − 1 (в результате получается N-лучевая звезда, центром которой является главный процесс, а каждый луч состоит из двух подчиненных процессов R и R + 1, причем ближайшим к центру является процесс нечетного ранга R). Переслать число A из каждого процесса всем процессам, связанным с ним ребрами (процессам-соседям). Для определения количества процессов-соседей и их рангов использовать функции MPI_Graph_neighbors_count и MPI_Graph_neighbors, пересылку выполнять с помощью функций MPI_Send и MPI_Recv. Во всех процессах вывести полученные числа в порядке возрастания рангов переславших их процессов. MPIBegin99 . Число процессов K является четным: K = 2N (1 < N < 6); в каждом процессе дано целое число A. Используя функцию MPI_Graph_create, определить для всех процессов топологию графа, в которой все процессы четного ранга (включая главный процесс) связаны в цепочку: 0 — 2 — 4 — 6 — … — (2N − 2), а каждый процесс нечетного ранга R (1, 3, …, 2N − 1) связан с процессом ранга R − 1 (в результате каждый процесс нечетного ранга будет иметь единственного соседа, первый и последний процессы четного ранга будут иметь по два соседа, а остальные — «внутренние» — процессы четного ранга будут иметь по три соседа). Переслать число A из каждого процесса всем процессам-соседям. Для определения количества процессов-соседей и их рангов использовать функции MPI_Graph_neighbors_count и MPI_Graph_neighbors, пересылку выполнять с помощью функции MPI_Sendrecv. Во всех процессах вывести полученные числа в порядке возрастания рангов переславших их процессов. MPIBegin100. Количество процессов K равно 3N + 1 (1 < N < 5); в каждом процессе дано целое число A. Используя функцию MPI_Graph_create, определить для всех процессов топологию графа, в которой процессы R, R + 1, R + 2, где R = 1, 4, 7, …, связаны между собой ребрами, и, кроме того, каждый процесс положительного ранга, кратного трем (3, 6, …), связан ребром с главным процессом (в результате получается N-лучевая звезда, центром которой является главный процесс, а каждый луч состоит из трех связанных между собой процессов, причем с центром связан процесс ранга, кратного трем). Переслать число A из каждого процесса всем процессамсоседям. Для определения количества процессов-соседей и их рангов ис- 83 пользовать функции MPI_Graph_neighbors_count и MPI_Graph_neighbors, пересылку выполнять с помощью функции MPI_Sendrecv. Во всех процессах вывести полученные числа в порядке возрастания рангов переславших их процессов. 84 Литература 1. Антонов А. С. Параллельное программирование с использованием технологии MPI. — М.: Изд-во МГУ, 2004. — 71 с. 2. Букатов А. А., Дацюк В. Н., Жегуло А. Н. Программирование для многопроцессорных вычислительных систем. — Ростов н/Д: ЦВВР, 2003. — 208 с. 3. Корнеев В. Д. Параллельное программирование в MPI. — Новосибирск: Изд-во ИВМиМГ СО РАН, 2002. — 215 с. 4. Немнюгин С. А., Стесик О. Л. Параллельное программирование для многопроцессорных вычислительных систем. — СПб: БХВ–Петербург, 2002. — 396 с. 5. Шпаковский Г. И., Серикова Н. В. Программирование для многопроцессорных систем в стандарте MPI. — Минск: БГУ, 2002. — 323 с. 6. Message Passing Interface Forum. MPI: A message-passing interface standard. International Journal of Supercomputer Applications, 8 (3/4), 1994. Special issue on MPI. 85 Содержание Предисловие ......................................................................................................... 3 1 Модуль № 1. Выполнение программ в параллельном режиме ................ 5 1.1 Комплексная цель ................................................................................... 5 1.2 Содержание модуля ................................................................................ 5 1.3 Проектное задание................................................................................ 19 1.4 Тест рубежного контроля .................................................................... 20 2 Модуль № 2. Пересылка сообщений между двумя процессами ............. 21 2.1 Комплексная цель ................................................................................. 21 2.2 Содержание модуля .............................................................................. 21 2.3 Проектное задание................................................................................ 28 2.4 Тест рубежного контроля .................................................................... 30 3 Модуль № 3. Операции редукции и составные типы данных ................. 30 3.1 Комплексная цель ................................................................................. 30 3.2 Содержание модуля .............................................................................. 30 3.3 Проектное задание................................................................................ 38 3.4 Тест рубежного контроля .................................................................... 40 4 Модуль № 4. Коллективные операции и создание новых коммуникаторов ............................................................................... 41 4.1 Комплексная цель ................................................................................. 41 4.2 Содержание модуля .............................................................................. 41 4.3 Проектное задание................................................................................ 46 4.4 Тест рубежного контроля .................................................................... 48 5 Модуль № 5. Использование виртуальных топологий ............................ 49 5.1 Комплексная цель ................................................................................. 49 5.2 Содержание модуля .............................................................................. 49 5.3 Проектное задание................................................................................ 65 5.4 Тест рубежного контроля .................................................................... 66 6 Приложение. Формулировки учебных заданий ........................................ 67 6.1 Процессы и их ранги ............................................................................ 67 6.2 Обмен сообщениями между отдельными процессами ..................... 68 6.3 Коллективная пересылка данных ....................................................... 71 6.4 Коллективные операции редукции ..................................................... 73 6.5 Производные типы и упаковка данных .............................................. 75 6.6 Группы процессов и коммуникаторы................................................. 76 6.7 Виртуальные топологии....................................................................... 78 Литература ......................................................................................................... 84