Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tasks/volkov_a_radix_batcher/common/include/common.hpp
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
Binary file added tasks/volkov_a_radix_batcher/data/pic.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions tasks/volkov_a_radix_batcher/info.json
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"
}
}
38 changes: 38 additions & 0 deletions tasks/volkov_a_radix_batcher/mpi/include/ops_mpi.hpp
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
237 changes: 237 additions & 0 deletions tasks/volkov_a_radix_batcher/mpi/src/ops_mpi.cpp
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],
Copy link
Member

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

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
91 changes: 91 additions & 0 deletions tasks/volkov_a_radix_batcher/report.md
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++.
Loading
Loading