diff --git a/tasks/denisov_a_ring/common/include/common.hpp b/tasks/denisov_a_ring/common/include/common.hpp new file mode 100644 index 00000000..6ad0c4c1 --- /dev/null +++ b/tasks/denisov_a_ring/common/include/common.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +#include "task/include/task.hpp" + +namespace denisov_a_ring { + +struct RingMessage { + int source = 0; + int destination = 0; + std::vector data; +}; + +using InType = RingMessage; +using OutType = std::vector; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +} // namespace denisov_a_ring diff --git a/tasks/denisov_a_ring/info.json b/tasks/denisov_a_ring/info.json new file mode 100644 index 00000000..0446aa26 --- /dev/null +++ b/tasks/denisov_a_ring/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Артём", + "group_number": "3823Б1ПР4", + "last_name": "Денисов", + "middle_name": "Андреевич", + "task_number": "2" + } +} diff --git a/tasks/denisov_a_ring/mpi/include/ops_mpi.hpp b/tasks/denisov_a_ring/mpi/include/ops_mpi.hpp new file mode 100644 index 00000000..e01f502f --- /dev/null +++ b/tasks/denisov_a_ring/mpi/include/ops_mpi.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "denisov_a_ring/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace denisov_a_ring { + +class RingTopologyMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + + explicit RingTopologyMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + void static SendVector(const std::vector &data, int to_rank); + void static ReceiveVector(std::vector &data, int from_rank); + void static BroadcastResult(std::vector &output, int rank, int root); +}; + +} // namespace denisov_a_ring diff --git a/tasks/denisov_a_ring/mpi/src/ops_mpi.cpp b/tasks/denisov_a_ring/mpi/src/ops_mpi.cpp new file mode 100644 index 00000000..82e8cfaa --- /dev/null +++ b/tasks/denisov_a_ring/mpi/src/ops_mpi.cpp @@ -0,0 +1,118 @@ +#include "denisov_a_ring/mpi/include/ops_mpi.hpp" + +#include + +#include + +#include "denisov_a_ring/common/include/common.hpp" + +namespace denisov_a_ring { + +RingTopologyMPI::RingTopologyMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput().clear(); +} + +bool RingTopologyMPI::ValidationImpl() { + int world_size = 0; + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + + const auto &in = GetInput(); + if (in.source < 0 || in.source >= world_size) { + return false; + } + + if (in.destination < 0 || in.destination >= world_size) { + return false; + } + + return true; +} + +bool RingTopologyMPI::PreProcessingImpl() { + GetOutput().clear(); + return true; +} + +bool RingTopologyMPI::RunImpl() { + int rank = 0; + int world_size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + + const auto &in = GetInput(); + int start_node = in.source; + int target_node = in.destination; + + if (start_node == target_node) { + if (rank == start_node) { + GetOutput() = in.data; + } + BroadcastResult(GetOutput(), rank, start_node); + return true; + } + + int next = (rank + 1) % world_size; + int prev = (rank - 1 + world_size) % world_size; + + int total_steps = (target_node - start_node + world_size) % world_size; + int local_step = (rank - start_node + world_size) % world_size; + + std::vector local_buf; + + if (local_step == 0) { + local_buf = in.data; + SendVector(local_buf, next); + } else if (local_step < total_steps) { + ReceiveVector(local_buf, prev); + SendVector(local_buf, next); + } else if (local_step == total_steps) { + ReceiveVector(local_buf, prev); + } + + if (rank == target_node) { + GetOutput() = local_buf; + } + + BroadcastResult(GetOutput(), rank, target_node); + return true; +} + +bool RingTopologyMPI::PostProcessingImpl() { + return true; +} + +void RingTopologyMPI::SendVector(const std::vector &data, int to_rank) { + int count = static_cast(data.size()); + MPI_Send(&count, 1, MPI_INT, to_rank, 0, MPI_COMM_WORLD); + + if (count > 0) { + MPI_Send(data.data(), count, MPI_INT, to_rank, 1, MPI_COMM_WORLD); + } +} + +void RingTopologyMPI::ReceiveVector(std::vector &data, int from_rank) { + int count = 0; + MPI_Recv(&count, 1, MPI_INT, from_rank, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); + + data.resize(count); + if (count > 0) { + MPI_Recv(data.data(), count, MPI_INT, from_rank, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE); + } +} + +void RingTopologyMPI::BroadcastResult(std::vector &out, int rank, int root) { + int size = (rank == root) ? static_cast(out.size()) : 0; + MPI_Bcast(&size, 1, MPI_INT, root, MPI_COMM_WORLD); + + if (rank != root) { + out.resize(size); + } + + if (size > 0) { + MPI_Bcast(out.data(), size, MPI_INT, root, MPI_COMM_WORLD); + } +} + +} // namespace denisov_a_ring diff --git a/tasks/denisov_a_ring/report.md b/tasks/denisov_a_ring/report.md new file mode 100644 index 00000000..5f87a240 --- /dev/null +++ b/tasks/denisov_a_ring/report.md @@ -0,0 +1,231 @@ +# Топология кольцо + +- Студент: Денисов Артём Андреевич, группа 3823Б1ПР4 +- Технология: SEQ + MPI +- Вариант: 7 + +## 1. Введение + +Реализация виртуальной кольцевой топологии относится к классическим задачам параллельных вычислений. Требуется организовать передачу данных от произвольного процесса к любому другому, используя механизмы MPI для взаимодействия между процессами, но без применения специализированных функций построения топологий, таких как MPI_Cart_Create или MPI_Graph_Create. + +Основная цель работы — разработать последовательный и распределённый варианты алгоритма передачи сообщений по кольцу, выполнить проверку корректности их работы, провести измерения производительности и оценить эффективность выбранного подхода. + +## 2. Постановка задачи + +**Входные данные:** Структура `RingMessage`, которая включает: +- номер процесса‑отправителя; +- номер процесса‑получателя; +- последовательность целых чисел, предназначенную для пересылки по кольцу. + +**Выходные данные:** Вектор целых чисел, содержащий данные, доставленные получателю. + +**Ограничения:** + +- `RingMessage.source >= 0`, `RingMessage.destination >= 0` +- Не используются `MPI_Cart_Create` и `MPI_Graph_Create` + +## 3. Базовый алгоритм (последовательный) + +В последовательной версии передаваемые данные просто копируются из входа на выход, так как маршрутизация не требуется. + +```cpp +bool RingTopologySEQ::RunImpl() { + const auto& in = GetInput(); + GetOutput() = in.data; + return true; +} +``` + +**Сложность:** O(N) — копирование вектора из N элементов. + +## 4. Схема распараллеливания + +Используется кольцевая маршрутизация: процессы образуют виртуальное кольцо, +данные передаются последовательно от источника к получателю по часовой стрелке +через промежуточные узлы. + +### 4.1. Определение соседей в кольце + +```cpp +int next = (rank + 1) % world_size; +int prev = (rank - 1 + world_size) % world_size; +``` + +Для каждого процесса вычисляется правый и левый сосед в кольцевой топологии. + +### 4.2. Обработка случая source == destination + +```cpp +int start_node = in.source; +int target_node = in.destination; + +if (start_node == target_node) { + if (rank == start_node) { + GetOutput() = in.data; + } + BroadcastResult(GetOutput(), rank, start_node); + return true; +} +``` + +Когда источник и получатель совпадают, данные уже находятся на нужном процессе. +Для синхронизации всех процессов результат рассылается через `MPI_Bcast`. + +### 4.3. Вычисление количества шагов + +```cpp +int total_steps = (target_node - start_node + world_size) % world_size; +int local_step = (rank - start_node + world_size) % world_size; +``` + +Количество шагов `total_steps` определяет длину пути по кольцу от источника к получателю. +Количество шагов `local_step` показывает расстояние текущего процесса от источника в направлении передачи. Это позволяет каждому процессу определить свою роль: отправитель, промежуточный узел или получатель. + +### 4.4. Передача данных по кольцу + +```cpp +if (local_step == 0) { + local_buf = in.data; + SendVector(local_buf, next); +} else if (local_step < total_steps) { + ReceiveVector(local_buf, prev); + SendVector(local_buf, next); +} else if (local_step == total_steps) { + ReceiveVector(local_buf, prev); +} +``` + +Данные передаются по кольцу.Каждый промежуточный процесс принимает данные от левого соседа и пересылает правому. + +### 4.5. Рассылка результата всем процессам + +```cpp +int size = (rank == root) ? static_cast(out.size()) : 0; +MPI_Bcast(&size, 1, MPI_INT, root, MPI_COMM_WORLD); + +if (rank != root) { + out.resize(size); +} + +if (size > 0) { + MPI_Bcast(out.data(), size, MPI_INT, root, MPI_COMM_WORLD); +} +``` + +После доставки данных получателю результат рассылается всем процессам через `MPI_Bcast` для обеспечения согласованности выходных данных. + +## 5. Детали реализации + +### Структура кода + +Проект организован по шаблону PPC: общие типы описаны в `common`, а реализация алгоритмов разделена на последовательную и MPI-версию. + +1. **Common-слой** ([common/include/common.hpp](../common/include/common.hpp)): + - Структура `RingMessage` содержит поля `source`, `destination` и вектор `data`. + - Псевдонимы `InType` и `OutType` определены как `RingMessage` и `std::vector` соответственно. + - `BaseTask` — шаблонный класс из фреймворка PPC, от которого наследуются оба варианта решения. + - В `common` же задан тип `TestType`, используемый в модульных тестах для генерации входа. + +2. **Последовательная реализация** ([seq/include/ops_seq.hpp](../seq/include/ops_seq.hpp), [seq/src/ops_seq.cpp](../seq/src/ops_seq.cpp)): + - Класс `RingTopologySEQ` наследуется от `BaseTask` и реализует тривиальную копию данных из входа в выход. + - `ValidationImpl()` проверяет, что номера процессов неотрицательны. + - `PreProcessingImpl()` очищает выходной вектор. + - `RunImpl()` присваивает `GetOutput() = in.data`. + - `PostProcessingImpl()` пустой (нет дополнительной обработки). + +3. **MPI реализация** ([mpi/include/ops_mpi.hpp](../mpi/include/ops_mpi.hpp), [mpi/src/ops_mpi.cpp](../mpi/src/ops_mpi.cpp)): + - Класс `RingTopologyMPI` также производит наследование от `BaseTask`. + - Вспомогательные статические методы `SendVector`, `ReceiveVector` и `BroadcastResult` инкапсулируют низкоуровневые операции `MPI_Send`, `MPI_Recv` и `MPI_Bcast` для передачи векторов. + - Основной алгоритм в `RunImpl()` вычисляет соседей в кольце, обрабатывает ситуацию `source == destination`, рассчитывает количество шагов и локальную роль (отправитель, промежуточный узел, получатель), затем передаёт данные по кольцу, а по завершении рассылает результат всем процессам. + - `ValidationImpl()` дополнительно проверяет, что `source` и `destination` находятся в пределах `[0, world_size)`. + +### Важные предположения и граничные случаи + +- **Индексы процессов**: `source` и `destination` должны быть ≥ 0 и < mpi_size. Валидация возвращает `false` при нарушении. +- **Пустой вектор**: разрешён, передаётся как пустой результат. Передача пустого массива выполняется без MPI-контента. +- **Случай source == destination**: данные не передаются по кольцу, а просто копируются на корневом процессе и рассылаются через `MPI_Bcast`. + +### Использование памяти + +- **Seq**: хранится только входной `RingMessage` и выходной `std::vector` (копия данных). +- **MPI**: каждый процесс может иметь временный буфер `local_buf` равный размеру передаваемого вектора. Дополнительные небольшие буферы нужны для передачи размера (int) при отправке вектора. + +## 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) + +**Подготовка данных:** + +Вся входная информация генерируется программно; никаких внешних файлов не +используется. Для повторяемости при измерениях применяется детерминированное +семя генератора, а размеры задаются параметрами задачи. Результаты запусков +(время, скорость и др.) сохраняются в директорию `build` и затем анализируются +скриптом `scripts/create_perf_table.py`. + +### Наборы для тестов + +- **Функциональные**: векторы размером 1, 5, 50, 100, 500 и 1000 + элементов. Значения выбирались случайно в диапазоне [−1000, 1000]. Эти наборы + используются для проверки корректности работы. + +- **Нагрузочный**: один длинный вектор длиной 1 000 000 элементов с числами из + диапазона [−10000, 10000]. Эксперимент исполнялся с отправителем на процессе 0 + и получателем на процессе `size − 1`. +## 7. Результаты + +### 7.1 Корректность + +Все функциональные тесты проходят успешно. + +### 7.2 Производительность + +#### Общие результаты + +| Mode | Proc count | Time, s | Speedup | Efficiency | +|-----------------|------------|----------|---------|------------| +| seq (pipeline) | 1 | 0.009 | 1.00 | — | +| seq (task_run) | 1 | 0.008 | 1.00 | — | +| mpi (pipeline) | 2 | 0.056 | 0.16 | 8% | +| mpi (task_run) | 2 | 0.047 | 0.17 | 9% | +| mpi (pipeline) | 4 | 0.084 | 0.11 | 3% | +| mpi (task_run) | 4 | 0.077 | 0.10 | 3% | +| mpi (pipeline) | 8 | 0.173 | 0.05 | 1% | +| mpi (task_run) | 8 | 0.142 | 0.06 | 1% | + +Время MPI растёт с числом процессов, так как количество переходов +по кольцу увеличивается: 1 при NP=2, 3 при NP=4, 7 при NP=8. +Задача коммуникационная, а не вычислительная — ускорения от добавления процессов не ожидается. + +## 8. Выводы + +В ходе работы реализованы последовательная и параллельная версии алгоритма +передачи данных по кольцевой топологии. Обе версии успешно +проходят функциональные тесты. + +Реализация не использует встроенные функции создания топологий +(`MPI_Cart_Create`, `MPI_Graph_Create`), а вручную организует маршрутизацию +данных по кольцу через соседей каждого процесса. + +Алгоритм обеспечивает корректную передачу данных от любого процесса-источника +к любому процессу-получателю, последовательно проходя через промежуточные +узлы кольца. Количество шагов передачи равно `(destination - source + world_size) % world_size`. + +## 9. Ссылки + +1. Сысоев А. В. Лекции курса «Параллельное программирование для кластерных систем» +2. Документация лабораторных работ — diff --git a/tasks/denisov_a_ring/seq/include/ops_seq.hpp b/tasks/denisov_a_ring/seq/include/ops_seq.hpp new file mode 100644 index 00000000..7e89f530 --- /dev/null +++ b/tasks/denisov_a_ring/seq/include/ops_seq.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "denisov_a_ring/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace denisov_a_ring { + +class RingTopologySEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit RingTopologySEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace denisov_a_ring diff --git a/tasks/denisov_a_ring/seq/src/ops_seq.cpp b/tasks/denisov_a_ring/seq/src/ops_seq.cpp new file mode 100644 index 00000000..f95117f9 --- /dev/null +++ b/tasks/denisov_a_ring/seq/src/ops_seq.cpp @@ -0,0 +1,33 @@ +#include "denisov_a_ring/seq/include/ops_seq.hpp" + +#include "denisov_a_ring/common/include/common.hpp" + +namespace denisov_a_ring { + +RingTopologySEQ::RingTopologySEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput().clear(); +} + +bool RingTopologySEQ::ValidationImpl() { + const auto &in = GetInput(); + return (in.source >= 0) && (in.destination >= 0); +} + +bool RingTopologySEQ::PreProcessingImpl() { + GetOutput().clear(); + return true; +} + +bool RingTopologySEQ::RunImpl() { + const auto &in = GetInput(); + GetOutput() = in.data; + return true; +} + +bool RingTopologySEQ::PostProcessingImpl() { + return true; +} + +} // namespace denisov_a_ring diff --git a/tasks/denisov_a_ring/settings.json b/tasks/denisov_a_ring/settings.json new file mode 100644 index 00000000..16f25e42 --- /dev/null +++ b/tasks/denisov_a_ring/settings.json @@ -0,0 +1,7 @@ +{ + "tasks": { + "mpi": "enabled", + "seq": "enabled" + }, + "tasks_type": "processes" +} diff --git a/tasks/denisov_a_ring/tests/.clang-tidy b/tasks/denisov_a_ring/tests/.clang-tidy new file mode 100644 index 00000000..ef43b7aa --- /dev/null +++ b/tasks/denisov_a_ring/tests/.clang-tidy @@ -0,0 +1,13 @@ +InheritParentConfig: true + +Checks: > + -modernize-loop-convert, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-non-const-global-variables, + -misc-use-anonymous-namespace, + -modernize-use-std-print, + -modernize-type-traits + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 50 # Relaxed for tests diff --git a/tasks/denisov_a_ring/tests/functional/main.cpp b/tasks/denisov_a_ring/tests/functional/main.cpp new file mode 100644 index 00000000..350788f7 --- /dev/null +++ b/tasks/denisov_a_ring/tests/functional/main.cpp @@ -0,0 +1,96 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "denisov_a_ring/common/include/common.hpp" +#include "denisov_a_ring/mpi/include/ops_mpi.hpp" +#include "denisov_a_ring/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace denisov_a_ring { + +class DenisovARingFuncTest : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType ¶m) { + return std::to_string(std::get<0>(param)) + "_" + std::get<1>(param); + } + + protected: + void SetUp() override { + int world_size = 1; + + if (ppc::util::IsUnderMpirun()) { + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + } else { + world_size = ppc::util::GetNumProc(); + } + + const auto ¶ms = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + + int vec_len = std::get<0>(params); + + input_.source = 0; + input_.destination = (world_size > 1 ? world_size - 1 : 0); + input_.data.resize(static_cast(vec_len)); + + std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(-1000, 1000); + + for (int i = 0; i < vec_len; ++i) { + input_.data[static_cast(i)] = dist(rng); + } + + expected_ = input_.data; + } + + bool CheckTestOutputData(OutType &out) final { + if (ppc::util::IsUnderMpirun()) { + int rank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + if (rank != 0) { + return true; + } + } + return out == expected_; + } + + InType GetTestInputData() final { + return input_; + } + + private: + InType input_{}; + OutType expected_; +}; + +namespace { + +TEST_P(DenisovARingFuncTest, RingTransfer) { + ExecuteTest(GetParam()); +} + +const std::array kTestParams = { + std::make_tuple(1, "single"), std::make_tuple(5, "small"), std::make_tuple(50, "medium_50"), + std::make_tuple(100, "medium"), std::make_tuple(500, "large_500"), std::make_tuple(1000, "large"), +}; + +const auto kTasks = + std::tuple_cat(ppc::util::AddFuncTask(kTestParams, PPC_SETTINGS_denisov_a_ring), + ppc::util::AddFuncTask(kTestParams, PPC_SETTINGS_denisov_a_ring)); + +const auto kValues = ppc::util::ExpandToValues(kTasks); + +const auto kNameGen = DenisovARingFuncTest::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(RingTopologyTests, DenisovARingFuncTest, kValues, kNameGen); + +} // namespace + +} // namespace denisov_a_ring diff --git a/tasks/denisov_a_ring/tests/performance/main.cpp b/tasks/denisov_a_ring/tests/performance/main.cpp new file mode 100644 index 00000000..b50c0b1d --- /dev/null +++ b/tasks/denisov_a_ring/tests/performance/main.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include +#include + +#include "denisov_a_ring/common/include/common.hpp" +#include "denisov_a_ring/mpi/include/ops_mpi.hpp" +#include "denisov_a_ring/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" +#include "util/include/util.hpp" + +namespace denisov_a_ring { + +class DenisovARingPerfTest : public ppc::util::BaseRunPerfTests { + static constexpr int kVectorLength = 1'000'000; + + InType input_{}; + OutType expected_; + + void SetUp() override { + int world_size = 1; + + if (ppc::util::IsUnderMpirun()) { + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + } else { + world_size = ppc::util::GetNumProc(); + } + + input_.source = 0; + input_.destination = (world_size > 1 ? world_size - 1 : 0); + input_.data.resize(kVectorLength); + + std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution distrib(-10000, 10000); + + for (int i = 0; i < kVectorLength; ++i) { + input_.data[static_cast(i)] = distrib(rng); + } + + expected_ = input_.data; + } + + bool CheckTestOutputData(OutType &out) final { + if (ppc::util::IsUnderMpirun()) { + int rank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + if (rank != 0) { + return true; + } + } + return out == expected_; + } + + InType GetTestInputData() final { + return input_; + } +}; + +TEST_P(DenisovARingPerfTest, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kPerfTasks = + ppc::util::MakeAllPerfTasks(PPC_SETTINGS_denisov_a_ring); + +const auto kValues = ppc::util::TupleToGTestValues(kPerfTasks); + +const auto kNameGen = DenisovARingPerfTest::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, DenisovARingPerfTest, kValues, kNameGen); + +} // namespace denisov_a_ring