Files
SSPR_25/shipilov_nikita_lab_2

Лабораторная работа №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 проще в интеграции и подходят для локального многопоточного исполнения.