Розподілені обчислення на FreePascal під Windows h2>
Ілля Аввакумов, Freepascal.ru p>
Введення. Про що ця стаття. B>
p>
Стаття
присвячена питанню розробки розподілених (паралельних) обчислень з
використанням компілятора FreePascal (використовувалася версія 2.0.1) p>
Проблема
паралельних обчислень зацікавила мене зовсім не тому що це зараз
модно. Зіткнувся із завданням, коли треба було сформувати (для дальнейнего
аналізу) великий масив даних. Хотілося зменшити час обчислень наявними
засобами. Виявляється, організувати паралельні обчислення з використанням
мого улюбленого компілятора - цілком розв'язувана задача. p>
Стандартом
для паралельних програм для багатопроцесорних обчислювальних систем
де-факто є MPI. p>
Ідея
MPI-програми така: паралельна програма представляється у вигляді безлічі
взаємодіючих (за допомогою комунікаційних процедур MPI) процесів. p>
Паралельні
обчислення вимагають p>
1.
Підрозділи процесів p>
2.
Взаємодія між ними p>
MPI
(Message Passing Interface) - стандарт на програмний інструментарій для
забезпечення зв'язку між гілками паралельного додатка. p>
В
цій статті розглядається MPICH (MPI CHameleon), що вільно розповсюджується
реалізація MPI. Використовувалася версія MPICH 1.2.5 для Windows. p>
Встановлення та налаштування MPICH. b>
p>
MPICH
для Windows вимагає p>
1. Windows NT4/2000/XP (
Professional або
Server). Під Win9x/ME працювати не буде! p>
2.
Мережеве підключення по протоколу TCP/IP між машинами. p>
Відразу
обговорити, що всі приклади тестувалися на двох машинах, об'єднаних у
локальну мережу. Один комп'ютер (мережне ім'я ILYA) - мій, а другий (мережне ім'я
EKATERINA) - дружини. p>
Установка. p>
Комп'ютери,
що беруть участь в обчисленнях, назвемо кластером. MPICH повинен бути встановлений на
кожному комп'ютері в кластері. p>
Для
установки потрібно p>
1.
Завантажити mpich.nt.1.2.5.src.exe (5278 Кб) або mpich.nt.1.2.5.src.zip (5248 Кб) p>
Або
з офіційної сторінки MPICH p>
http://www.mcs.anl.gov/mpi/mpich/download.html
p>
Або
з ftp сервера ftp.mcs.anl.gov/pub/mpi/nt. p>
2.
Якщо запустити exe файл, то після розпакування запуститься інтерактивна програма
установки MPICH. Щоб не втомлювати себе вибором встановлюваних компонент,
зручніше встановити MPICH в неінтерактивному режимі. p>
Для
цього p>
а.
Разархівіруйте вміст до спільної папки (наприклад, ILYAcommon) p>
b.
Відредагуйте файл setup.iss p>
c.
Рядок p>
szDir = C: Program
FilesMPICH p>
визначає
каталог, куди встановиться MPICH. Це розташування можна змінити. p>
d.
Строки p>
Component-count = 7 p>
Component-0 = runtime dlls p>
Component-1 = mpd p>
Component-2 = SDK p>
Component-3 = Help p>
Component-4 = SDK.gcc p>
Component-5 = RemoteShell p>
Component-6 = Jumpshot p>
визначають число встановлюваних компонент. Для головного комп'ютера
(звідки запускається головний процес) відповідні опції такі p>
Component-count = 4 p>
Component-0 = runtime dlls p>
Component-1 = mpd p>
Component-2 = SDK p>
Component-3 = Help p>
Для
простого комп'ютера (якому відводиться лише роль обчислювача) число
компонент може бути скорочено до двох. p>
Component-count = 2 p>
Component-0 = runtime dlls p>
Component-1 = mpd p>
На
кожному комп'ютері кластеру виконати команду установки в неінтерактивному
режимі. У моєму випадку запуск програми установки такий: p>
> ILYAcommonsetup
-s-f1ILYAcommonsetup.iss p>
Після
встановлення на кожному комп'ютері має запуститися служба mpich_mpd (MPICH
Daemon (C) 2001 Argonne National Lab). (дивіться малюнок) p>
Якщо
був встановлений компонент SDK (що необхідно зробити на тому комп'ютері, звідки
буде проводитися запуск програм), то в каталозі MPICH (прописаному в пункті
szDir) присутні підкаталоги SDK і SDK.gcc. Вміст цих каталогів --
бібліотечні та заголовки для мов C, С + + та Fortran. p>
Каталог
SDK призначений для компіляторів MS VC + + 6.x і Compaq Visual Fortran 6.x, а
каталог SDK.gcc - для компіляторів gcc і g77. p>
Налаштування p>
Настройку
можна здійснити за допомогою простих утиліт, що є в дистрибутиві. p>
Зупинимося
докладніше на каталозі mpdbin в директорії MPICH. Вміст каталогу: p>
mpd.exe p>
виконуваний файл служби mpich_mpd p>
потрібна p>
MPIRun.exe p>
файл, який здійснює запуск кожної MPI-програми. p>
потрібна p>
MPIRegister.exe p>
програма для шифрування паролів при обміні даними по
LAN. p>
іноді корисна p>
MPDUpdate.exe p>
програма для оновлення бібліотек MPI p>
не потрібна p>
MPIConfig.exe p>
програма налаштування хостів у кластері p>
не потрібна p>
guiMPIRun.exe p>
GUI версія mpirun. p>
не потрібна p>
MPIJob.exe p>
програма для управління MPI-процесами p>
не потрібна p>
guiMPIJob.exe p>
GUI версія mpijob.exe p>
не потрібна p>
Використання
команд mpirun і mpiregister чекає нас попереду. Щоб упевнитися, що служби
MPICH, що працюють на різних комп'ютерах, взаємодіють належним чином, можна
скористатися утилітою MPIconfig. Для цього слід p>
1.
Запустити MPIConfig.exe (можна скористатися посиланням у головному меню, вона там
повинна бути) p>
2.
Натиснути на кнопку "Select" p>
3.
У вікні, що з'явилося вибрати пункт меню "Action" - "Scan hosts"
p>
4.
Навпаки імені кожної машини повинна зайнятися піктограма "MPI" (
приблизно ось так) p>
Модуль
mpi на FreePascal. p>
Всі
вищеописане відносилося до установки власне MPICH. Для того, щоб
прикрутити бібліотеки MPICH до FreePascal, варто ще трохи попрацювати. p>
Cледует
скористатися динамічної бібліотекою mpich.dll, яка розташовується в
системному каталозі (копіюється туди при установці MPICH). p>
1.
Завантажити модуль FreePascal, що реалізує функції цієї динамічної бібліотеки.
Файл mpi.pp завантажити zip-архів (10 КБ) p>
2.
Для використання модуля mpi слід просто скопіювати файл mpi.pp у каталог
де FreePascal шукає модулі (unit searchpath). p>
Модуль
написаний з використанням утиліти h4pas.exe і заголовків файлів *. h з
SDKInclude. p>
Найпростіша MPI програма на FreePascal.
p>
Під
іменах всіх функціях бібліотеки MPICH використовується префікс MPI_. Повертане
значення більшості функцій - 0, якщо виклик був успішним, а інакше - код
помилки. p>
Основні
функції. p>
Основні
функції MPI, за допомогою яких можна організувати паралельне обчислення p>
1 p>
MPI_Init p>
підключення до MPI p>
2 p>
MPI_Finalize p>
завершення роботи з MPI p>
3 p>
MPI_Comm_size p>
визначення розміру області взаємодії p>
4 p>
MPI_Comm_rank p>
визначення номеру процесу p>
5 p>
MPI_Send p>
стандартна блокуюча передача p>
6 p>
MPI_Recv p>
блокуючий прийом p>
Стверджується,
що цього вистачить. Причому перші чотири функції повинні викликатися тільки один
разів, а власне взаємодія процесів - це останні два пункти. p>
Опис
функцій, що здійснюють передачу, залишимо на потім, а зараз розглянемо
опис функцій ініціалізації/завершення p>
function MPI_Init (var argc:
longint; p>
var argv: ppchar): longint; p>
Ініціалізація MPI. Аргументи argc і argv --
змінні модуля system, що визначають число параметрів командного рядка і самі
ці параметри, відповідно. p>
При
успішному виконанні функції MPI_Init створюється комунікатор (область
взаємодії процесів), під ім'ям MPI_COMM_WORLD. p>
function MPI_Comm_size (comm:
MPI_Comm; p>
var
nump: longint): longint; p>
Визначає
число процесів, що входять в комунікатор comm. p>
function MPI_Comm_rank (comm:
MPI_Comm; p>
var proc_id: longint): longint; p>
Визначається
ранг процесу всередині комунікатора. Після виклику цієї функції всі процеси,
запущені завантажувачем MPI-програми, отримують свій унікальний номер (значення
що підлягає поверненню змінної proc_id у всіх різне). Після виклику функції
MPI_Comm_rank можна, таким чином, призначати різним процесам різні
обчислення. p>
functionnn MPI_Finalize: longint; p>
Завершує роботу з MPI. p>
Порядок
виклику такий: p>
1.
MPI_Init - підключення до MPI p>
2.
MPI_Comm_size - визначення розміру області взаємодії p>
3.
MPI_Comm_rank - визначення номеру процесу p>
4.
Далі йде будь-яка сукупність команд обміну (передача, прийом, і тп.) P>
5.
MPI_Finalize - завершення роботи з MPI p>
Найпростіша
MPI програма така. p>
test.pas
p>
uses mpi; p>
var namelen, numprocs, myid:
longint; p>
processor_name: pchar; p>
begin p>
MPI_Init (argc, argv); p>
MPI_Comm_size (MPI_COMM_WORLD,
numprocs); p>
MPI_Comm_rank (MPI_COMM_WORLD,
myid); p>
GetMem (processor_name,
MPI_MAX_PROCESSOR_NAME +1);// константа MPI_MAX_PROCESSOR_NAME дорівнює 256 p>
namelen: = MPI_MAX_PROCESSOR_NAME; p>
MPI_Get_processor_name (
processor_name, namelen); p>
Writeln ( 'Hello from', myid, 'on',
processor_name); p>
FreeMem (processor_name); p>
MPI_Finalize; p>
end. p>
Тут,
як видно, ніякого обміну немає, кожен процес тільки "доповідає"
свій ранг. p>
Для
наочності виводиться також ім'я комп'ютера, де кожен запущений процес. Для його
визначення використовується функція MPI_Get_processor_name. p>
function MPI_Get_processor_name (
proc_name: Pchar; p>
var name_len: longint): longint; p>
При
успішному виклику цієї функції мінлива proc_name містить рядок з ім'ям
комп'ютера, а name_len - довжину цього рядка. p>
Після
компіляції (з відповідними опціями) p>
> fpc
-dRELEASE [-Fu <каталог, де розміщено файл mpi.pp>] test.pas p>
повинен
з'явитися виконуваний файл test.exe, проте рано радіти. Запуск цього
exe-файлу не є запуск паралельної програми. p>
Запуск MPI-програми.
p>
Запуск
MPI-програми здійснюється за допомогою завантажувача програми mpirun. Формат
виклику такий: p>
> mpirun
[ключі mpirun] програма [ключі програми] p>
Ось
деякі з опцій команди mpirun: p>
-np x p>
запуск x процесів. Значення x може не збігатися з числом
комп'ютерів у кластері. У цьому випадку на деяких машинах запуститься
кілька процесів. Те, як вони будуть розподілені, mpirun вирішить сам
(залежить від установок, зроблених програмою MPIConfig.exe) p>
-localonly x p>
-np x-localonly p>
запуск x процесів тільки на локальній машині p>
-machinefile filename p>
використовувати файл з іменами машин p>
-hosts n
host1 host2 ... hostn p>
-hosts n
host1 m1 host2 m2 ... hostn mn p>
запустити на n явно зазначених машинах. Якщо при цьому явно
вказати число процесів на кожній з машин, то опція-np стає
необов'язковою p>
-map drive: hostshare p>
використовувати тимчасовий диск p>
-dir
drive: myworkingdirectory p>
запускати процеси у вказаній директорії p>
-env
"var1 = val1 | var2 = val2 | var3 = val3 ..." p>
присвоїти значення змінних оточення p>
-logon p>
запитати ім'я користувача і пароль p>
-pwdfile filename p>
використовувати вказаний файл для зчитування імені
користувача та пароля. p>
Перший рядок у файлі повинна містити ім'я користувача, а
другий - його пароль) p>
-nocolor p>
придушити висновок від процесів різними кольорами p>
-priority class [: level] p>
встановити клас пріоритету процесів і, опціонально,
рівень пріоритету. p>
class =
0,1,2,3,4 = idle, below, normal, above, high p>
level =
0,1,2,3,4,5 = idle, lowest, below, normal, above, highest p>
за замовчуванням використовується-priority 1:3, то є дуже
низький пріоритет. p>
Для
організації паралельного обчислення на декількох машинах слід p>
1.
На кожному комп'ютері, що входить в кластер, завести користувача з одним і тим же
ім'ям (наприклад, MPIUSER) та паролем (я дав йому пароль "1"), з
обмеженими привілеями. p>
2.
На головному комп'ютері (в моєму випадку це, зрозуміло, ILYA) створити мережеву
папку (наприклад, COMMON). Слід потурбуватися, щоб користувач MPIUSER мав
до неї повний доступ. p>
3.
В той же папці створити файл, що містить ім'я користувача, від чийого імені будуть
запускатися процеси, а також його пароль. У моєму випадку вміст цього файлу
має бути таким: p>
mpiuser p>
1 p>
Я
назвав це файл lgn. p>
Після
всіх цих дій запуск MPI програми test здійснити можна як p>
> mpirun-pwdfile
ILYACOMMONlgn-hosts 2 ILYA 1 EKATERINA 1 ILYACOMMONtest.exe p>
Змінивши
відповідні опції, можна запускати різне число процесів. Наприклад p>
> mpirun-pwdfile
ILYACOMMONlgn-hosts 2 ILYA 3 EKATERINA 3 ILYACOMMONtest.exe p>
На
малюнку видно результат такого дзвінка. Висновок від різних процесів виділяється
різним кольором, оскільки опція-nocolor відключена. Зверніть увагу на те,
що послідовність номер виводиться рядка зовсім не збігається з номером
процесу. Цей порядок буде змінюватися від випадку до випадку. p>
На
цьому малюнку зображено Диспетчер завдань під час запуску на комп'ютері EKATERINA
чотирьох процесів. Встановлено пріоритет за замовчуванням. p>
Утиліта
MPIRegister.exe. P>
Оскільки
комп'ютери ILYA і EKATERINA об'єднані в локальну мережу, у мене немає ніяких
проблем з безпекою. Пароль для користувача mpiuser зберігається у відкритому
вигляді у файлі lgn. На жаль, так можна робити далеко не завжди. Якщо комп'ютери,
що входять в кластер, є частиною більш розгалуженої мережі, або, більше того,
використовують підключення до Internet, так робити не просто не бажано, а
неприпустимо. p>
В
таких випадках слід зберігати пароль користувача, від імені якого будуть
запускатися процеси, в системному реєстрі Windows в зашифрованому вигляді. Для
цього призначена програма MPIRegister.exe. p>
Опції
такі p>
mpiregister p>
Запит ім'я користувача і пароль (двічі). Після
введення запитує, зробити чи установки постійними. При відповіді 'yes' дані
будуть збережені на диску, а інакше - залишаться в оперативній пам'яті і при
перезавантаження буде втрачено. p>
mpiregister-remove p>
Видаляє дані про користувача і пароль. p>
mpiregister-validate p>
Перевіряє правильність збережених даних. p>
Запускати
mpiregister слід тільки на головному комп'ютері. Завантажувач програми mpirun
без опції-pwdfile буде запитувати дані, збережені програмою
mpiregister. Якщо таких не виявить, то запитає ім'я користувача і пароль
сам. p>
Більш складні програми.
p>
Зараз,
коли запрацювала найпростіша програма, можна почати освоювати функції обміну
даними - саме те, що дозволяє здійснити взаємодія між процесами.
p>
Опції
двоточковим обміну. p>
блокуюча
передача (прийом) - означає, що програма припиняє своє виконання, до
тих пір, поки передача (прийом) не завершиться. Це гарантує саме той
порядок виконання операцій передачі (прийому), який заданий в програмі. p>
блокуюча
передача здійснюється за допомогою функції MPI_Send. p>
function MPI_Send (buf: pointer; p>
count: longint; p>
datatype: MPI_Datatype; p>
destination: longint; p>
tag: longint; p>
comm: MPI_Comm): longint; p>
Здійснює
передачу count елементів зазначеного типу процесу під номером destination. p>
buf p>
- адреса першого елемента в буфері передачі p>
count p>
- кількість переданих елементів у буфері p>
datatype p>
- MPI-тип цих елементів p>
destination p>
- ранг процесу-одержувача (приймає значення від нуля до
n-1, де n - повне число процесів) p>
tag p>
- тег повідомлення p>
comm p>
- комунікатор p>
В
як MPI-типу слід вказати один із зазначених нижче типів. Більшості
базових типів Паскаля відповідає свій MPI-тип. Усі вони перераховані у
наступній таблиці. Останній стовпчик вказує на кількість байт, необхідних для
зберігання однієї змінної відповідного типу. p>
MPI_CHAR p>
shortint p>
1 p>
MPI_SHORT p>
smallint p>
2 p>
MPI_INT p>
longint p>
4 p>
MPI_LONG p>
longint p>
4 p>
MPI_UNSIGNED_CHAR p>
byte p>
1 p>
MPI_UNSIGNED_SHORT p>
word p>
2 p>
MPI_UNSIGNED p>
longword p>
4 p>
MPI_UNSIGNED_LONG p>
longword p>
4 p>
MPI_FLOAT p>
single p>
4 p>
MPI_DOUBLE p>
double p>
8 p>
MPI_LONG_DOUBLE p>
double p>
8 p>
MPI_BYTE p>
untyped data p>
1 p>
MPI_PACKED p>
складовою тип p>
- p>
Змінна
tag - допоміжна цілочисельних змінна. p>
MPI-тип
MPI_PACKED використовується при передачі даних похідних типів
(сконструйованих з базових типів). Їх розгляд виходить за рамки цієї
статті. p>
Функція
MPI_Recv реалізує блокуючий отримання даних. p>
function MPI_Recv (buf: pointer; p>
count: longint; p>
datatype: MPI_Datatype; p>
source: longint; p>
tag: longint; p>
comm: MPI_Comm; p>
var status: MPI_Status): longint; p>
buf p>
- початкова адреса буфера прийому p>
count p>
- максимальна кількість прийнятих елементів у буфері p>
datatype p>
- MPI-тип цих елементів p>
source p>
- ранг джерела p>
tag p>
- тег повідомлення p>
comm p>
- комунікатор p>
status p>
- статус обміну p>
Ця
функція здійснює запит на отримання даних. При її виклику процес буде
очікувати надходження даних від процесу під номером source. Якщо та?? овой НЕ
буде, то це призведе до повісанію програми (глухий кут). Так що при
використання цих функцій слід проявляти пильність. p>
Число
прийнятих елементів може бути менше значення змінної count. Якщо ж
посилають дані мають більший розмір, то буде виведено попередження про
обривання передачі. p>
Возвращаемая
мінлива status містить інформацію про передачу. Наприклад, її можна
використовувати, щоб визначити фактичну кількість прийнятих елементів. Для цього використовується функція MPI_Get_count p>
function MPI_Get_count (var status:
MPI_Status; p>
datatype: MPI_Datatype; p>
var count: longint): longint; p>
Число
фактично прийнятих елементів - у повертається змінної count. p>
Використання
функцій двоточковим обміну. p>
В
наступному прикладі обчислення значень елементів масиву "розлучається"
по двох процесів p>
uses mpi; p>
const num = 10; p>
var p>
teg, numprocs, myid: longint; p>
i: longint; p>
status: MPI_Status; p>
z, x: double; p>
arr: array [0 .. num] of double; p>
function f (x: double): double; p>
begin p>
f: = sqr (x); p>
end; p>
begin p>
MPI_Init (argc, argv); p>
teg: = 0; p>
MPI_Comm_size (MPI_COMM_WORLD,
numprocs); p>
MPI_Comm_rank (MPI_COMM_WORLD, myid); p>
for i: = 0 to num do p>
case myid of p>
0: p>
if i mod 2 = 0 then arr [i]: =
f (1.0 * i) p>
else p>
begin p>
MPI_Recv (@ x, 1, MPI_DOUBLE, 1, teg, MPI_COMM_WORLD, status); p>
arr [i]: = x p>
end; p>
1: p>
if i mod 2 = 1 then p>
begin p>
z: = f (1.0 * i); p>
MPI_Send (@ z, 1, MPI_DOUBLE, 0, teg, MPI_COMM_WORLD); p>
end; p>
end;// case statement p>
if myid = 0 then for i: = 0 to num
do writeln (i, '', arr [i ]); p>
MPI_Finalize; p>
end. p>
Формується
масив певної кількості елементів так, що елементи з парними номерами
розраховує процес з myid = 0, а непарними - з myid = 1. Звичайно, замість функції
sqr може стояти будь-яка інша. Програма написана, звичайно ж, у розрахунку на те,
що процесів буде всього два. Оскільки значення myid, відмінні від 0 і 1, не
використовуються, процеси з такими номерами будуть простоювати. p>
Поліпшити
програму, тобто написати такий її варіант, щоб використовувалися всі
процеси, надаю читачеві:) p>
Опції
колективного обміну. p>
Колективний
обмін даними зачіпає не два процеси, а всі процеси всередині комунікатора.
p>
Найпростішими
(і найбільш часто використовуваними) різновидами такого виду взаємодії
процесів є розсилка MPI_Bcast і колективний збір даних MPI_Reduce. p>
function MPI_Bcast (buff: pointer; p>
count: longint; p>
datatype: MPI_Datatype; p>
root: longint; p>
comm: MPI_Comm): longint; p>
buf p>
- адреса першого елемента буфера передачі p>
count p>
- максимальна кількість прийнятих елементів у буфері p>
datatype p>
- MPI-тип цих елементів p>
root p>
- ранг джерела розсилки p>
comm p>
- комунікатор p>
Функція
MPI_Bcast реалізує "трансляцію передачу". Один процес (
головний або root процес) розсилає всім (і собі, у тому числі) повідомлення довжини
count, а інші отримують це повідомлення. p>
function MPI_Reduce (buf: pointer; p>
result: pointer; p>
count: longint; p>
datatype: MPI_Datatype; p>
operation: MPI_Op; p>
root: longint; p>
comm
: MPI_Comm): longint; p>
buf p>
- адреса першого елемента буфера передачі p>
count p>
- кількість елементів у буфері передачі p>
datatype p>
- MPI-тип цих елементів p>
operation p>
- операція приведення p>
root p>
- ранг головного процесу p>
comm p>
- комунікатор p>
Функція
MPI_Reduce виконує операцію приведення над масивів даних buf, отриманими від
всіх процесів, і пересилає результат у result одному процесу (ранг якого
визначений параметром root). p>
Як
і функція MPI_Bcast, ця функція повинна викликатися всіма процесами в заданому
комунікаторі, і аргументи count, datatype і operation повинні збігатися. p>
Є
12 наперед визначених операцій приведення p>
MPI_MAX p>
максимальне значення p>
MPI_MIN p>
мінімальне значення p>
MPI_SUM p>
сумарне значення p>
MPI_PROD p>
значення твори всіх елементів p>
MPI_LAND p>
логічне "і" p>
MPI_BAND p>
побітове "і" p>
MPI_LOR p>
логічне "або" p>
MPI_BOR p>
побітове "або" p>
MPI_LXOR p>
логічне що виключає "або" p>
MPI_BXOR p>
побітове що виключає "або" p>
MPI_MAXLOC p>
індекс максимального елементу p>
MPI_MINLOC p>
індекс мінімального елементу p>
Використання
колективних функцій (обчислення числа