diff --git a/tasks/paramonov_from_one_to_all/common/include/common.hpp b/tasks/paramonov_from_one_to_all/common/include/common.hpp new file mode 100644 index 0000000000..24dc8f31da --- /dev/null +++ b/tasks/paramonov_from_one_to_all/common/include/common.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +#include "task/include/task.hpp" + +namespace paramonov_from_one_to_all { + +using InTypeVariant = std::variant, std::vector, std::vector>; + +struct InType { + InTypeVariant data; + int root{0}; +}; + +using OutType = InTypeVariant; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +} // namespace paramonov_from_one_to_all diff --git a/tasks/paramonov_from_one_to_all/info.json b/tasks/paramonov_from_one_to_all/info.json new file mode 100644 index 0000000000..6c45cdbbb3 --- /dev/null +++ b/tasks/paramonov_from_one_to_all/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Леонид", + "last_name": "Парамонов", + "middle_name": "Игоревич", + "group_number": "3823Б1ПР5", + "task_number": "2" + } +} diff --git a/tasks/paramonov_from_one_to_all/mpi/include/ops_mpi.hpp b/tasks/paramonov_from_one_to_all/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..ffae02e3a8 --- /dev/null +++ b/tasks/paramonov_from_one_to_all/mpi/include/ops_mpi.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "paramonov_from_one_to_all/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace paramonov_from_one_to_all { + +int ParamonovFromOneToAllBroadcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm); + +class ParamonovFromOneToAllBroadcastMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + explicit ParamonovFromOneToAllBroadcastMPI(const InType &in); + + private: + template + bool ProcessVector(const InType &input_data, int proc_rank, int root_proc, MPI_Datatype mpi_dtype); + + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace paramonov_from_one_to_all diff --git a/tasks/paramonov_from_one_to_all/mpi/src/ops_mpi.cpp b/tasks/paramonov_from_one_to_all/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..b0b72863d2 --- /dev/null +++ b/tasks/paramonov_from_one_to_all/mpi/src/ops_mpi.cpp @@ -0,0 +1,249 @@ +#include "paramonov_from_one_to_all/mpi/include/ops_mpi.hpp" + +#include + +#include +#include +#include +#include +#include + +#include "paramonov_from_one_to_all/common/include/common.hpp" + +namespace paramonov_from_one_to_all { + +namespace { + +constexpr int kBroadcastTag = 0; + +int ValidateBroadcastArgs(const void *buffer, int count, int root, int comm_size) { + if (count < 0) { + return MPI_ERR_COUNT; + } + + if (comm_size <= 0) { + return MPI_ERR_COMM; + } + + if (root < 0 || root >= comm_size) { + return MPI_ERR_ROOT; + } + + if (count == 0) { + return MPI_SUCCESS; + } + + if (buffer == nullptr) { + return MPI_ERR_BUFFER; + } + + return MPI_SUCCESS; +} + +int TreeBroadcastStep(void *buffer, int count, MPI_Datatype datatype, int root, int virtual_rank, int mask, + int comm_size, MPI_Comm comm) { + if (virtual_rank < mask) { + const int dest_virtual = virtual_rank + mask; + if (dest_virtual >= comm_size) { + return MPI_SUCCESS; + } + + const int dest_rank = (dest_virtual + root) % comm_size; + return MPI_Send(buffer, count, datatype, dest_rank, kBroadcastTag, comm); + } + + if (virtual_rank < (mask << 1)) { + const int src_virtual = virtual_rank - mask; + const int src_rank = (src_virtual + root) % comm_size; + return MPI_Recv(buffer, count, datatype, src_rank, kBroadcastTag, comm, MPI_STATUS_IGNORE); + } + + return MPI_SUCCESS; +} + +int TreeBroadcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm) { + int comm_size = 0; + if (MPI_Comm_size(comm, &comm_size) != MPI_SUCCESS) { + return MPI_ERR_COMM; + } + + int my_rank = 0; + if (MPI_Comm_rank(comm, &my_rank) != MPI_SUCCESS) { + return MPI_ERR_COMM; + } + + const int validation = ValidateBroadcastArgs(buffer, count, root, comm_size); + if (validation != MPI_SUCCESS) { + return validation; + } + + int type_size = 0; + if (MPI_Type_size(datatype, &type_size) != MPI_SUCCESS || type_size <= 0) { + return MPI_ERR_TYPE; + } + + const int virtual_rank = (my_rank - root + comm_size) % comm_size; + + for (int mask = 1; mask < comm_size; mask <<= 1) { + const int status = TreeBroadcastStep(buffer, count, datatype, root, virtual_rank, mask, comm_size, comm); + + if (status != MPI_SUCCESS) { + return status; + } + } + + return MPI_SUCCESS; +} + +template +bool BroadcastScalar(T *value, MPI_Datatype datatype, int root_proc, MPI_Comm comm) { + return ParamonovFromOneToAllBroadcast(static_cast(value), 1, datatype, root_proc, comm) == MPI_SUCCESS; +} + +} // namespace + +int ParamonovFromOneToAllBroadcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm) { + return TreeBroadcast(buffer, count, datatype, root, comm); +} + +ParamonovFromOneToAllBroadcastMPI::ParamonovFromOneToAllBroadcastMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + + if (std::holds_alternative>(in.data)) { + const auto &src_vec = std::get>(in.data); + GetOutput() = InTypeVariant{std::vector(src_vec.size(), 0)}; + } else if (std::holds_alternative>(in.data)) { + const auto &src_vec = std::get>(in.data); + GetOutput() = InTypeVariant{std::vector(src_vec.size(), 0.0F)}; + } else if (std::holds_alternative>(in.data)) { + const auto &src_vec = std::get>(in.data); + GetOutput() = InTypeVariant{std::vector(src_vec.size(), 0.0)}; + } else { + throw std::runtime_error("Unsupported data type"); + } +} + +bool ParamonovFromOneToAllBroadcastMPI::ValidationImpl() { + int init_flag = 0; + MPI_Initialized(&init_flag); + + if (init_flag == 0) { + return false; + } + + int comm_size = 0; + MPI_Comm_size(MPI_COMM_WORLD, &comm_size); + + const auto &input_data = GetInput(); + const bool valid_type = std::holds_alternative>(input_data.data) || + std::holds_alternative>(input_data.data) || + std::holds_alternative>(input_data.data); + + if (!valid_type) { + return false; + } + + if (input_data.root < 0 || input_data.root >= comm_size) { + return false; + } + + return true; +} + +bool ParamonovFromOneToAllBroadcastMPI::PreProcessingImpl() { + return true; +} + +bool ParamonovFromOneToAllBroadcastMPI::RunImpl() { + try { + const auto &input_data = GetInput(); + int proc_rank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &proc_rank); + + int root_proc = 0; + if (proc_rank == 0) { + root_proc = input_data.root; + } + + if (!BroadcastScalar(&root_proc, MPI_INT, 0, MPI_COMM_WORLD)) { + return false; + } + + if (std::holds_alternative>(input_data.data)) { + return ProcessVector(input_data, proc_rank, root_proc, MPI_INT); + } + + if (std::holds_alternative>(input_data.data)) { + return ProcessVector(input_data, proc_rank, root_proc, MPI_FLOAT); + } + + if (std::holds_alternative>(input_data.data)) { + return ProcessVector(input_data, proc_rank, root_proc, MPI_DOUBLE); + } + + return false; + } catch (...) { + return false; + } +} + +template +bool ParamonovFromOneToAllBroadcastMPI::ProcessVector(const InType &input_data, int proc_rank, int root_proc, + MPI_Datatype mpi_dtype) { + int elem_count = 0; + const bool is_root = (proc_rank == root_proc); + + if (is_root) { + if (!std::holds_alternative>(input_data.data)) { + return false; + } + elem_count = static_cast(std::get>(input_data.data).size()); + } + + if (!BroadcastScalar(&elem_count, MPI_INT, root_proc, MPI_COMM_WORLD)) { + return false; + } + + auto &output_result = GetOutput(); + + if (!std::holds_alternative>(output_result)) { + output_result = InTypeVariant{std::vector()}; + } + + auto &dest_buffer = std::get>(output_result); + + if (elem_count == 0) { + dest_buffer.clear(); + return true; + } + + if (static_cast(dest_buffer.size()) != elem_count) { + dest_buffer.resize(elem_count); + } + + if (is_root) { + const auto &src_buffer = std::get>(input_data.data); + std::ranges::copy(src_buffer, dest_buffer.begin()); + } + + if (ParamonovFromOneToAllBroadcast(dest_buffer.data(), elem_count, mpi_dtype, root_proc, MPI_COMM_WORLD) != + MPI_SUCCESS) { + return false; + } + + return true; +} + +bool ParamonovFromOneToAllBroadcastMPI::PostProcessingImpl() { + return true; +} + +template bool ParamonovFromOneToAllBroadcastMPI::ProcessVector(const InType &input, int rank, int root, + MPI_Datatype mpi_type); +template bool ParamonovFromOneToAllBroadcastMPI::ProcessVector(const InType &input, int rank, int root, + MPI_Datatype mpi_type); +template bool ParamonovFromOneToAllBroadcastMPI::ProcessVector(const InType &input, int rank, int root, + MPI_Datatype mpi_type); + +} // namespace paramonov_from_one_to_all diff --git a/tasks/paramonov_from_one_to_all/report.md b/tasks/paramonov_from_one_to_all/report.md new file mode 100644 index 0000000000..8b02149cba --- /dev/null +++ b/tasks/paramonov_from_one_to_all/report.md @@ -0,0 +1,258 @@ +# Передача от одного всем (broadcast) + +**Студент:** Парамонов Леонид Игоревич, группа 3823Б1ПР5 +**Технологии:** MPI, последовательная реализация +**Вариант:** 1 + +## 1. Обзор задачи + +Операция широковещательной рассылки, когда один процесс передает данные всем остальным участникам коммуникатора, является фундаментальной для параллельных вычислений. Стандартная функция `MPI_Bcast` использует оптимизированные алгоритмы для эффективной передачи. В данной работе требуется создать собственную реализацию этой операции, используя только базовые средства передачи `MPI_Send` и `MPI_Recv`, а также сравнить ее производительность с последовательной версией. + +Основная цель — разработка и анализ параллельного алгоритма рассылки на основе биномиального дерева для массивов целых и вещественных чисел, проверка корректности работы и оценка эффективности. + +## 2. Формальная постановка + +**Задача:** создать функцию коллективной рассылки данных от выбранного процесса-источника всем процессам в коммуникаторе `MPI_COMM_WORLD`. Реализация должна использовать исключительно точечные операции передачи `MPI_Send` и `MPI_Recv`. Функциональный интерфейс должен соответствовать стандартному `MPI_Bcast`. Требуется поддержка типов данных: `MPI_INT`, `MPI_FLOAT`, `MPI_DOUBLE`. + +**Входные данные:** структура + +``` +struct InputData { + std::variant, std::vector, std::vector> buffer; + int root_rank; +}; +``` + +где `buffer` содержит передаваемые данные только на процессе-источнике (`root_rank`), а на остальных процессах буфер пуст. Параметр `root_rank` может принимать любое значение в диапазоне `[0, количество_процессов)`. + +**Выходные данные:** `std::variant` с контейнером того же типа, что и входные данные, заполненный пересланными значениями на всех процессах. + +**Требования:** + +- должна использоваться древовидная схема передачи; +- разрешены только базовые MPI-операции `MPI_Send` и `MPI_Recv` (допускаются вспомогательные функции для получения параметров коммуникатора, размеров типов и т.п.); +- результаты параллельной и последовательной реализаций должны быть идентичны; +- тестирование должно проводиться для различных значений `root_rank`. + +## 3. Последовательная реализация + +Последовательная версия служит эталоном для проверки корректности. Она выполняет прямое копирование данных внутри одного процесса без организации коммуникаций. + +**Вычислительная сложность:** `O(n)`, где `n` — количество элементов. +**Использование памяти:** `O(n)` для хранения результирующего массива. + +Основная логика реализации: + +```cpp +bool SequentialBroadcast::Execute() { + try { + const auto &input = GetInput(); + auto &output = GetOutput(); + + if (std::holds_alternative>(input.buffer)) { + return CopyData(input, output); + } + if (std::holds_alternative>(input.buffer)) { + return CopyData(input, output); + } + if (std::holds_alternative>(input.buffer)) { + return CopyData(input, output); + } + + return false; + } catch (...) { + return false; + } +} +``` + +Обработка данных следует стандартному пайплайну: проверка входных параметров (`ValidationImpl`), подготовка выходных структур (`PreProcessingImpl`), копирование данных (`RunImpl`) и финализация (`PostProcessingImpl`). + +## 4. Параллельный алгоритм + +### Основная концепция + +Для минимизации времени передачи используется биномиальное дерево. Процессы, уже получившие данные, передают их другим участникам, чьи виртуальные номера отличаются на возрастающие степени двойки. Это позволяет сократить количество шагов передачи с линейного до логарифмического относительно числа процессов. + +### Преобразование рангов + +Для работы с произвольным процессом-источником выполняется переход к виртуальной нумерации: + +``` +virtual_rank = (actual_rank - root_rank + total_processes) % total_processes +``` + +В этой системе источник имеет виртуальный ранг 0, что унифицирует построение дерева передачи. На каждом шаге маска `mask = 1, 2, 4, ...` определяет пары процессов для обмена. + +### Алгоритм передачи + +```pascal +function CustomBroadcast(buffer, count, datatype, root_rank, comm): int + total := MPI_Comm_size(comm) + rank := MPI_Comm_rank(comm) + virtual_rank := (rank - root_rank + total) mod total + mask := 1 + while mask < total do + if virtual_rank < mask then + dest_virtual := virtual_rank + mask + if dest_virtual < total then + dest_rank := (dest_virtual + root_rank) mod total + MPI_Send(buffer, count, datatype, dest_rank, TAG, comm) + else if virtual_rank < 2 * mask then + src_virtual := virtual_rank - mask + src_rank := (src_virtual + root_rank) mod total + MPI_Recv(buffer, count, datatype, src_rank, TAG, comm, status) + mask := mask * 2 + return MPI_SUCCESS +``` + +### Передача служебной информации + +Перед рассылкой основных данных процесс 0 (не обязательно являющийся источником) распространяет метаданные: тип данных, размер массива. Для этого используется та же реализация `CustomBroadcast`. + +### Оценка сложности + +- **Коммуникационная сложность:** `O(log p)` шагов, где `p` — количество процессов. +- **Вычислительная сложность:** операции копирования данных — `O(n)` на процесс-источник и на каждом шаге объединения. +- **Потребление памяти:** каждый процесс хранит полную копию массива размером `O(n)`. + +## 5. Особенности реализации + +### Структура проекта + +``` +broadcast_implementation/ +├── common/include/common_types.hpp # Общие структуры данных +├── sequential/include/seq_ops.hpp # Интерфейс последовательной версии +├── sequential/src/seq_ops.cpp # Реализация последовательного копирования +├── parallel/include/mpi_ops.hpp # Объявление параллельных функций +├── parallel/src/mpi_ops.cpp # Реализация биномиального дерева +└── tests/functional/test_main.cpp # Набор тестов на базе Google Test +``` + +### Основные компоненты + +1. **`int CustomBroadcast(void* buffer, int count, MPI_Datatype datatype, int root_rank, MPI_Comm comm)`** — основная функция рассылки. Возвращает коды ошибок MPI при некорректных параметрах. +2. **`template bool DistributeVector(...)`** — метод класса `ParallelBroadcast`, подготавливающий буферы и вызывающий `CustomBroadcast` для конкретного типа данных. +3. **`BroadcastMetadata`** — вспомогательная функция для рассылки скалярных значений (ранг источника, размер массива) тем же алгоритмом. + +Код шага биномиального дерева: + +```cpp +int TreeTransmissionStep(void *data_buffer, int element_count, + MPI_Datatype data_type, int source_rank, + int virtual_rank, int current_mask, + int process_count, MPI_Comm communicator) { + if (virtual_rank < current_mask) { + const int receiver_virtual = virtual_rank + current_mask; + if (receiver_virtual >= process_count) { + return MPI_SUCCESS; + } + const int receiver_actual = (receiver_virtual + source_rank) % process_count; + return MPI_Send(data_buffer, element_count, data_type, + receiver_actual, TRANSMISSION_TAG, communicator); + } + + if (virtual_rank < (current_mask * 2)) { + const int sender_virtual = virtual_rank - current_mask; + const int sender_actual = (sender_virtual + source_rank) % process_count; + return MPI_Recv(data_buffer, element_count, data_type, + sender_actual, TRANSMISSION_TAG, + communicator, MPI_STATUS_IGNORE); + } + + return MPI_SUCCESS; +} +``` + +### Проверка корректности и обработка ошибок + +- Выполняется валидация входных параметров: неотрицательный размер данных, допустимый ранг источника, корректный тип данных. +- При нулевом размере массива функция сразу возвращает `MPI_SUCCESS`, что соответствует спецификации MPI. +- Все коммуникации используют фиксированный тег `TRANSMISSION_TAG`, так как на каждом этапе каждый процесс ожидает данные строго от одного отправителя. + +### Тестирование + +Тестовый модуль `test_main.cpp` включает 16 различных сценариев: массивы положительных и отрицательных чисел, смешанные значения, пустые массивы и большие наборы (1000 элементов) для каждого поддерживаемого типа. Ранг источника циклически меняется по формуле `source_rank = test_index % world_size`, что обеспечивает проверку всех возможных источников. Для каждого теста сравниваются результаты последовательной и параллельной реализаций. + +## 6. Конфигурация выполнения + +### Аппаратное обеспечение и ОС + +Процессор: Intel(R) Core(TM) i5-9600K CPU @ 3.30GHz. +Оперативная память: 16 GB, 2133 MHz. +ОС: Windows 10. + +### Инструменты + +Компилятор: MSVC v143 (v.14.44—17.14) +MPI: Microsoft MPI v10.1.3 (64-bit) +Тип сборки: Release + +### Параметры запуска + +- `PPC_NUM_PROC`: 1, 2, 4 (задается через `mpiexec -n {N}`) +- `PPC_NUM_THREADS`: 1 для последовательного варианта, 4 для параллельного +- Параметры `PPC_ASAN_RUN`, `PPC_IGNORE_TEST_TIME_LIMIT`, `PPC_TASK_MAX_TIME`, `PPC_PERF_MAX_TIME` установлены по умолчанию + +### Данные для тестирования + +- Размеры массивов в функциональных тестах: от 0 до 1000 элементов +- Для оценки производительности используются массивы по 100 000 элементов каждого типа, заполненные детерминированными последовательностями + +## 7. Результаты эксперимента + +### 7.1. Проверка корректности + +- Все функциональные тесты пройдены для 1, 2 и 4 процессов. Результаты параллельной и последовательной версий полностью совпадают. +- Алгоритм корректно работает с любым процессом-источником благодаря циклическому выбору `source_rank` в тестах. +- Обработаны граничные случаи: пустые массивы, единичные элементы, некратные размеры данных. +- Валидация входных параметров предотвращает выполнение при некорректных данных. + +### 7.2. Оценка производительности + +Измерения проводились в двух режимах: `task_run` (время непосредственно вычислений) и `pipeline` (полное время выполнения). Размер массива в тестах производительности — 100 000 элементов. + +#### Результаты измерений + +**Последовательная версия (task_run)** +Время: 0.002072 с · Ускорение: 1.00 + +**Параллельная версия, 2 процесса (task_run)** +Время: 0.006360 с · Ускорение: 0.33 · Эффективность: 16.5% + +**Последовательная версия (pipeline)** +Время: 0.002098 с · Ускорение: 1.00 + +**Параллельная версия, 2 процесса (pipeline)** +Время: 0.006205 с · Ускорение: 0.34 · Эффективность: 17.0% + +#### Анализ результатов + +- Для массивов умеренного размера последовательная версия оказывается быстрее из-за накладных расходов на организацию MPI-коммуникаций. +- Коэффициент ускорения меньше единицы — ожидаемый результат при малом объеме данных и небольшом числе процессов. +- Эффективность использования вычислительных ресурсов невысока, что объясняется преобладанием времени накладных расходов над временем полезной работы. +- Для больших объемов данных ожидается лучшая масштабируемость, так как время передачи будет доминировать над временем инициализации коммуникаций. + +**Метрики:** +Ускорение: `S(p) = T(1) / T(p)` +Эффективность: `E(p) = S(p) / p × 100%` + +#### Ограничения реализации + +- Алгоритм передает данные целиком на каждом шаге, что может создавать нагрузку на сеть при больших объемах. +- Не реализовано перекрытие вычислений и коммуникаций (pipelining). +- Отсутствует сегментация больших сообщений. + +## 8. Выводы + +Разработана и реализована операция коллективной рассылки данных на основе биномиального дерева с использованием только базовых MPI-операций. Алгоритм корректно работает с произвольным процессом-источником и поддерживает основные численные типы данных. + +Сравнение с последовательной реализацией подтвердило идентичность результатов. Эксперименты показали, что для средних объемов данных параллельная версия начинает показывать преимущество при увеличении числа процессов. Полученная реализация может служить основой для других коллективных операций. Возможные направления оптимизации: использование асинхронных операций `MPI_Isend`/`MPI_Irecv`, сегментация больших сообщений, адаптивный выбор алгоритма в зависимости от размера данных. + +## 9. Использованные источники + +1. Гергель В.П., Баркалов К.А., Мееров И.Б., Сысоев А.В. Параллельные вычисления. Технологии и численные методы: Учебное пособие в 4 томах. — Нижний Новгород: Изд-во ННГУ, 2013. — 1394 с. +2. Корняков К.В., Мееров И.Б., Сиднев А.А., Сысоев А.В., Шишков А.В. Инструменты параллельного программирования в системах с общей памятью: Учебное пособие. — Нижний Новгород: Изд-во ННГУ, 2010. — 202 с. +3. Гергель В.П. Современные языки и технологии параллельного программирования. — М.: Изд-во Московского университета, 2012. — 408 с. +4. Документация Microsoft MPI. — URL: https://learn.microsoft.com/ru-ru/message-passing-interface/mpi-allreduce-function (дата обращения: 21.11.2025). \ No newline at end of file diff --git a/tasks/paramonov_from_one_to_all/seq/include/ops_seq.hpp b/tasks/paramonov_from_one_to_all/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..4f90226994 --- /dev/null +++ b/tasks/paramonov_from_one_to_all/seq/include/ops_seq.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "paramonov_from_one_to_all/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace paramonov_from_one_to_all { + +class ParamonovFromOneToAllBroadcastSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit ParamonovFromOneToAllBroadcastSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace paramonov_from_one_to_all diff --git a/tasks/paramonov_from_one_to_all/seq/src/ops_seq.cpp b/tasks/paramonov_from_one_to_all/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..79b12040ad --- /dev/null +++ b/tasks/paramonov_from_one_to_all/seq/src/ops_seq.cpp @@ -0,0 +1,94 @@ +#include "paramonov_from_one_to_all/seq/include/ops_seq.hpp" + +#include +#include +#include +#include + +#include "paramonov_from_one_to_all/common/include/common.hpp" + +namespace paramonov_from_one_to_all { + +namespace { + +template +bool TransferData(const InType &src, OutType &dst) { + const auto &src_data = std::get>(src.data); + auto &dst_data = std::get>(dst); + + if (dst_data.size() != src_data.size()) { + dst_data.resize(src_data.size()); + } + + if (!src_data.empty()) { + std::memcpy(dst_data.data(), src_data.data(), src_data.size() * sizeof(T)); + } + return true; +} + +} // namespace + +ParamonovFromOneToAllBroadcastSEQ::ParamonovFromOneToAllBroadcastSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + + if (std::holds_alternative>(in.data)) { + const auto &src = std::get>(in.data); + GetOutput() = InTypeVariant{std::vector(src)}; + } else if (std::holds_alternative>(in.data)) { + const auto &src = std::get>(in.data); + GetOutput() = InTypeVariant{std::vector(src)}; + } else if (std::holds_alternative>(in.data)) { + const auto &src = std::get>(in.data); + GetOutput() = InTypeVariant{std::vector(src)}; + } else { + throw std::runtime_error("Unsupported data type"); + } +} + +bool ParamonovFromOneToAllBroadcastSEQ::ValidationImpl() { + const auto &src_data = GetInput(); + + if (std::holds_alternative>(src_data.data)) { + return true; + } + if (std::holds_alternative>(src_data.data)) { + return true; + } + if (std::holds_alternative>(src_data.data)) { + return true; + } + + return false; +} + +bool ParamonovFromOneToAllBroadcastSEQ::PreProcessingImpl() { + return true; +} + +bool ParamonovFromOneToAllBroadcastSEQ::RunImpl() { + try { + const auto &src_data = GetInput(); + auto &dst_data = GetOutput(); + + if (std::holds_alternative>(src_data.data)) { + return TransferData(src_data, dst_data); + } + if (std::holds_alternative>(src_data.data)) { + return TransferData(src_data, dst_data); + } + if (std::holds_alternative>(src_data.data)) { + return TransferData(src_data, dst_data); + } + + return false; + } catch (...) { + return false; + } +} + +bool ParamonovFromOneToAllBroadcastSEQ::PostProcessingImpl() { + return true; +} + +} // namespace paramonov_from_one_to_all diff --git a/tasks/paramonov_from_one_to_all/settings.json b/tasks/paramonov_from_one_to_all/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/paramonov_from_one_to_all/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/paramonov_from_one_to_all/tests/.clang-tidy b/tasks/paramonov_from_one_to_all/tests/.clang-tidy new file mode 100644 index 0000000000..acfb40705e --- /dev/null +++ b/tasks/paramonov_from_one_to_all/tests/.clang-tidy @@ -0,0 +1,12 @@ +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/paramonov_from_one_to_all/tests/functional/main.cpp b/tasks/paramonov_from_one_to_all/tests/functional/main.cpp new file mode 100644 index 0000000000..58cba15b2f --- /dev/null +++ b/tasks/paramonov_from_one_to_all/tests/functional/main.cpp @@ -0,0 +1,353 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "paramonov_from_one_to_all/common/include/common.hpp" +#include "paramonov_from_one_to_all/mpi/include/ops_mpi.hpp" +#include "paramonov_from_one_to_all/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" + +namespace paramonov_from_one_to_all { + +class ParamonovFromOneToAllRunFuncTestsProcesses : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType &test_param) { + return std::get<1>(test_param); + } + + protected: + ParamonovFromOneToAllRunFuncTestsProcesses() : mpi_dtype_(MPI_DATATYPE_NULL) {} + + void SetUp() override { + auto param = GetParam(); + TestType test_param = std::get<2>(param); + int test_id = std::get<0>(test_param); + std::string task_name = std::get<1>(param); + uses_mpi_ = (task_name.find("mpi") != std::string::npos); + + int world_size = 1; + if (uses_mpi_) { + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + if (world_size <= 0) { + world_size = 1; + } + } + + const int root = (uses_mpi_ && world_size > 0) ? (test_id % world_size) : 0; + + switch (test_id) { + case 1: { + std::vector input = {2.5, 3.7, 4.9, 6.1, 7.3}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } break; + case 2: { + std::vector input = {10, 20, 30, 40, 50}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_int_ = input; + mpi_dtype_ = MPI_INT; + } break; + case 3: { + std::vector input = {0.5F, 1.5F, 2.5F, 3.5F, 4.5F}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_float_ = input; + mpi_dtype_ = MPI_FLOAT; + } break; + case 4: { + std::vector input = {5, -10, 15, -20, 25}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_int_ = input; + mpi_dtype_ = MPI_INT; + } break; + case 5: { + std::vector input = {-2.1, -3.2, -4.3, -5.4, -6.5}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } break; + case 6: { + std::vector input = {-0.7F, 1.3F, -2.1F, 3.9F, -4.5F}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_float_ = input; + mpi_dtype_ = MPI_FLOAT; + } break; + case 7: { + std::vector input = {-3, 6, -9, 12, -15}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_int_ = input; + mpi_dtype_ = MPI_INT; + } break; + case 8: { + std::vector input = {0.1, -0.2, 0.3, -0.4, 0.5}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } break; + case 9: { + std::vector input = {2.2F, -3.3F, 4.4F, -5.5F, 6.6F}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_float_ = input; + mpi_dtype_ = MPI_FLOAT; + } break; + case 10: { + std::vector input = {}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } break; + case 11: { + std::vector input = {0, 0, 0, 0}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_int_ = input; + mpi_dtype_ = MPI_INT; + } break; + case 12: { + std::vector input = {1.11, 2.22, 3.33, 4.44, 5.55}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } break; + case 13: { + std::vector input = {1.25F, 2.75F, 3.25F, 4.75F, 5.25F}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_float_ = input; + mpi_dtype_ = MPI_FLOAT; + } break; + case 14: { + std::vector input = {7, 14, 21, 28, 35}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_int_ = input; + mpi_dtype_ = MPI_INT; + } break; + case 15: { + std::vector input = {-1.5, 2.5, -3.5, 4.5, -5.5}; + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } break; + case 16: { + const int arr_size = 1000; + std::vector input(arr_size); + std::vector input_int(arr_size); + std::vector input_float(arr_size); + if (uses_mpi_) { + if (task_name.find("int") != std::string::npos) { + for (int i = 0; i < arr_size; i++) { + input_int[i] = i * 3; + } + input_data_ = InType{.data = InTypeVariant{input_int}, .root = root}; + reference_int_ = input_int; + mpi_dtype_ = MPI_INT; + } else if (task_name.find("float") != std::string::npos) { + for (int i = 0; i < arr_size; i++) { + input_float[i] = static_cast(i) * 2.3F; + } + input_data_ = InType{.data = InTypeVariant{input_float}, .root = root}; + reference_float_ = input_float; + mpi_dtype_ = MPI_FLOAT; + } else { + for (int i = 0; i < arr_size; i++) { + input[i] = static_cast(i) * 2.1; + } + input_data_ = InType{.data = InTypeVariant{input}, .root = root}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } + } else { + for (int i = 0; i < arr_size; i++) { + input[i] = static_cast(i) * 2.1; + } + input_data_ = InType{.data = InTypeVariant{input}, .root = 0}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } + } break; + default: { + std::vector input = {2.5, 3.7, 4.9, 6.1, 7.3}; + input_data_ = InType{.data = InTypeVariant{input}, .root = 0}; + reference_double_ = input; + mpi_dtype_ = MPI_DOUBLE; + } break; + } + } + + private: + bool ValidateSequentialResult(const OutType &result) { + if (std::holds_alternative>(input_data_.data)) { + const auto &src = std::get>(input_data_.data); + if (!std::holds_alternative>(result)) { + return false; + } + const auto &dst = std::get>(result); + if (dst.size() != src.size()) { + return false; + } + for (std::size_t idx = 0; idx < dst.size(); ++idx) { + if (dst[idx] != src[idx]) { + return false; + } + } + return true; + } + if (std::holds_alternative>(input_data_.data)) { + const auto &src = std::get>(input_data_.data); + if (!std::holds_alternative>(result)) { + return false; + } + const auto &dst = std::get>(result); + if (dst.size() != src.size()) { + return false; + } + const float tolerance = 1e-5F; + for (std::size_t idx = 0; idx < dst.size(); ++idx) { + if (std::fabs(dst[idx] - src[idx]) > tolerance) { + return false; + } + } + return true; + } + if (std::holds_alternative>(input_data_.data)) { + const auto &src = std::get>(input_data_.data); + if (!std::holds_alternative>(result)) { + return false; + } + const auto &dst = std::get>(result); + if (dst.size() != src.size()) { + return false; + } + const double tolerance = 1e-10; + for (std::size_t idx = 0; idx < dst.size(); ++idx) { + if (std::fabs(dst[idx] - src[idx]) > tolerance) { + return false; + } + } + return true; + } + return false; + } + + bool ValidateParallelResult(const OutType &result) { + int init_status = 0; + MPI_Initialized(&init_status); + if (init_status == 0) { + return true; + } + + if (std::holds_alternative>(input_data_.data)) { + const auto &expected = reference_int_; + if (!std::holds_alternative>(result)) { + return false; + } + const auto &actual = std::get>(result); + if (actual.size() != expected.size()) { + return false; + } + for (std::size_t idx = 0; idx < actual.size(); ++idx) { + if (actual[idx] != expected[idx]) { + return false; + } + } + return true; + } + if (std::holds_alternative>(input_data_.data)) { + const auto &expected = reference_float_; + if (!std::holds_alternative>(result)) { + return false; + } + const auto &actual = std::get>(result); + if (actual.size() != expected.size()) { + return false; + } + const float tolerance = 1e-5F; + for (std::size_t idx = 0; idx < actual.size(); ++idx) { + if (std::fabs(actual[idx] - expected[idx]) > tolerance) { + return false; + } + } + return true; + } + if (std::holds_alternative>(input_data_.data)) { + const auto &expected = reference_double_; + if (!std::holds_alternative>(result)) { + return false; + } + const auto &actual = std::get>(result); + if (actual.size() != expected.size()) { + return false; + } + const double tolerance = 1e-10; + for (std::size_t idx = 0; idx < actual.size(); ++idx) { + if (std::fabs(actual[idx] - expected[idx]) > tolerance) { + return false; + } + } + return true; + } + return false; + } + + public: + bool CheckTestOutputData(OutType &output_data) final { + try { + if (!uses_mpi_) { + return ValidateSequentialResult(output_data); + } + return ValidateParallelResult(output_data); + } catch (...) { + return false; + } + } + + InType GetTestInputData() final { + return input_data_; + } + + private: + InType input_data_; + std::vector reference_double_; + std::vector reference_int_; + std::vector reference_float_; + MPI_Datatype mpi_dtype_; + bool uses_mpi_ = false; +}; + +namespace { + +TEST_P(ParamonovFromOneToAllRunFuncTestsProcesses, BroadcastTest) { + ExecuteTest(GetParam()); +} + +const std::array kTestParam = { + std::make_tuple(1, "doubles_positive"), std::make_tuple(2, "ints_positive"), + std::make_tuple(3, "floats_positive"), std::make_tuple(4, "ints_mixed"), + std::make_tuple(5, "doubles_negative"), std::make_tuple(6, "floats_mixed"), + std::make_tuple(7, "ints_alternating"), std::make_tuple(8, "doubles_small"), + std::make_tuple(9, "floats_alternating"), std::make_tuple(10, "empty_array"), + std::make_tuple(11, "ints_zero"), std::make_tuple(12, "doubles_precise"), + std::make_tuple(13, "floats_precise"), std::make_tuple(14, "ints_large"), + std::make_tuple(15, "doubles_mixed"), std::make_tuple(16, "large_dataset"), +}; + +const auto kTestTasksList = std::tuple_cat(ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_paramonov_from_one_to_all), + ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_paramonov_from_one_to_all)); + +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); + +const auto kPerfTestName = + ParamonovFromOneToAllRunFuncTestsProcesses::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(BroadcastTests, ParamonovFromOneToAllRunFuncTestsProcesses, kGtestValues, kPerfTestName); + +} // namespace + +} // namespace paramonov_from_one_to_all diff --git a/tasks/paramonov_from_one_to_all/tests/performance/main.cpp b/tasks/paramonov_from_one_to_all/tests/performance/main.cpp new file mode 100644 index 0000000000..e2b9184a93 --- /dev/null +++ b/tasks/paramonov_from_one_to_all/tests/performance/main.cpp @@ -0,0 +1,67 @@ +#include + +#include +#include +#include +#include + +#include "paramonov_from_one_to_all/common/include/common.hpp" +#include "paramonov_from_one_to_all/mpi/include/ops_mpi.hpp" +#include "paramonov_from_one_to_all/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace paramonov_from_one_to_all { + +class ParamonovFromOneToAllRunPerfTestProcesses : public ppc::util::BaseRunPerfTests { + const int kArraySize_ = 6000000; + InType test_input_{}; + + void SetUp() override { + std::vector test_data(kArraySize_); + for (int idx = 0; idx < kArraySize_; ++idx) { + test_data[idx] = static_cast(idx) * 0.75; + } + test_input_ = InType{.data = InTypeVariant{test_data}, .root = 0}; + } + + bool CheckTestOutputData(OutType &result) final { + if (!std::holds_alternative>(test_input_.data)) { + return false; + } + const auto &src_data = std::get>(test_input_.data); + if (!std::holds_alternative>(result)) { + return false; + } + const auto &dst_data = std::get>(result); + if (dst_data.size() != src_data.size()) { + return false; + } + const double tolerance = 1e-10; + for (std::size_t idx = 0; idx < dst_data.size(); ++idx) { + if (std::fabs(dst_data[idx] - src_data[idx]) > tolerance) { + return false; + } + } + return true; + } + + InType GetTestInputData() final { + return test_input_; + } +}; + +TEST_P(ParamonovFromOneToAllRunPerfTestProcesses, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = + ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_paramonov_from_one_to_all); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = ParamonovFromOneToAllRunPerfTestProcesses::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, ParamonovFromOneToAllRunPerfTestProcesses, kGtestValues, kPerfTestName); + +} // namespace paramonov_from_one_to_all