Skip to content

Commit 23cfe73

Browse files
authored
Еремин Василий. Технология SEQ-MPI. Алгоритм глобального поиска (Стронгина) для одномерных задач оптимизации. Распараллеливание по характеристикам. Вариант 11 (#282)
## Описание - **Задача**: Алгоритм глобального поиска (Стронгина) для одномерных задач оптимизации. Распараллеливание по характеристикам. - **Вариант**: 11 - **Технология**: SEQ, MPI - **Описание** вашей реализации и отчёта. **Реализация:** Реализован алгоритм Стронгина для поиска глобального минимума. Последовательная версия итерационно уточняет оценку константы Липшица и выбирает новые точки на основе характеристик интервалов. В параллельной версии на базе MPI вычисление наклонов и характеристик распределено между процессами по циклической схеме (stride-подход), что обеспечивает балансировку нагрузки при росте числа точек. **Ключевые MPI операции:** - **`MPI_Bcast`** — рассылка параметров поиска и координат каждой новой точки испытания. - **`MPI_Allreduce` (MAX)** — нахождение глобальной оценки константы Липшица $M$. - **`MPI_Allreduce` (MAXLOC)** — выбор интервала с максимальной характеристикой (возвращает значение и индекс процесса). **Отчёт:** Содержит описание модели Стронгина, схему распараллеливания характеристик, результаты верификации на тестовых функциях и анализ ускорения (Speedup/Efficiency) MPI-версии. --- ## Чек-лист <!-- Пожалуйста, убедитесь, что следующие пункты выполнены **до** отправки pull request'а и запроса его ревью: --> - [x] **Статус CI**: Все CI-задачи (сборка, тесты, генерация отчёта) успешно проходят на моей ветке в моем форке - [x] **Директория и именование задачи**: Я создал директорию с именем `<фамилия>_<первая_буква_имени>_<короткое_название_задачи>` - [x] **Полное описание задачи**: Я предоставил полное описание задачи в теле pull request - [x] **clang-format**: Мои изменения успешно проходят `clang-format` локально в моем форке (нет ошибок форматирования) - [x] **clang-tidy**: Мои изменения успешно проходят `clang-tidy` локально в моем форке (нет предупреждений/ошибок) - [x] **Функциональные тесты**: Все функциональные тесты успешно проходят локально на моей машине - [x] **Тесты производительности**: Все тесты производительности успешно проходят локально на моей машине - [x] **Ветка**: Я работаю в ветке, названной точно так же, как директория моей задачи (например, `nesterov_a_vector_sum`), а не в `master` - [x] **Правдивое содержание**: Я подтверждаю, что все сведения, указанные в этом pull request, являются точными и достоверными
1 parent 3809418 commit 23cfe73

11 files changed

Lines changed: 694 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#pragma once
2+
3+
#include <functional>
4+
#include <tuple>
5+
6+
#include "task/include/task.hpp"
7+
8+
namespace eremin_v_strongin_algorithm {
9+
10+
using InType = std::tuple<double, double, double, int, std::function<double(double)>>;
11+
using OutType = double;
12+
using TestType = std::tuple<int, double, double, double, int, std::function<double(double)>, double>;
13+
using BaseTask = ppc::task::Task<InType, OutType>;
14+
15+
} // namespace eremin_v_strongin_algorithm
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"student": {
3+
"first_name": "Василий",
4+
"last_name": "Еремин",
5+
"middle_name": "Евгеньевич",
6+
"group_number": "3823Б1ФИ2",
7+
"task_number": "3"
8+
}
9+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <vector>
4+
5+
#include "eremin_v_strongin_algorithm/common/include/common.hpp"
6+
#include "task/include/task.hpp"
7+
8+
namespace eremin_v_strongin_algorithm {
9+
10+
class EreminVStronginAlgorithmMPI : public BaseTask {
11+
public:
12+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
13+
return ppc::task::TypeOfTask::kMPI;
14+
}
15+
explicit EreminVStronginAlgorithmMPI(const InType &in);
16+
17+
private:
18+
bool ValidationImpl() override;
19+
bool PreProcessingImpl() override;
20+
bool RunImpl() override;
21+
bool PostProcessingImpl() override;
22+
23+
struct IntervalCharacteristic {
24+
double value;
25+
int index;
26+
};
27+
28+
static double CalculateLipschitzEstimate(int rank, int size, const std::vector<double> &search_points,
29+
const std::vector<double> &function_values);
30+
static IntervalCharacteristic FindBestInterval(int rank, int size, const std::vector<double> &search_points,
31+
const std::vector<double> &function_values, double m_parameter);
32+
};
33+
34+
} // namespace eremin_v_strongin_algorithm
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#include "eremin_v_strongin_algorithm/mpi/include/ops_mpi.hpp"
2+
3+
#include <mpi.h>
4+
5+
#include <algorithm>
6+
#include <cmath>
7+
#include <cstddef>
8+
#include <functional>
9+
#include <tuple>
10+
#include <vector>
11+
12+
#include "eremin_v_strongin_algorithm/common/include/common.hpp"
13+
14+
namespace eremin_v_strongin_algorithm {
15+
16+
EreminVStronginAlgorithmMPI::EreminVStronginAlgorithmMPI(const InType &in) {
17+
SetTypeOfTask(GetStaticTypeOfTask());
18+
GetInput() = in;
19+
GetOutput() = 0;
20+
}
21+
22+
bool EreminVStronginAlgorithmMPI::ValidationImpl() {
23+
auto &input = GetInput();
24+
25+
double lower_bound = std::get<0>(input);
26+
double upper_bound = std::get<1>(input);
27+
double epsilon = std::get<2>(input);
28+
int max_iters = std::get<3>(input);
29+
30+
return (lower_bound < upper_bound) && (epsilon > 0.0 && epsilon <= (upper_bound - lower_bound)) &&
31+
(max_iters > 0 && max_iters <= 100000000) && (lower_bound >= -1e9 && lower_bound <= 1e9) &&
32+
(upper_bound >= -1e9 && upper_bound <= 1e9) && (GetOutput() == 0);
33+
}
34+
35+
bool EreminVStronginAlgorithmMPI::PreProcessingImpl() {
36+
return true;
37+
}
38+
39+
double EreminVStronginAlgorithmMPI::CalculateLipschitzEstimate(int rank, int size,
40+
const std::vector<double> &search_points,
41+
const std::vector<double> &function_values) {
42+
double lipschitz_estimate = 0.0;
43+
for (std::size_t i = 1 + static_cast<std::size_t>(rank); i < search_points.size();
44+
i += static_cast<std::size_t>(size)) {
45+
double interval_width = search_points[i] - search_points[i - 1];
46+
double value_difference = std::abs(function_values[i] - function_values[i - 1]);
47+
double current_slope = value_difference / interval_width;
48+
lipschitz_estimate = std::max(current_slope, lipschitz_estimate);
49+
}
50+
51+
double global_lipschitz_estimate = 0.0;
52+
MPI_Allreduce(&lipschitz_estimate, &global_lipschitz_estimate, 1, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD);
53+
54+
return global_lipschitz_estimate;
55+
}
56+
57+
EreminVStronginAlgorithmMPI::IntervalCharacteristic EreminVStronginAlgorithmMPI::FindBestInterval(
58+
int rank, int size, const std::vector<double> &search_points, const std::vector<double> &function_values,
59+
double m_parameter) {
60+
IntervalCharacteristic local{.value = -1e18, .index = 1};
61+
62+
for (std::size_t i = 1 + static_cast<std::size_t>(rank); i < search_points.size();
63+
i += static_cast<std::size_t>(size)) {
64+
double interval_width = search_points[i] - search_points[i - 1];
65+
double value_difference = function_values[i] - function_values[i - 1];
66+
67+
double characteristic = (m_parameter * interval_width) +
68+
((value_difference * value_difference) / (m_parameter * interval_width)) -
69+
(2.0 * (function_values[i] + function_values[i - 1]));
70+
71+
if (characteristic > local.value) {
72+
local.value = characteristic;
73+
local.index = static_cast<int>(i);
74+
}
75+
}
76+
77+
IntervalCharacteristic global{};
78+
MPI_Allreduce(&local, &global, 1, MPI_DOUBLE_INT, MPI_MAXLOC, MPI_COMM_WORLD);
79+
return global;
80+
}
81+
82+
bool EreminVStronginAlgorithmMPI::RunImpl() {
83+
int rank = 0;
84+
int size = 0;
85+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
86+
MPI_Comm_size(MPI_COMM_WORLD, &size);
87+
88+
double lower_bound = 0.0;
89+
double upper_bound = 0.0;
90+
double epsilon = 0.0;
91+
int max_iterations = 0;
92+
93+
if (rank == 0) {
94+
auto &input = GetInput();
95+
lower_bound = std::get<0>(input);
96+
upper_bound = std::get<1>(input);
97+
epsilon = std::get<2>(input);
98+
max_iterations = std::get<3>(input);
99+
}
100+
101+
MPI_Bcast(&lower_bound, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
102+
MPI_Bcast(&upper_bound, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
103+
MPI_Bcast(&epsilon, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
104+
MPI_Bcast(&max_iterations, 1, MPI_INT, 0, MPI_COMM_WORLD);
105+
106+
std::function<double(double)> objective_function = std::get<4>(GetInput());
107+
;
108+
109+
std::vector<double> search_points = {lower_bound, upper_bound};
110+
std::vector<double> function_values = {objective_function(lower_bound), objective_function(upper_bound)};
111+
search_points.reserve(max_iterations + 2);
112+
function_values.reserve(max_iterations + 2);
113+
114+
int current_iteration = 0;
115+
double r_coefficient = 2.0;
116+
double max_interval_width = upper_bound - lower_bound;
117+
118+
while (max_interval_width > epsilon && current_iteration < max_iterations) {
119+
++current_iteration;
120+
121+
double lipschitz_estimate = CalculateLipschitzEstimate(rank, size, search_points, function_values);
122+
123+
double m_parameter = (lipschitz_estimate > 0.0) ? r_coefficient * lipschitz_estimate : 1.0;
124+
125+
auto best_interval = FindBestInterval(rank, size, search_points, function_values, m_parameter);
126+
int best_interval_index = best_interval.index;
127+
128+
double left_point = search_points[best_interval_index - 1];
129+
double right_point = search_points[best_interval_index];
130+
double left_value = function_values[best_interval_index - 1];
131+
double right_value = function_values[best_interval_index];
132+
133+
double new_point = 0.0;
134+
double new_value = 0.0;
135+
136+
if (rank == 0) {
137+
new_point = (0.5 * (left_point + right_point)) - ((right_value - left_value) / (2.0 * m_parameter));
138+
139+
if (new_point <= left_point || new_point >= right_point) {
140+
new_point = 0.5 * (left_point + right_point);
141+
}
142+
143+
new_value = objective_function(new_point);
144+
}
145+
MPI_Bcast(&new_point, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
146+
MPI_Bcast(&new_value, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
147+
148+
search_points.insert(search_points.begin() + static_cast<std::ptrdiff_t>(best_interval_index), new_point);
149+
function_values.insert(function_values.begin() + static_cast<std::ptrdiff_t>(best_interval_index), new_value);
150+
151+
max_interval_width = 0.0;
152+
for (std::size_t i = 1 + static_cast<std::size_t>(rank); i < search_points.size();
153+
i += static_cast<std::size_t>(size)) {
154+
double current_width = search_points[i] - search_points[i - 1];
155+
max_interval_width = std::max(current_width, max_interval_width);
156+
}
157+
double local_max_interval_width = max_interval_width;
158+
MPI_Allreduce(&local_max_interval_width, &max_interval_width, 1, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD);
159+
}
160+
GetOutput() = *std::ranges::min_element(function_values);
161+
return true;
162+
}
163+
164+
bool EreminVStronginAlgorithmMPI::PostProcessingImpl() {
165+
return true;
166+
}
167+
168+
} // namespace eremin_v_strongin_algorithm
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Алгоритм глобального поиска (Стронгина) для одномерных задач оптимизации. Распараллеливание по характеристикам.
2+
3+
- **Студент:** Еремин Василий Егвеньевич, группа 3823Б1ФИ2
4+
- **Технология:** SEQ | MPI
5+
- **Вариант:** №11
6+
7+
## 1. Введение
8+
9+
**Цель работы:** Реализация и сравнительный анализ последовательной и параллельной версий алгоритма Стронгина для поиска глобального минимума функции на заданном отрезке.
10+
11+
**Задачи:**
12+
1. Реализовать последовательную версию алгоритма Стронгина.
13+
2. Реализовать параллельную версию с использованием технологии MPI.
14+
3. Провести сравнительный анализ производительности и эффективности обеих реализаций.
15+
16+
## 2. Постановка задачи
17+
18+
**Задача**: Найти глобальный минимум непрерывной функции $f(x)$ на отрезке $[a,b]$ с заданной точностью ϵ или за ограниченное число итераций
19+
20+
**Описание метода решения:**
21+
Алгоритм Стронгина — это итерационный метод, который на каждом шаге использует информацию о всех предыдущих испытаниях для выбора следующей точки. Основная идея заключается в вычислении **характеристики** каждого интервала между соседними точками. Характеристика отражает вероятность нахождения глобального минимума внутри данного интервала.
22+
23+
24+
**Входные данные:**
25+
- `lower_bound` ($a$), `upper_bound` ($b$) — границы поиска.
26+
- `epsilon` ($\epsilon$) — требуемая точность.
27+
- `max_iterations` — ограничение на количество шагов.
28+
- `objective_function` ($f(x)$) — минимизируемая функция.
29+
30+
**Выходные данные:**
31+
- Минимальное найденное значение функции на отрезке.
32+
33+
**Ограничения:** - Границы отрезка: $[-10^9, 10^9]$
34+
- Максимальное число итераций: $100,000,000$
35+
36+
## 3. Описание алгоритма (последовательного)
37+
38+
**Алгоритм работы:**
39+
1. **Инициализация:** Вычисляются значения функции в граничных точках $a$ и $b$.
40+
2. **Оценка константы Липшица:** На основе всех проведенных испытаний вычисляется наклон:
41+
$$M = \max \frac{|f(x_i) - f(x_{i-1})|}{x_i - x_{i-1}}$$
42+
3. **Параметр плотности:** $m = r \cdot M$ (где $r > 1$ — параметр надежности, в реализации $r=2.0$).
43+
4. **Вычисление характеристик:** Для каждого интервала $(x_{i-1}, x_i)$ рассчитывается значение $R_i$.
44+
5. **Выбор нового испытания:** Выбирается интервал с максимальным $R_i$, и внутри него вычисляется новая точка $x_{new}$.
45+
6. **Критерий остановки:** Если длина максимального интервала становится меньше $\epsilon$ или достигнут лимит итераций.
46+
47+
48+
## 4. Схема распараллеливания (MPI)
49+
50+
Основная вычислительная нагрузка в алгоритме приходится на перебор всех интервалов для поиска константы Липшица и расчет характеристик.
51+
52+
**Алгоритм параллельного вычисления:**
53+
1. **Рассылка параметров:** Процесс 0 получает входные данные и рассылает их всем остальным через `MPI_Bcast`.
54+
2. **Распределенный поиск наклона:** Каждый процесс обходит свою часть интервалов (с шагом `size`), вычисляя локальный максимум $M_{local}$.
55+
3. **Глобальная синхронизация:** С помощью `MPI_Allreduce` находится общее значение $M$ для всех процессов.
56+
4. **Параллельный расчет характеристик:** Процессы вычисляют $R_i$ для "своих" интервалов и находят локальный максимум характеристики.
57+
5. **Выбор лучшего интервала:** Используется `MPI_Allreduce` с операцией `MPI_MAXLOC` для нахождения индекса интервала с глобально максимальной характеристикой.
58+
6. **Обновление данных:** Процесс 0 вычисляет новую точку и её значение, затем рассылает их всем (`MPI_Bcast`). Все процессы синхронно вставляют точку в свои локальные векторы.
59+
**Принцип разделения отрезков:**
60+
61+
```cpp
62+
63+
for (std::size_t i = 1 + rank; i < search_points.size(); i += size) {
64+
// Вычисления для конкретного интервала i
65+
}
66+
```
67+
68+
## 5. Детали реализации
69+
**Дополнительные функции в тестах:**
70+
- **MPI_MAXLOC:** Позволяет эффективно находить не только значение максимальной характеристики, но и ранг процесса/индекс интервала, которому она принадлежит.
71+
- **Динамические массивы:** Использованы `std::vector` для хранения точек испытаний.
72+
- **Устойчивость:** Добавлена проверка, чтобы новая точка $x_{new}$ не выходила за границы интервала из-за погрешностей типа `double`.
73+
74+
## 6. Экспериментальная среда
75+
- **Hardware/OS:** Apple M1 Pro, 6 ядер производительности и 2 эффективности, 16 ГБ, Ubuntu 24.04.2 (DevContainer)
76+
- **Toolchain:** GCC 13.3.0, C++20, CMake 3.28.3, build type Release
77+
- **Environment:** `PPC_NUM_PROC` = 8
78+
79+
## 7. Результаты и обсуждение
80+
81+
### 7.1 Проверка корректности
82+
83+
**Методы верификации корректности:**
84+
85+
1. **Сравнение с эталоном:**
86+
* Используются аналитически известные точки минимума для тестовых функций.
87+
* Проверка осуществляется через `expected_result_` в юнит-тестах.
88+
89+
2. **Автоматизированное тестирование:**
90+
* Реализованы тесты для 5 различных сценариев, включающих тригонометрические, полиномиальные и экспоненциальные функции.
91+
* Сравнение последовательной (SEQ) и параллельной (MPI) версий подтверждает идентичность логики.
92+
93+
3. **Допустимая погрешность:**
94+
```cpp
95+
double tolerance = 1e-2;
96+
return std::abs(output_data - expected_result_) <= tolerance;
97+
```
98+
* Установленная точность позволяет нивелировать разницу в точности вычислений `double` на разных архитектурах и учитывать дискретность шага алгоритма.
99+
**Тестовые случаи:**
100+
* **Интервалы:** от узких $[-5, 5]$ до широких $[-2, 100]$.
101+
* **Функции:**
102+
* $f(x) = x^2$ — классическая парабола.
103+
* $f(x) = \sin(x)$ — периодическая функция с множеством локальных экстремумов.
104+
* $f(x) = x^4 - 3x^2$ — функция с двумя симметричными минимумами.
105+
* $f(x) = e^x$ — монотонная функция (минимум на границе).
106+
107+
**Результат:** Все тесты проходят успешно, что подтверждает корректность обеих реализаций.
108+
109+
### 7.2 Производительность
110+
111+
- **Сложная тестовая функция:** $f(x) = 0.002 \cdot x^2 + 5 \cdot \sin(30 \cdot x) + \sin(200 * \sin(50 \cdot x)) + 0.1 \cdot \cos(300 \cdot x)$
112+
- **Лимит итераций:** `40 000`
113+
- **Точность (epsilon):** `0.0001`
114+
115+
**Полученные результаты:**
116+
117+
| **Режим** | **Количество процессов** | **Время, с** | **Speedup** | **Efficiency** |
118+
|-----------|--------------------------|--------------|-------------|----------------|
119+
| SEQ | 1 | 4.25 | 1.00 | N/A |
120+
| MPI | 2 | 2.83 | 1.57 | 79% |
121+
| MPI | 4 | 2.22 | 2.00 | 50% |
122+
| MPI | 6 | 2.81 | 1.58 | 26% |
123+
124+
125+
## 8. Заключение
126+
127+
В ходе выполнения работы были успешно реализованы последовательная и параллельная (MPI) версии алгоритма Стронгина для глобальной одномерной оптимизации.
128+
129+
Основные выводы:
130+
* Параллельная версия корректно находит глобальный минимум функции наравне с последовательной, что подтверждено набором автоматизированных тестов.
131+
* Реализованная схема распараллеливания по характеристикам интервалов демонстрирует стабильное ускорение на многоядерных системах.
132+
* Выявлено, что для алгоритмов с высокой частотой синхронизаций (как в случае с методом Стронгина) оптимальное число процессов ограничено балансом между вычислительной сложностью целевой функции и задержками обмена данными в MPI-среде.
133+
134+
Данная реализация может быть использована для ускорения поиска экстремумов в задачах, где вычисление целевой функции является ресурсоемкой операцией.
135+
136+
## 9. Источники
137+
1. [Презентация по курсу](https://learning-process.github.io/parallel_programming_slides/slides/01-intro.pdf)
138+
2. [Wikipedia](https://ru.wikipedia.org/wiki/Метод_Стронгина)

0 commit comments

Comments
 (0)