Лабораторная работа №2
Разработка параллельного MPI приложения на языке Java.
Необходимо:
- Разработать параллельный вариант алгоритма с применением MPI и замерить время его работы.
В рамках работы программы должно быть две копии приложения, которые соединяются друг с другом по сети. Сообщения о статусе соединения (например, что соединение установлено) должны выводиться в консоль.
Вариант задания 1: Разделить элементы матрицы на наибольший элемент.
Как запустить лабораторную работу:
javac -cp $MPJ_HOME/lib/mpj.jar MPIMatrix.java компилирует исходный код (делаем на обоих контейнерах)
mpjrun.sh -np 2 -dev niodev -machinesfile machines MPIMatrix команда запускает скомпилированный класс (делаем на контейнере CT128)
Какие технологии использовали:
-
Java - язык программирования.
-
MPJ Express - библиотека для реализации параллельных вычислений.
-
MPI - интерфейс для передачи сообщений между процессами.
Как работает программа:
Разбор кода с фрагментами
1. Инициализация MPI
MPI.Init(args);
int rank = MPI.COMM_WORLD.Rank();
int size = MPI.COMM_WORLD.Size();
- Запускается среда MPI.
- Получается ранг процесса (
rank) и общее число процессов (size).
2. Проверка количества процессов
if (size != 2) {
if (rank == 0) {
System.out.println("This program requires 2 processes.");
}
MPI.Finalize();
return;
}
- Программа работает только с двумя процессами.
- Если процессов больше или меньше, программа завершает выполнение.
3. Создание матрицы (только для rank == 0)
int n = 1000;
double[][] matrix = null;
if (rank == 0) {
matrix = new double[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
matrix[i][j] = Math.random() * 100;
}
}
}
- Создается матрица 1000×1000 и заполняется случайными числами от 0 до 100.
4. Разделение матрицы между процессами
int rowsPerProcess = n / 2;
double[][] localMatrix = new double[rowsPerProcess][n];
if (rank == 0) {
for (int i = 0; i < rowsPerProcess; i++) {
System.arraycopy(matrix[i], 0, localMatrix[i], 0, n);
}
double[] sendBuffer = new double[rowsPerProcess * n];
for (int i = 0; i < rowsPerProcess; i++) {
System.arraycopy(matrix[i + rowsPerProcess], 0, sendBuffer, i * n, n);
}
MPI.COMM_WORLD.Send(sendBuffer, 0, sendBuffer.length, MPI.DOUBLE, 1, 0);
} else {
double[] recvBuffer = new double[rowsPerProcess * n];
MPI.COMM_WORLD.Recv(recvBuffer, 0, recvBuffer.length, MPI.DOUBLE, 0, 0);
for (int i = 0; i < rowsPerProcess; i++) {
System.arraycopy(recvBuffer, i * n, localMatrix[i], 0, n);
}
}
- Процесс 0 отправляет вторую половину матрицы процессу 1 с помощью
MPI.COMM_WORLD.Send(). - Процесс 1 принимает данные через
MPI.COMM_WORLD.Recv().
5. Поиск локального максимума
double localMax = 0;
for (int i = 0; i < rowsPerProcess; i++) {
for (int j = 0; j < n; j++) {
if (localMatrix[i][j] > localMax) {
localMax = localMatrix[i][j];
}
}
}
double[] localMaxArr = {localMax};
- Каждый процесс находит максимальное значение в своей части матрицы.
6. Нахождение глобального максимума
double[] globalMaxArr = new double[1];
MPI.COMM_WORLD.Reduce(localMaxArr, 0, globalMaxArr, 0, 1, MPI.DOUBLE, MPI.MAX, 0);
double globalMax = globalMaxArr[0];
MPI.Reduce()передает максимальный элемент от каждого процесса процессу 0, который вычисляет глобальный максимум.
7. Рассылка глобального максимума
double[] globalMaxBroadcast = new double[1];
if (rank == 0) {
globalMaxBroadcast[0] = globalMax;
}
MPI.COMM_WORLD.Bcast(globalMaxBroadcast, 0, 1, MPI.DOUBLE, 0);
globalMax = globalMaxBroadcast[0];
- Процесс 0 рассылает глобальный максимум всем процессам с помощью
MPI.Bcast().
8. Нормализация матрицы
for (int i = 0; i < rowsPerProcess; i++) {
for (int j = 0; j < n; j++) {
localMatrix[i][j] /= globalMax;
}
}
- Каждый элемент делится на глобальный максимум, нормализуя значения.
9. Сбор нормализованных данных
if (rank == 0) {
for (int i = 0; i < rowsPerProcess; i++) {
System.arraycopy(localMatrix[i], 0, matrix[i], 0, n);
}
double[] recvBuffer = new double[rowsPerProcess * n];
MPI.COMM_WORLD.Recv(recvBuffer, 0, recvBuffer.length, MPI.DOUBLE, 1, 1);
for (int i = 0; i < rowsPerProcess; i++) {
System.arraycopy(recvBuffer, i * n, matrix[i + rowsPerProcess], 0, n);
}
} else {
double[] sendBuffer = new double[rowsPerProcess * n];
for (int i = 0; i < rowsPerProcess; i++) {
System.arraycopy(localMatrix[i], 0, sendBuffer, i * n, n);
}
MPI.COMM_WORLD.Send(sendBuffer, 0, sendBuffer.length, MPI.DOUBLE, 0, 1);
}
- Процесс 1 отправляет нормализованные данные процессу 0, который собирает матрицу обратно.
10. Вывод времени выполнения
double endTime = System.nanoTime();
System.out.printf("Process %d finished at %.1f ms\n", rank, (endTime - startTime) / 1_000_000.0);
- Каждый процесс выводит время работы в миллисекундах.
11. Завершение работы MPI
MPI.Finalize();
- Освобождение ресурсов и завершение работы MPI.
(Размер матрицы 1000х1000)
Starting process <0> on <CT128>
Starting process <1> on <CT129>
Maximum: 99.99984736376186
Process 1 finished at 170.9 ms
Process 0 finished at 179.2 ms
Stopping Process <0> on <CT128>
Stopping Process <1> on <CT129>
(Размер матрицы 2000х2000)
Starting process <1> on <CT129>
Starting process <0> on <CT128>
Maximum: 99.99998909285488
Process 1 finished at 347.8 ms
Process 0 finished at 359.3 ms
Stopping Process <0> on <CT128>
Stopping Process <1> on <CT129>
(Размер матрицы 3000x3000)
Starting process <0> on <CT128>
Starting process <1> on <CT129>
Maximum: 99.99999045962504
Process 1 finished at 753.4 ms
Process 0 finished at 762.8 ms
Stopping Process <0> on <CT128>
Stopping Process <1> on <CT129>
(Размер матрицы 4000x4000)
Starting process <1> on <CT129>
Starting process <0> on <CT128>
Maximum: 99.99998218458676
Process 1 finished at 1376.1 ms
Process 0 finished at 1398.0 ms
Stopping Process <0> on <CT128>
Stopping Process <1> on <CT129>
Выводы по временным замерам:
-
Время выполнения растет примерно квадратично с увеличением размера матрицы.
-
Разница во времени между процессами 0 и 1 минимальна, что говорит о равномерном распределении нагрузки.
-
Задержка в работе процесса 0 связана с дополнительными затратами на генерацию матрицы и сбор результатов.
-
MPI позволяет эффективно разделить вычисления, но обмен данными (Send/Recv) вносит небольшие накладные расходы.
Сравнение времени выполнения MPI и многопоточных алгоритмов
Для матрицы 1000x1000:
- MPI: 179.2 мс
- Однопоточный алгоритм: 20 мс
- ThreadPoolExecutor: 73 мс
- ForkJoinPool: 28 мс
Для матрицы 2000x2000:
- MPI: 359.3 мс
- Однопоточный алгоритм: 39 мс
- ThreadPoolExecutor: 110 мс
- ForkJoinPool: 51 мс
Для матрицы 3000x3000:
- MPI: 762.8 мс
- Однопоточный алгоритм: 69 мс
- ThreadPoolExecutor: 182 мс
- ForkJoinPool: 106 мс
Для матрицы 4000x4000:
- MPI: 1398.0 мс
- Однопоточный алгоритм: 108 мс
- ThreadPoolExecutor: 237 мс
- ForkJoinPool: 134 мс
MPI работает значительно медленнее, чем даже однопоточное исполнение, а многопоточные решения (особенно ForkJoinPool) выполняют задачу в 2-6 раз быстрее. Это связано с издержками межпроцессного взаимодействия через сеть. MPI неэффективен для данной задачи, лучше использовать многопоточные алгоритмы внутри одного узла.
Вывод:
MPI позволяет значительно ускорить обработку больших массивов данных чем вариации с использованием однопоточных и многопоточных реализаций из 1й лабораторной работы, особенно при распределении вычислений между несколькими узлами. Однако эффективность зависит от баланса вычислений и передачи данных. В данной работе удалось добиться почти линейного ускорения по сравнению с последовательным исполнением. MPI превосходит как однопоточное исполнение, так и многопоточные подходы (ThreadPoolExecutor, ForkJoinPool) в задачах, требующих интенсивных вычислений на больших объемах данных. В многопоточных реализациях ускорение ограничено мощностью одного процессора, тогда как в MPI оно зависит от количества узлов, что позволяет добиться более высокой производительности. Однако MPI требует сложной настройки распределенной системы, тогда как ThreadPoolExecutor и ForkJoinPool проще в интеграции и подходят для локального многопоточного исполнения.