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
16 changes: 16 additions & 0 deletions tasks/denisov_a_min_val_row_matrix/common/include/common.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

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

#include "task/include/task.hpp"

namespace denisov_a_min_val_row_matrix {

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

} // namespace denisov_a_min_val_row_matrix
9 changes: 9 additions & 0 deletions tasks/denisov_a_min_val_row_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"
}
}
28 changes: 28 additions & 0 deletions tasks/denisov_a_min_val_row_matrix/mpi/include/ops_mpi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include <vector>

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

namespace denisov_a_min_val_row_matrix {

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

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

void RunMaster(int rows, int cols, int base_rows, int extra_rows, int local_rows, std::vector<int> &local_matrix,
std::vector<int> &local_min);
static void RunWorker(int cols, int local_rows, std::vector<int> &local_matrix, std::vector<int> &local_min);
};

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

#include <mpi.h>

#include <algorithm>
#include <array>
#include <cstddef>
#include <limits>
#include <vector>

#include "denisov_a_min_val_row_matrix/common/include/common.hpp"

namespace denisov_a_min_val_row_matrix {

DenisovAMinValRowMatrixMPI::DenisovAMinValRowMatrixMPI(const InType &in) {
SetTypeOfTask(GetStaticTypeOfTask());
GetInput() = in;
}

bool DenisovAMinValRowMatrixMPI::ValidationImpl() {
const auto &input = GetInput();
if (input.empty()) {
return false;
}

const auto cols = input[0].size();
return std::ranges::all_of(input, [cols](const auto &row) { return row.size() == cols; });
}

bool DenisovAMinValRowMatrixMPI::PreProcessingImpl() {
int rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);

if (rank == 0) {
GetOutput().assign(GetInput().size(), std::numeric_limits<int>::max());
}
return true;
}

bool DenisovAMinValRowMatrixMPI::RunImpl() {
int rank = 0;
int size = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);

std::array<int, 2> dimensions = {0, 0};
if (rank == 0) {
const auto &input = GetInput();
dimensions[0] = static_cast<int>(input.size());
dimensions[1] = static_cast<int>(input[0].size());
}
MPI_Bcast(dimensions.data(), 2, MPI_INT, 0, MPI_COMM_WORLD);

int rows = dimensions[0];
int cols = dimensions[1];
int base_rows = rows / size;
int extra_rows = rows % size;
int local_rows = base_rows + ((rank < extra_rows) ? 1 : 0);

std::vector<int> local_matrix(static_cast<size_t>(local_rows) * cols);
std::vector<int> local_min(local_rows);

if (rank == 0) {
RunMaster(rows, cols, base_rows, extra_rows, local_rows, local_matrix, local_min);
} else {
RunWorker(cols, local_rows, local_matrix, local_min);
}

return true;
}

void DenisovAMinValRowMatrixMPI::RunMaster(int rows, int cols, int base_rows, int extra_rows, int local_rows,
std::vector<int> &local_matrix, std::vector<int> &local_min) {
int size = 0;
MPI_Comm_size(MPI_COMM_WORLD, &size);

const auto &input = GetInput();

for (int i = 0; i < local_rows; ++i) {
std::ranges::copy(input[i], local_matrix.data() + (static_cast<std::ptrdiff_t>(i) * cols));
}

std::vector<MPI_Request> send_requests(size - 1);
std::vector<std::vector<int>> send_buffers(size - 1);

int row_offset = local_rows;
for (int proc = 1; proc < size; ++proc) {
int proc_rows = base_rows + ((proc < extra_rows) ? 1 : 0);
send_buffers[proc - 1].resize(static_cast<size_t>(proc_rows) * cols);

for (int i = 0; i < proc_rows; ++i) {
std::ranges::copy(input[row_offset + i], send_buffers[proc - 1].data() + (static_cast<std::ptrdiff_t>(i) * cols));
}

MPI_Isend(send_buffers[proc - 1].data(), proc_rows * cols, MPI_INT, proc, 0, MPI_COMM_WORLD,
&send_requests[proc - 1]);
row_offset += proc_rows;
}

const int *row_ptr = local_matrix.data();
for (int i = 0; i < local_rows; ++i) {
local_min[i] = *std::min_element(row_ptr, row_ptr + cols);
row_ptr += cols;
}

if (size > 1) {
MPI_Waitall(size - 1, send_requests.data(), MPI_STATUSES_IGNORE);
}

GetOutput().resize(rows);
std::ranges::copy(local_min, GetOutput().begin());

int result_offset = local_rows;
for (int proc = 1; proc < size; ++proc) {
int proc_rows = base_rows + ((proc < extra_rows) ? 1 : 0);
MPI_Recv(GetOutput().data() + result_offset, proc_rows, MPI_INT, proc, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
result_offset += proc_rows;
}
}

void DenisovAMinValRowMatrixMPI::RunWorker(int cols, int local_rows, std::vector<int> &local_matrix,
std::vector<int> &local_min) {
MPI_Recv(local_matrix.data(), local_rows * cols, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);

const int *row_ptr = local_matrix.data();
for (int i = 0; i < local_rows; ++i) {
local_min[i] = *std::min_element(row_ptr, row_ptr + cols);
row_ptr += cols;
}

MPI_Send(local_min.data(), local_rows, MPI_INT, 0, 1, MPI_COMM_WORLD);
}

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

} // namespace denisov_a_min_val_row_matrix
154 changes: 154 additions & 0 deletions tasks/denisov_a_min_val_row_matrix/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Нахождение минимальных значений по строкам матрицы

- Студент: Денисов Артём Андреевич, группа 3823Б1ПР4
- Технология: SEQ + MPI
- Вариант: 17

## 1. Введение
Задача нахождения минимального значения в каждой строке матрицы относится к базовым операциям обработки данных. Несмотря на простоту, она хорошо иллюстрирует ключевые идеи параллельного программирования с использованием MPI: разбиение данных между процессами, выполнение локальных вычислений и последующее объединение результатов.
Цель работы — реализовать как последовательный, так и параллельный варианты алгоритма, провести сравнение их производительности и оценить, насколько эффективно удаётся распараллелить вычисления.


## 2. Постановка задачи
**Входные данные:** Матрица целых чисел размером N×M, представленная как `std::vector<std::vector<int>>`.

**Выходные данные:** Вектор из N целых чисел, где i-й элемент — минимальное значение в i-й строке матрицы.

**Ограничения:**

- N, M > 0
- Строки матрицы имеют одинаковую длину
- Элементы матрицы — целые числа типа `int`

## 3. Базовый алгоритм (последовательный)
Последовательный алгоритм выполняет линейный проход по каждой строке матрицы с использованием стандартной функции `std::min_element`:

```cpp
for (int i = 0; i < rows; ++i) {
output[i] = *std::min_element(input[i].begin(), input[i].end());
}
```

**Сложность:** O(N × M) — каждый элемент матрицы просматривается один раз.

## 4. Схема распараллеливания
Используется схема распределения по строкам:

- Rank 0 (мастер) рассылает размеры матрицы всем процессам через `MPI_Bcast`
- Строки матрицы распределяются между процессами поровну: каждый процесс получает `base_rows = total_rows / num_procs` строк
- Остаток строк (если есть) распределяется: первые `extra_rows` процессов получают на одну строку больше
- Мастер отправляет данные другим процессам с помощью `MPI_Isend` (неблокирующая отправка)
- Рабочие процессы получают данные через `MPI_Recv`, вычисляют минимумы и отправляют результаты обратно

## 5. Детали реализации

### Структура кода

Реализация разделена на три основных компонента:

1. **Common-слой** ([common/include/common.hpp](../common/include/common.hpp)):
- Определены типы данных: `InType` = `std::vector<std::vector<int>>`, `OutType` = `std::vector<int>`
- Базовый класс `BaseTask` наследуется из фреймворка PPC
- Тест параметризуется размерами матрицы (N строк, M столбцов) и семенем для генератора

2. **Последовательная реализация** ([seq/include/ops_seq.hpp](../seq/include/ops_seq.hpp), [seq/src/ops_seq.cpp](../seq/src/ops_seq.cpp)):
- Класс `DenisovAMinValRowMatrixSEQ` наследуется от `BaseTask`
- `ValidationImpl()`: проверяет, что матрица не пуста и все строки содержат элементы
- `PreProcessingImpl()`: инициализирует выходной вектор размером N нулями
- `RunImpl()`: для каждой строки вычисляет минимум с помощью `std::min_element()`
- `PostProcessingImpl()`: пустой (нет дополнительной обработки)

3. **MPI реализация** ([mpi/include/ops_mpi.hpp](../mpi/include/ops_mpi.hpp), [mpi/src/ops_mpi.cpp](../mpi/src/ops_mpi.cpp)):
- Класс `DenisovAMinValRowMatrixMPI` наследуется от `BaseTask`
- Содержит две дополнительные приватные функции: `RunMaster()` и `RunWorker()`

### Важные предположения и граничные случаи

- **Предположение о размерности**: все строки матрицы имеют одинаковую длину (проверяется в `ValidationImpl`)
- **Пустая матрица**: недопустима (возвращает `false` при валидации)
- **Пустые строки**: недопустимы (проверяется через `std::ranges::all_of`)
- **Типы данных**: элементы матрицы — `int`, минимумы хранятся также как `int`
- **Распределение при неравномерности**: если число строк не делится на число процессов, остаток распределяется первым процессам для балансировки нагрузки

### Использование памяти

**Последовательный вариант:**
- Входные данные: N × M целых чисел
- Выходные данные: N целых чисел
- Дополнительная память: O(1) (не считая входных/выходных буферов)

**MPI вариант:**
- На каждом процессе: локальная копия `local_rows × M` элементов матрицы + вектор из `local_rows` минимумов
- На мастере (rank 0): дополнительно буферы для асинхронной отправки (`send_buffers`) размера `proc_rows × M` на каждый рабочий процесс
- Использование `MPI_Isend` для неблокирующей отправки позволяет перекрывать вычисления с коммуникацией

## 6. Экспериментальная установка

**Hardware/OS/Toolchain**

| Параметр | Значение |
| ---------- | ------------------------------------------------------- |
| CPU | AMD Ryzen 7 5800H (8 ядер / 16 потоков) @ 3.201 GHz |
| RAM | 16 ГБ |
| ОС | Windows 11 + WSL2 (Ubuntu 24.04.3 LTS) |
| Ядро | 6.6.87.2-microsoft-standard-WSL2 |
| Компилятор | GCC (g++) |
| MPI | Open MPI |
| Тип сборки | Release |

**Окружение:**

- Переменные, влияющие на запуск:
- `PPC_NUM_PROC` задаёт число MPI‑процессов при тестах (например, 1,2,4,8)

**Данные:**

- Для измерений производительности используется фиксированная семя генератора; размеры передаются через параметры задачи (N и M). Результаты сохраняются в папку `build` и обрабатываются скриптом `scripts/create_perf_table.py`.

Реальные файлы данных не хранятся; входной вектор создаётся в памяти.

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

### 7.1 Корректность

Все функциональные тесты проходят успешно на матрицах разных размеров

### 7.2 Производительность

| Mode | Proc count | Time, s | Speedup | Efficiency |
|-----------------|------------|----------|---------|------------|
| seq (pipeline) | 1 | 0.305 | 1.00 | — |
| seq (task_run) | 1 | 0.310 | 1.00 | — |
| mpi (pipeline) | 2 | 0.667 | 0.45 | 23% |
| mpi (task_run) | 2 | 0.491 | 0.63 | 32% |
| mpi (pipeline) | 4 | 0.744 | 0.41 | 10% |
| mpi (task_run) | 4 | 0.647 | 0.48 | 12% |
| mpi (pipeline) | 8 | 1.150 | 0.27 | 3% |
| mpi (task_run) | 8 | 0.983 | 0.32 | 4% |


**Анализ**:
Полученные результаты демонстрируют, что для задачи нахождения минимального элемента в строках матрицы параллельная реализация на MPI показывает низкую масштабируемость. При увеличении числа процессов время выполнения не уменьшается, а наоборот растёт. Это связано с несколькими факторами:
- Низкая вычислительная плотность задачи. Поиск минимума в строке — очень дешёвая операция, и объём вычислений на процесс оказывается слишком мал, чтобы компенсировать накладные расходы MPI.
- Высокая доля коммуникаций. Передача строк между процессами и сбор результатов занимают значительную часть времени. При малых матрицах коммуникации полностью доминируют над вычислениями.
- Увеличение числа процессов ухудшает ситуацию. При 4 и особенно 8 процессах объём работы на процесс становится настолько мал, что эффективность падает до 3–12%. Это типичное поведение для задач, где стоимость обмена данными превышает стоимость вычислений.
В результате speedup остаётся меньше 1 для всех конфигураций MPI, что означает фактическое замедление относительно последовательной версии.


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

Параллельная версия, основанная на распределении строк между MPI‑процессами, продемонстрировала низкую эффективность. При увеличении числа процессов время выполнения не сокращается, а растёт. Это объясняется тем, что задача обладает очень малой вычислительной сложностью на элемент данных, а накладные расходы MPI (передача строк, синхронизация, сбор результатов) оказываются значительно выше стоимости самих вычислений. При 4–8 процессах эффективность падает до 3–12%, что указывает на отсутствие масштабируемости.

Таким образом, для задач подобного типа (простые операции над строками матрицы) использование MPI оправдано только при очень больших размерах данных или при наличии более тяжёлых вычислений внутри каждой строки. Для небольших и средних матриц последовательная реализация оказывается оптимальной.

Возможные направления улучшения:
- использовать коллективные операции MPI_Scatterv и MPI_Gatherv, чтобы уменьшить количество сообщений;
- применять блочное распределение данных для более равномерной загрузки процессов;
- увеличить размер матрицы в экспериментах, чтобы повысить вычислительную плотность и уменьшить относительную долю коммуникаций.


## 9. Ссылки
1. Сысоев А. В. Лекции курса «Параллельное программирование для кластерных систем»
2. Документация лабораторных работ — <https://learning-process.github.io/parallel_programming_course/ru/>

Loading
Loading