Skip to content
Merged
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
25 changes: 25 additions & 0 deletions tasks/egashin_k_radix_simple_merge/all/include/ops_all.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include "egashin_k_radix_simple_merge/common/include/common.hpp"
#include "task/include/task.hpp"

namespace egashin_k_radix_simple_merge {

class EgashinKRadixSimpleMergeALL : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kALL;
}

explicit EgashinKRadixSimpleMergeALL(const InType &in);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;

OutType result_;
};

} // namespace egashin_k_radix_simple_merge
80 changes: 80 additions & 0 deletions tasks/egashin_k_radix_simple_merge/all/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Поразрядная сортировка double с простым слиянием - ALL

- Student: Егашин Кирилл Олегович, group 3823Б1ФИ2
- Technology: ALL
- Variant: 19

## 1. Введение

ALL-версия использует гибридную схему: MPI делит данные между рангами, а внутри каждого ранга блок
сортируется потоками OpenMP.

## 2. Постановка задачи

Нужно отсортировать общий входной `std::vector<double>` по возрастанию. После выполнения результат должен быть
одинаковым на всех рангах, чтобы общий тест мог проверить выход каждой задачи.

## 3. Базовый алгоритм

Внутри каждого локального блока применяется тот же radix sort по 64-битным ключам. После локальной сортировки
блоки собираются на ранге 0 и сливаются простым merge.

## 4. Межпроцессная схема

Каждый ранг получает свой диапазон индексов. В текущей тестовой инфраструктуре вход доступен каждому рангу,
поэтому ранг локально берет свой срез входного массива.

После локальной сортировки используется `MPI_Gatherv`, чтобы собрать отсортированные блоки на ранге 0. Ранг 0
выполняет простое слияние всех частей. Затем итоговый массив рассылается всем рангам через `MPI_Bcast`.

## 5. Внутрипроцессная схема

Внутри ранга локальный блок дополнительно делится на подблоки по `PPC_NUM_THREADS`. Подблоки сортируются
через `#pragma omp parallel for`, затем сливаются уровнями.

## 6. Детали реализации

Основные файлы:

- `common/include/radix_utils.hpp`
- `all/include/ops_all.hpp`
- `all/src/ops_all.cpp`

`ValidationImpl` проверяет, что размер входа помещается в `int`, потому что MPI counts и displacements
передаются как `int`.

## 7. Проверка корректности

ALL подключена к общим functional и performance tests. При обычном запуске без MPI functional runner
пропускает ALL-тесты. При запуске через `mpirun` результат проверяется на каждом ранге после `MPI_Bcast`.

## 8. Экспериментальная среда

Окружение:

- OS: Ubuntu 24.04
- Compiler: `g++-14`
- Build type: `Release`
- Processes: `PPC_NUM_PROC=2`
- Threads per process: `PPC_NUM_THREADS=2`
- Metric: `task_run`

Команды:

```bash
cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=Release
cmake --build build --parallel
PPC_NUM_THREADS=2 scripts/run_tests.py --running-type=processes --counts 1 2 4 --build-dir build
PPC_NUM_THREADS=2 PPC_NUM_PROC=2 scripts/run_tests.py --running-type=performance --build-dir build
```

## 9. Результаты

| Ранги | Потоки на ранг | Total workers | Time, s | Speedup vs seq | Efficiency |
| ----- | -------------- | ------------- | -------- | -------------- | ---------- |
| 2 | 2 | 4 | 0.030749 | 1.16 | 0.29 |

## 10. Вывод

Гибридная версия полезна для проверки процессного и потокового уровней параллелизма. Для малых размеров
ожидаются заметные накладные расходы на `MPI_Gatherv`, `MPI_Bcast` и финальное слияние на ранге 0.
149 changes: 149 additions & 0 deletions tasks/egashin_k_radix_simple_merge/all/src/ops_all.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#include "egashin_k_radix_simple_merge/all/include/ops_all.hpp"

#include <mpi.h>

#include <cstddef>
#include <limits>
#include <utility>
#include <vector>

#include "egashin_k_radix_simple_merge/common/include/common.hpp"
#include "egashin_k_radix_simple_merge/common/include/radix_utils.hpp"
#include "util/include/util.hpp"

namespace egashin_k_radix_simple_merge {

namespace {

std::vector<int> MakeCounts(size_t size, int rank_count) {
std::vector<int> counts(static_cast<size_t>(rank_count), 0);
const size_t base = size / static_cast<size_t>(rank_count);
const size_t extra = size % static_cast<size_t>(rank_count);

for (int rank = 0; rank < rank_count; ++rank) {
const auto rank_index = static_cast<size_t>(rank);
counts[rank_index] = static_cast<int>(base + (std::cmp_less(rank, extra) ? size_t{1} : size_t{0}));
}
return counts;
}

std::vector<int> MakeDispls(const std::vector<int> &counts) {
std::vector<int> displs(counts.size(), 0);
for (size_t i = 1; i < counts.size(); ++i) {
displs[i] = displs[i - 1] + counts[i - 1];
}
return displs;
}

void SortLocal(std::vector<double> &data) {
if (data.size() < 2) {
return;
}

int workers = radix_utils::WorkerCount(data.size(), ppc::util::GetNumThreads());
auto ranges = radix_utils::MakeRanges(data.size(), workers);

#pragma omp parallel for default(none) shared(data, ranges, workers) num_threads(workers) schedule(static)
for (int i = 0; i < workers; ++i) {
radix_utils::SortRange(data, ranges[static_cast<size_t>(i)].first, ranges[static_cast<size_t>(i)].second);
}

auto parts = radix_utils::MakeParts(data, ranges);
while (parts.size() > 1) {
size_t pair_count = parts.size() / 2;
std::vector<std::vector<double>> next((parts.size() + 1) / 2);

#pragma omp parallel for default(none) shared(parts, next, pair_count, workers) num_threads(workers) schedule(static)
for (size_t i = 0; i < pair_count; ++i) {
next[i] = radix_utils::Merge(parts[2 * i], parts[(2 * i) + 1]);
}

if (parts.size() % 2 != 0) {
next.back() = std::move(parts.back());
}
parts = std::move(next);
}

data = std::move(parts.front());
}

std::vector<double> MergeGathered(const std::vector<double> &data, const std::vector<int> &counts,
const std::vector<int> &displs) {
std::vector<std::vector<double>> parts(counts.size());
for (size_t rank = 0; rank < counts.size(); ++rank) {
const auto begin = data.begin() + displs[rank];
parts[rank] = std::vector<double>(begin, begin + counts[rank]);
}

while (parts.size() > 1) {
const size_t pair_count = parts.size() / 2;
std::vector<std::vector<double>> next((parts.size() + 1) / 2);

for (size_t i = 0; i < pair_count; ++i) {
next[i] = radix_utils::Merge(parts[2 * i], parts[(2 * i) + 1]);
}

if (parts.size() % 2 != 0) {
next.back() = std::move(parts.back());
}
parts = std::move(next);
}

return parts.empty() ? std::vector<double>{} : std::move(parts.front());
}

} // namespace

EgashinKRadixSimpleMergeALL::EgashinKRadixSimpleMergeALL(const InType &in) {
SetTypeOfTask(GetStaticTypeOfTask());
GetInput() = in;
GetOutput() = {};
}

bool EgashinKRadixSimpleMergeALL::ValidationImpl() {
return GetInput().size() <= static_cast<size_t>(std::numeric_limits<int>::max());
}

bool EgashinKRadixSimpleMergeALL::PreProcessingImpl() {
result_ = GetInput();
return true;
}

bool EgashinKRadixSimpleMergeALL::RunImpl() {
int rank = 0;
int rank_count = 1;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &rank_count);

const auto counts = MakeCounts(result_.size(), rank_count);
const auto displs = MakeDispls(counts);
const int local_count = counts[static_cast<size_t>(rank)];
const int local_shift = displs[static_cast<size_t>(rank)];

std::vector<double> local(result_.begin() + local_shift, result_.begin() + local_shift + local_count);
SortLocal(local);

std::vector<double> gathered;
if (rank == 0) {
gathered = result_;
}

MPI_Gatherv(local.data(), local_count, MPI_DOUBLE, gathered.data(), counts.data(), displs.data(), MPI_DOUBLE, 0,
MPI_COMM_WORLD);

if (rank == 0) {
result_ = MergeGathered(gathered, counts, displs);
} else {
result_.resize(GetInput().size());
}

MPI_Bcast(result_.data(), static_cast<int>(result_.size()), MPI_DOUBLE, 0, MPI_COMM_WORLD);
return true;
}

bool EgashinKRadixSimpleMergeALL::PostProcessingImpl() {
GetOutput() = result_;
return true;
}

} // namespace egashin_k_radix_simple_merge
25 changes: 25 additions & 0 deletions tasks/egashin_k_radix_simple_merge/omp/include/ops_omp.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include "egashin_k_radix_simple_merge/common/include/common.hpp"
#include "task/include/task.hpp"

namespace egashin_k_radix_simple_merge {

class EgashinKRadixSimpleMergeOMP : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kOMP;
}

explicit EgashinKRadixSimpleMergeOMP(const InType &in);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;

OutType result_;
};

} // namespace egashin_k_radix_simple_merge
78 changes: 78 additions & 0 deletions tasks/egashin_k_radix_simple_merge/omp/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Поразрядная сортировка double с простым слиянием - OMP

- Student: Егашин Кирилл Олегович, group 3823Б1ФИ2
- Technology: OMP
- Variant: 19

## 1. Введение

OMP-версия распараллеливает сортировку за счет разбиения входного массива на независимые блоки. Каждый блок
сортируется поразрядной сортировкой, затем блоки попарно сливаются.

## 2. Постановка задачи

Вход и выход совпадают с SEQ-версией: нужно получить отсортированный `std::vector<double>`. Корректность
проверяется сравнением с результатом `std::ranges::sort`.

## 3. Базовый алгоритм

Внутри каждого блока используется тот же radix sort, что и в последовательной версии: 8 проходов counting sort
по байтам 64-битного ключа.

## 4. Схема распараллеливания

Количество рабочих потоков берется из `PPC_NUM_THREADS`. Если элементов меньше, чем потоков, число потоков
уменьшается до размера массива.

Массив делится на почти равные диапазоны. Для сортировки блоков используется `#pragma omp parallel for` с
`schedule(static)`. Запись в `result_` безопасна, потому что каждый поток работает только со своим диапазоном.

После сортировки создаются отсортированные части. На каждом уровне merge пары частей сливаются параллельно.
Каждая пара записывает результат в отдельную ячейку `next`, поэтому `critical`, `atomic` и `reduction` не
нужны.

## 5. Детали реализации

Основные файлы:

- `common/include/radix_utils.hpp`
- `omp/include/ops_omp.hpp`
- `omp/src/ops_omp.cpp`

Общий helper содержит преобразование `double` в сортируемый ключ, counting pass, сортировку диапазона и
простое слияние двух отсортированных массивов.

## 6. Проверка корректности

Тесты подключают SEQ и OMP реализации к одному набору входов. Для OMP проверяются те же случаи, что и для SEQ:
пустой массив, один элемент, отрицательные числа, повторы и бесконечности.

## 7. Экспериментальная среда

Окружение:

- OS: Ubuntu 24.04
- Compiler: `g++-14`
- Build type: `Release`
- Threads: `PPC_NUM_THREADS=2`
- Metric: `task_run`

Команды:

```bash
cmake -S . -B build -G Ninja -D CMAKE_BUILD_TYPE=Release
cmake --build build --parallel
scripts/run_tests.py --running-type=threads --counts 1 2 4 --build-dir build
PPC_NUM_THREADS=2 PPC_NUM_PROC=2 scripts/run_tests.py --running-type=performance --build-dir build
```

## 8. Результаты

| Threads | Time, s | Speedup vs seq | Efficiency |
| ------- | -------- | -------------- | ---------- |
| 2 | 0.024104 | 1.48 | 0.74 |

## 9. Вывод

OMP-версия подходит для этой задачи, потому что сортировка блоков независима, а простое слияние можно
выполнять уровнями без общей записи в один результат.
Loading
Loading