- •Лекция 1 Классификация многопроцессорных систем
- •Сетевой закон Амдала
- •Лекция 2 Техническая реализация многопроцессорных систем
- •Лекция 3 Система передачи сообщений mpi
- •Принципы работы mpich
- •Лекция 4 Структура mpi-программы
- •Int main(int argc, char* argv[]) { int procs_rank, procs_count; // Определяем переменные
- •Int mpi_Init (int* argc, char*** argv)
- •Int mpi_Comm_size(mpi_Comm comm, int *size)
- •Int mpi_Comm_rank(mpi_Comm comm, int *rank)
- •Int mpi_Finalize(void)
- •Определение времени выполнения mpi программы
- •Понятие коммуникатора
- •Лекция 5 Передача сообщений
- •Общие сведения о функциях передачи сообщений
- •Передача сообщения «точка-точка»
- •Int mpi_Send(void *buf, int count, mpi_Datatype type, int dest, int tag, mpi_Comm comm)
- •Прием сообщений «точка-точка» с блокировкой
- •Int mpi_Recv(void *buf, int count, mpi_Datatype type, int source, int tag, mpi_Comm comm, mpi_Status *status)
- •Int main(int argc, char* argv[])
- •Int ProcNum, ProcRank, RecvRank;
- •Лекция 6 Определение времени выполнения программы
- •Int s1; // Место под результат
- •Int size; // Размер принимаемого массива
- •Int Summa; // Сумма его элементов
- •Input n: 1000
- •Лекция 7 Функции неблокирующего обмена данными
- •Int mpi_Test(mpi_Request *request, int *flag,
- •Int mpi_Wait(mpi_Request *request, mpi_status *status)
- •Одновременное выполнение приема и передачи данных
- •Int mpi_Sendrecv (void *sbuf, int scount,
- •Int stag, void *rbuf, int rcount,
- •Int rtag, mpi_Comm comm,
- •Int mpi_Sendrecv_replace (void *buf, int count,
- •Int stag, int source,
- •Int rtag, mpi_Comm comm,
- •Лекция 8 Коллективные операции передачи данных
- •Функция синхронизации процессов
- •Int mpi_Barrier(mpi_Comm comm);
- •Широковещательная рассылка данных
- •Int mpi_Bcast (void* buffer, int count,
- •Int main(int argc, char* argv[])
- •Int proc_rank, proc_count;
- •Широковещательная рассылка индивидуальных данных
- •Int mpi_Scatter
- •Void *rbuf, int rcount, mpi_Datatype rtype,
- •Int root, mpi_Comm comm)
- •Лекция 9 Функции сбора блоков данных от всех процессов группы
- •Int mpi_Gather
- •Void* recvbuf, int recvcount, mpi_Datatype recvtype,
- •Int root, mpi_Comm comm);
- •Int array[100];
- •Int root, *rbuf, proc_count;
- •Int mpi_Allgather
- •Void *rbuf, int rcount, mpi_Datatype rtype,
- •Передача данных от всех процессов всем процессам
- •Int mpi_Alltoall
- •Void *rbuf, int rcount, mpi_Datatype rtype,
- •Лекция 10 Редукция данных
- •Int mpi_Reduce (void *sendbuf, void *recvbuf,
- •Int count, mpi_Datatype type,
Int s1; // Место под результат
// Ждем сообщение от процесса 1
MPI_Recv (&S1, 1, MPI_INT, 1, 0, MPI_COMM_WORLD, &Status);
S=S+S1; // Складываем обе суммы
cout << "S = " << S << endl;
delete [] mas; // Освобождаем память, выделенную под массив
} // if - процесс 0
//===================================================================
if (proc_rank==1) // Если это процесс с номером 1
{
Int size; // Размер принимаемого массива
Int Summa; // Сумма его элементов
// Готовимся принять размер массива
// Ждем сообщение от процесса 0
MPI_Recv (&size, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &Status);
size=size/2; // Мы получили размер всего массива,
// но принимать будем только половину
// Динамически выделяем память под принимаемый массив
int *ptr = new int [size];
// Готовимся принять массив данных
// Ждем сообщение от процесса 0
MPI_Recv (ptr, size, MPI_INT, 0, 0, MPI_COMM_WORLD, &Status);
// Ищем сумму элементов массива
Summa=0;
for (int i=0; i<size; i++)
Summa+=ptr[i];
// Отсылаем ответ процессу 0
MPI_Send (&Summa, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);
delete [] ptr; // Освобождаем память
} // if - процесс 1
//===================================================================
MPI_Finalize();
// закрываем MPI-библиотеку
return 0;
}
Пример результата работы программы:
Input n: 1000
S = 5175
Рассмотренная программа полностью работоспособна и решает поставленную задачу. Однако данный код не позволяет отследить и проанализировать процессы, вызванные распараллеливанием программы. Во многих случаях, особенно на этапе отладки программы, в ее листинг полезно добавлять команды вывода сведений о выполняемых этапах работы и промежуточных результатах. Ниже приводится пример этой же программы с добавленными командами вывода информационных сообщений.
#include "stdafx.h"
#include <stdio.h>
#include <conio.h>
#include <mpi.h>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int proc_rank, proc_count;
MPI_Status Status;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &proc_count);
MPI_Comm_rank(MPI_COMM_WORLD, &proc_rank);
double t0, t1, t2, t3, t4; // Переменные для измерения времени
t0=MPI_Wtime(); // Запоминаем начальный момент времени
//===================================================================
if ((proc_rank==0) && (proc_count>1)) // Если это "главный" процесс
{ // и процессов минимум два
int n; // Размер массива (изначально неизвестен)
cout << "Input n: ";
cin >> n; // Вводим с клавиатуры размер массива
t1 = MPI_Wtime(); // Запоминаем момент времени до начала работы
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): started.\n";
// Заводим массив на n элементов и заполняем его случайными числами:
int *mas; // Указатель на массив типа int
mas = new int [n]; // Выделяем память под массив (без проверки)
for (int i=0; i<n; i++) // и заполняем случайными числами от 0 до 10
mas[i]=rand()%11;
// Распараллелим поиск суммы ровно на два процесса.
// Даже если доступных процессорных ядер будет больше,
// они не будут использоваться. Ровно два процесса:
// текущий процесс с рангом 0 и процесс с рангом 1.
// Поскольку процесс 1 не знает о размере массива, он
// не может знать, какой объем сообщения ему принимать.
// Поэтому мы сначала передадим процессу 1 число, равное
// размеру массива. Так процесс 1 сможет подготовить
// память под принимаемый массив.
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): sanding size...\n";
MPI_Send (&n, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
cout <<"Process 0 ("<< MPI_Wtime()-t0 << "): size=" << n << " sended.\n";
// Теперь отсылаем первую половину массива процессу 1
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): sanding array...\n";
MPI_Send (mas, n/2, MPI_INT, 1, 0, MPI_COMM_WORLD);
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): array sended.\n";
// Пока процесс 1 обрабатывает первую половину массива,
// делаем пока свою часть работы: ищем сумму элементов
// оставшейся части массива
int S=0;
for (int i=n/2; i<n; i++)
S=S+mas[i];
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): local summa calculated, S=" << S << ".\n";
// Готовимся получать результат работы процесса 1.
// Результатом будет одно число типа int.
int S1; // Место под результат
// Ждем сообщение от процесса 1
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): waiting answer from process 1...\n";
MPI_Recv (&S1, 1, MPI_INT, 1, 0, MPI_COMM_WORLD, &Status);
cout <<"Process 0 ("<< MPI_Wtime()-t0 << "): answer received, S1=" << S1 << ".\n";
S=S+S1; // Складываем обе суммы
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): S = " << S << endl;
delete [] mas; // Освобождаем память, выделенную под массив
cout << "Process 0 ("<< MPI_Wtime()-t0 << "): finished.\n";
t2 = MPI_Wtime(); // Запоминаем момент времени по окончании работы
cout <<"Process 0 ("<<MPI_Wtime()-t0<<"): total time = "<<(t2-t1)<<" seconds.\n";
} // if - процесс 0
//===================================================================
if (proc_rank==1) // Если это процесс с номером 1
{
int size; // Размер принимаемого массива
int Summa; // Сумма его элементов
t3 = MPI_Wtime(); // Запоминаем момент времени до начала работы
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): started.\n";
// Готовимся принять размер массива
// Ждем сообщение от процесса 0
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): waiting size...\n";
MPI_Recv (&size, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &Status);
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): size=" << size << " received.\n";
size=size/2; // Мы получили размер всего массива,
// но принимать будем только половину
// Динамически выделяем память под принимаемый массив
int *ptr = new int [size];
// Готовимся принять массив данных
// Ждем сообщение от процесса 0
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): waiting array...\n";
MPI_Recv (ptr, size, MPI_INT, 0, 0, MPI_COMM_WORLD, &Status);
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): array received.\n";
// Ищем сумму элементов массива
Summa=0;
for (int i=0; i<size; i++)
Summa+=ptr[i];
cout << " Process 1 ("<<MPI_Wtime()-t0<<"): local summa calculated:"<<Summa<<endl;
// Отсылаем ответ процессу 0
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): sanding Summa...\n";
MPI_Send (&Summa, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): summa sended.\n";
delete [] ptr; // Освобождаем память
cout << " Process 1 ("<< MPI_Wtime()-t0 << "): finished.\n";
t4 = MPI_Wtime(); // Запоминаем момент времени по окончании работы
cout<<" Process 1 ("<< MPI_Wtime()-t0 << "): total time = "<<(t4-t3)<<" sec.\n";
} // if - процесс 1
//===================================================================
MPI_Finalize();
// закрываем MPI-библиотеку
return 0;
}
В начале программы объявляются переменные t0-t4 типа double. Переменная t0 служит для момента времени запуска процесса, переменные t1 и t2 – для хранения начального и конечного времени работы процесса 0, t3 и t4 – процесса 1. В каждом из операторов cout происходит печать значения MPI_Wtime()-t0, которое равно количеству секунд, прошедших от момента t0.
Ниже приведен пример результатов работы программы. Для удобства дальнейшего пояснения каждая строка была вручную пронумерована.
1 Input n: 100000000
2 Process 0 (4.36572): started.
3 Process 0 (6.53677): sanding size...
4 Process 0 (6.53681): size=100000000 sended.
5 Process 0 (6.53682): sanding array...
6 Process 0 (6.65851): array sended.
7 Process 0 (6.77024): local summa calculated, S=250004717.
8 Process 0 (6.77027): waiting answer from process 1...
9 Process 0 (6.7703): answer received, S1=249962239.
10 Process 0 (6.77031): S = 499966956
11 Process 1 (3.01869e-007): started.
12 Process 1 (8.78439e-005): waiting size...
13 Process 1 (6.53681): size=100000000 received.
14 Process 1 (6.60401): waiting array...
15 Process 1 (6.65853): array received.
16 Process 1 (6.77026): local summa calculated:249962239
17 Process 0 (6.86812): finished.
18 Process 0 (6.86815): total time = 2.50244 seconds.
19 Process 1 (6.77029): sanding Summa...
20 Process 1 (6.7703): summa sended.
21 Process 1 (6.82206): finished.
22 Process 1 (6.82216): total time = 6.82216 sec.
Дадим пояснение некоторых моментов в работе программы.
1. В строке 2 указано время старта процесса 0 (4.36 сек.), в строке 11 – время старта процесса 1 (почти ноль). Дело в том, что время старта процесса 0 отсчитывается в программе с момента после ввода значения n с клавиатуры. Очевидно, что в данном примере, ввод с клавиатуры занял 4.36 секунды. Время старта процесса 1 отсчитывается сразу с момента запуска процесса 1, то есть почти сразу после момента времени t0. Но процесс1, запустившись, пока ничего не делает и ждет получения сообщения от процесса 0 (строка 12). Таким образом, 4.36 секунды – это время, потраченное на ввод с клавиатуры, и его не следует относить ко времени обработки массива данных.
2. Между моментами, отмеченными в строках 3 и 4, происходит отправка процессом 0 процессу 1 значения n. Как можно видеть, отправка одного значения типа int занимает десятитысячную долю секунды. Следует, однако, понимать, что в строке 4 указан момент времени, когда сообщение было отправлено процессом 0. Это не значит, что в этот же момент оно было получено процессом 1. Это значит, что в данный момент система MPI приняла сообщение от процесса 0. В реальной жизни этот момент соответствует ситуации, когда с телефона было отправлено SMS-сообщение, или когда бумажное письмо было опущено в почтовый ящик. Сколько времени будет осуществляться доставка сообщения – неизвестно. В худшем случае оно вообще не будет доставлено. Тем не менее, процессу 0 было сообщено, что сообщение ушло, о чем он проинформировал нас в строке 4.
К слову говоря, строка 13 соответствует моменту времени, когда значение n было получено процессом 1. Очевидно, что этот момент совпадает с моментом отправки сообщения процессом 0 (строка 4). Поскольку пример программы запускался на двух ядрах одного процессора, передача сообщения произошла почти мгновенно. Однако в общем случае передача сообщения может занимать достаточно большое время.
3. Строки 5 и 6 соответствуют началу и концу отправки процессом 0 сообщения, содержащего половину массива (50 млн. чисел типа int). Момент вывода строки 15 соответствует моменту приема данного сообщения процессом 1. Разница между временем в строке 15 и временем в строке 5 говорит о том, что процесс передачи и приема этого сообщения занял 0.12 секунды.
4. Все строки, выдаваемые программой на экран, следуют не в хронологическом порядке. Студентам предлагается самостоятельно расставить их в порядке возникновения соответствующих событий. Подсказка: первой будет идти строка 11, затем 12, затем 2, затем 3 и т.д.
Также необходимо обратить внимание на следующее. Как известно, переменные t0-t4, объявляемые в функции main(), создаются внутри каждого процесса. Процесс 0 имеет свои переменные t0-t4, процесс 1 – свои. После создания переменных каждый процесс выполняет команду
t0=MPI_Wtime();
занося в переменную t0 текущий момент времени.
Программист, использующий MPI, должен понимать, что два процесса не могут быть запущены в абсолютно один и тот же момент времени. Моменты запуска каждого из процессов ВСЕГДА различаются. Если процессы запускаются на одном компьютере, то их запуском руководит операционная система, которая сначала запустит один процесс, а потом (пусть даже сразу, без лишних задержек) – второй. Но это будут разные моменты времени. Если в программу после команды t0=MPI_Wtime() добавить строки
cout.precision(20);
cout << "process " << proc_rank << ": " << t0 << endl;
cout.precision(6);
то результатом их работы могут быть, например, следующие сообщения:
process 0: 2905.3188425134222
process 1: 2905.3188428152912
Как можно видеть, процесс 1 запускается примерно на 0.3 микросекунды (миллионных доли секунды) позже второго. Такая ничтожная разница незначительна для нашего примера (он, как мы помним, тоже запускался на одном процессоре), поэтому различия в строках, например, 4 и 13 незаметны.
В реальной жизни процессы 0 и 1 могут запускаться на разных компьютерах, находящихся в локальной сети или в сети Интернет. При этом разница в моментах запуска процессов может быть значительно больше и измеряться секундами. При этом для разных процессов значения переменной t0 будут существенно различаться.
К чему это приведет? Это приведет к тому, что нельзя будет сравнивать моменты возникновения событий в нулевом и первом процессах. Если, допустим, процесс 1 был запущен на одну секунду позже процесса 0, то все события, генерируемые процессом 1, нужно мысленно сместить во времени на одну секунду вперед. Такое смещение усложняет анализ работы параллельной программы.
Существует, тем не менее, простой способ добиться того, чтобы переменные t0 в обоих процессах имели абсолютно одинаковые значения. Для этого процесс 0 выполняет команду t0=MPI_Wtime(), после чего передает значение переменной t0 процессу 1. Процесс 1 принимает это значение и кладет его в свою переменную t0. Студентам предлагается самостоятельно реализовать данный механизм. Следует учитывать, что в командах MPI_Send и MPI_Recv в качестве типа пересылаемых данных следует указывать тип MPI_DOUBLE.
