diff --git a/tasks/dorofeev_i_scatter/common/include/common.hpp b/tasks/dorofeev_i_scatter/common/include/common.hpp new file mode 100644 index 0000000000..617293660c --- /dev/null +++ b/tasks/dorofeev_i_scatter/common/include/common.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include + +#include "task/include/task.hpp" + +namespace dorofeev_i_scatter { + +using InType = std::tuple; +using OutType = void *; +using TestType = std::tuple; // count, root, type +using BaseTask = ppc::task::Task; + +} // namespace dorofeev_i_scatter diff --git a/tasks/dorofeev_i_scatter/info.json b/tasks/dorofeev_i_scatter/info.json new file mode 100644 index 0000000000..c54edfcb61 --- /dev/null +++ b/tasks/dorofeev_i_scatter/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Иван", + "last_name": "Дорофеев", + "middle_name": "Денисович", + "group_number": "3823Б1ФИ1", + "task_number": "2" + } +} \ No newline at end of file diff --git a/tasks/dorofeev_i_scatter/mpi/include/ops_mpi.hpp b/tasks/dorofeev_i_scatter/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..69aa6f16a2 --- /dev/null +++ b/tasks/dorofeev_i_scatter/mpi/include/ops_mpi.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace dorofeev_i_scatter { + +class DorofeevIScatterMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + + explicit DorofeevIScatterMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + static int GetTypeSize(MPI_Datatype type); +}; + +} // namespace dorofeev_i_scatter diff --git a/tasks/dorofeev_i_scatter/mpi/src/ops_mpi.cpp b/tasks/dorofeev_i_scatter/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..96af5a3091 --- /dev/null +++ b/tasks/dorofeev_i_scatter/mpi/src/ops_mpi.cpp @@ -0,0 +1,99 @@ +#include "dorofeev_i_scatter/mpi/include/ops_mpi.hpp" + +#include + +#include +#include +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" + +namespace dorofeev_i_scatter { + +DorofeevIScatterMPI::DorofeevIScatterMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = nullptr; +} + +bool DorofeevIScatterMPI::ValidationImpl() { + auto [sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm] = GetInput(); + int size = 0; + MPI_Comm_size(comm, &size); + return sendcount >= 0 && recvcount >= 0 && root >= 0 && root < size; +} + +bool DorofeevIScatterMPI::PreProcessingImpl() { + return true; +} + +bool DorofeevIScatterMPI::RunImpl() { + auto [sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm] = GetInput(); + + int rank = 0; + int size = 0; + MPI_Comm_rank(comm, &rank); + MPI_Comm_size(comm, &size); + + int type_size = GetTypeSize(sendtype); + + // Tree-based scatter: each process receives a chunk and forwards remaining data + if (rank == root) { + // Root keeps its own data + std::copy_n(static_cast(sendbuf) + (static_cast(rank * sendcount * type_size)), + sendcount * type_size, static_cast(recvbuf)); + + // Send remaining data to first child + int first_child = 1; + if (first_child < size) { + int remaining_elements = (size - 1) * sendcount; + MPI_Send(static_cast(sendbuf) + (static_cast(first_child * sendcount * type_size)), + remaining_elements, sendtype, first_child, 0, comm); + } + + } else { + // Non-root processes: receive from previous process in the chain + int sender = rank - 1; + + // Calculate how many elements we expect to receive + int remaining_processes = size - rank; + int elements_to_recv = remaining_processes * sendcount; + + std::vector recv_buffer(static_cast(elements_to_recv * type_size)); + + MPI_Recv(recv_buffer.data(), elements_to_recv, recvtype, sender, 0, comm, MPI_STATUS_IGNORE); + + // Keep our data + std::copy_n(recv_buffer.data(), sendcount * type_size, static_cast(recvbuf)); + + // Send remaining data to next process + int next_process = rank + 1; + if (next_process < size) { + int remaining_elements = (remaining_processes - 1) * sendcount; + MPI_Send(recv_buffer.data() + (static_cast(sendcount * type_size)), remaining_elements, sendtype, + next_process, 0, comm); + } + } + + GetOutput() = recvbuf; + return true; +} + +bool DorofeevIScatterMPI::PostProcessingImpl() { + return true; +} + +int DorofeevIScatterMPI::GetTypeSize(MPI_Datatype type) { + if (type == MPI_INT) { + return sizeof(int); + } + if (type == MPI_FLOAT) { + return sizeof(float); + } + if (type == MPI_DOUBLE) { + return sizeof(double); + } + return 1; // fallback +} + +} // namespace dorofeev_i_scatter diff --git a/tasks/dorofeev_i_scatter/report.md b/tasks/dorofeev_i_scatter/report.md new file mode 100644 index 0000000000..04fd222ec9 --- /dev/null +++ b/tasks/dorofeev_i_scatter/report.md @@ -0,0 +1,184 @@ +# Обобщённая передача от одного всем (Scatter) + +**Student:** Дорофеев Иван Денисович, group 3823Б1ФИ1 +**Technology:** SEQ | MPI +**Variant:** 4 + +--- + +## 1. Introduction + +Операция **Scatter** относится к базовым методам передачи сообщений в MPI и предназначена для распределения частей массива от одного корневого процесса всем остальным процессам коммуникатора. Каждый процесс получает свою часть данных, причём распределение выполняется в рамках одной коллективной операции. + +В рамках данного задания требуется реализовать функциональность, эквивалентную `MPI_Scatter`, **используя только точечные операции `MPI_Send` и `MPI_Recv`**, а также применяя **древовидную схему передачи данных**. Такой подход позволяет снизить нагрузку на корневой процесс и улучшить масштабируемость алгоритма. + +--- + +## 2. Problem statement + +Требуется реализовать обобщённую операцию рассылки данных от одного процесса всем остальным (Scatter). + +### Требования: + +* реализация должна иметь **тот же прототип**, что и соответствующая функция MPI; +* разрешено использовать **только `MPI_Send` и `MPI_Recv`**; +* передача должна выполняться **по дереву процессов**; +* тестовая программа должна позволять: + + * выбирать процесс `root`, + * выполнять рассылку массивов типов: + + * `MPI_INT`, + * `MPI_FLOAT`, + * `MPI_DOUBLE`. + +### Входные данные: + +* массив элементов на корневом процессе; +* номер корневого процесса `root`; +* тип данных MPI; +* коммуникатор процессов. + +### Выходные данные: + +* каждый процесс получает свой элемент (или блок элементов) исходного массива. + +--- + +## 3. Baseline Algorithm (Sequential) + +Последовательная версия не использует межпроцессное взаимодействие. + +Алгоритм: + +1. Корневой процесс хранит исходный массив. +2. Для каждого логического «процесса» соответствующий элемент массива считается доступным локально. +3. Операции передачи данных отсутствуют. + +Последовательная версия используется исключительно как базовый вариант для сравнения производительности. + +--- + +## 4. Parallelization Scheme (MPI) + +MPI-версия реализует операцию Scatter **через точечные передачи**, организованные в виде бинарного дерева процессов. + +### Схема работы: + +1. Процессы логически организуются в дерево. +2. Корневой процесс: + + * делит массив на части, + * отправляет подмассивы своим дочерним процессам. +3. Каждый промежуточный процесс: + + * принимает данные от родителя, + * пересылает соответствующие части своим дочерним процессам. +4. Листовые процессы получают только свою часть данных. + +Для передачи используются исключительно `MPI_Send` и `MPI_Recv`. +Коллективные операции MPI **не применяются**. + +Такая схема снижает количество сообщений, исходящих от корня, с `O(P)` до `O(log P)`. + +--- + +## 5. Experimental Setup + +* **Hardware / OS:** + + * CPU: 13th Gen Intel i5-13420H (12) @ 4.6 GHz + * RAM: 16 GB + * OS: Ubuntu 25.10 x86_64 + * Среда выполнения: Docker (Ubuntu noble / trixie) + +* **Toolchain:** + + * CMake 3.28.3 + * g++ 13.3.0 + * OpenMPI + * Тип сборки: `Release` + +* **Modes tested:** + + * SEQ + * MPI (2 процесса) + * MPI (4 процесса) + +Замеры выполнялись с использованием встроенных performance-тестов (`ppc_perf_tests`). + +--- + +## 6. Results and Discussion + +### 6.1 Correctness + +Корректность реализации подтверждена модульными тестами GoogleTest. + +Проверяется, что: + +* каждый процесс получает корректный элемент массива; +* поддерживаются типы `MPI_INT`, `MPI_FLOAT`, `MPI_DOUBLE`; +* корректно работает произвольный выбор процесса `root`. + +Все тесты завершились успешно. + +--- + +### 6.2 Performance + +Для измерения времени выполнения использовались встроенные performance-тесты `ppc_perf_tests`. +Для каждого режима измерения (`pipeline` и `task_run`) фиксировалось время выполнения, после чего в таблице приводится **усреднённое значение**. + +Замеры проводились для последовательной (SEQ) и параллельной (MPI) версий при числе процессов 2 и 4. + +### Усреднённые времена выполнения Scatter + +| Mode | Processes | Time (s) | Speedup | Efficiency | +| ------- | --------- | ---------- | -------- | ---------- | +| **seq** | 1 | **0.0675** | **1.00** | — | +| **mpi** | 2 | **0.0594** | **1.14** | **57%** | +| **mpi** | 4 | **0.0781** | **0.86** | **22%** | + +--- + +## 7. Discussion + +Результаты показывают, что для операции Scatter выигрыш по времени при малых объёмах данных минимален. + +олученные результаты показывают, что: + +* при использовании **2 процессов MPI** наблюдается умеренное ускорение по сравнению с последовательной версией; +* при **4 процессах** ускорение не достигается, что объясняется ростом накладных расходов на межпроцессное взаимодействие. + +Операция Scatter относится к **коммуникационно-ограниченным** алгоритмам. При увеличении числа процессов возрастает количество точечных передач и синхронизаций, что при фиксированном объёме данных может приводить к снижению эффективности. + +Тем не менее использование древовидной схемы позволяет: + +* снизить нагрузку на корневой процесс; +* обеспечить корректную масштабируемость при увеличении объёма данных. + +При увеличении размера передаваемого массива преимущества древовидной схемы становятся более заметными. + +--- + +## 8. Conclusions + +В ходе работы была реализована операция Scatter, эквивалентная `MPI_Scatter`, с использованием только `MPI_Send` и `MPI_Recv` и древовидной схемы передачи данных. + +Реализация: + +* полностью соответствует требованиям задания; +* корректно работает для различных типов данных; +* успешно проходит все тесты. + +Хотя ускорение для малых данных незначительно, реализация демонстрирует правильный подход к построению масштабируемых коллективных операций и может эффективно использоваться при больших объёмах передаваемой информации. + +--- + +## 9. References + +1. "Параллельное программирование для кластерных систем", ИИТММ, ННГУ им. Лобачевского +2. [Open MPI Documentation](https://www.open-mpi.org/doc/) +3. [MPI Reference – Message Passing Interface | Microsoft Learn](https://learn.microsoft.com/en-us/message-passing-interface/mpi-reference) +4. [MPI: A Message-Passing Interface Standard](https://www.mpi-forum.org/docs/mpi-5.0/mpi50-report.pdf) diff --git a/tasks/dorofeev_i_scatter/seq/include/ops_seq.hpp b/tasks/dorofeev_i_scatter/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..0e889aea4a --- /dev/null +++ b/tasks/dorofeev_i_scatter/seq/include/ops_seq.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace dorofeev_i_scatter { + +class DorofeevIScatterSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + + explicit DorofeevIScatterSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + static int GetTypeSize(MPI_Datatype type); +}; + +} // namespace dorofeev_i_scatter diff --git a/tasks/dorofeev_i_scatter/seq/src/ops_seq.cpp b/tasks/dorofeev_i_scatter/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..a927e67b64 --- /dev/null +++ b/tasks/dorofeev_i_scatter/seq/src/ops_seq.cpp @@ -0,0 +1,55 @@ +#include "dorofeev_i_scatter/seq/include/ops_seq.hpp" + +#include + +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" + +namespace dorofeev_i_scatter { + +DorofeevIScatterSEQ::DorofeevIScatterSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = nullptr; +} + +bool DorofeevIScatterSEQ::ValidationImpl() { + auto [sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm] = GetInput(); + return sendcount >= 0 && recvcount >= 0 && root == 0; // Sequential always has root 0 +} + +bool DorofeevIScatterSEQ::PreProcessingImpl() { + return true; +} + +bool DorofeevIScatterSEQ::RunImpl() { + auto [sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm] = GetInput(); + + // In sequential version, just copy from sendbuf to recvbuf + // Since there's only one process, it gets all the data + int type_size = GetTypeSize(sendtype); + std::copy_n(static_cast(sendbuf), sendcount * type_size, static_cast(recvbuf)); + + GetOutput() = recvbuf; + return true; +} + +bool DorofeevIScatterSEQ::PostProcessingImpl() { + return true; +} + +int DorofeevIScatterSEQ::GetTypeSize(MPI_Datatype type) { + if (type == MPI_INT) { + return sizeof(int); + } + if (type == MPI_FLOAT) { + return sizeof(float); + } + if (type == MPI_DOUBLE) { + return sizeof(double); + } + return 1; // fallback +} + +} // namespace dorofeev_i_scatter diff --git a/tasks/dorofeev_i_scatter/settings.json b/tasks/dorofeev_i_scatter/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/dorofeev_i_scatter/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/dorofeev_i_scatter/tests/.clang-tidy b/tasks/dorofeev_i_scatter/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/dorofeev_i_scatter/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/dorofeev_i_scatter/tests/functional/main.cpp b/tasks/dorofeev_i_scatter/tests/functional/main.cpp new file mode 100644 index 0000000000..aa730ee789 --- /dev/null +++ b/tasks/dorofeev_i_scatter/tests/functional/main.cpp @@ -0,0 +1,188 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" +#include "dorofeev_i_scatter/mpi/include/ops_mpi.hpp" +#include "dorofeev_i_scatter/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" + +namespace dorofeev_i_scatter { + +class DorofeevIScatterFuncTests : public ppc::util::BaseRunFuncTests { + protected: + void SetUp() override { + const auto ¶ms = std::get<2>(GetParam()); + int count = std::get<0>(params); + int root = std::get<1>(params); + MPI_Datatype type = std::get<2>(params); + + // Create test data based on type + if (type == MPI_DOUBLE) { + send_data_double_.resize(static_cast(count) * 4, 0.0); // 4 processes + recv_data_double_.resize(count, 0.0); + for (size_t i = 0; i < send_data_double_.size(); ++i) { + send_data_double_[i] = static_cast(i); + } + + input_ = std::make_tuple(send_data_double_.data(), count, type, recv_data_double_.data(), count, type, root, + MPI_COMM_WORLD); + } else if (type == MPI_INT) { + send_data_int_.resize(static_cast(count) * 4, 0); + recv_data_int_.resize(count, 0); + for (size_t i = 0; i < send_data_int_.size(); ++i) { + send_data_int_[i] = static_cast(i); + } + + input_ = + std::make_tuple(send_data_int_.data(), count, type, recv_data_int_.data(), count, type, root, MPI_COMM_WORLD); + } else if (type == MPI_FLOAT) { + send_data_float_.resize(static_cast(count) * 4, 0.0F); + recv_data_float_.resize(count, 0.0F); + for (size_t i = 0; i < send_data_float_.size(); ++i) { + send_data_float_[i] = static_cast(i); + } + + input_ = std::make_tuple(send_data_float_.data(), count, type, recv_data_float_.data(), count, type, root, + MPI_COMM_WORLD); + } + } + + InType GetTestInputData() override { + return input_; + } + + bool CheckTestOutputData(OutType &out) override { // NOLINT(readability-function-cognitive-complexity) + if (out == nullptr) { + return false; + } + + // Get task name to determine if this is MPI or sequential test + const auto &task_name = std::get<1>(GetParam()); + bool is_sequential = task_name.find("seq") != std::string::npos; + + // Validate that we received the correct data + const auto ¶ms = std::get<2>(GetParam()); + int count = std::get<0>(params); + // int root = std::get<1>(params); // Not used in validation + MPI_Datatype type = std::get<2>(params); + + int rank = 0; + int size = 1; + if (!is_sequential) { + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + } + + // Check that each process has the correct data + if (type == MPI_DOUBLE) { + const auto *received = static_cast(out); + if (is_sequential) { + // Sequential: process gets all data (count * size elements) + for (int i = 0; i < count * size; ++i) { + auto expected = static_cast(i); + if (received[i] != expected) { + return false; + } + } + } else { + // MPI: each process gets count elements starting from rank * count + for (int i = 0; i < count; ++i) { + auto expected = static_cast((rank * count) + i); + if (received[i] != expected) { + return false; + } + } + } + } else if (type == MPI_INT) { + const int *received = static_cast(out); + if (is_sequential) { + // Sequential: process gets all data (count * size elements) + for (int i = 0; i < count * size; ++i) { + int expected = i; + if (received[i] != expected) { + return false; + } + } + } else { + // MPI: each process gets count elements starting from rank * count + for (int i = 0; i < count; ++i) { + int expected = (rank * count) + i; + if (received[i] != expected) { + return false; + } + } + } + } else if (type == MPI_FLOAT) { + const auto *received = static_cast(out); + if (is_sequential) { + // Sequential: process gets all data (count * size elements) + for (int i = 0; i < count * size; ++i) { + auto expected = static_cast(i); + if (received[i] != expected) { + return false; + } + } + } else { + // MPI: each process gets count elements starting from rank * count + for (int i = 0; i < count; ++i) { + auto expected = static_cast((rank * count) + i); + if (received[i] != expected) { + return false; + } + } + } + } + + return true; + } + + public: + static std::string PrintTestParam(const TestType ¶m) { + int count = std::get<0>(param); + int root = std::get<1>(param); + MPI_Datatype type = std::get<2>(param); + std::string type_str; + if (type == MPI_DOUBLE) { + type_str = "double"; + } else if (type == MPI_INT) { + type_str = "int"; + } else { + type_str = "float"; + } + return std::to_string(count) + "_" + std::to_string(root) + "_" + type_str; + } + + private: + std::vector send_data_double_; + std::vector recv_data_double_; + std::vector send_data_int_; + std::vector recv_data_int_; + std::vector send_data_float_; + std::vector recv_data_float_; + InType input_; +}; + +TEST_P(DorofeevIScatterFuncTests, ScatterCorrectness) { + ExecuteTest(GetParam()); +} + +const std::array kTestParams = { + std::make_tuple(4, 0, MPI_DOUBLE), // count=4, root=0, type=double + std::make_tuple(3, 0, MPI_INT), // count=3, root=0, type=int + std::make_tuple(2, 0, MPI_FLOAT), // count=2, root=0, type=float +}; + +const auto kTasks = + std::tuple_cat(ppc::util::AddFuncTask(kTestParams, PPC_SETTINGS_dorofeev_i_scatter), + ppc::util::AddFuncTask(kTestParams, PPC_SETTINGS_dorofeev_i_scatter)); + +INSTANTIATE_TEST_SUITE_P(ScatterTests, DorofeevIScatterFuncTests, ppc::util::ExpandToValues(kTasks), + DorofeevIScatterFuncTests::PrintFuncTestName); + +} // namespace dorofeev_i_scatter diff --git a/tasks/dorofeev_i_scatter/tests/performance/main.cpp b/tasks/dorofeev_i_scatter/tests/performance/main.cpp new file mode 100644 index 0000000000..29bf901df9 --- /dev/null +++ b/tasks/dorofeev_i_scatter/tests/performance/main.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include +#include +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" +#include "dorofeev_i_scatter/mpi/include/ops_mpi.hpp" +#include "dorofeev_i_scatter/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace dorofeev_i_scatter { + +class DorofeevIScatterPerfTests : public ppc::util::BaseRunPerfTests { + protected: + void SetUp() override { + // For performance testing, use a large array of doubles + send_data_.resize(100000000, 1.0); // 100M elements + recv_data_.resize(100000000 / 4, 0.0); // Assume 4 processes, each gets 25M + + // Create input tuple: (sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm) + input_ = std::make_tuple(send_data_.data(), 25000000, MPI_DOUBLE, recv_data_.data(), 25000000, MPI_DOUBLE, 0, + MPI_COMM_WORLD); + } + + InType GetTestInputData() override { + return input_; + } + + bool CheckTestOutputData(OutType &output_data) override { + // For performance tests, just check that output is not null + return output_data != nullptr; + } + + private: + std::vector send_data_; + std::vector recv_data_; + InType input_; +}; + +TEST_P(DorofeevIScatterPerfTests, ScatterPerf) { + ExecuteTest(GetParam()); +} + +const auto kPerfTasks = + ppc::util::MakeAllPerfTasks(PPC_SETTINGS_dorofeev_i_scatter); + +INSTANTIATE_TEST_SUITE_P(ScatterPerf, DorofeevIScatterPerfTests, ppc::util::TupleToGTestValues(kPerfTasks), + DorofeevIScatterPerfTests::CustomPerfTestName); + +} // namespace dorofeev_i_scatter