Skip to content

Commit 0163d08

Browse files
authored
Чащин Владимир. Технология SEQ-MPI. Нахождение максимальных значений по строкам матрицы. Вариант 15. (#63)
### Описание Задача: Нахождение максимальных значений по строкам матрицы Вариант: 15 Технология: SEQ, MPI Описание SEQ: Алгоритм поиска максимальных значений по строкам матрицы основан на последовательном проходе по каждой строке. Для каждой строки последовательно просматриваются все элементы, поддерживается текущее максимальное значение, после чего результат записывается в соответствующую позицию выходного вектора. MPI: Параллельная версия распределяет строки входной матрицы между MPI-процессами. Для каждого процесса по его rank вычисляются индексы start и count — границы блока строк, назначенных к обработке. Распределение учитывает возможный остаток при делении числа строк на количество процессов, поэтому некоторые процессы могут получить больше строк. Процесс rank 0 отправляет назначенные строки каждому рабочему процессу с помощью MPI_Send, передавая сначала длину строки, затем её элементы. Рабочие процессы принимают свои строки через MPI_Recv, формируя локальную подматрицу. Каждый процесс независимо вычисляет максимальный элемент в каждой строке своей локальной подматрицы, формируя локальный вектор максимальных значений. Затем выполняется сбор результатов: процесс rank 0 получает локальные максимумы от остальных процессов через MPI_Recv и размещает их в соответствующие позиции итогового вектора. Рабочие процессы передают свои результаты обратно корневому процессу с помощью MPI_Send. После завершения сборки корневой процесс выполняет MPI_Bcast, чтобы разослать финальный вектор максимальных значений всем MPI-процессам. ### Чек-лист - [x] Статус CI: Все CI-задачи (сборка, тесты, генерация отчёта) успешно проходят на моей ветке в моем форке - [x] Директория и именование задачи: Я создал директорию с именем <фамилия>_<первая_буква_имени>_<короткое_название_задачи> - [x] Полное описание задачи: Я предоставил полное описание задачи в теле pull request - [x] clang-format: Мои изменения успешно проходят clang-format локально в моем форке (нет ошибок форматирования) - [x] clang-tidy: Мои изменения успешно проходят clang-tidy локально в моем форке (нет предупреждений/ошибок) - [x] Функциональные тесты: Все функциональные тесты успешно проходят локально на моей машине - [x] Тесты производительности: Все тесты производительности успешно проходят локально на моей машине - [x] Ветка: Я работаю в ветке, названной точно так же, как директория моей задачи (например, nesterov_a_vector_sum), а не в master - [x] Правдивое содержание: Я подтверждаю, что все сведения, указанные в этом pull request, являются точными и достоверными
1 parent aef4392 commit 0163d08

11 files changed

Lines changed: 631 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <tuple>
5+
#include <vector>
6+
7+
#include "task/include/task.hpp"
8+
9+
namespace chaschin_v_max_for_each_row {
10+
11+
using InType = std::vector<std::vector<float>>;
12+
using OutType = std::vector<float>;
13+
using TestType = std::tuple<int, std::string>;
14+
using BaseTask = ppc::task::Task<InType, OutType>;
15+
16+
} // namespace chaschin_v_max_for_each_row
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"student": {
3+
"first_name": "Владимир",
4+
"last_name": "Чащин",
5+
"middle_name": "Александрович",
6+
"group_number": "3823Б1ФИ3",
7+
"task_number": "1"
8+
}
9+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include <vector>
4+
5+
#include "chaschin_v_max_for_each_row/common/include/common.hpp"
6+
#include "task/include/task.hpp"
7+
8+
namespace chaschin_v_max_for_each_row {
9+
10+
class ChaschinVMaxForEachRow : public BaseTask {
11+
public:
12+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
13+
return ppc::task::TypeOfTask::kMPI;
14+
}
15+
explicit ChaschinVMaxForEachRow(const InType &in);
16+
17+
private:
18+
bool ValidationImpl() override;
19+
bool PreProcessingImpl() override;
20+
bool RunImpl() override;
21+
bool PostProcessingImpl() override;
22+
23+
struct RowRange {
24+
int start;
25+
int count;
26+
};
27+
28+
static RowRange ComputeRange(int nrows, int rank, int size);
29+
static std::vector<std::vector<float>> DistributeRows(const std::vector<std::vector<float>> &mat, int rank, int size,
30+
const RowRange &range);
31+
static std::vector<float> ComputeLocalMax(const std::vector<std::vector<float>> &local_mat);
32+
33+
static void GatherResults(std::vector<float> &out, const std::vector<float> &local_out, int rank, int size,
34+
const RowRange &range);
35+
static void SendRowsToWorkers(const std::vector<std::vector<float>> &mat, int size);
36+
static void ReceiveRowsFromRoot(std::vector<std::vector<float>> &local_mat);
37+
};
38+
39+
} // namespace chaschin_v_max_for_each_row
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#include "chaschin_v_max_for_each_row/mpi/include/ops_mpi.hpp"
2+
3+
#include <mpi.h>
4+
5+
#include <algorithm>
6+
#include <cstddef>
7+
#include <limits>
8+
#include <utility>
9+
#include <vector>
10+
11+
#include "chaschin_v_max_for_each_row/common/include/common.hpp"
12+
13+
namespace chaschin_v_max_for_each_row {
14+
15+
ChaschinVMaxForEachRow::ChaschinVMaxForEachRow(const InType &in) {
16+
SetTypeOfTask(GetStaticTypeOfTask());
17+
auto in_copy = in;
18+
GetInput() = std::move(in_copy);
19+
this->GetOutput().clear();
20+
}
21+
22+
bool ChaschinVMaxForEachRow::ValidationImpl() {
23+
const auto &in = GetInput();
24+
25+
if (in.empty()) {
26+
return in.empty();
27+
}
28+
29+
if (in[0].empty()) {
30+
return in[0].empty();
31+
}
32+
33+
return true;
34+
}
35+
36+
bool ChaschinVMaxForEachRow::PreProcessingImpl() {
37+
return true;
38+
}
39+
40+
void chaschin_v_max_for_each_row::ChaschinVMaxForEachRow::SendRowsToWorkers(const std::vector<std::vector<float>> &mat,
41+
int size) {
42+
for (int pi = 1; pi < size; ++pi) {
43+
// Inline ComputeRange
44+
int nrows = static_cast<int>(mat.size());
45+
int base = nrows / size;
46+
int rem = nrows % size;
47+
int start = (pi * base) + std::min(pi, rem);
48+
int count = base + (pi < rem ? 1 : 0);
49+
50+
for (int ii = 0; ii < count; ++ii) {
51+
const auto &row = mat[start + ii];
52+
int len = static_cast<int>(row.size());
53+
54+
MPI_Send(&len, 1, MPI_INT, pi, 100, MPI_COMM_WORLD);
55+
if (len > 0) {
56+
MPI_Send(row.data(), len, MPI_FLOAT, pi, 101, MPI_COMM_WORLD);
57+
}
58+
}
59+
}
60+
}
61+
62+
void chaschin_v_max_for_each_row::ChaschinVMaxForEachRow::ReceiveRowsFromRoot(
63+
std::vector<std::vector<float>> &local_mat) {
64+
for (auto &row : local_mat) {
65+
int len = 0;
66+
MPI_Recv(&len, 1, MPI_INT, 0, 100, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
67+
68+
row.resize(len);
69+
if (len > 0) {
70+
MPI_Recv(row.data(), len, MPI_FLOAT, 0, 101, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
71+
}
72+
}
73+
}
74+
75+
std::vector<std::vector<float>> chaschin_v_max_for_each_row::ChaschinVMaxForEachRow::DistributeRows(
76+
const std::vector<std::vector<float>> &mat, int rank, int size, const RowRange &range) {
77+
std::vector<std::vector<float>> local_mat(range.count);
78+
79+
if (rank == 0) {
80+
SendRowsToWorkers(mat, size);
81+
82+
for (int ii = 0; ii < range.count; ++ii) {
83+
local_mat[ii] = mat[range.start + ii];
84+
}
85+
} else {
86+
ReceiveRowsFromRoot(local_mat);
87+
}
88+
89+
return local_mat;
90+
}
91+
92+
std::vector<float> chaschin_v_max_for_each_row::ChaschinVMaxForEachRow::ComputeLocalMax(
93+
const std::vector<std::vector<float>> &local_mat) {
94+
std::vector<float> local_out(local_mat.size());
95+
for (size_t ii = 0; ii < local_mat.size(); ++ii) {
96+
local_out[ii] = local_mat[ii].empty() ? std::numeric_limits<float>::lowest()
97+
: *std::max_element(local_mat[ii].begin(), local_mat[ii].end());
98+
}
99+
return local_out;
100+
}
101+
102+
namespace {
103+
inline void GetRangeForRank(int rank, int total, int world_size, int &start, int &count) {
104+
int base = total / world_size;
105+
int rem = total % world_size;
106+
start = (rank * base) + std::min(rank, rem);
107+
count = base + (rank < rem ? 1 : 0);
108+
}
109+
110+
inline void RecvRows(int src_rank, std::vector<float> &out, int start, int count) {
111+
std::vector<float> tmp(count);
112+
MPI_Recv(tmp.data(), count, MPI_FLOAT, src_rank, 2, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
113+
std::ranges::copy(tmp, out.begin() + start);
114+
}
115+
} // namespace
116+
117+
void chaschin_v_max_for_each_row::ChaschinVMaxForEachRow::GatherResults(std::vector<float> &out,
118+
const std::vector<float> &local_out, int rank,
119+
int size, const RowRange &range) {
120+
if (rank != 0) {
121+
if (!local_out.empty()) {
122+
MPI_Send(local_out.data(), static_cast<int>(local_out.size()), MPI_FLOAT, 0, 2, MPI_COMM_WORLD);
123+
}
124+
return;
125+
}
126+
127+
for (int i = 0; i < range.count; ++i) {
128+
out[range.start + i] = local_out[i];
129+
}
130+
131+
int total = static_cast<int>(out.size());
132+
for (int pi = 1; pi < size; ++pi) {
133+
int start = 0;
134+
int count = 0;
135+
136+
GetRangeForRank(pi, total, size, start, count);
137+
if (count > 0) {
138+
RecvRows(pi, out, start, count);
139+
}
140+
}
141+
}
142+
143+
bool ChaschinVMaxForEachRow::RunImpl() {
144+
int rank = 0;
145+
int size = 0;
146+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
147+
MPI_Comm_size(MPI_COMM_WORLD, &size);
148+
149+
const auto &mat = GetInput();
150+
int nrows = (rank == 0) ? static_cast<int>(mat.size()) : 0;
151+
MPI_Bcast(&nrows, 1, MPI_INT, 0, MPI_COMM_WORLD);
152+
153+
int base = nrows / size;
154+
int rem = nrows % size;
155+
int start = (rank * base) + std::min(rank, rem);
156+
int count = base + (rank < rem ? 1 : 0);
157+
RowRange range{.start = start, .count = count};
158+
159+
auto local_mat = DistributeRows(mat, rank, size, range);
160+
auto local_out = ComputeLocalMax(local_mat);
161+
162+
if (rank == 0) {
163+
GetOutput().resize(nrows);
164+
}
165+
GatherResults(GetOutput(), local_out, rank, size, range);
166+
167+
auto &out = GetOutput();
168+
if (rank != 0) {
169+
out.resize(nrows);
170+
}
171+
172+
if (nrows > 0) {
173+
MPI_Bcast(out.data(), nrows, MPI_FLOAT, 0, MPI_COMM_WORLD);
174+
}
175+
176+
return true;
177+
}
178+
179+
bool ChaschinVMaxForEachRow::PostProcessingImpl() {
180+
int rank = 0;
181+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
182+
return true;
183+
}
184+
} // namespace chaschin_v_max_for_each_row
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Поиск максимального значения в каждой строке матрицы
2+
3+
- Студент: Чащин Владимир Александрович, группа 3823Б1ФИ3
4+
- Технология: SEQ, MPI
5+
- Вариант: 15
6+
7+
## 1. Введение
8+
9+
Обработка больших матриц — фундаментальная задача во множестве вычислительных областей. Одним из базовых примитивов является поиск экстремальных значений по строкам или столбцам, который широко используется в статистике, линейной алгебре, анализе данных и машинном обучении.
10+
11+
Задачи такого типа обладают выраженной структурой, но часто плохо масштабируются при параллелизации из-за малого объёма вычислений на единицу данных: вычисление максимума в строке — операция линейной сложности с крайне низкими вычислительными затратами на элемент.
12+
13+
Цель работы — реализовать последовательную и параллельную MPI-версии поиска максимума в каждой строке квадратной матрицы и исследовать поведение этих реализаций на больших входах, проанализировать их эффективность и определить причины, влияющие на масштабируемость.
14+
## 2. Постановка задачи
15+
16+
**Дано:** матрица `N × N`, элементы — числа с плавающей точкой (тип `float`).
17+
18+
**Требуется:** построить вектор длины `N`, где каждый элемент — максимальный элемент соответствующей строки матрицы.
19+
20+
**Входные данные:** `std::vector<std::vector<float>>` — двумерная матрица.
21+
**Выходные данные:** `std::vector<float>` — вектор максимальных значений.
22+
Матрица гарантированно квадратная, строки могут иметь любую длину, включая крайние случаи: 0 и 1 элемент.
23+
24+
**Ограничения:** Матрица и её строки могут быть непустыми. Реализация должна корректно обрабатывать случаи, когда количество строк не делится нацело на количество используемых процессов.
25+
26+
## 3. Последовательный алгоритм
27+
28+
Последовательный алгоритм представляет собой прямой одномерный проход по каждой строке с поиском максимума.
29+
## Анализ алгоритма
30+
* На каждую строку выполняется вызов `std::max_element`, который работает за `O(N)` и хорошо оптимизируется компилятором.
31+
* Общая сложность — `O(N²)` для матрицы `N×N`.
32+
* Вычислительных операций мало, основная задержка — чтение элементов из памяти.
33+
* Современные компиляторы (включая Intel C++ Compiler 2025) легко векторизуют такие циклы, что делает последовательную версию очень быстрой.
34+
Алгоритм имеет временную сложность O(M * N), так как требует полного обхода всех элементов матрицы.
35+
* Важное следствие: при попытках распараллеливания на высокоуровневых технологиях типа MPI вычисления часто оказываются быстрее, чем коммуникации.
36+
Данный факт определяет ограничение масштабируемости MPI-версии.
37+
38+
## 4. Схема распараллеливания
39+
40+
Используется классическая модель Master–Worker.
41+
42+
## Разбиение строк (анализ)
43+
44+
Процесс 0 вычисляет диапазон строк для каждого процесса:
45+
* `base = N / size` — минимальное число строк на процесс
46+
* `rem = N % size` — остаток, который равномерно распределяется между первыми процессами
47+
Таким образом, распределение сбалансировано и обеспечивает почти равное количество строк на каждый процесс.
48+
49+
## Передача данных
50+
Алгоритм распределения данных работает следующим образом:
51+
52+
1. Определение диапазона строк для каждого процесса.
53+
* Сначала вычисляется, сколько строк матрицы должно достаться каждому процессу.
54+
* Если общее количество строк не делится нацело на число процессов, остаток распределяется по одному на первые процессы.
55+
56+
2. Отправка строк рабочим процессам.
57+
* Процесс с рангом 0 (Мастер) последовательно проходит по всем рабочим процессам.
58+
* Для каждого процесса мастер отправляет сначала длину строки, затем саму строку чисел.
59+
* Каждая строка передаётся отдельно, чтобы рабочий процесс точно знал, сколько элементов ему принимать.
60+
61+
3. Получение данных на стороне рабочих процессов.
62+
* Каждый рабочий процесс получает длину строки.
63+
* После этого создаётся буфер нужного размера, и в него принимаются элементы строки.
64+
* Так происходит для всех строк, выделенных данному процессу.
65+
66+
4. Обработка локальных данных.
67+
* После приёма всех строк процесс вычисляет максимальные элементы по каждой строке.
68+
* Результаты затем собираются обратно у мастера (сбор не описан в этом фрагменте).
69+
70+
## 4. Экспериментальная установка
71+
72+
* CPU: Intel Core i5-12500H
73+
* 4 производительных ядра
74+
* 8 энергоэффективных ядер
75+
* RAM: 16 GB
76+
* OS: Windows 11 Pro 24H2
77+
* Компилятор: Intel C++ Compiler 2025
78+
* Матрица: детерминированная, квадратная, 20000×20000
79+
* Время — среднее по 8 повторениям.
80+
81+
## 6. Результаты и обсуждение
82+
83+
### 6.1 Корректность
84+
85+
Функциональные тесты покрывают 97% кода, включая:
86+
* матрицы разных размеров;
87+
* строку длины 0;
88+
* матрицу из одной строки;
89+
* большие матрицы;
90+
* соответствие MPI-версии последовательному алгоритму.
91+
92+
Все тесты пройдены, расхождений нет.
93+
Корректность параллельной реализации подтверждена.
94+
95+
### 6.2 Производительность
96+
97+
Для оценки производительности измерялось чистое время выполнения алгоритма без учета создания тестовых данных. На основе полученных данных были рассчитаны метрики ускорения (Speedup) и эффективности (Efficiency).
98+
99+
| Режим | Число процессов | Время, ms | Ускорение | Эффективность |
100+
| ----- | --------------- | --------- | --------- | ------------- |
101+
| seq | 1 | 856 | 1.00 ||
102+
| mpi | 1 | 945 | 0.91 | 91% |
103+
| mpi | 2 | 1222 | 0.70 | 35% |
104+
| mpi | 4 | 955 | 0.89 | 22% |
105+
| mpi | 8 | 717 | 1.19 | 14% |
106+
| mpi | 16 | 645 | 1.32 | 8% |
107+
108+
**Анализ результатов:**
109+
110+
* На 1 процессе MPI хуже seq. Причиной этому послужили издержки, созданные MPI
111+
* На 2–4 процессах происходит замедление.
112+
Коммуникации занимают много времени
113+
* Ускорение появляется только при 8+ процессах, но эффективность падает.
114+
* 16 процессов дают ускорение всего 1.32×, что крайне мало для 16 логических ядер.
115+
* до 90% времени MPI-версии уходит на передачу данных,
116+
— полученный результат полностью подтверждает архитектурные проблемы реализации.
117+
* Реализация SEQ оптимальна и векторизована, поэтому конкурировать с ней сложно.
118+
## 8. Выводы
119+
120+
*Последовательный алгоритм прост, эффективен, хорошо векторизуется и полностью memory-bound.
121+
122+
* MPI-версия корректна, но неэффективна.
123+
* Master является узким местом;
124+
* объём вычислений на строку минимален и не окупает расходы на коммуникации.
125+
* На тестовой матрице 20000×20000 ускорение составляет лишь 1.32× при 16 процессах, что подтверждает низкое соотношение computation-to-communication.
126+
127+
## 9. Источники
128+
129+
1. Parallel Programming Course - [https://learning-process.github.io/parallel_programming_course/ru/](https://learning-process.github.io/parallel_programming_course/ru/)
130+
2. Parallel Programming 2025-2026 Video-Records - [https://disk.yandex.ru/d/NvHFyhOJCQU65w](https://disk.yandex.ru/d/NvHFyhOJCQU65w)
131+
3. Open MPI: Documentation — [https://www.open-mpi.org/doc/](https://www.open-mpi.org/doc/)
132+
4. C++ reference (cppreference.com) — [https://en.cppreference.com/w/cpp/algorithm/ranges/min_element](https://en.cppreference.com/w/cpp/algorithm/ranges/min_element)

0 commit comments

Comments
 (0)