diff --git a/tasks/kutuzov_i_elem_vec_average/common/include/common.hpp b/tasks/kutuzov_i_elem_vec_average/common/include/common.hpp new file mode 100644 index 0000000000..7fc2dcf13c --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/common/include/common.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include "task/include/task.hpp" + +namespace kutuzov_i_elem_vec_average { + +using InType = std::vector; +using OutType = double; +using TestType = size_t; +using BaseTask = ppc::task::Task; + +} // namespace kutuzov_i_elem_vec_average diff --git a/tasks/kutuzov_i_elem_vec_average/data/pic.jpg b/tasks/kutuzov_i_elem_vec_average/data/pic.jpg new file mode 100644 index 0000000000..637624238c Binary files /dev/null and b/tasks/kutuzov_i_elem_vec_average/data/pic.jpg differ diff --git a/tasks/kutuzov_i_elem_vec_average/info.json b/tasks/kutuzov_i_elem_vec_average/info.json new file mode 100644 index 0000000000..9269a9082d --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Иван", + "last_name": "Кутузов", + "middle_name": "Арсеньевич", + "group_number": "3823Б1ФИ3", + "task_number": "1" + } +} diff --git a/tasks/kutuzov_i_elem_vec_average/mpi/include/ops_mpi.hpp b/tasks/kutuzov_i_elem_vec_average/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..ed15ef9033 --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/mpi/include/ops_mpi.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "kutuzov_i_elem_vec_average/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace kutuzov_i_elem_vec_average { + +class KutuzovIElemVecAverageMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + explicit KutuzovIElemVecAverageMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace kutuzov_i_elem_vec_average diff --git a/tasks/kutuzov_i_elem_vec_average/mpi/src/ops_mpi.cpp b/tasks/kutuzov_i_elem_vec_average/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..cbea0ed934 --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/mpi/src/ops_mpi.cpp @@ -0,0 +1,89 @@ +#include "kutuzov_i_elem_vec_average/mpi/include/ops_mpi.hpp" + +#include + +#include +#include + +#include "kutuzov_i_elem_vec_average/common/include/common.hpp" + +namespace kutuzov_i_elem_vec_average { + +KutuzovIElemVecAverageMPI::KutuzovIElemVecAverageMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0.0; +} + +bool KutuzovIElemVecAverageMPI::ValidationImpl() { + return !GetInput().empty(); +} + +bool KutuzovIElemVecAverageMPI::PreProcessingImpl() { + return true; +} + +bool KutuzovIElemVecAverageMPI::RunImpl() { + GetOutput() = 0.0; + + double result = 0.0; + double global_sum = 0.0; + + // MPI Data + int rank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + + int num_processes = 0; + MPI_Comm_size(MPI_COMM_WORLD, &num_processes); + + // Syncing input size data + int total_elements_num = 0; + if (rank == 0) { + total_elements_num = static_cast(GetInput().size()); + } + MPI_Bcast(&total_elements_num, 1, MPI_INT, 0, MPI_COMM_WORLD); + + // Calculating batch size + int batch_size = 0; + batch_size = total_elements_num / num_processes; + + // If batch size isn't negative: Scatter the data among processes, + // sum it and reduce back to the process-0 + if (batch_size > 0) { + auto *local_buffer = new double[batch_size]; + MPI_Scatter(GetInput().data(), batch_size, MPI_DOUBLE, local_buffer, batch_size, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + double sum = 0.0; + for (int i = 0; i < batch_size; i++) { + sum += local_buffer[i]; + } + + MPI_Reduce(&sum, &global_sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); + delete[] local_buffer; + } + + if (rank == 0) { + // Add remaining elements on process-0 + if (num_processes * batch_size < total_elements_num) { + for (int i = num_processes * batch_size; i < total_elements_num; i++) { + global_sum += GetInput()[i]; + } + } + // Get the average + result = global_sum / static_cast(total_elements_num); + } + + // Broadcast the result to all the processes + MPI_Bcast(&result, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); + GetOutput() = result; + + // Wait for all processes to finish working + MPI_Barrier(MPI_COMM_WORLD); + return true; +} + +bool KutuzovIElemVecAverageMPI::PostProcessingImpl() { + return true; +} + +} // namespace kutuzov_i_elem_vec_average diff --git a/tasks/kutuzov_i_elem_vec_average/report.md b/tasks/kutuzov_i_elem_vec_average/report.md new file mode 100644 index 0000000000..97aa9d6ed6 --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/report.md @@ -0,0 +1,163 @@ +# Вычисление среднего значения элементов вектора + +- Student: Кутузов Иван Арсеньевич, group 3823Б1ФИ3 +- Technology: SEQ | MPI +- Variant: 2 + +## 1. Введение + +Вычисление среднего значения элементов вектора - типовая задача, которая легко адаптируется к параллельному исполнению. Идеальное ускорение соответствует количеству задействованных процессоров. + +## 2. Постановка задачи + +### Входные данные: + +Дан вектор длины `N`, состоящий из элементов типа `double`. + +### Выходные данные: + +Значение типа `double`, равное среднему значению всех `N` элементов вектора. + +### 3. Последовательный алгоритм + +Решение "в лоб": суммируем элементы вектора, затем делим полученную сумму на число элементов в изначальном векторе. + +```c++ +  GetOutput() = 0.0; + +  for (size_t i = 0; i < GetInput().size(); i++) { +    GetOutput() += GetInput()[i]; +  } + +  GetOutput() /= GetInput().size(); +``` + +### 4. Параллельный алгоритм + +Объявлеются две переменные типа `double`: +* `result` - на конец вычислений будет содержать среднее значение элеметов вектора на (всех процессах) - ответ на поставленную задачу; +* `global_sum` - будет содержать значение суммы всех элеметов вектора, частично вычисленное на отдельных процессах и собранное в итогувую сумму на процессе с рангом 0; + +```c++ +double result = 0.0; +double global_sum = 0.0; +``` + +Через MPI вызовы получаются значения ранга исполняемого процесса и числа всех процессов; + +```c++ +int rank = 0; +MPI_Comm_rank(MPI_COMM_WORLD, &rank); + +int num_processes = 0; +MPI_Comm_size(MPI_COMM_WORLD, &num_processes); +``` + +На процессе с рангом 0 определяется количество элементов в векторе. С помощью вызова `MPI_Bcast` это значение рассылается по всем остальным процессам. + +```c++ +int total_elements_num = 0; +if (rank == 0) { + total_elements_num = static_cast(GetInput().size()); +} +MPI_Bcast(&total_elements_num, 1, MPI_INT, 0, MPI_COMM_WORLD); +``` + +Определяется размер "пачки" - число элеметов вектора, отдаваемых на обработку каждому процессу. Заметим, что `batch_size * num_processes` может быть меньше числа элеметов в векторе. + +```c++ +int batch_size = 0; +batch_size = total_elements_num / num_processes; +``` + +Если размер "пачки" больше нуля, то: +* Выделяем память под локальный буффер данных, который будет содержать данные, выданные процессу для обработки; +* При помощи вызова `MPI_Scatter` распределяем элементы изначального вектора по процессам поровну (по `batch_size` штук); +* Суммируем данные, лежащие в локальном буфере; +* При помощи вызова `MPI_Reduce` собираем и складываем частичные суммы со всех процессов на процессе с рангом 0 (значение помещается в `global_sum`); +* Здесь же освобождаем память, выделенную под локальный буфер. + +```c++ +if (batch_size > 0) { + double *local_buffer = new double[batch_size]; + MPI_Scatter(GetInput().data(), batch_size, MPI_DOUBLE, local_buffer, batch_size, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + double sum = 0.0; + for (int i = 0; i < batch_size; i++) { + sum += local_buffer[i]; + } + + MPI_Reduce(&sum, &global_sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); + delete[] local_buffer; +} +``` + +На процессе с рангом ноль обрабатываем элементы, не попавшие в рассылку, просто добавляя их к значению `global_sum`. Затем делим итоговую сумму на число элеметов в изначельном векторе, т.е. получаем ответ задачи - среднее значение элементов вектора. Полученный ответ записываем в `result`. + +```c++ +if (rank == 0) { + if (num_processes * batch_size < total_elements_num) { + for (int i = num_processes * batch_size; i < total_elements_num; i++) { + global_sum += GetInput()[i]; + } + } + + result = global_sum / static_cast(total_elements_num); +} +``` + +Рассылаем значение `result` на все остальные процессы (чтобы проходили тесты корректности ответа). + +```c++ +MPI_Bcast(&result, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); +GetOutput() = result; +``` + +Вызываем `MPI_Barrier`, чтобы убедится, что все процессы завершили свою работу. + +```c++ +MPI_Barrier(MPI_COMM_WORLD); +``` + +### 5. Среда экспериментов + +- Hardware/OS: 14th gen Intel(R) Core(TM) i5-14600KF, 14 ядер (6P+8E ядер), 32 GB RAM, Windows 11 x64 +- Toolchain: compiler, version, build type (Release/RelWithDebInfo) + - Cmake 3.28.3 + - Компилятор: g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 + - Использовался Docker-контейнер. + - Режим сборки: Release. +- Data: Вектор длиной 50 000 000. + +## 6. Результаты + +### 6.1 Корректность + +Корректность работы проверена с помощью тестов Google Test на векторах размером: 1, 10, 1000 и 10000. + +### 6.2 Производительность + +| Mode | Count | Time, s | Speedup | Efficiency | +| ---- | ----- | ------- | ------- | ---------- | +| seq | 1 | 0.0325 | 1.00 | N/A | +| mpi | 2 | 0.1479 | 0.22 | 11.0% | +| mpi | 4 | 0.1289 | 0.25 | 6.25% | +| mpi | 6 | 0.1329 | 0.24 | 4.0% | +| mpi | 8 | 0.1646 | 0.20 | 2.5% | +| mpi | 10 | 0.1629 | 0.20 | 2.0% | + +График времени исполнения (в секундах) в зависомости от кол-ва процессов: + +![](report_img/PerfTime.png) + +График эффективности параллелизма в зависомости от кол-ва процессов: + +![](report_img/PerfEfficiency.png) + +## 7. Заключение + +Операции рассылки данных по процессам и их последующий сбор требуют значительно больше времени, чем решение данной задачи последовательно, что привело к очень низкой эффективности параллелизма. + +## 8. Ссылки + +1. "Параллельное программирование для кластерных систем" ННГУ им. Лобачевского, ИИТММ \ No newline at end of file diff --git a/tasks/kutuzov_i_elem_vec_average/report_img/PerfEfficiency.png b/tasks/kutuzov_i_elem_vec_average/report_img/PerfEfficiency.png new file mode 100644 index 0000000000..02e231cf1d Binary files /dev/null and b/tasks/kutuzov_i_elem_vec_average/report_img/PerfEfficiency.png differ diff --git a/tasks/kutuzov_i_elem_vec_average/report_img/PerfTime.png b/tasks/kutuzov_i_elem_vec_average/report_img/PerfTime.png new file mode 100644 index 0000000000..300312264e Binary files /dev/null and b/tasks/kutuzov_i_elem_vec_average/report_img/PerfTime.png differ diff --git a/tasks/kutuzov_i_elem_vec_average/seq/include/ops_seq.hpp b/tasks/kutuzov_i_elem_vec_average/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..1945776f5b --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/seq/include/ops_seq.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "kutuzov_i_elem_vec_average/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace kutuzov_i_elem_vec_average { + +class KutuzovIElemVecAverageSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit KutuzovIElemVecAverageSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace kutuzov_i_elem_vec_average diff --git a/tasks/kutuzov_i_elem_vec_average/seq/src/ops_seq.cpp b/tasks/kutuzov_i_elem_vec_average/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..8a567b7d76 --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/seq/src/ops_seq.cpp @@ -0,0 +1,38 @@ +#include "kutuzov_i_elem_vec_average/seq/include/ops_seq.hpp" + +#include + +#include "kutuzov_i_elem_vec_average/common/include/common.hpp" + +namespace kutuzov_i_elem_vec_average { + +KutuzovIElemVecAverageSEQ::KutuzovIElemVecAverageSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0.0; +} + +bool KutuzovIElemVecAverageSEQ::ValidationImpl() { + return !GetInput().empty(); +} + +bool KutuzovIElemVecAverageSEQ::PreProcessingImpl() { + return true; +} + +bool KutuzovIElemVecAverageSEQ::RunImpl() { + GetOutput() = 0.0; + for (double x : GetInput()) { + GetOutput() += x; + } + + GetOutput() /= static_cast(GetInput().size()); + + return true; +} + +bool KutuzovIElemVecAverageSEQ::PostProcessingImpl() { + return true; +} + +} // namespace kutuzov_i_elem_vec_average diff --git a/tasks/kutuzov_i_elem_vec_average/settings.json b/tasks/kutuzov_i_elem_vec_average/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/kutuzov_i_elem_vec_average/tests/.clang-tidy b/tasks/kutuzov_i_elem_vec_average/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/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/kutuzov_i_elem_vec_average/tests/functional/main.cpp b/tasks/kutuzov_i_elem_vec_average/tests/functional/main.cpp new file mode 100644 index 0000000000..2767309543 --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/tests/functional/main.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "kutuzov_i_elem_vec_average/common/include/common.hpp" +#include "kutuzov_i_elem_vec_average/mpi/include/ops_mpi.hpp" +#include "kutuzov_i_elem_vec_average/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace kutuzov_i_elem_vec_average { + +class KutuzovIElemVecAverageFuncTests : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType &test_param) { + return std::to_string(test_param); + } + + protected: + void SetUp() override { + TestType param = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + input_data_ = std::vector(param, 0.0); + + answer_ = 0.0; + int num = static_cast(param); + for (int i = 0; i < num; i++) { + double value = static_cast(i * i) - (static_cast(param) / 2.0); + input_data_[i] = value; + answer_ += value; + } + answer_ /= static_cast(param); + } + + bool CheckTestOutputData(OutType &output_data) final { + bool result = abs(output_data - answer_) < 0.0001; + + return result; + } + + InType GetTestInputData() final { + return input_data_; + } + + private: + InType input_data_; + double answer_ = 0.0; +}; + +namespace { + +TEST_P(KutuzovIElemVecAverageFuncTests, MatmulFromPic) { + ExecuteTest(GetParam()); +} + +const std::array kTestParam = {1, 10, 1000, 10000}; + +const auto kTestTasksList = std::tuple_cat( + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_kutuzov_i_elem_vec_average), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_kutuzov_i_elem_vec_average)); + +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); + +const auto kPerfTestName = KutuzovIElemVecAverageFuncTests::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(PicMatrixTests, KutuzovIElemVecAverageFuncTests, kGtestValues, kPerfTestName); + +} // namespace + +} // namespace kutuzov_i_elem_vec_average diff --git a/tasks/kutuzov_i_elem_vec_average/tests/performance/main.cpp b/tasks/kutuzov_i_elem_vec_average/tests/performance/main.cpp new file mode 100644 index 0000000000..860eca3889 --- /dev/null +++ b/tasks/kutuzov_i_elem_vec_average/tests/performance/main.cpp @@ -0,0 +1,54 @@ +#include + +#include +#include + +#include "kutuzov_i_elem_vec_average/common/include/common.hpp" +#include "kutuzov_i_elem_vec_average/mpi/include/ops_mpi.hpp" +#include "kutuzov_i_elem_vec_average/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace kutuzov_i_elem_vec_average { + +class KutuzovIElemVecAveragePerfTests : public ppc::util::BaseRunPerfTests { + const int kCount_ = 50000000; + InType input_data_; + double answer_ = 0.0; + + void SetUp() override { + input_data_ = std::vector(kCount_, 0.0); + + answer_ = 0.0; + for (int i = 0; i < kCount_; i++) { + double value = static_cast(i * i) - (static_cast(kCount_) / 2.0); + input_data_[i] = value; + answer_ += value; + } + answer_ /= static_cast(kCount_); + } + + bool CheckTestOutputData(OutType &output_data) final { + bool result = abs(output_data - answer_) < abs(answer_ * 0.0001); + + return result; + } + + InType GetTestInputData() final { + return input_data_; + } +}; + +TEST_P(KutuzovIElemVecAveragePerfTests, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_kutuzov_i_elem_vec_average); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = KutuzovIElemVecAveragePerfTests::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, KutuzovIElemVecAveragePerfTests, kGtestValues, kPerfTestName); + +} // namespace kutuzov_i_elem_vec_average