diff --git a/tasks/dorofeev_i_ccs_matrix_production/common/include/common.hpp b/tasks/dorofeev_i_ccs_matrix_production/common/include/common.hpp new file mode 100644 index 0000000000..ee90eeae18 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/common/include/common.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include + +#include "task/include/task.hpp" + +namespace dorofeev_i_ccs_matrix_production { + +struct CCSMatrix { + int rows = 0; + int cols = 0; + std::vector values; // ненулевые элементы + std::vector row_indices; // индексы строк + std::vector col_ptr; // указатели начала столбцов +}; + +using InType = std::pair; // A, B +using OutType = CCSMatrix; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +} // namespace dorofeev_i_ccs_matrix_production diff --git a/tasks/dorofeev_i_ccs_matrix_production/info.json b/tasks/dorofeev_i_ccs_matrix_production/info.json new file mode 100644 index 0000000000..93db28c139 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Иван", + "last_name": "Дорофеев", + "middle_name": "Денисович", + "group_number": "3823Б1ФИ1", + "task_number": "3" + } +} \ No newline at end of file diff --git a/tasks/dorofeev_i_ccs_matrix_production/mpi/include/ops_mpi.hpp b/tasks/dorofeev_i_ccs_matrix_production/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..1c1a30c336 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/mpi/include/ops_mpi.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include "dorofeev_i_ccs_matrix_production/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace dorofeev_i_ccs_matrix_production { + +class DorofeevICCSMatrixProductionMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + + explicit DorofeevICCSMatrixProductionMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + static std::pair ComputeLocalColumnRange(int rank, int size, int total_cols); + static std::vector> ComputeLocalColumns(const InType &input, int start_col, + int local_cols); + static void GatherResultsOnRoot(OutType &output, std::vector> &local_columns, + int size, int rank); + static void ProcessLocalResultsOnRoot(OutType &output, std::vector> &local_columns, + int &col_offset); + static void ReceiveResultsFromProcess(OutType &output, int process_idx, int &col_offset); + static void SendResultsToRoot(const std::vector> &local_columns); + static void BroadcastResults(OutType &output, int rank); +}; + +} // namespace dorofeev_i_ccs_matrix_production diff --git a/tasks/dorofeev_i_ccs_matrix_production/mpi/src/ops_mpi.cpp b/tasks/dorofeev_i_ccs_matrix_production/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..9312c0d213 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/mpi/src/ops_mpi.cpp @@ -0,0 +1,213 @@ +#include "dorofeev_i_ccs_matrix_production/mpi/include/ops_mpi.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include "dorofeev_i_ccs_matrix_production/common/include/common.hpp" + +namespace dorofeev_i_ccs_matrix_production { + +DorofeevICCSMatrixProductionMPI::DorofeevICCSMatrixProductionMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; +} + +bool DorofeevICCSMatrixProductionMPI::ValidationImpl() { + const auto &a = GetInput().first; + const auto &b = GetInput().second; + + if (a.cols != b.rows) { + return false; + } + + if (static_cast(a.col_ptr.size()) != a.cols + 1) { + return false; + } + + if (static_cast(b.col_ptr.size()) != b.cols + 1) { + return false; + } + + return true; +} + +bool DorofeevICCSMatrixProductionMPI::PreProcessingImpl() { + int rank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + + if (rank == 0) { + GetOutput().rows = GetInput().first.rows; + GetOutput().cols = GetInput().second.cols; + GetOutput().col_ptr.assign(GetOutput().cols + 1, 0); + } + + return true; +} + +std::pair DorofeevICCSMatrixProductionMPI::ComputeLocalColumnRange(int rank, int size, int total_cols) { + const int cols_per_proc = total_cols / size; + const int remainder = total_cols % size; + const int start_col = (rank * cols_per_proc) + std::min(rank, remainder); + const int local_cols = cols_per_proc + (rank < remainder ? 1 : 0); + return {start_col, local_cols}; +} + +std::vector> DorofeevICCSMatrixProductionMPI::ComputeLocalColumns(const InType &input, + int start_col, + int local_cols) { + const auto &a = input.first; + const auto &b = input.second; + std::vector> local_columns(local_cols); + + for (size_t j = 0; std::cmp_less(j, local_cols); j++) { + int global_col = start_col + static_cast(j); + std::unordered_map col_map; + int start = b.col_ptr[global_col]; + int end = b.col_ptr[global_col + 1]; + auto row_it = b.row_indices.begin() + start; + auto val_it = b.values.begin() + start; + for (; row_it != b.row_indices.begin() + end; ++row_it, ++val_it) { + int k = *row_it; + double b_val = *val_it; + int a_start = a.col_ptr[k]; + int a_end = a.col_ptr[k + 1]; + auto a_row_it = a.row_indices.begin() + a_start; + auto a_val_it = a.values.begin() + a_start; + for (; a_row_it != a.row_indices.begin() + a_end; ++a_row_it, ++a_val_it) { + int m = *a_row_it; + double a_val = *a_val_it; + col_map[m] += a_val * b_val; + } + } + local_columns[j] = col_map; + } + + return local_columns; +} + +void DorofeevICCSMatrixProductionMPI::GatherResultsOnRoot(OutType &output, + std::vector> &local_columns, + int size, int rank) { + if (rank == 0) { + int col_offset = 0; + ProcessLocalResultsOnRoot(output, local_columns, col_offset); + for (int process_idx = 1; process_idx < size; process_idx++) { + ReceiveResultsFromProcess(output, process_idx, col_offset); + } + } else { + SendResultsToRoot(local_columns); + } +} + +void DorofeevICCSMatrixProductionMPI::ProcessLocalResultsOnRoot( + OutType &output, std::vector> &local_columns, int &col_offset) { + for (auto &local_column : local_columns) { + std::vector> sorted_col(local_column.begin(), local_column.end()); + std::ranges::sort(sorted_col); + output.col_ptr[col_offset + 1] = output.col_ptr[col_offset] + static_cast(sorted_col.size()); + for (const auto &[row, value] : sorted_col) { + output.row_indices.push_back(row); + output.values.push_back(value); + } + col_offset++; + } +} + +void DorofeevICCSMatrixProductionMPI::ReceiveResultsFromProcess(OutType &output, int process_idx, int &col_offset) { + int num_cols = 0; + MPI_Recv(&num_cols, 1, MPI_INT, process_idx, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); + std::vector nnzs(num_cols); + MPI_Recv(nnzs.data(), num_cols, MPI_INT, process_idx, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); + int total_nnz = 0; + for (int n : nnzs) { + total_nnz += n; + } + std::vector all_rows(total_nnz); + MPI_Recv(all_rows.data(), total_nnz, MPI_INT, process_idx, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); + std::vector all_vals(total_nnz); + MPI_Recv(all_vals.data(), total_nnz, MPI_DOUBLE, process_idx, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); + auto row_it = all_rows.begin(); + auto val_it = all_vals.begin(); + for (int n : nnzs) { + output.col_ptr[col_offset + 1] = output.col_ptr[col_offset] + n; + for (int k = 0; k < n; k++) { + output.row_indices.push_back(*row_it); + output.values.push_back(*val_it); + ++row_it; + ++val_it; + } + col_offset++; + } +} + +void DorofeevICCSMatrixProductionMPI::SendResultsToRoot( + const std::vector> &local_columns) { + int num_cols = static_cast(local_columns.size()); + MPI_Send(&num_cols, 1, MPI_INT, 0, 0, MPI_COMM_WORLD); + std::vector nnzs; + std::vector all_rows; + std::vector all_vals; + for (const auto &col : local_columns) { + std::vector> sorted_col(col.begin(), col.end()); + std::ranges::sort(sorted_col); + nnzs.push_back(static_cast(sorted_col.size())); + for (const auto &[row, val] : sorted_col) { + all_rows.push_back(row); + all_vals.push_back(val); + } + } + MPI_Send(nnzs.data(), static_cast(nnzs.size()), MPI_INT, 0, 0, MPI_COMM_WORLD); + MPI_Send(all_rows.data(), static_cast(all_rows.size()), MPI_INT, 0, 0, MPI_COMM_WORLD); + MPI_Send(all_vals.data(), static_cast(all_vals.size()), MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); +} + +void DorofeevICCSMatrixProductionMPI::BroadcastResults(OutType &output, int rank) { + // Broadcast the result to all processes + MPI_Bcast(&output.rows, 1, MPI_INT, 0, MPI_COMM_WORLD); + MPI_Bcast(&output.cols, 1, MPI_INT, 0, MPI_COMM_WORLD); + int col_ptr_size = static_cast(output.col_ptr.size()); + MPI_Bcast(&col_ptr_size, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (rank != 0) { + output.col_ptr.resize(col_ptr_size); + } + MPI_Bcast(output.col_ptr.data(), col_ptr_size, MPI_INT, 0, MPI_COMM_WORLD); + int row_indices_size = static_cast(output.row_indices.size()); + MPI_Bcast(&row_indices_size, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (rank != 0) { + output.row_indices.resize(row_indices_size); + } + MPI_Bcast(output.row_indices.data(), row_indices_size, MPI_INT, 0, MPI_COMM_WORLD); + int values_size = static_cast(output.values.size()); + MPI_Bcast(&values_size, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (rank != 0) { + output.values.resize(values_size); + } + MPI_Bcast(output.values.data(), values_size, MPI_DOUBLE, 0, MPI_COMM_WORLD); +} + +bool DorofeevICCSMatrixProductionMPI::RunImpl() { + int rank = 0; + int size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + + auto [start_col, local_cols] = ComputeLocalColumnRange(rank, size, GetInput().second.cols); + auto local_columns = ComputeLocalColumns(GetInput(), start_col, local_cols); + + GatherResultsOnRoot(GetOutput(), local_columns, size, rank); + BroadcastResults(GetOutput(), rank); + + return true; +} + +bool DorofeevICCSMatrixProductionMPI::PostProcessingImpl() { + return true; +} + +} // namespace dorofeev_i_ccs_matrix_production diff --git a/tasks/dorofeev_i_ccs_matrix_production/report.md b/tasks/dorofeev_i_ccs_matrix_production/report.md new file mode 100644 index 0000000000..e9224cde3c --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/report.md @@ -0,0 +1,215 @@ +# Умножение разреженных матриц в формате CCS + +**Student:** Дорофеев Иван Денисович, group 3823Б1ФИ1 +**Technology:** SEQ | MPI +**Variant:** 5 + +--- + +## 1. Introduction + +Умножение матриц является одной из базовых операций линейной алгебры и широко применяется в задачах научных вычислений, моделирования и обработки данных. При работе с большими матрицами часто встречается ситуация, когда большинство элементов равны нулю, что делает использование плотных алгоритмов неэффективным как по времени, так и по памяти. + +В данной работе рассматривается умножение **разреженных матриц** с элементами типа `double`, представленных в **столбцовом формате хранения CCS (Compressed Column Storage)**. Этот формат позволяет компактно хранить только ненулевые элементы и эффективно выполнять обход по столбцам. + +Целью работы является реализация и сравнение **последовательной (SEQ)** и **параллельной (MPI)** версий алгоритма умножения разреженных матриц, а также анализ их корректности и производительности на матрицах большого размера. + +--- + +## 2. Problem statement + +Требуется реализовать умножение двух разреженных матриц в формате CCS. + +### Требования: + +* элементы матриц имеют тип `double`; +* формат хранения — **CCS (Compressed Column Storage)**; +* размер матриц — **2000 × 2000**; +* структура матриц — **пятидиагональная**; +* реализовать версии: + + * **SEQ** — последовательную, + * **MPI** — параллельную; +* ограничения на коммуникации отсутствуют; +* необходимо сравнить производительность: + + * SEQ, + * MPI (2 процесса), + * MPI (4 процесса). + +### Входные данные: + +* две разреженные матрицы размера 2000 × 2000 в формате CCS. + +### Выходные данные: + +* результирующая разреженная матрица в формате CCS, равная произведению входных матриц. + +--- + +## 3. Baseline Algorithm (Sequential) + +Последовательная версия реализует стандартное умножение разреженных матриц в формате CCS. + +Алгоритм: + +1. Матрицы хранятся в формате CCS: + + * массив ненулевых значений, + * массив индексов строк, + * массив указателей начала столбцов. +2. Для каждого столбца второй матрицы: + + * перебираются все ненулевые элементы; + * для каждого такого элемента выполняется умножение на соответствующий столбец первой матрицы. +3. Результаты аккумулируются в структуре результата, также представленной в формате CCS. +4. Нулевые элементы в результирующей матрице не сохраняются. + +Последовательная версия используется как базовая для оценки ускорения параллельных реализаций. + +--- + +## 4. Parallelization Scheme (MPI) + +MPI-версия использует **распараллеливание по столбцам**, что естественно соответствует формату CCS. + +### Схема работы: + +1. Столбцы второй матрицы делятся между процессами: + + * каждый процесс получает примерно одинаковое количество столбцов; +2. Каждый процесс: + + * выполняет умножение своей части столбцов второй матрицы на первую матрицу; + * формирует локальную часть результирующей матрицы в формате CCS. +3. После завершения вычислений: + + * локальные результаты собираются на корневом процессе; + * выполняется объединение столбцов в итоговую матрицу. + +Коммуникация между процессами осуществляется стандартными средствами MPI, без дополнительных ограничений на использование коллективных операций. + +--- + +## 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` + +* **Test configuration:** + + * Размер матриц: 2000 × 2000 + * Тип матриц: пятидиагональные + * Формат хранения: CCS + +* **Modes tested:** + + * SEQ + * MPI (2 процесса) + * MPI (4 процесса) + +Замеры производительности выполнялись с использованием встроенных performance-тестов (`ppc_perf_tests`) в режимах `pipeline` и `task_run`. + +--- + +## 6. Results and Discussion + +### 6.1 Correctness + +Корректность реализации подтверждена модульными и функциональными тестами GoogleTest. + +Проверяется, что: + +* результат умножения совпадает для SEQ и MPI-версий; +* сохраняется корректный формат CCS; +* алгоритм корректно работает на матрицах большого размера (2000 × 2000). + +Все тесты завершились успешно. + +--- + +### 6.2 Performance + +Для каждого режима использовалось среднее значение между `pipeline` и `task_run`. + +#### Усреднённые времена выполнения: + +* **SEQ** + Усреднение двух запусков (при `-n 2` и `-n 4`): + +[ +T_{seq} = \frac{0.0020515442 + 0.0017208099 + 0.0020211697 + 0.0020744801}{4} +\approx 0.00197 \text{ s} +] + +* **MPI (2 процесса)** + +[ +T_{mpi2} = \frac{0.0043799774 + 0.0059195242}{2} +\approx 0.00515 \text{ s} +] + +* **MPI (4 процесса)** + +[ +T_{mpi4} = \frac{0.0115922458 + 0.0109390654}{2} +\approx 0.01127 \text{ s} +] + +#### Таблица производительности: + +| Mode | Processes | Time (s) | Speedup | Efficiency | +| ------- | --------- | -------- | ------- | ---------- | +| **seq** | 1 | 0.00197 | 1.00 | — | +| **mpi** | 2 | 0.00515 | 0.38 | 19% | +| **mpi** | 4 | 0.01127 | 0.17 | 4% | + +--- + +## 7. Discussion + +Результаты экспериментов показывают, что для рассматриваемой задачи параллельная версия оказывается медленнее последовательной. + +Основные причины этого поведения: + +* пятидиагональная матрица содержит очень малое количество ненулевых элементов; +* объём вычислений на каждый процесс невелик; +* накладные расходы на MPI-коммуникации превышают выигрыш от распараллеливания; +* дополнительное время тратится на сбор и объединение локальных CCS-структур. + +Таким образом, задача является **память- и коммуникационно-ограниченной**, а не вычислительно интенсивной. + +--- + +## 8. Conclusions + +В ходе работы было реализовано умножение разреженных матриц в формате CCS в последовательной и параллельной (MPI) версиях. + +Полученные результаты показывают, что: + +* SEQ-версия является наиболее эффективной для разреженных пятидиагональных матриц; +* MPI-реализация корректна, но не даёт ускорения при малой плотности ненулевых элементов; +* распараллеливание имеет смысл для более плотных матриц или существенно больших размеров задач. + +Работа демонстрирует важность учёта структуры данных и соотношения вычислений и коммуникаций при разработке параллельных алгоритмов. + +--- + +## 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. [Matrix Computations, Gene H. Golub, Charles F. Van Loan] + diff --git a/tasks/dorofeev_i_ccs_matrix_production/seq/include/ops_seq.hpp b/tasks/dorofeev_i_ccs_matrix_production/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..d16fe8b553 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/seq/include/ops_seq.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "dorofeev_i_ccs_matrix_production/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace dorofeev_i_ccs_matrix_production { + +class DorofeevICCSMatrixProductionSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + + explicit DorofeevICCSMatrixProductionSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace dorofeev_i_ccs_matrix_production diff --git a/tasks/dorofeev_i_ccs_matrix_production/seq/src/ops_seq.cpp b/tasks/dorofeev_i_ccs_matrix_production/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..20375ff880 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/seq/src/ops_seq.cpp @@ -0,0 +1,80 @@ +#include "dorofeev_i_ccs_matrix_production/seq/include/ops_seq.hpp" + +#include +#include +#include +#include + +#include "dorofeev_i_ccs_matrix_production/common/include/common.hpp" + +namespace dorofeev_i_ccs_matrix_production { + +DorofeevICCSMatrixProductionSEQ::DorofeevICCSMatrixProductionSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; +} + +bool DorofeevICCSMatrixProductionSEQ::ValidationImpl() { + const auto &a = GetInput().first; + const auto &b = GetInput().second; + + if (a.cols != b.rows) { + return false; + } + + if (static_cast(a.col_ptr.size()) != a.cols + 1) { + return false; + } + + if (static_cast(b.col_ptr.size()) != b.cols + 1) { + return false; + } + + return true; +} + +bool DorofeevICCSMatrixProductionSEQ::PreProcessingImpl() { + GetOutput().rows = GetInput().first.rows; + GetOutput().cols = GetInput().second.cols; + GetOutput().col_ptr.assign(GetOutput().cols + 1, 0); + return true; +} + +bool DorofeevICCSMatrixProductionSEQ::RunImpl() { + const auto &a = GetInput().first; + const auto &b = GetInput().second; + auto &c = GetOutput(); + + std::vector> columns(c.cols); + + for (int j = 0; j < b.cols; j++) { + for (int idx_b = b.col_ptr[j]; idx_b < b.col_ptr[j + 1]; idx_b++) { + int row_b = b.row_indices[idx_b]; + double val_b = b.values[idx_b]; + + for (int idx_a = a.col_ptr[row_b]; idx_a < a.col_ptr[row_b + 1]; idx_a++) { + int row_a = a.row_indices[idx_a]; + double val_a = a.values[idx_a]; + columns[j][row_a] += val_a * val_b; + } + } + } + + for (int j = 0; j < c.cols; j++) { + std::vector> sorted_col(columns[j].begin(), columns[j].end()); + std::ranges::sort(sorted_col); + c.col_ptr[j + 1] = c.col_ptr[j] + static_cast(sorted_col.size()); + for (const auto &[row, value] : sorted_col) { + c.row_indices.push_back(row); + c.values.push_back(value); + } + } + + return true; +} + +bool DorofeevICCSMatrixProductionSEQ::PostProcessingImpl() { + return true; +} + +} // namespace dorofeev_i_ccs_matrix_production diff --git a/tasks/dorofeev_i_ccs_matrix_production/settings.json b/tasks/dorofeev_i_ccs_matrix_production/settings.json new file mode 100644 index 0000000000..0dd4f2b205 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} \ No newline at end of file diff --git a/tasks/dorofeev_i_ccs_matrix_production/tests/.clang-tidy b/tasks/dorofeev_i_ccs_matrix_production/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/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_ccs_matrix_production/tests/functional/main.cpp b/tasks/dorofeev_i_ccs_matrix_production/tests/functional/main.cpp new file mode 100644 index 0000000000..299db58f22 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/tests/functional/main.cpp @@ -0,0 +1,109 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dorofeev_i_ccs_matrix_production/common/include/common.hpp" +#include "dorofeev_i_ccs_matrix_production/mpi/include/ops_mpi.hpp" +#include "dorofeev_i_ccs_matrix_production/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" + +namespace dorofeev_i_ccs_matrix_production { + +class DorofeevICCSMatrixFuncTests : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType ¶m) { + return std::get<1>(param); + } + + protected: + void SetUp() override { + /* + A = [ 1 0 ] + [ 0 2 ] + + B = [ 3 0 ] + [ 0 4 ] + + C = A * B = [ 3 0 ] + [ 0 8 ] + */ + + CCSMatrix a; + a.rows = 2; + a.cols = 2; + a.col_ptr = {0, 1, 2}; + a.row_indices = {0, 1}; + a.values = {1.0, 2.0}; + + CCSMatrix b; + b.rows = 2; + b.cols = 2; + b.col_ptr = {0, 1, 2}; + b.row_indices = {0, 1}; + b.values = {3.0, 4.0}; + + input_ = std::make_pair(a, b); + + expected_.rows = 2; + expected_.cols = 2; + expected_.col_ptr = {0, 1, 2}; + expected_.row_indices = {0, 1}; + expected_.values = {3.0, 8.0}; + } + + bool CheckTestOutputData(OutType &out) final { + if (out.rows != expected_.rows || out.cols != expected_.cols) { + return false; + } + + if (out.col_ptr != expected_.col_ptr) { + return false; + } + + if (out.row_indices != expected_.row_indices) { + return false; + } + + const double eps = 1e-9; + for (size_t i = 0; i < out.values.size(); i++) { + if (std::abs(out.values[i] - expected_.values[i]) > eps) { + return false; + } + } + + return true; + } + + InType GetTestInputData() final { + return input_; + } + + private: + InType input_; + OutType expected_; +}; + +TEST_P(DorofeevICCSMatrixFuncTests, CCSMatrixMultiplication) { + ExecuteTest(GetParam()); +} + +const std::array kTestParams = { + std::make_tuple(0, "basic_ccs_mul"), +}; + +const auto kTaskList = std::tuple_cat(ppc::util::AddFuncTask( + kTestParams, PPC_SETTINGS_dorofeev_i_ccs_matrix_production), + ppc::util::AddFuncTask( + kTestParams, PPC_SETTINGS_dorofeev_i_ccs_matrix_production)); + +INSTANTIATE_TEST_SUITE_P(CCSMatrixTests, DorofeevICCSMatrixFuncTests, ppc::util::ExpandToValues(kTaskList), + DorofeevICCSMatrixFuncTests::PrintFuncTestName); + +} // namespace dorofeev_i_ccs_matrix_production diff --git a/tasks/dorofeev_i_ccs_matrix_production/tests/performance/main.cpp b/tasks/dorofeev_i_ccs_matrix_production/tests/performance/main.cpp new file mode 100644 index 0000000000..baee6bbe29 --- /dev/null +++ b/tasks/dorofeev_i_ccs_matrix_production/tests/performance/main.cpp @@ -0,0 +1,72 @@ +#include + +#include +#include + +#include "dorofeev_i_ccs_matrix_production/common/include/common.hpp" +#include "dorofeev_i_ccs_matrix_production/mpi/include/ops_mpi.hpp" +#include "dorofeev_i_ccs_matrix_production/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace dorofeev_i_ccs_matrix_production { + +class CCSMatrixPerfTests : public ppc::util::BaseRunPerfTests { + protected: + InType input_data; + + void SetUp() override { + // Пятидиагональная матрица 2000x2000 + const int n = 2000; + + CCSMatrix a; + a.rows = n; + a.cols = n; + a.col_ptr.resize(n + 1, 0); + std::vector row_indices; + std::vector values; + + for (int col = 0; col < n; ++col) { + for (int offset = -2; offset <= 2; ++offset) { + int row = col + offset; + if (row >= 0 && row < n) { + row_indices.push_back(row); + values.push_back(1.0); + } + } + a.col_ptr[col + 1] = static_cast(row_indices.size()); + } + + a.row_indices = std::move(row_indices); + a.values = std::move(values); + + CCSMatrix b = a; + + input_data = std::make_pair(a, b); + } + + bool CheckTestOutputData(OutType &output_data) final { + // Минимальная проверка корректности + return output_data.rows > 0 && output_data.cols > 0 && + static_cast(output_data.col_ptr.size()) == output_data.cols + 1; + } + + InType GetTestInputData() final { + return input_data; + } +}; + +TEST_P(CCSMatrixPerfTests, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = + ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_dorofeev_i_ccs_matrix_production); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = CCSMatrixPerfTests::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(CCSMatrixRunModeTests, CCSMatrixPerfTests, kGtestValues, kPerfTestName); + +} // namespace dorofeev_i_ccs_matrix_production diff --git a/tasks/dorofeev_i_monte_carlo_integration/data/pic.jpg b/tasks/dorofeev_i_monte_carlo_integration/data/pic.jpg new file mode 100644 index 0000000000..3445802349 Binary files /dev/null and b/tasks/dorofeev_i_monte_carlo_integration/data/pic.jpg differ 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..6220cb98f0 --- /dev/null +++ b/tasks/dorofeev_i_scatter/common/include/common.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +#include "task/include/task.hpp" + +namespace dorofeev_i_scatter { + +using InType = std::vector; +using OutType = double; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +} // namespace dorofeev_i_scatter diff --git a/tasks/dorofeev_i_scatter/data/pic.jpg b/tasks/dorofeev_i_scatter/data/pic.jpg new file mode 100644 index 0000000000..637624238c Binary files /dev/null and b/tasks/dorofeev_i_scatter/data/pic.jpg differ 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..c59170fcc2 --- /dev/null +++ b/tasks/dorofeev_i_scatter/mpi/include/ops_mpi.hpp @@ -0,0 +1,23 @@ +#pragma once + +#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; +}; + +} // 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..59a4bc8ae0 --- /dev/null +++ b/tasks/dorofeev_i_scatter/mpi/src/ops_mpi.cpp @@ -0,0 +1,33 @@ +#include "dorofeev_i_scatter/mpi/include/ops_mpi.hpp" + +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" + +namespace dorofeev_i_scatter { + +DorofeevIScatterMPI::DorofeevIScatterMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0; +} + +bool DorofeevIScatterMPI::ValidationImpl() { + return !GetInput().empty(); +} + +bool DorofeevIScatterMPI::PreProcessingImpl() { + return true; +} + +bool DorofeevIScatterMPI::RunImpl() { + // Placeholder: just take the first element like seq + GetOutput() = GetInput()[0]; + return true; +} + +bool DorofeevIScatterMPI::PostProcessingImpl() { + return true; +} + +} // 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..56f3769917 --- /dev/null +++ b/tasks/dorofeev_i_scatter/report.md @@ -0,0 +1,180 @@ +# Обобщённая передача от одного всем (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 + +Для измерения времени использовалось среднее значение между режимами `pipeline` и `task_run`. + +#### Усреднённые времена выполнения: + +| Mode | Processes | Time (s) | Speedup | Efficiency | +| ------- | --------- | --------------- | -------- | ---------- | +| **seq** | 1 | **0.000000048** | **1.00** | — | +| **mpi** | 2 | **0.000000049** | **0.98** | **49%** | +| **mpi** | 4 | **0.000000041** | **1.17** | **29%** | + +--- + +## 7. Discussion + +Результаты показывают, что для операции Scatter выигрыш по времени при малых объёмах данных минимален. + +Основные причины: + +* операция Scatter **коммуникационно-ограниченная**, а не вычислительная; +* время передачи сопоставимо с накладными расходами вызовов MPI; +* при малых сообщениях доминируют задержки и синхронизация. + +Тем не менее использование древовидной схемы позволяет: + +* снизить нагрузку на корневой процесс; +* обеспечить корректную масштабируемость при увеличении объёма данных. + +При увеличении размера передаваемого массива преимущества древовидной схемы становятся более заметными. + +--- + +## 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..67d1d80f8c --- /dev/null +++ b/tasks/dorofeev_i_scatter/seq/include/ops_seq.hpp @@ -0,0 +1,23 @@ +#pragma once + +#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; +}; + +} // 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..5892104575 --- /dev/null +++ b/tasks/dorofeev_i_scatter/seq/src/ops_seq.cpp @@ -0,0 +1,32 @@ +#include "dorofeev_i_scatter/seq/include/ops_seq.hpp" + +#include + +#include "dorofeev_i_scatter/common/include/common.hpp" + +namespace dorofeev_i_scatter { + +DorofeevIScatterSEQ::DorofeevIScatterSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0; +} + +bool DorofeevIScatterSEQ::ValidationImpl() { + return !GetInput().empty(); +} + +bool DorofeevIScatterSEQ::PreProcessingImpl() { + return true; +} + +bool DorofeevIScatterSEQ::RunImpl() { + GetOutput() = GetInput()[0]; + return true; +} + +bool DorofeevIScatterSEQ::PostProcessingImpl() { + return true; +} + +} // 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..978ac2d516 --- /dev/null +++ b/tasks/dorofeev_i_scatter/tests/functional/main.cpp @@ -0,0 +1,57 @@ +#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 size = std::get<0>(params); + + input_.resize(size); + std::iota(input_.begin(), input_.end(), 0.0); // NOLINT(modernize-use-ranges) + } + + InType GetTestInputData() override { + return input_; + } + + bool CheckTestOutputData(OutType &out) override { + return out >= 0.0; + } + + public: + static std::string PrintTestParam(const TestType ¶m) { + return std::get<1>(param); + } + + private: + InType input_; +}; + +TEST_P(DorofeevIScatterFuncTests, ScatterCorrectness) { + ExecuteTest(GetParam()); +} + +const std::array kTestParams = { + std::make_tuple(4, "basic"), +}; + +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..c098d0f4b2 --- /dev/null +++ b/tasks/dorofeev_i_scatter/tests/performance/main.cpp @@ -0,0 +1,38 @@ +#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 { + input_.resize(100000); + } + + InType GetTestInputData() override { + return input_; + } + + bool CheckTestOutputData(OutType & /*output_data*/) override { + return true; + } + + private: + 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