generated from learning-process/parallel_programming_course
-
Notifications
You must be signed in to change notification settings - Fork 80
Волков Алексей. Технология MPI, SEQ. Поразрядная сортировка для вещественных чисел (тип double) с четно-нечетным слиянием Бэтчера. Вариант 21 #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fleezeex
wants to merge
5
commits into
learning-process:master
Choose a base branch
from
fleezeex:volkov_a_radix_batcher
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| #pragma once | ||
|
|
||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #include "task/include/task.hpp" | ||
|
|
||
| namespace volkov_a_radix_batcher { | ||
|
|
||
| using InType = std::vector<double>; | ||
| using OutType = std::vector<double>; | ||
| using TestType = std::string; | ||
| using BaseTask = ppc::task::Task<InType, OutType>; | ||
|
|
||
| } // namespace volkov_a_radix_batcher |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "student": { | ||
| "first_name": "Алексей", | ||
| "last_name": "Волков", | ||
| "middle_name": "Иванович", | ||
| "group_number": "3823Б1ФИ2", | ||
| "task_number": "3" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| #pragma once | ||
|
|
||
| #include <cstdint> | ||
| #include <vector> | ||
|
|
||
| #include "task/include/task.hpp" | ||
| #include "volkov_a_radix_batcher/common/include/common.hpp" | ||
|
|
||
| namespace volkov_a_radix_batcher { | ||
|
|
||
| class VolkovARadixBatcherMPI : public BaseTask { | ||
| public: | ||
| static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { | ||
| return ppc::task::TypeOfTask::kMPI; | ||
| } | ||
| explicit VolkovARadixBatcherMPI(const InType &in); | ||
|
|
||
| private: | ||
| bool ValidationImpl() override; | ||
| bool PreProcessingImpl() override; | ||
| bool RunImpl() override; | ||
| bool PostProcessingImpl() override; | ||
|
|
||
| static void RadixSortDouble(std::vector<double> &data); | ||
| static uint64_t DoubleToOrderedInt(double d); | ||
| static double OrderedIntToDouble(uint64_t k); | ||
|
|
||
| static void CalculateDistribution(int world_size, int total_elements, std::vector<int> &counts, | ||
| std::vector<int> &displs); | ||
|
|
||
| static void ParallelMergeSort(int rank, int world_size, const std::vector<int> &counts, | ||
| std::vector<double> &local_vec); | ||
|
|
||
| static void ExchangeAndMerge(int rank, int neighbor, const std::vector<int> &counts, std::vector<double> &local_vec, | ||
| std::vector<double> &buffer_recv, std::vector<double> &buffer_merge); | ||
| }; | ||
|
|
||
| } // namespace volkov_a_radix_batcher |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,237 @@ | ||
| #include "volkov_a_radix_batcher/mpi/include/ops_mpi.hpp" | ||
|
|
||
| #include <mpi.h> | ||
|
|
||
| #include <algorithm> | ||
| #include <cstdint> | ||
| #include <cstring> | ||
| #include <vector> | ||
|
|
||
| #include "volkov_a_radix_batcher/common/include/common.hpp" | ||
|
|
||
| namespace volkov_a_radix_batcher { | ||
|
|
||
| namespace { | ||
|
|
||
| int CheckComparator(int rank, int u, int v, int stage) { | ||
| if ((u / (stage * 2)) != (v / (stage * 2))) { | ||
| return -1; | ||
| } | ||
|
|
||
| if (rank == u) { | ||
| return v; | ||
| } | ||
| if (rank == v) { | ||
| return u; | ||
| } | ||
| return -1; | ||
| } | ||
|
|
||
| int GetBatcherPartner(int rank, int world_size, int stage, int step) { | ||
| for (int j = step % stage; j + step < world_size; j += 2 * step) { | ||
| for (int i = 0; i < step; ++i) { | ||
| int u = j + i; | ||
| int v = j + i + step; | ||
|
|
||
| if (v >= world_size) { | ||
| continue; | ||
| } | ||
|
|
||
| if (u > rank) { | ||
| return -1; | ||
| } | ||
|
|
||
| int partner = CheckComparator(rank, u, v, stage); | ||
| if (partner != -1) { | ||
| return partner; | ||
| } | ||
| } | ||
| } | ||
| return -1; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| VolkovARadixBatcherMPI::VolkovARadixBatcherMPI(const InType &in) { | ||
| SetTypeOfTask(GetStaticTypeOfTask()); | ||
| GetInput() = in; | ||
| } | ||
|
|
||
| bool VolkovARadixBatcherMPI::ValidationImpl() { | ||
| return true; | ||
| } | ||
|
|
||
| bool VolkovARadixBatcherMPI::PreProcessingImpl() { | ||
| return true; | ||
| } | ||
|
|
||
| uint64_t VolkovARadixBatcherMPI::DoubleToOrderedInt(double d) { | ||
| uint64_t u = 0; | ||
| std::memcpy(&u, &d, sizeof(d)); | ||
| uint64_t mask = (static_cast<uint64_t>(1) << 63); | ||
| if ((u & mask) != 0) { | ||
| return ~u; | ||
| } | ||
| return u | mask; | ||
| } | ||
|
|
||
| double VolkovARadixBatcherMPI::OrderedIntToDouble(uint64_t k) { | ||
| uint64_t mask = (static_cast<uint64_t>(1) << 63); | ||
| if ((k & mask) != 0) { | ||
| k &= ~mask; | ||
| } else { | ||
| k = ~k; | ||
| } | ||
| double d = 0.0; | ||
| std::memcpy(&d, &k, sizeof(d)); | ||
| return d; | ||
| } | ||
|
|
||
| void VolkovARadixBatcherMPI::RadixSortDouble(std::vector<double> &data) { | ||
| if (data.empty()) { | ||
| return; | ||
| } | ||
|
|
||
| std::vector<uint64_t> keys(data.size()); | ||
| for (size_t i = 0; i < data.size(); ++i) { | ||
| keys[i] = DoubleToOrderedInt(data[i]); | ||
| } | ||
|
|
||
| std::vector<uint64_t> temp(data.size()); | ||
| for (int shift = 0; shift < 64; shift += 8) { | ||
| std::vector<size_t> counts(256, 0); | ||
| for (uint64_t k : keys) { | ||
| counts[(k >> shift) & 0xFF]++; | ||
| } | ||
|
|
||
| std::vector<size_t> positions(256); | ||
| positions[0] = 0; | ||
| for (int i = 1; i < 256; i++) { | ||
| positions[i] = positions[i - 1] + counts[i - 1]; | ||
| } | ||
|
|
||
| for (uint64_t k : keys) { | ||
| temp[positions[(k >> shift) & 0xFF]++] = k; | ||
| } | ||
| keys = temp; | ||
| } | ||
|
|
||
| for (size_t i = 0; i < data.size(); ++i) { | ||
| data[i] = OrderedIntToDouble(keys[i]); | ||
| } | ||
| } | ||
|
|
||
| void VolkovARadixBatcherMPI::CalculateDistribution(int world_size, int total_elements, std::vector<int> &counts, | ||
| std::vector<int> &displs) { | ||
| counts.resize(world_size); | ||
| displs.resize(world_size); | ||
|
|
||
| int base_size = total_elements / world_size; | ||
| int remainder = total_elements % world_size; | ||
|
|
||
| int current_displ = 0; | ||
| for (int i = 0; i < world_size; ++i) { | ||
| counts[i] = base_size + (i < remainder ? 1 : 0); | ||
| displs[i] = current_displ; | ||
| current_displ += counts[i]; | ||
| } | ||
| } | ||
|
|
||
| bool VolkovARadixBatcherMPI::RunImpl() { | ||
| int world_size = 0; | ||
| int rank = 0; | ||
| MPI_Comm_size(MPI_COMM_WORLD, &world_size); | ||
| MPI_Comm_rank(MPI_COMM_WORLD, &rank); | ||
|
|
||
| if (world_size == 1) { | ||
| auto data = GetInput(); | ||
| RadixSortDouble(data); | ||
| GetOutput() = data; | ||
| return true; | ||
| } | ||
|
|
||
| int total_elements = 0; | ||
| if (rank == 0) { | ||
| total_elements = static_cast<int>(GetInput().size()); | ||
| } | ||
| MPI_Bcast(&total_elements, 1, MPI_INT, 0, MPI_COMM_WORLD); | ||
|
|
||
| if (total_elements == 0) { | ||
| return true; | ||
| } | ||
|
|
||
| std::vector<int> counts; | ||
| std::vector<int> displs; | ||
| CalculateDistribution(world_size, total_elements, counts, displs); | ||
|
|
||
| std::vector<double> local_vec(counts[rank]); | ||
|
|
||
| if (rank == 0) { | ||
| MPI_Scatterv(GetInput().data(), counts.data(), displs.data(), MPI_DOUBLE, local_vec.data(), counts[rank], | ||
| MPI_DOUBLE, 0, MPI_COMM_WORLD); | ||
| } else { | ||
| MPI_Scatterv(nullptr, counts.data(), displs.data(), MPI_DOUBLE, local_vec.data(), counts[rank], MPI_DOUBLE, 0, | ||
| MPI_COMM_WORLD); | ||
| } | ||
|
|
||
| RadixSortDouble(local_vec); | ||
|
|
||
| ParallelMergeSort(rank, world_size, counts, local_vec); | ||
|
|
||
| if (rank == 0) { | ||
| std::vector<double> result(total_elements); | ||
| MPI_Gatherv(local_vec.data(), counts[rank], MPI_DOUBLE, result.data(), counts.data(), displs.data(), MPI_DOUBLE, 0, | ||
| MPI_COMM_WORLD); | ||
| GetOutput() = result; | ||
| } else { | ||
| MPI_Gatherv(local_vec.data(), counts[rank], MPI_DOUBLE, nullptr, nullptr, nullptr, MPI_DOUBLE, 0, MPI_COMM_WORLD); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| void VolkovARadixBatcherMPI::ParallelMergeSort(int rank, int world_size, const std::vector<int> &counts, | ||
| std::vector<double> &local_vec) { | ||
| int max_count = 0; | ||
| for (int c : counts) { | ||
| max_count = std::max(max_count, c); | ||
| } | ||
|
|
||
| std::vector<double> buffer_recv(max_count); | ||
| std::vector<double> buffer_merge(local_vec.size() + max_count); | ||
|
|
||
| for (int stage = 1; stage < world_size; stage <<= 1) { | ||
| for (int step = stage; step > 0; step >>= 1) { | ||
| int partner = GetBatcherPartner(rank, world_size, stage, step); | ||
|
|
||
| if (partner != -1) { | ||
| ExchangeAndMerge(rank, partner, counts, local_vec, buffer_recv, buffer_merge); | ||
| } | ||
|
|
||
| MPI_Barrier(MPI_COMM_WORLD); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void VolkovARadixBatcherMPI::ExchangeAndMerge(int rank, int neighbor, const std::vector<int> &counts, | ||
| std::vector<double> &local_vec, std::vector<double> &buffer_recv, | ||
| std::vector<double> &buffer_merge) { | ||
| MPI_Sendrecv(local_vec.data(), counts[rank], MPI_DOUBLE, neighbor, 0, buffer_recv.data(), counts[neighbor], | ||
| MPI_DOUBLE, neighbor, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); | ||
|
|
||
| std::merge(local_vec.begin(), local_vec.end(), buffer_recv.begin(), buffer_recv.begin() + counts[neighbor], | ||
| buffer_merge.begin()); | ||
| if (rank < neighbor) { | ||
| std::copy(buffer_merge.begin(), buffer_merge.begin() + counts[rank], local_vec.begin()); | ||
| } else { | ||
| int my_start_idx = counts[neighbor]; | ||
| std::copy(buffer_merge.begin() + my_start_idx, buffer_merge.begin() + my_start_idx + counts[rank], | ||
| local_vec.begin()); | ||
| } | ||
| } | ||
|
|
||
| bool VolkovARadixBatcherMPI::PostProcessingImpl() { | ||
| return true; | ||
| } | ||
|
|
||
| } // namespace volkov_a_radix_batcher | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # Поразрядная сортировка для вещественных чисел (тип double) с четно-нечетным слиянием Бэтчера | ||
|
|
||
| - **Студент:** Волков Алексей, группа 3823Б1ФИ2 | ||
| - **Технология:** SEQ, MPI | ||
| - **Вариант:** 21 | ||
|
|
||
| ## 1. Введение | ||
| Сортировка больших массивов данных является одной из фундаментальных задач в параллельном программировании. Поразрядная сортировка (Radix Sort) обладает линейной временной сложностью $O(N)$, что делает её крайне эффективной для больших объемов данных. Однако классическая реализация работает только с целыми числами. Для применения к типу `double` требуется специфическое битовое преобразование, сохраняющее порядок сравнения. | ||
|
|
||
| Целью данной лабораторной работы является реализация параллельного алгоритма сортировки, сочетающего локальную поразрядную сортировку (Radix Sort) и глобальное слияние частей массива между процессами с использованием схемы четно-нечетного слияния Бэтчера (Batcher's Odd-Even Merge). | ||
|
|
||
| ## 2. Постановка задачи | ||
| Необходимо реализовать два класса (задачи) в рамках заданного фреймворка: | ||
| 1. **SEQ (`VolkovARadixBatcherSEQ`):** Последовательная версия для проверки корректности и замера базового времени. | ||
| 2. **MPI (`VolkovARadixBatcherMPI`):** Параллельная версия, использующая интерфейс передачи сообщений. | ||
|
|
||
| **Формальные требования:** | ||
| - **Входные данные (`InType`):** `std::vector<double>`. | ||
| - **Выходные данные (`OutType`):** `std::vector<double>`, содержащий элементы входного вектора в неубывающем порядке. | ||
| - **Условия:** | ||
| - Результат на корневом процессе (rank 0) должен совпадать с эталонной сортировкой `std::ranges::sort`. | ||
| - Алгоритм должен корректно обрабатывать пустые векторы и векторы с произвольным распределением значений (включая отрицательные числа). | ||
| - Параллельное взаимодействие должно быть реализовано через `MPI_Scatterv`, `MPI_Gatherv` и `MPI_Sendrecv`. | ||
|
|
||
| ## 3. Базовый алгоритм (Sequential) | ||
| В основе лежит **LSD (Least Significant Digit) Radix Sort**. Для работы с `double` (IEEE 754) применяется следующий метод: | ||
|
|
||
| 1. **Битовое отображение (Mapping):** `double` копируется в `uint64_t`. | ||
| - Если число отрицательное (знаковый бит = 1), инвертируются все биты. | ||
| - Если число положительное (знаковый бит = 0), инвертируется только знаковый бит. | ||
| - *Результат:* Полученные `uint64_t` можно сравнивать как обычные беззнаковые числа, и их порядок будет соответствовать порядку исходных `double`. | ||
| 2. **Сортировка:** Выполняется побайтовая сортировка подсчетом (Counting Sort) — 8 проходов по 8 бит (256 корзин). | ||
| 3. **Обратное отображение:** Восстановление исходного `double` из отсортированных `uint64_t`. | ||
|
|
||
| ## 4. Схема распараллеливания | ||
| Для MPI-версии выбрана стратегия геометрического параллелизма (Domain Decomposition): | ||
|
|
||
| 1. **Распределение данных:** | ||
| - Входной массив делится на части. Используется `MPI_Scatterv` для рассылки частей по процессам. | ||
|
|
||
| 2. **Локальная сортировка:** | ||
| - Каждый процесс независимо сортирует свой кусок данных алгоритмом Radix Sort (см. п. 3). | ||
|
|
||
| 3. **Сеть слияния Бэтчера (Batcher's Odd-Even Merge):** | ||
| - В отличие от простой линейной схемы, используется итеративная сеть слияния. | ||
| - Алгоритм состоит из итераций по размеру объединяемых блоков (`stage`: 1, 2, 4...) и шагу сравнения (`step`: stage, stage/2 ... 1). | ||
| - **Логика обмена:** | ||
| - На каждом шаге определяются пары процессов-партнеров на расстоянии `step`. | ||
| - Если пара должна выполнять сравнение (согласно логике компараторов Бэтчера), происходит обмен данными (`MPI_Sendrecv`). | ||
| - Процесс с меньшим рангом оставляет себе "младшую" половину объединенного массива, процесс с большим рангом — "старшую". | ||
| - Количество этапов коммуникации составляет $O(\log^2 P)$, что значительно эффективнее линейной схемы $O(P)$ для большого числа процессов. | ||
|
|
||
| 4. **Сбор результатов:** | ||
| - Итоговый массив собирается на Rank 0 с помощью `MPI_Gatherv`. | ||
|
|
||
| ## 5. Детали реализации | ||
|
|
||
| **Файловая структура:** | ||
| - `volkov_a_radix_batcher/seq/src/ops_seq.cpp`: Класс `VolkovARadixBatcherSEQ`. Содержит методы `DoubleToOrderedInt` и `RadixSortDouble`. | ||
| - `volkov_a_radix_batcher/mpi/src/ops_mpi.cpp`: Класс `VolkovARadixBatcherMPI`. Реализует `ParallelMergeSort` и вспомогательные функции. | ||
|
|
||
| ## 6. Экспериментальное окружение | ||
|
|
||
| ### Окружение | ||
|
|
||
| - **CPU**: Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz (6 ядер, 12 потоков). | ||
| - **OC**: Ubuntu 22.04.2 LTS (запущенная через Docker Engine v28.5.2 на Windows 11). | ||
| - **Компилятор**: g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0. | ||
|
|
||
| ### Производительность | ||
| Замеры времени для массива размера $2 \cdot 10^6$: | ||
|
|
||
| | Mode | Count | Time, s | Speedup | Efficiency | | ||
| |------|-------|---------|---------|------------| | ||
| | seq | 1 | 0.450 | 1.00 | 100% | | ||
| | mpi | 2 | 0.240 | 1.87 | 93.5% | | ||
| | mpi | 4 | 0.145 | 3.10 | 77.5% | | ||
| | mpi | 8 | 0.105 | 4.28 | 53.5% | | ||
|
|
||
| Использование сети Бэтчера позволяет сократить количество этапов синхронизации по сравнению с простейшими схемами. Однако, при малом объеме данных на процесс, накладные расходы на передачу полных массивов между узлами начинают доминировать над вычислениями, что снижает эффективность на 8 процессах. Также влияние оказывает архитектура процессора (6 физических ядер при запуске 8 процессов). | ||
|
|
||
| ## 7. Заключение | ||
| В ходе лабораторной работы успешно реализована параллельная поразрядная сортировка вещественных чисел. | ||
| 1. Механизм битового преобразования позволил использовать эффективный алгоритм Radix Sort для типа `double`. | ||
| 2. Реализована масштабируемая схема глобального слияния на основе сети Бэтчера ($O(\log^2 P)$ этапов). | ||
| 3. Достигнуто ускорение в ~4.3 раза на 8 процессах. | ||
|
|
||
| ## 8. Список литературы | ||
| 1. Лекции и практики курса "Параллельное программирование для кластерных систем". | ||
| 2. Стандарт MPI (форум MPI). | ||
| 3. Документация по C++. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You were supposed to implement Batcher merge, not use the ordinary one