diff --git a/tasks/kruglova_a_2d_multistep_par_opt/common/include/common.hpp b/tasks/kruglova_a_2d_multistep_par_opt/common/include/common.hpp new file mode 100644 index 0000000000..4009f4ab5c --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/common/include/common.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +#include "task/include/task.hpp" + +namespace kruglova_a_2d_multistep_par_opt { + +struct InType { + double x_min; + double x_max; + double y_min; + double y_max; + double eps; + int max_iters; + + InType() : x_min(0.0), x_max(0.0), y_min(0.0), y_max(0.0), eps(0.0), max_iters(0) {} + + InType(double xmin, double xmax, double ymin, double ymax, double e, int iters) + : x_min(xmin), x_max(xmax), y_min(ymin), y_max(ymax), eps(e), max_iters(iters) {} +}; + +struct OutType { + double x; + double y; + double f_value; + + OutType() : x(0.0), y(0.0), f_value(0.0) {} + + OutType(double x_val, double y_val, double f_val) : x(x_val), y(y_val), f_value(f_val) {} +}; + +struct Interval1D { + double a, b; + double f_a, f_b; + double characteristic; + int iteration; +}; + +struct Trial { + double point; + double value; +}; + +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +inline double ObjectiveFunction(double x, double y) { + constexpr double kA = 10.0; + constexpr double kN = 2.0; + return (kA * kN) + (x * x) + (y * y) - (kA * (std::cos(2.0 * M_PI * x) + std::cos(2.0 * M_PI * y))); +} + +} // namespace kruglova_a_2d_multistep_par_opt diff --git a/tasks/kruglova_a_2d_multistep_par_opt/info.json b/tasks/kruglova_a_2d_multistep_par_opt/info.json new file mode 100644 index 0000000000..84713e53aa --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Алёна", + "last_name": "Круглова", + "middle_name": "Витальевна", + "group_number": "3823Б1ФИ2", + "task_number": "3" + } +} diff --git a/tasks/kruglova_a_2d_multistep_par_opt/mpi/include/ops_mpi.hpp b/tasks/kruglova_a_2d_multistep_par_opt/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..31bb748df9 --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/mpi/include/ops_mpi.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "kruglova_a_2d_multistep_par_opt/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace kruglova_a_2d_multistep_par_opt { + +class KruglovaA2DMuitMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + explicit KruglovaA2DMuitMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + double Solve1DSequential(std::function &func, double a, double b, double eps, int max_iters); +}; + +} // namespace kruglova_a_2d_multistep_par_opt diff --git a/tasks/kruglova_a_2d_multistep_par_opt/mpi/src/ops_mpi.cpp b/tasks/kruglova_a_2d_multistep_par_opt/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..40fa0043ce --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/mpi/src/ops_mpi.cpp @@ -0,0 +1,242 @@ +#include "kruglova_a_2d_multistep_par_opt/mpi/include/ops_mpi.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "kruglova_a_2d_multistep_par_opt/common/include/common.hpp" + +namespace kruglova_a_2d_multistep_par_opt { + +namespace { + +struct Trial1D { + double x; + double z; + Trial1D(double x_val, double z_val) : x(x_val), z(z_val) {} +}; + +struct Trial2D { + double x; + double y; + double z; + Trial2D(double x_val, double y_val, double z_val) : x(x_val), y(y_val), z(z_val) {} +}; + +struct CharIdx { + double r_val; + size_t idx; +}; + +void ManualInsert1D(std::vector &trials, const Trial1D &value) { + size_t pos = 0; + while (pos < trials.size() && trials[pos].x < value.x) { + pos++; + } + trials.insert(trials.begin() + static_cast(pos), value); +} + +void ManualInsert2D(std::vector &trials, const Trial2D &value) { + size_t pos = 0; + while (pos < trials.size() && trials[pos].x < value.x) { + pos++; + } + if (pos == trials.size() || std::abs(trials[pos].x - value.x) > 1e-12) { + trials.insert(trials.begin() + static_cast(pos), value); + } +} + +void ManualSortRates(std::vector &rates) { + for (size_t i = 0; i < rates.size(); ++i) { + size_t max_i = i; + for (size_t j = i + 1; j < rates.size(); ++j) { + if (rates[j].r_val > rates[max_i].r_val) { + max_i = j; + } + } + if (max_i != i) { + std::swap(rates[i], rates[max_i]); + } + } +} + +template +double CalculateM(const std::vector &trials) { + double m_max = 0.0; + for (size_t i = 0; (i + 1) < trials.size(); ++i) { + const double dx = trials[i + 1].x - trials[i].x; + if (dx > 1e-15) { + const double dz = std::abs(trials[i + 1].z - trials[i].z); + m_max = std::max(m_max, dz / dx); + } + } + return m_max; +} + +void PrepareIntervals(const std::vector &trials, std::vector &buf, int size, double eps, int &stop) { + const double m_v = CalculateM(trials); + const double m_scaled = (m_v > 0.0) ? (2.0 * m_v) : 1.0; + std::vector rates; + for (size_t i = 0; (i + 1) < trials.size(); ++i) { + const double dx = trials[i + 1].x - trials[i].x; + const double dz = trials[i + 1].z - trials[i].z; + const double r = (m_scaled * dx) + ((dz * dz) / (m_scaled * dx)) - (2.0 * (trials[i + 1].z + trials[i].z)); + rates.push_back({r, i}); + } + ManualSortRates(rates); + if (rates.empty() || (trials[rates[0].idx + 1].x - trials[rates[0].idx].x < eps)) { + stop = 1; + return; + } + for (int i = 0; i < size; ++i) { + const size_t s_idx = (static_cast(i) < rates.size()) ? rates[static_cast(i)].idx : rates[0].idx; + buf[(static_cast(i) * 4) + 0] = trials[s_idx].x; + buf[(static_cast(i) * 4) + 1] = trials[s_idx + 1].x; + buf[(static_cast(i) * 4) + 2] = trials[s_idx].z; + buf[(static_cast(i) * 4) + 3] = trials[s_idx + 1].z; + } +} + +double Solve1DStrongin(const std::function &func, double a, double b, double eps, int max_iters, + double &best_x) { + const double r_param = 2.0; + std::vector trials; + trials.emplace_back(a, func(a)); + trials.emplace_back(b, func(b)); + if (trials[0].x > trials[1].x) { + std::swap(trials[0], trials[1]); + } + for (int iter = 0; iter < max_iters; ++iter) { + const double m_v = CalculateM(trials); + const double m_scaled = (m_v > 0.0) ? (r_param * m_v) : 1.0; + double max_rate = -std::numeric_limits::infinity(); + size_t idx = 0; + for (size_t i = 0; (i + 1) < trials.size(); ++i) { + const double dx = trials[i + 1].x - trials[i].x; + const double dz = trials[i + 1].z - trials[i].z; + const double rate = (m_scaled * dx) + ((dz * dz) / (m_scaled * dx)) - (2.0 * (trials[i + 1].z + trials[i].z)); + if (rate > max_rate) { + max_rate = rate; + idx = i; + } + } + if ((trials[idx + 1].x - trials[idx].x) < eps) { + break; + } + const double x_new = + (0.5 * (trials[idx + 1].x + trials[idx].x)) - ((trials[idx + 1].z - trials[idx].z) / (2.0 * m_scaled)); + ManualInsert1D(trials, Trial1D(x_new, func(x_new))); + } + size_t best = 0; + for (size_t i = 1; i < trials.size(); ++i) { + if (trials[i].z < trials[best].z) { + best = i; + } + } + best_x = trials[best].x; + return trials[best].z; +} + +void SyncBestResult(std::vector &trials, double *out_data) { + size_t b = 0; + for (size_t i = 1; i < trials.size(); ++i) { + if (trials[i].z < trials[b].z) { + b = i; + } + } + out_data[0] = trials[b].x; + out_data[1] = trials[b].y; + out_data[2] = trials[b].z; +} + +} // namespace + +KruglovaA2DMuitMPI::KruglovaA2DMuitMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; +} + +bool KruglovaA2DMuitMPI::ValidationImpl() { + const auto &in = GetInput(); + return (in.x_max > in.x_min) && (in.y_max > in.y_min) && (in.eps > 0.0) && (in.max_iters > 0); +} + +bool KruglovaA2DMuitMPI::PreProcessingImpl() { + GetOutput() = {0.0, 0.0, std::numeric_limits::max()}; + return true; +} + +bool KruglovaA2DMuitMPI::RunImpl() { + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + const auto &in = GetInput(); + std::vector trials; + + auto compute_z = [&](double x_v, double &y_b) { + return Solve1DStrongin([&](double y) { return ObjectiveFunction(x_v, y); }, in.y_min, in.y_max, in.eps, + std::max(50, in.max_iters / 15), y_b); + }; + + if (rank == 0) { + for (int i = 0; i < 15; ++i) { + double x = in.x_min + ((in.x_max - in.x_min) * (static_cast(i) / 14.0)); + double y = 0.0; + double z = compute_z(x, y); + ManualInsert2D(trials, Trial2D(x, y, z)); + } + } + + for (int iter = 0; iter < in.max_iters; ++iter) { + int stop_f = 0; + std::vector int_buf(static_cast(size) * 4, 0.0); + if (rank == 0) { + PrepareIntervals(trials, int_buf, size, in.eps, stop_f); + } + + MPI_Bcast(&stop_f, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (stop_f != 0) { + break; + } + + std::array my_int{}; + MPI_Scatter(int_buf.data(), 4, MPI_DOUBLE, my_int.data(), 4, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + double m_l = std::max(1.0, 2.0 * (std::abs(my_int[3] - my_int[2]) / (my_int[1] - my_int[0]))); + double x_n = (0.5 * (my_int[0] + my_int[1])) - ((my_int[3] - my_int[2]) / (2.0 * m_l)); + double y_res = 0.0; + double z_res = compute_z(x_n, y_res); + + std::array send_v = {x_n, y_res, z_res}; + std::vector recv_v(static_cast(size) * 3); + MPI_Gather(send_v.data(), 3, MPI_DOUBLE, recv_v.data(), 3, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + if (rank == 0) { + for (int i = 0; i < size; ++i) { + const size_t idx = static_cast(i) * 3; + ManualInsert2D(trials, Trial2D(recv_v[idx + 0], recv_v[idx + 1], recv_v[idx + 2])); + } + } + } + + std::array res = {0.0, 0.0, 0.0}; + if (rank == 0) { + SyncBestResult(trials, res.data()); + } + MPI_Bcast(res.data(), 3, MPI_DOUBLE, 0, MPI_COMM_WORLD); + GetOutput() = {res[0], res[1], res[2]}; + return true; +} + +bool KruglovaA2DMuitMPI::PostProcessingImpl() { + return true; +} + +} // namespace kruglova_a_2d_multistep_par_opt diff --git a/tasks/kruglova_a_2d_multistep_par_opt/report.md b/tasks/kruglova_a_2d_multistep_par_opt/report.md new file mode 100644 index 0000000000..20851de6ab --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/report.md @@ -0,0 +1,384 @@ +# Многошаговая схема решения двумерных задач глобальной оптимизации. Распараллеливание по характеристикам. + +- Студент: Круглова Алёна Витальевна, группа 3823Б1ФИ2 +- Технология: SEQ | MPI +- Вариант: 13 + +## 1. Введение +Задачи глобальной оптимизации функций нескольких переменных широко применяются в вычислительной математике и прикладных задачах. Их особенностью является необходимость поиска абсолютного минимума целевой функции на заданной области, что затруднено при наличии множества локальных экстремумов. + +Одним из подходов к решению таких задач являются многошаговые методы, основанные на выборе интервалов с наибольшей характеристикой. В двумерном случае задача сводится к последовательному поиску минимума по одной переменной с решением одномерных подзадач по другой переменной, для чего используется алгоритм Стронгина. + +С целью ускорения вычислений применяется распараллеливание по характеристикам интервалов. На каждом шаге несколько наиболее перспективных интервалов обрабатываются независимо различными процессами, а полученные результаты объединяются в общий набор данных. Это позволяет эффективно использовать параллельные вычислительные ресурсы. + +В данной лабораторной работе реализованы последовательная и параллельная версии алгоритма двумерной глобальной оптимизации с использованием библиотеки MPI, а также выполнено их сравнение по корректности и производительности. + +## 2. Постановка задачи +**Формальная постановка:** +Рассматривается задача глобальной оптимизации вещественной функции двух переменных + +f(x, y), (x, y) ∈ D, + +где область допустимых значений D задаётся прямоугольником + +x ∈ [x_min, x_max], +y ∈ [y_min, y_max]. + +Требуется найти точку глобального минимума (x*, y*) и соответствующее минимальное значение функции f(x*, y*) с заданной точностью ε с использованием многошагового алгоритма глобальной оптимизации. + +В рамках данной работы в качестве целевой функции используется тестовая негладкая многомодальная функция, обладающая несколькими локальными минимумами, что позволяет оценить корректность и эффективность реализованного алгоритма. + +**Входные данные:** +Входные данные задаются структурой `InType` и включают следующие параметры: + +- `x_min` — нижняя граница области поиска по переменной x +- `x_max` — верхняя граница области поиска по переменной x +- `y_min` — нижняя граница области поиска по переменной y +- `y_max` — верхняя граница области поиска по переменной y +- `eps` — требуемая точность поиска +- `max_iters` — максимальное количество итераций алгоритма + +**Выходные данные:** +Результат работы алгоритма возвращается в структуре `OutType`: + +- `x` — значение координаты x, в которой достигается минимум функции +- `y` — значение координаты y, в которой достигается минимум функции +- `f_value` — минимальное значение целевой функции + +**Ограничения:** +- Должны выполняться условия: `x_max > x_min`, `y_max > y_min` +- Параметр точности `eps` должен быть положительным +- Максимальное количество итераций `max_iters` должно быть больше нуля +- Алгоритм должен корректно работать для произвольных допустимых границ области поиска +- Параллельная MPI-версия должна поддерживать произвольное количество процессов и реализовывать распараллеливание по характеристикам интервалов, распределяя вычисление новых точек между процессами + +## 3. Последовательная версия(Baseline) +Последовательная версия реализует многошаговый алгоритм двумерной глобальной оптимизации без использования параллельных вычислений. Алгоритм основан на декомпозиции исходной задачи на последовательность одномерных подзадач и использовании метода Стронгина для поиска минимума. + +На начальном этапе на отрезке [x_min, x_max] формируется начальное множество точек. Для каждой точки x последовательно решается одномерная задача минимизации по переменной y, в результате чего вычисляется значение целевой функции f(x, y). Полученные точки сортируются по возрастанию координаты x и образуют начальный набор испытаний. + +На каждой итерации алгоритма: +- вычисляется оценка константы Липшица на основе текущего набора точек; +- для всех соседних интервалов по переменной x рассчитываются их характеристики; +- выбирается интервал с максимальной характеристикой; +- внутри выбранного интервала вычисляется новая точка x; +- для найденного значения x последовательно решается одномерная задача оптимизации по y; +- новая точка добавляется в общее множество испытаний. + +Итерационный процесс продолжается до достижения заданной точности ε либо до исчерпания максимального числа итераций. После завершения алгоритма в качестве результата выбирается точка с минимальным значением целевой функции. + +Для иллюстрации одного шага алгоритма ниже приведён фрагмент кода, демонстрирующий вычисление новой точки по переменной x и последующее решение одномерной подзадачи оптимизации по переменной y: + +```cpp +double x_new = (0.5 * (x_right + x_left)) - + ((z_right - z_left) / (2.0 * m_scaled)); + +double y_new = 0.0; +double z_new = Solve1DStrongin( + [&](double y) { return ObjectiveFunction(x_new, y); }, + y_min, y_max, eps, max_iters, y_new); +``` + +### Вычислительная сложность + +Временная сложность последовательного алгоритма определяется количеством итераций внешнего цикла и числом вычислений одномерных задач оптимизации. В общем виде сложность можно оценить как: + +O(K · N₁ · N₂), + +где K — число итераций двумерного алгоритма, +N₁ — количество испытаний по переменной x, +N₂ — количество итераций одномерного алгоритма Стронгина по переменной y. + +### Использование памяти + +Алгоритм использует дополнительную память для хранения текущего набора испытаний, размер которого линейно растёт с числом итераций. Дополнительное потребление памяти оценивается как O(N), где N — количество точек в множестве испытаний. + +## 4. Параллельная версия + +Параллельная версия алгоритма реализует многошаговую схему двумерной глобальной оптимизации с использованием библиотеки MPI. Распараллеливание осуществляется по характеристикам интервалов, что позволяет одновременно вычислять несколько новых испытательных точек на каждом шаге алгоритма. + +### 4.1. Разделение данных и вычислений + +Параллелизм реализуется на уровне внешнего одномерного поиска по переменной x. На каждой итерации алгоритма корневой процесс формирует набор наиболее перспективных интервалов по координате x на основе их характеристик. Количество таких интервалов соответствует числу процессов. + +Каждому процессу передаётся один интервал вида +[x₁, x₂] с известными значениями целевой функции f(x₁) и f(x₂). +Внутри полученного интервала процесс независимо выполняет: +- вычисление новой точки x; +- решение одномерной задачи глобальной оптимизации по переменной y; +- вычисление значения целевой функции f(x, y). + +Таким образом, каждый процесс выполняет независимый вычислительный шаг, что позволяет эффективно распараллелить наиболее затратную часть алгоритма. + +### 4.2. Взаимодействие процессов + +Взаимодействие между процессами организовано по схеме «master–worker»: + +1. **Инициализация** + Все процессы получают свой `rank` и общее количество процессов `size` с помощью `MPI_Comm_rank` и `MPI_Comm_size`. + +2. **Начальная инициализация (процесс 0)** + Корневой процесс формирует начальное множество точек по переменной x и для каждой из них последовательно решает одномерную задачу оптимизации по y. + +3. **Выбор интервалов** + На каждой итерации процесс 0: + - вычисляет характеристики всех текущих интервалов; + - выбирает `size` интервалов с наибольшими характеристиками; + - формирует массив интервалов для распределения между процессами. + +4. **Распределение интервалов** + Выбранные интервалы передаются процессам с использованием `MPI_Scatter`. + Каждый процесс получает один интервал для обработки. + +5. **Локальные вычисления** + Каждый процесс независимо: + - вычисляет новую точку x внутри своего интервала; + - решает одномерную задачу оптимизации по y; + - формирует локальный результат (x, y, f). + +6. **Сбор результатов** + Локальные результаты передаются на корневой процесс с помощью `MPI_Gather`. + +7. **Обновление множества точек** + Процесс 0 добавляет полученные точки в общее множество испытаний и проверяет условие остановки. + +8. **Завершение** + После завершения итерационного процесса найденный минимум рассылается всем процессам с использованием `MPI_Bcast`. + + +### 4.3. Иллюстрация параллельного шага + +Ниже приведён фрагмент кода, демонстрирующий вычисление новой точки внутри интервала и последующее решение одномерной подзадачи оптимизации на каждом процессе: + +```cpp +double x_new = (0.5 * (my_interval.x1 + my_interval.x2)) - + ((my_interval.f2 - my_interval.f1) / (2.0 * m_local)); + +double y_res = 0.0; +double f_res = Solve1DStrongin( + [&](double y) { return ObjectiveFunction(x_new, y); }, + in.y_min, in.y_max, in.eps, + std::max(25, in.max_iters / 20), y_res); +``` + +### 4.3. Псевдокод алгоритма +```pseudocode +MPI_Init() +Получить rank и size + +Если rank == 0: + сформировать начальное множество точек + +Для iter = 1 .. max_iters: + Если rank == 0: + выбрать size интервалов с максимальной характеристикой + MPI_Bcast(stop_flag) + + Если stop_flag: + выйти из цикла + + MPI_Scatter(интервалы → локальный интервал) + + вычислить новую точку x + решить одномерную задачу по y + получить локальный результат (x, y, f) + + MPI_Gather(локальные результаты → процесс 0) + + Если rank == 0: + обновить множество точек + +Если rank == 0: + выбрать глобальный минимум + +MPI_Bcast(результат) +MPI_Finalize() +``` + +## 5. Детали реализации + +### 5.1. Файловая структура проекта +kruglova_a_2d_multistep_par_opt/ +├── common/include/common.hpp +├── seq/include/ops_seq.hpp +├── seq/src/ops_seq.cpp +├── mpi/include/ops_mpi.hpp +├── mpi/src/ops_mpi.cpp +├── tests/functional/main.cpp +├── tests/performance/main.cpp +└── data/ + +### 5.2. Ключевые классы и функции + +- **InType** — структура входных данных задачи двумерной глобальной оптимизации, содержащая: + - границы области поиска по x и y (`x_min`, `x_max`, `y_min`, `y_max`); + - параметр точности `eps`; + - максимальное число итераций `max_iters`. + +- **OutType** — структура результата вычислений, включающая: + - координаты найденной точки минимума `(x*, y*)`; + - значение целевой функции `f(x*, y*)`. + +- **`RunImpl()`** — основная реализация параллельного алгоритма. + Функция организует многошаговый процесс оптимизации, в котором: + - корневой процесс выбирает наиболее перспективные интервалы по переменной x; + - интервалы распределяются между процессами с использованием MPI; + - каждый процесс независимо вычисляет новую точку и решает одномерную подзадачу оптимизации по y; + - результаты собираются и используются для обновления множества испытательных точек. + +- **`ValidationImpl()`** — проверка корректности входных параметров задачи: + - корректность границ области поиска; + - положительность параметра точности `eps`; + - корректность максимального числа итераций. + +- **`PreProcessingImpl()`** — начальная инициализация выходной структуры и подготовка данных перед запуском основного алгоритма. + +- **`PostProcessingImpl()`** — завершающий этап вычислений; дополнительная обработка результатов не требуется. + +- **Вспомогательные функции и структуры:** + - **`Solve1DStrongin()`** — реализация одномерного глобального поиска минимума по алгоритму Стронгина; + - **`CalculateM1D()`** — вычисление оценки липшицевой константы по текущим испытательным точкам; + - **`MasterCalculateIntervals()`** — выбор интервалов с наибольшими характеристиками для последующего распараллеливания; + - **`Trial1D`, `Trial2D`** — структуры для хранения одномерных и двумерных испытательных точек; + - **`IntervalData`** — структура, описывающая интервал по переменной x и значения функции на его границах. + +- **Тестовые классы:** + - **`KruglovaA2DMuitSEQ`** — последовательная версия алгоритма двумерной оптимизации; + - **`KruglovaA2DMuitMPI`** — параллельная версия с использованием MPI, реализующая распределение интервалов между процессами. + +### 5.3. Использование памяти + +**В последовательной версии (SEQ):** +- В памяти хранится весь набор пробных точек `Trial2D` для координаты `x` и соответствующие значения `y` и `f`. +- Хранится входная информация `InType` и результирующий объект `OutType`. + +**В параллельной версии (MPI):** +- Процесс 0 хранит: + - Полный массив пробных точек `Trial2D`. + - Вспомогательные массивы выбранных интервалов `IntervalData` для рассылки на другие процессы. +- Остальные процессы хранят: + - Локальный интервал `IntervalData`, полученный через `MPI_Scatter`. + - Локальные результаты `x_new`, `y_res`, `f_res` для передачи на процесс 0 через `MPI_Gather`. + +**Особенности организации памяти:** +- Каждому процессу достаточно хранить только свои локальные данные, что снижает объём памяти и обеспечивает масштабируемость при увеличении числа MPI-процессов. +- На процессе 0 аккумулируются и упорядочиваются новые пробные точки, что позволяет корректно формировать глобальное множество испытаний для следующей итерации. +- Использование простых структур (`Trial1D`, `Trial2D`, `IntervalData`) упрощает сериализацию для MPI и снижает накладные расходы на управление памятью. + +Таким образом, память распределена эффективно: процессы работают с минимально необходимым набором данных, а процесс-координатор хранит полное состояние задачи для управления итерациями и окончательного выбора оптимальной точки. + +## 6. Экспериментальное окружение +Экспериментальные исследования проводились на вычислительной системе с процессором **AMD Ryzen 5 5500U**, оснащённым **6** вычислительными ядрами с поддержкой одновременной многопоточности. Аппаратная конфигурация включает **16 ГБ** оперативной памяти **DDR4** и твердотельный накопитель объёмом **512 ГБ**, функционирующий под управлением операционной системы **Windows 10**. + +Разработка и компиляция программного кода выполнялись в среде **Microsoft Visual Studio 2019** с использованием компилятора C++. Для реализации параллельных вычислений применялась библиотека **Microsoft MPI** версии **10.0.12498.5**, а сборка проекта осуществлялась с помощью системы **CMake версии 3.30.4** в режиме **Release**. + +Рассматривалась задача глобальной оптимизации функции Растригина в 2D. Проверялись **последовательная (`KruglovaA2DMuitSEQ`) и параллельная MPI-версии (`KruglovaA2DMuitMPI`)**. Тесты выполнялись для 1, 2, 4 и 6 MPI-процессов. + +Корректность проверялась сравнением значений функции в найденных экстремумах, производительность — усреднением времени многократных запусков. Эксперименты оценивали **точность, масштабируемость и эффективность параллельного алгоритма**. + +## 7. Результаты + +### 7.1 Корректность + Корректность параллельной реализации подтверждена с использованием фреймворка Google Test. Результаты, полученные MPI-версией алгоритма многошаговой 2D оптимизации (`KruglovaA2DMuitMPI`), сравнивались с эталонной последовательной реализацией (`KruglovaA2DMuitSEQ`). + +### 7.2 Производительность +Измерения проводились для функции Растригина на области `[-5.12, 5.12] × [-5.12, 5.12]` с параметрами: +- ε = `10⁻⁶` +- max_iters = `2000` + + +| Mode | Count | Time, s | Speedup | Efficiency | +|-------------|-------|---------|---------|------------| +| seq | 1 | 4.94 | 1.00 | N/A | +| mpi | 2 | 3.12 | 1.58 | 79.0% | +| mpi | 4 | 1.89 | 2.61 | 65.3% | +| mpi | 6 | 1.42 | 3.48 | 58.0% | + +**Анализ результатов:** + +**Общая характеристика:** +- Сублинейное ускорение с ростом числа процессов +- Эффективность снижается: `79%` → `65%` → `58%` +- Наибольший выигрыш: `1` → `2` процесса (`-37%` времени) + + +## 8. Заключение +В результате выполнения работы был успешно разработан и реализован параллельный алгоритм многошаговой глобальной оптимизации для функции двух переменных. Основной задачей являлось создание эффективного алгоритма поиска глобального минимума функции Растригина на заданной области с использованием технологии MPI для параллельных вычислений. + +Ключевым достижением работы стала разработка адаптивного алгоритма на основе метода Стронгина, который продемонстрировал высокую эффективность при решении задачи глобальной оптимизации. Алгоритм успешно находит минимум функции с заданной точностью, что подтверждено комплексным тестированием на различных конфигурациях входных данных. Параллельная реализация с использованием MPI показала способность ускорять вычисления, обеспечивая ускорение до 3.48 раз при использовании 6 процессов. + +Тестирование подтвердило корректность работы обеих версий алгоритма (последовательной и параллельной), которые демонстрируют идентичные результаты во всех тестовых случаях. Анализ производительности выявил, что наилучшая эффективность (79%) достигается при использовании 2 процессов, при дальнейшем увеличении числа процессов эффективность снижается из-за роста коммуникационных накладных расходов. + +Разработанный алгоритм обладает практической ценностью и может применяться для решения широкого класса задач оптимизации мультимодальных функций. Его основные преимущества включают гарантированную сходимость, адаптивность к характеристикам функции и возможность распараллеливания вычислений. + +## 9. Источники +1. Microsoft MPI : документация [Электронный ресурс] // Microsoft Learn. – URL: https://learn.microsoft.com/ru-ru/message-passing-interface/microsoft-mpi (дата обращения: 12.11.2025). +2. Сысоев А. В. Курс лекций по параллельному программированию +3. Нестеров А.Ю и Оболенский А.А Практические занятия + +## 9. Приложение +Основной алгоритм MPI +```cpp +bool KruglovaA2DMuitMPI::RunImpl() { + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + const auto &in = GetInput(); + std::vector trials; + + if (rank == 0) { + for (int i = 0; i < 15; ++i) { + const double x = in.x_min + ((in.x_max - in.x_min) * (static_cast(i) / 14.0)); + double y_b = 0.0; + const double f = Solve1DStrongin([&](double y) { return ObjectiveFunction(x, y); }, in.y_min, in.y_max, in.eps, + std::max(40, in.max_iters / 15), y_b); + InsertSorted2D(trials, Trial2D(x, y_b, f)); + } + } + + for (int iter = 0; iter < in.max_iters; ++iter) { + int stop_f = 0; + std::vector intervals_buf(static_cast(size) * 4, 0.0); + if (rank == 0) PrepareIntervals(trials, intervals_buf, size, in.eps, stop_f); + + MPI_Bcast(&stop_f, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (stop_f != 0) break; + + std::array my_int{}; + MPI_Scatter(intervals_buf.data(), 4, MPI_DOUBLE, my_int.data(), 4, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + const double dx_l = my_int[1] - my_int[0]; + const double m_l = std::max(1.0, 2.0 * (std::abs(my_int[3] - my_int[2]) / dx_l)); + const double x_n = (0.5 * (my_int[0] + my_int[1])) - ((my_int[3] - my_int[2]) / (2.0 * m_l)); + double y_res = 0.0; + const double f_res = Solve1DStrongin([&](double y) { return ObjectiveFunction(x_n, y); }, in.y_min, in.y_max, in.eps, + std::max(50, in.max_iters / 15), y_res); + + std::array send_v = {x_n, y_res, f_res}; + std::vector recv_v(static_cast(size) * 3); + MPI_Gather(send_v.data(), 3, MPI_DOUBLE, recv_v.data(), 3, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + if (rank == 0) { + for (int i = 0; i < size; ++i) { + const size_t base = static_cast(i) * 3; + InsertSorted2D(trials, Trial2D(recv_v[base + 0], recv_v[base + 1], recv_v[base + 2])); + } + } + } + + std::array res = {0.0, 0.0, 0.0}; + if (rank == 0) { + size_t best_idx = 0; + for (size_t i = 1; i < trials.size(); ++i) { + if (trials[i].f < trials[best_idx].f) best_idx = i; + } + res = {trials[best_idx].x, trials[best_idx].y, trials[best_idx].f}; + } + MPI_Bcast(res.data(), 3, MPI_DOUBLE, 0, MPI_COMM_WORLD); + GetOutput() = {res[0], res[1], res[2]}; + return true; +} + + +``` \ No newline at end of file diff --git a/tasks/kruglova_a_2d_multistep_par_opt/seq/include/ops_seq.hpp b/tasks/kruglova_a_2d_multistep_par_opt/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..54851c523e --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/seq/include/ops_seq.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "kruglova_a_2d_multistep_par_opt/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace kruglova_a_2d_multistep_par_opt { + +class KruglovaA2DMuitSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit KruglovaA2DMuitSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace kruglova_a_2d_multistep_par_opt diff --git a/tasks/kruglova_a_2d_multistep_par_opt/seq/src/ops_seq.cpp b/tasks/kruglova_a_2d_multistep_par_opt/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..5d652b898d --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/seq/src/ops_seq.cpp @@ -0,0 +1,242 @@ +#include "kruglova_a_2d_multistep_par_opt/seq/include/ops_seq.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "kruglova_a_2d_multistep_par_opt/common/include/common.hpp" + +namespace kruglova_a_2d_multistep_par_opt { + +namespace { + +struct Trial1D { + double x; + double z; + + Trial1D(double x_val, double z_val) : x(x_val), z(z_val) {} +}; + +struct Trial2D { + double x; + double y; + double z; + + Trial2D(double x_val, double y_val, double z_val) : x(x_val), y(y_val), z(z_val) {} +}; + +template +double CalculateM(const std::vector &trials) { + double m_max = 0.0; + for (size_t i = 0; i + 1 < trials.size(); ++i) { + const double dx = trials[i + 1].x - trials[i].x; + if (dx > 1e-15) { + const double dz = std::abs(trials[i + 1].z - trials[i].z); + const double ratio = dz / dx; + m_max = std::max(ratio, m_max); + } + } + return m_max; +} + +size_t FindBestInterval1D(const std::vector &trials, double m_scaled) { + double max_rate = -std::numeric_limits::infinity(); + size_t best_idx = 0; + + for (size_t i = 0; i + 1 < trials.size(); ++i) { + const double dx = trials[i + 1].x - trials[i].x; + const double dz = trials[i + 1].z - trials[i].z; + + const double term1 = m_scaled * dx; + const double term2 = (dz * dz) / (m_scaled * dx); + const double term3 = 2.0 * (trials[i + 1].z + trials[i].z); + const double rate = term1 + term2 - term3; + + if (rate > max_rate) { + max_rate = rate; + best_idx = i; + } + } + + return best_idx; +} + +size_t FindBestInterval2D(const std::vector &trials, double m_scaled) { + double max_rate = -std::numeric_limits::infinity(); + size_t best_idx = 0; + + for (size_t i = 0; i + 1 < trials.size(); ++i) { + const double dx = trials[i + 1].x - trials[i].x; + const double dz = trials[i + 1].z - trials[i].z; + + const double term1 = m_scaled * dx; + const double term2 = (dz * dz) / (m_scaled * dx); + const double term3 = 2.0 * (trials[i + 1].z + trials[i].z); + const double rate = term1 + term2 - term3; + + if (rate > max_rate) { + max_rate = rate; + best_idx = i; + } + } + + return best_idx; +} + +size_t FindBestZ1D(const std::vector &trials) { + size_t best = 0; + for (size_t i = 1; i < trials.size(); ++i) { + if (trials[i].z < trials[best].z) { + best = i; + } + } + return best; +} + +size_t FindBestZ2D(const std::vector &trials) { + size_t best = 0; + for (size_t i = 1; i < trials.size(); ++i) { + if (trials[i].z < trials[best].z) { + best = i; + } + } + return best; +} + +void InsertSorted1D(std::vector &trials, const Trial1D &value) { + size_t pos = 0; + while (pos < trials.size() && trials[pos].x < value.x) { + ++pos; + } + trials.insert(trials.begin() + static_cast(pos), value); +} + +void InsertSorted2D(std::vector &trials, const Trial2D &value) { + size_t pos = 0; + while (pos < trials.size() && trials[pos].x < value.x) { + ++pos; + } + + if (pos == trials.size() || std::abs(trials[pos].x - value.x) > 1e-12) { + trials.insert(trials.begin() + static_cast(pos), value); + } +} + +double Solve1DStrongin(const std::function &func, double a, double b, double eps, int max_iters, + double &best_x) { + const double r_param = 2.0; + + std::vector trials; + trials.emplace_back(a, func(a)); + trials.emplace_back(b, func(b)); + + if (trials[0].x > trials[1].x) { + std::swap(trials[0], trials[1]); + } + + for (int iter = 0; iter < max_iters; ++iter) { + const double m_val = CalculateM(trials); + const double m_scaled = (m_val > 0.0) ? (r_param * m_val) : 1.0; + + const size_t idx = FindBestInterval1D(trials, m_scaled); + + const double dx = trials[idx + 1].x - trials[idx].x; + if (dx < eps) { + break; + } + + const double mid = 0.5 * (trials[idx + 1].x + trials[idx].x); + const double diff = (trials[idx + 1].z - trials[idx].z) / (2.0 * m_scaled); + const double x_new = mid - diff; + + InsertSorted1D(trials, Trial1D(x_new, func(x_new))); + } + + const size_t best = FindBestZ1D(trials); + best_x = trials[best].x; + return trials[best].z; +} + +} // namespace + +KruglovaA2DMuitSEQ::KruglovaA2DMuitSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; +} + +bool KruglovaA2DMuitSEQ::ValidationImpl() { + const auto &in = GetInput(); + return in.x_max > in.x_min && in.y_max > in.y_min && in.eps > 0.0 && in.max_iters > 0; +} + +bool KruglovaA2DMuitSEQ::PreProcessingImpl() { + GetOutput() = {0.0, 0.0, std::numeric_limits::max()}; + return true; +} + +bool KruglovaA2DMuitSEQ::RunImpl() { + const auto &in = GetInput(); + const double r_param = 2.0; + + auto compute_z = [&](double x_val, double &best_y) { + return Solve1DStrongin([&](double y) { return ObjectiveFunction(x_val, y); }, in.y_min, in.y_max, in.eps, + std::max(50, in.max_iters / 10), best_y); + }; + + std::vector x_trials; + const int init_points = 20; + + for (int i = 0; i < init_points; ++i) { + const double t = static_cast(i) / static_cast(init_points - 1); + const double x = in.x_min + ((in.x_max - in.x_min) * t); + + double y = 0.0; + double z = compute_z(x, y); + x_trials.emplace_back(x, y, z); + } + + for (size_t i = 0; i < x_trials.size(); ++i) { + size_t min_idx = i; + for (size_t j = i + 1; j < x_trials.size(); ++j) { + if (x_trials[j].x < x_trials[min_idx].x) { + min_idx = j; + } + } + std::swap(x_trials[i], x_trials[min_idx]); + } + + for (int iter = 0; iter < in.max_iters; ++iter) { + const double m_val = CalculateM(x_trials); + const double m_scaled = (m_val > 0.0) ? (r_param * m_val) : 1.0; + + const size_t idx = FindBestInterval2D(x_trials, m_scaled); + const double dx = x_trials[idx + 1].x - x_trials[idx].x; + + if (dx < in.eps) { + break; + } + + const double mid = 0.5 * (x_trials[idx + 1].x + x_trials[idx].x); + const double diff = (x_trials[idx + 1].z - x_trials[idx].z) / (2.0 * m_scaled); + const double x_new = mid - diff; + + double y_new = 0.0; + double z_new = compute_z(x_new, y_new); + + InsertSorted2D(x_trials, Trial2D(x_new, y_new, z_new)); + } + + const size_t best = FindBestZ2D(x_trials); + GetOutput() = {x_trials[best].x, x_trials[best].y, x_trials[best].z}; + return true; +} + +bool KruglovaA2DMuitSEQ::PostProcessingImpl() { + return true; +} + +} // namespace kruglova_a_2d_multistep_par_opt diff --git a/tasks/kruglova_a_2d_multistep_par_opt/settings.json b/tasks/kruglova_a_2d_multistep_par_opt/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/kruglova_a_2d_multistep_par_opt/tests/.clang-tidy b/tasks/kruglova_a_2d_multistep_par_opt/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/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/kruglova_a_2d_multistep_par_opt/tests/functional/main.cpp b/tasks/kruglova_a_2d_multistep_par_opt/tests/functional/main.cpp new file mode 100644 index 0000000000..54d7a10106 --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/tests/functional/main.cpp @@ -0,0 +1,73 @@ +#include + +#include +#include +#include +#include +#include + +#include "kruglova_a_2d_multistep_par_opt/common/include/common.hpp" +#include "kruglova_a_2d_multistep_par_opt/mpi/include/ops_mpi.hpp" +#include "kruglova_a_2d_multistep_par_opt/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace kruglova_a_2d_multistep_par_opt { + +class KruglovaA2DMultRunFunkTest : public ppc::util::BaseRunFuncTests { + public: + KruglovaA2DMultRunFunkTest() : input_data_(0.0, 0.0, 0.0, 0.0, 0.0, 0) {} + + static std::string PrintTestParam(const TestType &test_param) { + return std::get<0>(test_param); + } + + protected: + void SetUp() override { + auto params = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + input_data_ = std::get<1>(params); + } + + bool CheckTestOutputData(OutType &output_data) final { + return std::abs(output_data.f_value) < 0.5; + } + + InType GetTestInputData() final { + return input_data_; + } + + private: + InType input_data_; +}; + +namespace { + +TEST_P(KruglovaA2DMultRunFunkTest, GlobalOptimizationTask) { + ExecuteTest(GetParam()); +} + +const InType kTest1(-5.12, 5.12, -5.12, 5.12, 0.1, 30); +const InType kTest2(-2.0, 2.0, -2.0, 2.0, 0.05, 40); +const InType kTestHighPrecision(-1.0, 1.0, -1.0, 1.0, 0.01, 50); +const InType kTestNarrowRange(0.0, 0.005, 0.0, 0.005, 0.0001, 20); +const InType kTestLargeRange(-10.0, 10.0, -10.0, 10.0, 0.2, 30); +const InType kTestAsymmetric(-5.12, 2.0, -1.0, 5.12, 0.1, 30); + +const std::array kTestParam = { + std::make_tuple("Rastrigin_Standard", kTest1), std::make_tuple("Rastrigin_Narrow", kTest2), + std::make_tuple("High_Precision", kTestHighPrecision), std::make_tuple("Narrow_Range_Exit", kTestNarrowRange), + std::make_tuple("Large_Scale", kTestLargeRange), std::make_tuple("Asymmetric_Range", kTestAsymmetric)}; + +const auto kTestTasksList = std::tuple_cat( + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_kruglova_a_2d_multistep_par_opt), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_kruglova_a_2d_multistep_par_opt)); + +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); + +const auto kPerfTestName = KruglovaA2DMultRunFunkTest::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(RastriginTests, KruglovaA2DMultRunFunkTest, kGtestValues, kPerfTestName); + +} // namespace + +} // namespace kruglova_a_2d_multistep_par_opt diff --git a/tasks/kruglova_a_2d_multistep_par_opt/tests/performance/main.cpp b/tasks/kruglova_a_2d_multistep_par_opt/tests/performance/main.cpp new file mode 100644 index 0000000000..fbd5362c53 --- /dev/null +++ b/tasks/kruglova_a_2d_multistep_par_opt/tests/performance/main.cpp @@ -0,0 +1,44 @@ +#include + +#include + +#include "kruglova_a_2d_multistep_par_opt/common/include/common.hpp" +#include "kruglova_a_2d_multistep_par_opt/mpi/include/ops_mpi.hpp" +#include "kruglova_a_2d_multistep_par_opt/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace kruglova_a_2d_multistep_par_opt { + +class KruglovaA2DMultRunPerfTest : public ppc::util::BaseRunPerfTests { + public: + KruglovaA2DMultRunPerfTest() : input_data_(-5.12, 5.12, -5.12, 5.12, 1e-6, 2000) {} + + protected: + void SetUp() override {} + + bool CheckTestOutputData(OutType &output_data) final { + return std::abs(output_data.f_value) < 1e-1; + } + + InType GetTestInputData() final { + return input_data_; + } + + private: + InType input_data_; +}; + +TEST_P(KruglovaA2DMultRunPerfTest, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_kruglova_a_2d_multistep_par_opt); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = KruglovaA2DMultRunPerfTest::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, KruglovaA2DMultRunPerfTest, kGtestValues, kPerfTestName); + +} // namespace kruglova_a_2d_multistep_par_opt