Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions tasks/kaur_a_min_matrix/common/include/common.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include <cstddef>
#include <string>
#include <tuple>
#include <vector>

#include "task/include/task.hpp"

namespace kaur_a_min_matrix {

using InType = std::tuple<size_t, size_t, std::vector<int>>;
using OutType = int;
using TestType = std::tuple<int, int, int, std::string>;
using BaseTask = ppc::task::Task<InType, OutType>;

} // namespace kaur_a_min_matrix
Binary file added tasks/kaur_a_min_matrix/data/pic.ppm
Binary file not shown.
9 changes: 9 additions & 0 deletions tasks/kaur_a_min_matrix/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"student": {
"first_name": "Александр",
"group_number": "3823Б1ПР4",
"last_name": "Каур",
"middle_name": "Максимович",
"task_number": "1"
}
}
22 changes: 22 additions & 0 deletions tasks/kaur_a_min_matrix/mpi/include/ops_mpi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include "kaur_a_min_matrix/common/include/common.hpp"
#include "task/include/task.hpp"

namespace kaur_a_min_matrix {

class KaurAMinMatrixMPI : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kMPI;
}
explicit KaurAMinMatrixMPI(const InType &in);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;
};

} // namespace kaur_a_min_matrix
98 changes: 98 additions & 0 deletions tasks/kaur_a_min_matrix/mpi/src/ops_mpi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include "kaur_a_min_matrix/mpi/include/ops_mpi.hpp"

#include <mpi.h>

#include <algorithm>
#include <climits>
#include <cstddef>
#include <utility>
#include <vector>

#include "kaur_a_min_matrix/common/include/common.hpp"

namespace kaur_a_min_matrix {

KaurAMinMatrixMPI::KaurAMinMatrixMPI(const InType &in) {
SetTypeOfTask(GetStaticTypeOfTask());
GetInput() = in;
GetOutput() = 0;
}

bool KaurAMinMatrixMPI::ValidationImpl() {
int world_rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);

int validation = 0;

if (world_rank == 0) {
auto &rows = std::get<0>(GetInput());
auto &columns = std::get<1>(GetInput());
auto &matrix = std::get<2>(GetInput());

if ((rows != 0 && columns != 0) && (!matrix.empty()) && (matrix.size() == rows * columns)) {
validation = 1;
}
}

MPI_Bcast(&validation, 1, MPI_INT, 0, MPI_COMM_WORLD);

return validation != 0;
}

bool KaurAMinMatrixMPI::PreProcessingImpl() {
GetOutput() = 0;
return true;
}

bool KaurAMinMatrixMPI::RunImpl() {
int world_size = 1;
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
int world_rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);

std::vector<int> matrix;
std::vector<int> sendcounts(world_size);
std::vector<int> displs(world_size);
std::vector<int> local_matrix;

if (world_rank == 0) {
auto &input = GetInput();
matrix = std::get<2>(input);

size_t matrix_size = matrix.size();
size_t part_size = matrix_size / world_size;
size_t remainder = matrix_size % world_size;

int offset = 0;
for (size_t i = 0; std::cmp_less(i, world_size); ++i) {
sendcounts[i] = static_cast<int>(part_size) + (i < remainder ? 1 : 0);
displs[i] = offset;
offset += sendcounts[i];
}
}

MPI_Bcast(sendcounts.data(), world_size, MPI_INT, 0, MPI_COMM_WORLD);
int local_size = sendcounts[world_rank];
local_matrix.resize(local_size);

MPI_Scatterv(world_rank == 0 ? matrix.data() : nullptr, sendcounts.data(), displs.data(), MPI_INT,
local_matrix.data(), local_size, MPI_INT, 0, MPI_COMM_WORLD);

int local_min = INT_MAX;
for (int elem : local_matrix) {
local_min = std::min(local_min, elem);
}

int global_min = 0;
MPI_Allreduce(&local_min, &global_min, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD);

GetOutput() = global_min;

return true;
}

bool KaurAMinMatrixMPI::PostProcessingImpl() {
return true;
}

} // namespace kaur_a_min_matrix
201 changes: 201 additions & 0 deletions tasks/kaur_a_min_matrix/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
```markdown
# Минимальное значение элементов матрицы

- Студент: Каур Андрей Михайлович
- Технология: SEQ | MPI
- Вариант: __

## 1. Введение

Поиск минимального элемента матрицы является одной из базовых операций линейной алгебры и важным компонентом во множестве алгоритмов обработки данных, компьютерной графики и др.
В рамках этого исследования проводится сравнительный анализ производительности последовательной и параллельной MPI-реализаций данного алгоритма.

## 2. Постановка задачи

**Цель работы:**
Реализовать последовательную и MPI-параллельную версии алгоритма поиска минимального элемента матрицы, провести их сравнение и анализ эффективности.

**Определение задачи:**
Для матрицы `A` размера `m × n` необходимо определить значение: `min(A) = min(A[i][j])`, где `0 <= i < n`, `0 <= j < m`.

**Ограничения:**
- Матрица должна быть представлена как вектор целых чисел.
- Размеры матрицы могут быть большими.
- Корректность должна сохраняться при любых значениях элементов.
- Результаты SEQ и MPI версий должны совпадать.

## 3. Алгоритм (Последовательная версия)

**Входные данные:** количество строк `rows`, количество колонок `columns`, вектор размера `rows * columns`.

**Выходные данные:** целое число - минимальный элемент матрицы.

**Алгоритм**:
1. Получить входной вектор `matrix`.
2. Присвоить переменной `min_val` значение первого элемента вектора.
3. В цикле пройтись по всем оставшимся элементам, начиная со второго:
- Записать в `min_val` минимальное значение из текущего элемента и `min_val`.
4. Вернуть значение `min_val`.

**Сложность**: O(n), где `n` - размер вектора `matrix`.

### Код последовательной версии алгоритма
```cpp
bool KaurAMinMatrixSEQ::RunImpl() {
auto &matrix = std::get<2>(GetInput());

int min_val = matrix[0];
for (size_t i = 1; i < matrix.size(); i++) {
min_val = std::min(min_val, matrix[i]);
}

GetOutput() = min_val ;
return true ;
}
```

## 4. Схема распараллеливания

Алгоритм параллельного вычисления минимального элемента матрицы реализует распределение данных между процессами с последующей редукцией результатов. Процессы работают с определёнными блоками элементов матрицы, что обеспечивает равномерную нагрузку.

- **Инициализация:** Все процессы запускаются в коммуникаторе MPI_COMM_WORLD, определяется общее количество процессов и ранг текущего процесса.
- **Распределение данных:** Нулевой процесс загружает входную матрицу и вычисляет параметры для распределения данных:
- Определяет размер каждого блока: `part_size = matrix_size / world_size`.
- Распределяет остаточные элементы: первые `remainder` процессов получают по одному дополнительному элементу, если размер матрицы нацело не делится на количество процессов.
- Формирует векторы `sendcounts` (размеры блоков) и `displs` (смещения в матрице).
- **Широковещательная рассылка:** Вектор `sendcounts` рассылается всем процессам через `MPI_Bcast`, позволяя каждому процессу определить размер своего локального буфера.
- **Распределение данных:** Матрица распределяется между процессами при помощи функции `MPI_Scatterv`.
- **Локальные вычисления:** Каждый процесс находит минимальный элемент в своем локальном буфере.
- **Глобальная редукция:** Локальные минимумы со всех процессов объединяются с помощью операции `MPI_Allreduce` с параметром `MPI_MIN`.
- **Формирование итога:** Глобальный минимум становится доступным на всех процессах одновременно и сохраняется в выходные данные.

### Код параллельной версии алгоритма
```cpp
bool KaurAMinMatrixMPI::RunImpl() {
int world_size = 1;
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
int world_rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);

std::vector<int> matrix;
std::vector<int> sendcounts(world_size);
std::vector<int> displs(world_size);
std::vector<int> local_matrix;

if (world_rank == 0) {
auto &input = GetInput();
matrix = std::get<2>(input);

size_t matrix_size = matrix.size();
size_t part_size = matrix_size / world_size;
size_t remainder = matrix_size % world_size;

int offset = 0;
for (size_t i = 0; std::cmp_less(i, world_size); ++i) {
sendcounts[i] = static_cast<int>(part_size) + (i < remainder ? 1 : 0);
displs[i] = offset;
offset += sendcounts[i];
}
}

MPI_Bcast(sendcounts.data(), world_size, MPI_INT, 0, MPI_COMM_WORLD);
int local_size = sendcounts[world_rank];
local_matrix.resize(local_size);

MPI_Scatterv(world_rank == 0 ? matrix.data() : nullptr, sendcounts.data(), displs.data(), MPI_INT,
local_matrix.data(), local_size, MPI_INT, 0, MPI_COMM_WORLD);

int local_min = INT_MAX;
for (int elem : local_matrix) {
local_min = std::min(local_min, elem);
}

int global_min = 0;
MPI_Allreduce(&local_min, &global_min, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD);

GetOutput() = global_min;

return true;
}
```

## 5. Экспериментальные исследования

### Окружение

| Параметр | Значение |
|--------------------|--------------------------------|
| **ОС** | Windows 11 Pro 25H2 |
| **Процессор** | AMD Ryzen 5 5600X (6 ядер, 12 потоков, 3.70 ГГц) |
| **ОЗУ** | 16 ГБ DDR4 3400 МГц |
| **Компилятор** | MSVC 14.43.34808 |
| **Версия MPI** | Microsoft MPI 10.1.12498.52 |

### Тестовые данные

1. **Функциональные тесты:**
- Используют заранее подготовленные матрицы размеров от 3×3 до 10×10 с известным минимальным элементом, размещенным в центре матрицы.
- Значения элементов матрицы генерируются случайным образом в диапазоне от минимального элемента до 250.
- Тесты проверяют корректность работы на различных формах матриц (квадратные, прямоугольные, единичные).

2. **Тесты производительности:**
- Используют матрицу `5000×5000` (25 миллионов элементов).
- Элементы вычисляются по формуле `i * i + j`.
- Минимальный элемент имеет фиксированное значение `-1_000_000` и размещается в центре матрицы.
- Тесты выполняются в двух режимах: `pipeline` и `task_run`.

### Сравнение производительности

Для сравнения производительности использовалась матрица размером `5000×5000`.

Вычисления производились по следующим формулам:

- `Ускорение = Время_последовательное / Время_параллельное`
- `Эффективность = (Ускорение / Количество_процессов) × 100%`

| Режим выполнения | Количество процессов | Время выполнения (сек) | Ускорение | Эффективность |
|------------------|---------------------|------------------------|-----------|---------------|
| **Последовательный** | | | | |
| pipeline | 1 | 0.01138 | 1.00x | - |
| task_run | 1 | 0.01138 | 1.00x | - |
| **MPI (2 процесса)** | | | | |
| pipeline | 2 | 0.03687 | 0.31x | 15.5% |
| task_run | 2 | 0.03709 | 0.31x | 15.5% |
| **MPI (4 процесса)** | | | | |
| pipeline | 4 | 0.03823 | 0.30x | 7.5% |
| task_run | 4 | 0.03550 | 0.32x | 8.0% |
| **MPI (6 процессов)** | | | | |
| pipeline | 6 | 0.03614 | 0.31x | 5.2% |
| task_run | 6 | 0.03730 | 0.31x | 5.2% |

## 6. Результаты

1. **Результаты функционального тестирования**
- Все функциональные тесты успешно пройдены.
- Все реализации (SEQ и MPI) работают корректно и выдают идентичные результаты для всех тестовых наборов.

2. **Результаты сравнения производительности**
- Параллельные MPI-версии работают медленнее последовательной при всех конфигурациях процессов.
- Максимальное ускорение достигнуто при использовании 4 процессов в режиме `task_run` (0.32x от последовательной версии).
- Минимальное время выполнения среди MPI-версий: 0.03550 сек (4 процесса, режим `task_run`).
- Эффективность использования процессов снижается с 15.5% при 2 процессах до 5.2% при 6 процессах.

## 7. Выводы

1. Разработанные алгоритмы корректно решают поставленную задачу, что подтверждается успешным прохождением всех функциональных тестов.

2. Параллельная реализация с использованием MPI показала снижение производительности по сравнению с последовательной версией. MPI-версии работают в среднем в 3.2-3.3 раза медленнее, что связано с высокими накладными расходами на межпроцессное взаимодействие (передача данных, синхронизация, редукция) при относительно небольшой вычислительной нагрузке задачи поиска минимума.

3. При увеличении числа процессов эффективность снижается с 15.5% (2 процесса) до 5.2% (6 процессов). Это объясняется ростом коммуникационных издержек между процессами при неизменном объеме вычислений на процесс.

4. Для данной задачи (поиск минимума в матрице 5000×5000) оптимальным является использование 2-4 процессов, однако даже в этом случае параллельная версия уступает последовательной.

5. Результаты демонстрируют, что не все задачи выигрывают от распараллеливания. Задачи с низкой вычислительной сложностью и интенсивным обменом данными могут показывать отрицательное ускорение из-за преобладания накладных расходов над полезными вычислениями.

## 8. Источники

1. Сысоев А. В. Лекции по курсу «Параллельное программирование для кластерных систем».
2. Официальная документация Microsoft MPI — https://learn.microsoft.com/ru-ru/message-passing-interface
3. Документация Open MPI — https://www.open-mpi.org/doc/
4. C++ Reference — https://en.cppreference.com
```
22 changes: 22 additions & 0 deletions tasks/kaur_a_min_matrix/seq/include/ops_seq.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include "kaur_a_min_matrix/common/include/common.hpp"
#include "task/include/task.hpp"

namespace kaur_a_min_matrix {

class KaurAMinMatrixSEQ : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kSEQ;
}
explicit KaurAMinMatrixSEQ(const InType &in);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;
};

} // namespace kaur_a_min_matrix
Loading
Loading