# Лабораторная работа 2

## 1. Задание

Разработка параллельного MPI приложения на языке Java.
Необходимо разработать параллельный вариант алгоритма с применением MPI и замерить время его работы.
Запуск каждого экземпляра MPI происходит в своём контейнере.

**Вариант:** "Определить минимальный элемент матрицы ниже главной диагонали"

## 2. Описание кода
```java
package ru.kuznec;

import mpi.MPI;

public class MatrixMinBelowMainDiagonal {
    public static void main(String[] args) {
        MPI.Init(args);
        
        int rank = MPI.COMM_WORLD.Rank();
        int size = MPI.COMM_WORLD.Size();

        int n = 4;
        double[] matrix = new double[n * n];

        if (rank == 0) {
            double[][] m = {
                    {1, 2, 3, 4},
                    {5, 6, 7, 8},
                    {9, 10, 11, 12},
                    {13, 14, 15, 16}
            };
            
            for (int i = 0; i < n; i++) {
                System.arraycopy(m[i], 0, matrix, i * n, n);
            }
        }
        
        int[] dim = new int[]{n};
        MPI.COMM_WORLD.Bcast(dim, 0, 1, MPI.INT, 0);
        n = dim[0];

        MPI.COMM_WORLD.Bcast(matrix, 0, n * n, MPI.DOUBLE, 0);

        int rowsPerProc = n / size;
        int extra = n % size;
        int startRow, endRow;
        if (rank < extra) {
            startRow = rank * (rowsPerProc + 1);
            endRow = startRow + rowsPerProc;
        } else {
            startRow = rank * rowsPerProc + extra;
            endRow = startRow + rowsPerProc - 1;
        }

        double localMin = Double.MAX_VALUE;
        for (int i = startRow; i <= endRow; i++) {
            for (int j = 0; j < i; j++) {
                double value = matrix[i * n + j];
                if (value < localMin) {
                    localMin = value;
                }
            }
        }

        double[] sendBuf = new double[]{ localMin };
        double[] globalMin = new double[1];
        MPI.COMM_WORLD.Reduce(sendBuf, 0, globalMin, 0, 1, MPI.DOUBLE, MPI.MIN, 0);

        if (rank == 0) {
            if (globalMin[0] == Double.MAX_VALUE) {
                System.out.println("Элементы ниже главной диагонали отсутствуют.");
            } else {
                System.out.println("Минимальный элемент ниже главной диагонали: " + globalMin[0]);
            }
        }

        MPI.Finalize();
    }
}
```


1. Метод `MPI.Init(args)` запускает параллельную среду. Каждый процесс получает свой уникальный ранг и общее число процессов через `MPI.COMM_WORLD.Rank()` и `MPI.COMM_WORLD.Size()`.

2. Только процесс с рангом 0 (мастер-процесс) задаёт исходную матрицу. Для удобства обмена данными матрица преобразуется в одномерный массив с помощью `System.arraycopy`.

3. Сначала размер матрицы (n) передаётся всем процессам, затем вся матрица передаётся всем процессам с помощью коллективной операции `Bcast`.

4. Матрица разбивается на блоки строк для каждого процесса. Если число процессов не делит размер матрицы нацело, первые процессы получают на одну строку больше (`extra`).

5. Каждый процесс просматривает свои строки и ищет минимальный элемент среди элементов ниже главной диагонали (для строки с индексом `i` обрабатываются столбцы от `0` до `i-1`).

6. Операция `Reduce` с оператором `MPI.MIN` объединяет локальные минимумы в глобальный, который выводится на экран процессом с рангом 0.

7. После выполнения всех вычислений вызывается `MPI.Finalize()`, что корректно завершает параллельную среду.

## 4. Действия, производимые на контейнерах

Для запуска этого MPI-приложения в распределённом режиме использовались контейнеры, каждый из которых настроен следующим образом:

- **Установка Java и MPJ Express:**  
  На каждом контейнере установлена jdk-21 и распакована библиотека MPJ Express. Переменная `MPJ_HOME` настроена на путь к MPJ Express, а каталог `MPJ_HOME/bin` добавлен в переменную `PATH`.

- **Настройка сети и SSH:**  
  Контейнеры настроены так, чтобы иметь возможность обмениваться данными по сети. Для работы MPJ Express настроен passwordless SSH между контейнерами. Файл `machines` содержит IP-адреса всех контейнеров:
  ```
  192.168.10.101
  192.168.10.102
  192.168.10.103
  192.168.10.104
  ```
  Это позволяет MPJ Express распределить процессы по разным контейнерам.

- **Запуск демонов MPJ Express:**  
  На каждом контейнере запущен MPJ daemon, который слушает входящие соединения от мастера. Демоны обеспечивают обмен информацией о портах и корректное распределение процессов.
  ```bash
  $MPJ_HOME/bin/mpjdaemon -boot 192.168.10.101
  $MPJ_HOME/bin/mpjdaemon -boot 192.168.10.102
  $MPJ_HOME/bin/mpjdaemon -boot 192.168.10.103
  $MPJ_HOME/bin/mpjdaemon -boot 192.168.10.104
  ```

- **Добавление хостов в каждый контейнер**
  На каждом контейнере добавляем в файл ``/etc/hosts`` каждый хост, на одном из контейнеров это выглядит примерно так:
  ```bash
    # --- BEGIN PVE ---
    192.168.10.101 node1.com node1
    192.168.10.102 node2.com node2
    192.168.10.103 node3.com node3
    192.168.10.104 node4.com node4
    # --- END PVE ---
  ```

- **Запуск MPI-приложения:**  
  Из одного из контейнеров запускается приложение с помощью команды:
  ```bash
  mpjrun.sh -np 4 -dev niodev MatrixMinBelowMainDiagonal
  ```
  Эта команда запускает 4 процесса, которые распределяются между контейнерами согласно файлу `machines`.