From 6a473b522039213e3930ab150ca056e6195a5b67 Mon Sep 17 00:00:00 2001 From: Sokolov235 Date: Sun, 19 Apr 2026 20:09:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=20=D0=BE=D1=82=D1=87=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../all/include/ops_all.hpp | 31 ++ .../all/src/ops_all.cpp | 195 +++++++ tasks/sokolov_k_matrix_double_fox/report.md | 501 ++++++++++++++++++ .../tests/functional/main.cpp | 2 + .../tests/performance/main.cpp | 8 +- 5 files changed, 734 insertions(+), 3 deletions(-) create mode 100644 tasks/sokolov_k_matrix_double_fox/all/include/ops_all.hpp create mode 100644 tasks/sokolov_k_matrix_double_fox/all/src/ops_all.cpp create mode 100644 tasks/sokolov_k_matrix_double_fox/report.md diff --git a/tasks/sokolov_k_matrix_double_fox/all/include/ops_all.hpp b/tasks/sokolov_k_matrix_double_fox/all/include/ops_all.hpp new file mode 100644 index 000000000..0e41e9f96 --- /dev/null +++ b/tasks/sokolov_k_matrix_double_fox/all/include/ops_all.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "sokolov_k_matrix_double_fox/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace sokolov_k_matrix_double_fox { + +class SokolovKMatrixDoubleFoxALL : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kALL; + } + explicit SokolovKMatrixDoubleFoxALL(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + int n_{}; + int block_size_{}; + int q_{}; + std::vector blocks_a_; + std::vector blocks_b_; + std::vector blocks_c_; +}; + +} // namespace sokolov_k_matrix_double_fox diff --git a/tasks/sokolov_k_matrix_double_fox/all/src/ops_all.cpp b/tasks/sokolov_k_matrix_double_fox/all/src/ops_all.cpp new file mode 100644 index 000000000..34f3acf77 --- /dev/null +++ b/tasks/sokolov_k_matrix_double_fox/all/src/ops_all.cpp @@ -0,0 +1,195 @@ +#include "sokolov_k_matrix_double_fox/all/include/ops_all.hpp" + +#include + +#include +#include +#include +#include + +#include "sokolov_k_matrix_double_fox/common/include/common.hpp" + +namespace sokolov_k_matrix_double_fox { + +namespace { + +void DecomposeToBlocksAll(const std::vector &flat, std::vector &blocks, int n, int bs, int q) { + for (int bi = 0; bi < q; bi++) { + for (int bj = 0; bj < q; bj++) { + int block_off = ((bi * q) + bj) * (bs * bs); + for (int i = 0; i < bs; i++) { + for (int j = 0; j < bs; j++) { + blocks[block_off + (i * bs) + j] = flat[(((bi * bs) + i) * n) + ((bj * bs) + j)]; + } + } + } + } +} + +void AssembleFromBlocksAll(const std::vector &blocks, std::vector &flat, int n, int bs, int q) { + for (int bi = 0; bi < q; bi++) { + for (int bj = 0; bj < q; bj++) { + int block_off = ((bi * q) + bj) * (bs * bs); + for (int i = 0; i < bs; i++) { + for (int j = 0; j < bs; j++) { + flat[(((bi * bs) + i) * n) + ((bj * bs) + j)] = blocks[block_off + (i * bs) + j]; + } + } + } + } +} + +void MultiplyBlocksAll(const double *a, const double *b, double *c, int bs) { + for (int i = 0; i < bs; i++) { + for (int k = 0; k < bs; k++) { + double val = a[(i * bs) + k]; + for (int j = 0; j < bs; j++) { + c[(i * bs) + j] += val * b[(k * bs) + j]; + } + } + } +} + +void FoxStepMpiOmp(const std::vector &a, const std::vector &b, std::vector &c, int bs, int q, + int step, int row_begin, int row_end) { + int bsq = bs * bs; +#pragma omp parallel for default(none) shared(a, b, c, bs, q, bsq, step, row_begin, row_end) schedule(static) + for (int i = row_begin; i < row_end; i++) { + int k = (i + step) % q; + for (int j = 0; j < q; j++) { + int a_off = ((i * q) + k) * bsq; + int b_off = ((k * q) + j) * bsq; + int c_off = ((i * q) + j) * bsq; + MultiplyBlocksAll(a.data() + a_off, b.data() + b_off, c.data() + c_off, bs); + } + } +} + +int ChooseBlockSizeAll(int n) { + for (int div = static_cast(std::sqrt(static_cast(n))); div >= 1; div--) { + if (n % div == 0) { + return div; + } + } + return 1; +} + +void ComputeRowRange(int rank, int num_procs, int rows_per, int leftover, int &row_start, int &row_count) { + if (rank < num_procs) { + row_start = (rank * rows_per) + std::min(rank, leftover); + row_count = rows_per + (rank < leftover ? 1 : 0); + } else { + row_start = 0; + row_count = 0; + } +} + +void GatherResults(std::vector &blocks_c, int rank, int num_procs, int rows_per, int leftover, int q, int bsq) { + if (rank == 0) { + for (int pr = 1; pr < num_procs; pr++) { + int pr_start = 0; + int pr_count = 0; + ComputeRowRange(pr, num_procs, rows_per, leftover, pr_start, pr_count); + if (pr_count > 0) { + int offset = pr_start * q * bsq; + int count = pr_count * q * bsq; + MPI_Recv(blocks_c.data() + offset, count, MPI_DOUBLE, pr, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); + } + } + } else if (rank < num_procs) { + int my_start = 0; + int my_count = 0; + ComputeRowRange(rank, num_procs, rows_per, leftover, my_start, my_count); + if (my_count > 0) { + int offset = my_start * q * bsq; + int count = my_count * q * bsq; + MPI_Send(blocks_c.data() + offset, count, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); + } + } +} + +} // namespace + +SokolovKMatrixDoubleFoxALL::SokolovKMatrixDoubleFoxALL(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0; +} + +bool SokolovKMatrixDoubleFoxALL::ValidationImpl() { + return (GetInput() > 0) && (GetOutput() == 0); +} + +bool SokolovKMatrixDoubleFoxALL::PreProcessingImpl() { + GetOutput() = 0; + n_ = GetInput(); + block_size_ = ChooseBlockSizeAll(n_); + q_ = n_ / block_size_; + auto sz = static_cast(n_) * n_; + std::vector a(sz, 1.5); + std::vector b(sz, 2.0); + blocks_a_.resize(sz); + blocks_b_.resize(sz); + blocks_c_.assign(sz, 0.0); + DecomposeToBlocksAll(a, blocks_a_, n_, block_size_, q_); + DecomposeToBlocksAll(b, blocks_b_, n_, block_size_, q_); + return true; +} + +bool SokolovKMatrixDoubleFoxALL::RunImpl() { + std::ranges::fill(blocks_c_, 0.0); + + int rank = 0; + int world_size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + + int total = static_cast(blocks_a_.size()); + MPI_Bcast(&n_, 1, MPI_INT, 0, MPI_COMM_WORLD); + MPI_Bcast(&block_size_, 1, MPI_INT, 0, MPI_COMM_WORLD); + MPI_Bcast(&q_, 1, MPI_INT, 0, MPI_COMM_WORLD); + MPI_Bcast(&total, 1, MPI_INT, 0, MPI_COMM_WORLD); + + if (rank != 0) { + blocks_a_.resize(total); + blocks_b_.resize(total); + blocks_c_.assign(total, 0.0); + } + + MPI_Bcast(blocks_a_.data(), total, MPI_DOUBLE, 0, MPI_COMM_WORLD); + MPI_Bcast(blocks_b_.data(), total, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + int num_procs = std::min(world_size, q_); + int rows_per = q_ / std::max(num_procs, 1); + int leftover = q_ % std::max(num_procs, 1); + + int my_row_start = 0; + int my_row_count = 0; + ComputeRowRange(rank, num_procs, rows_per, leftover, my_row_start, my_row_count); + + for (int step = 0; step < q_; step++) { + FoxStepMpiOmp(blocks_a_, blocks_b_, blocks_c_, block_size_, q_, step, my_row_start, my_row_start + my_row_count); + } + + int bsq = block_size_ * block_size_; + GatherResults(blocks_c_, rank, num_procs, rows_per, leftover, q_, bsq); + + MPI_Bcast(blocks_c_.data(), total, MPI_DOUBLE, 0, MPI_COMM_WORLD); + + MPI_Barrier(MPI_COMM_WORLD); + return true; +} + +bool SokolovKMatrixDoubleFoxALL::PostProcessingImpl() { + std::vector result(static_cast(n_) * n_); + AssembleFromBlocksAll(blocks_c_, result, n_, block_size_, q_); + double expected = 3.0 * n_; + bool ok = std::ranges::all_of(result, [expected](double v) { return std::abs(v - expected) <= 1e-9; }); + GetOutput() = ok ? GetInput() : -1; + std::vector().swap(blocks_a_); + std::vector().swap(blocks_b_); + std::vector().swap(blocks_c_); + return true; +} + +} // namespace sokolov_k_matrix_double_fox diff --git a/tasks/sokolov_k_matrix_double_fox/report.md b/tasks/sokolov_k_matrix_double_fox/report.md new file mode 100644 index 000000000..b7954ae21 --- /dev/null +++ b/tasks/sokolov_k_matrix_double_fox/report.md @@ -0,0 +1,501 @@ +# Соколов Кирилл. Технология STL. Умножение плотных матриц. Элементы типа double. Блочная схема, алгоритм Фокса + +- **Студент:** Соколов Кирилл Денисович +- **Группа:** 3823Б1ПР4 +- **Технология:** ALL (SEQ + OMP + TBB + STL + MPI+OMP) +- **Вариант:** 2 + +--- + +## 1. Введение + +Алгоритм Фокса (Fox's algorithm) - это блочный метод умножения матриц, изначально разработанный для систем +с распределенной памятью. Он отличается хорошей локальностью обращений к памяти и равномерной загрузкой +вычислительных узлов. Цель данной работы: реализовать и исследовать производительность алгоритма Фокса +во всех поддерживаемых технологиях параллелизма: последовательной, OpenMP, Intel TBB, стандартной библиотеки +потоков C++ и гибридной MPI+OpenMP. Проводятся замеры ускорения и эффективности распараллеливания. + +--- + +## 2. Постановка задачи + +**Вход:** целое число n > 0 - размер квадратных матриц A и B (n x n, тип double). + +**Ограничения:** n > 0; обе матрицы заполняются фиксированными константами (A\[i\]\[j\] = 1.5, +B\[i\]\[j\] = 2.0), что позволяет аналитически проверить результат. + +**Выход:** целое число - n при успешной проверке результата, иначе -1. + +**Проверка результата:** каждый элемент матрицы C = A *B должен равняться 3.0* n с точностью 1e-9. + +Размер матрицы для тестов производительности - 400 x 400. + +--- + +## 3. Описание базового алгоритма + +### 3.1. Выбор размера блока + +Перед разбиением матриц на блоки выбирается наибольший делитель числа n, не превосходящий sqrt(n). +Это обеспечивает квадратные блоки одинакового размера, покрывающие всю матрицу без остатка. + +```text +ChooseBlockSize(n): + div = floor(sqrt(n)) + while div >= 1: + if n % div == 0: + return div + div -= 1 + return 1 +``` + +Количество блоков по каждому измерению: q = n / block_size. + +### 3.2. Декомпозиция в блочное представление + +Исходная строчно-упорядоченная матрица переводится в блочно-упорядоченный массив: + +```text +DecomposeToBlocks(flat, blocks, n, bs, q): + for bi in [0, q): + for bj in [0, q): + block_off = (bi * q + bj) * bs * bs + for i in [0, bs): + for j in [0, bs): + blocks[block_off + i*bs + j] = flat[(bi*bs + i)*n + bj*bs + j] +``` + +### 3.3. Шаг Фокса + +На каждом шаге step (от 0 до q-1) для каждой блочной строки i: + +- бродкастный блок A: A\[i\]\[(i + step) % q\] +- локальный блок B: B\[(i + step) % q\]\[j\] +- результат: C\[i\]\[j\] += A_broadcast * B_local + +```text +FoxStep(A, B, C, bs, q, step): + for i in [0, q): + k = (i + step) % q + for j in [0, q): + MultiplyBlocks(A[i][k], B[k][j], C[i][j], bs) +``` + +### 3.4. Умножение блоков + +Умножение двух блоков размера bs x bs выполняется по схеме ikj для улучшения cache-локальности: + +```text +MultiplyBlocks(A_block, B_block, C_block, bs): + for i in [0, bs): + for k in [0, bs): + val = A_block[i][k] + for j in [0, bs): + C_block[i][j] += val * B_block[k][j] +``` + +### 3.5. Сборка результата + +После всех шагов блочный массив переводится обратно в строчно-упорядоченную матрицу: + +```text +AssembleFromBlocks(blocks, flat, n, bs, q): + for bi in [0, q): + for bj in [0, q): + block_off = (bi * q + bj) * bs * bs + for i in [0, bs): + for j in [0, bs): + flat[(bi*bs + i)*n + bj*bs + j] = blocks[block_off + i*bs + j] +``` + +**Сложность:** O(n^3) по числу операций умножения-сложения, O(n^2) по памяти. + +--- + +## 4. Схема распараллеливания + +Ключевое место для распараллеливания - внешний цикл по блочным строкам внутри функции FoxStep. Блочные строки +независимы между собой на каждом шаге, поэтому их можно распределять по потокам или процессам без +необходимости синхронизации внутри шага. + +### 4.1. OMP + +Параллелится цикл `for i in [0, q)` в функции FoxStepOmp с помощью директивы +`#pragma omp parallel for ... schedule(static)`. Дополнительно параллелятся фазы декомпозиции и сборки. +Синхронизация между шагами Фокса неявная: каждый шаг завершается до начала следующего. + +```text +#pragma omp parallel for default(none) shared(a, b, c, bs, q, step, bsq) schedule(static) +for i in [0, q): + k = (i + step) % q + for j in [0, q): + MultiplyBlocks(A[i][k], B[k][j], C[i][j], bs) +``` + +### 4.2. TBB + +Аналогично OMP, но с использованием `tbb::parallel_for(0, q, lambda)`. Планировщик TBB динамически +управляет пулом потоков и балансировкой нагрузки. + +```text +tbb::parallel_for(0, q, [&](int i) { + k = (i + step) % q + for j in [0, q): + MultiplyBlocks(A[i][k], B[k][j], C[i][j], bs) +}); +``` + +### 4.3. STL + +Блочные строки статически делятся между `std::thread`-потоками: поток idx обрабатывает строки +[row_begin, row_end). Каждый поток независимо выполняет все q шагов Фокса для своего диапазона строк. +Синхронизация - `join()` после завершения всех потоков. + +```text +num_threads = min(PPC_NUM_THREADS, q) +rows_per = q / num_threads +extra = q % num_threads +// для каждого потока idx: + row_begin = ... + row_end = row_begin + rows_per + (idx < extra ? 1 : 0) + FoxWorker(A, B, C, bs, q, row_begin, row_end) +``` + +Отличие от OMP/TBB: поток обходит все шаги step внутри своей функции FoxWorker, что устраняет +барьер синхронизации между шагами и сохраняет результат корректным благодаря независимости строк. + +### 4.4. ALL (MPI + OMP) + +Гибридная стратегия: данные распределяются по MPI-процессам, а внутри каждого процесса +параллелизуется шаг Фокса с OpenMP. + +**Распределение данных:** + +- Ранг 0 подготавливает матрицы A и B. +- `MPI_Bcast` рассылает параметры (n, block_size, q, total) и полные блочные массивы A и B всем процессам. +- Каждый процесс вычисляет свой диапазон блочных строк по формуле: + `row_start = rank * rows_per + min(rank, leftover)`. + +**Схема обмена и топология:** + +- Топология: flat (MPI_COMM_WORLD), без виртуальных топологий. +- Шаги Фокса: каждый процесс независимо вычисляет свой диапазон строк для каждого шага с локальным + OpenMP-параллелизмом. +- Сборка результатов: ненулевые ранги отправляют свои блоки в ранг 0 через `MPI_Send`; ранг 0 + принимает через `MPI_Recv` (`GatherResults`). +- Финальный бродкаст: ранг 0 рассылает собранный результат C всем процессам через `MPI_Bcast`. + +```text +MPI_Bcast(n, block_size, q, total) +MPI_Bcast(A, B) +ComputeRowRange(rank) -> [my_row_start, my_row_start + my_row_count) +for step in [0, q): + FoxStepMpiOmp(A, B, C, bs, q, step, my_row_start, my_row_start + my_row_count) +GatherResults(C, rank) // MPI_Send/MPI_Recv +MPI_Bcast(C) +MPI_Barrier() +``` + +--- + +## 5. Детали реализации + +### 5.1. Структура файлов + +```text +sokolov_k_matrix_double_fox/ + common/include/common.hpp -- общие типы InType, OutType, TestType + seq/include/ops_seq.hpp -- класс SokolovKMatrixDoubleFoxSEQ + seq/src/ops_seq.cpp -- последовательная реализация + omp/include/ops_omp.hpp -- класс SokolovKMatrixDoubleFoxOMP + omp/src/ops_omp.cpp -- реализация на OpenMP + tbb/include/ops_tbb.hpp -- класс SokolovKMatrixDoubleFoxTBB + tbb/src/ops_tbb.cpp -- реализация на Intel TBB + stl/include/ops_stl.hpp -- класс SokolovKMatrixDoubleFoxSTL + stl/src/ops_stl.cpp -- реализация на std::thread + all/include/ops_all.hpp -- класс SokolovKMatrixDoubleFoxALL + all/src/ops_all.cpp -- гибридная реализация MPI + OpenMP + tests/functional/main.cpp -- функциональные тесты (GTest) + tests/performance/main.cpp -- тесты производительности (GTest Perf) +``` + +### 5.2. Ключевые архитектурные решения + +- **Единый блочный макет памяти:** все варианты используют строчно-упорядоченный блочный массив + `blocks_[a|b|c]` размера n*n, что обеспечивает cache-friendly доступ внутри блока. +- **Выбор размера блока:** ChooseBlockSize ищет наибольший делитель n, не превышающий sqrt(n), тем + самым минимизируя q и уменьшая накладные расходы на синхронизацию. +- **Ограничение числа потоков STL:** `num_threads = min(PPC_NUM_THREADS, q)` -- число потоков не + превышает число блочных строк, что исключает создание "пустых" потоков. +- **MPI: полный бродкаст вместо scatter:** размер матрицы 400x400 невелик, а + MPI_Bcast упрощает код и исключает необходимость allgather. +- **Проверка корректности в PostProcessing:** при A=1.5, B=2.0 ожидаемый результат: + C\[i\]\[j\] = sum_k(1.5 *2.0) = 3.0* n. + +### 5.3. Угловые случаи + +- n = 1: q = 1, block_size = 1 - один блок 1x1, работает корректно. +- n - простое число: `ChooseBlockSize` вернёт 1, q = n - блоки размером 1x1. +- Число процессов MPI > q: лишние процессы получают my_row_count = 0 и не участвуют в вычислениях. + +--- + +## 6. Условия экспериментов + +- **CPU:** Intel Core i5-10400kf +- **Ядра/Потоки:** 6/12 +- **ОС:** Windows 10 +- **Компилятор:** MSVC 14.44 +- **Тип сборки:** Release +- **Размер задачи:** матрица 400 x 400, тип double; block_size = 20, q = 20 +- **Тестирование потоков (OMP, TBB, STL):** переменная окружения `PPC_NUM_THREADS` = 1, 3, 5, 7 +- **Тестирование MPI:** `mpiexec -n k`, где k = 1, 3, 5, 7 + +--- + +## 7. Результаты и обсуждение + +### 7.1. Проверка корректности + +Корректность реализаций проверяется двумя способами: + +1. **Аналитическая проверка в PostProcessingImpl:** при матрицах A (все элементы = 1.5) и B (все + элементы = 2.0) каждый элемент результата C должен равняться `3.0 * n` с абсолютной погрешностью + не более 1e-9. Если хотя бы один элемент нарушает условие, метод возвращает -1. + +2. **Функциональные тесты (GTest):** 12 тест-кейсов для матриц размером 1x1, 2x2, ..., 10x10, 50x50, + 100x100 запускаются для всех пяти реализаций (SEQ, OMP, TBB, STL, ALL). Тест считается пройденным, + когда выходное значение совпадает со входным (n). + +Все 12 тест-кейсов успешно пройдены для каждой из пяти реализаций на CI + +### 7.2. Производительность + +Замеры приведены для матрицы 400 x 400. Ускорение S = T_seq / T_para, эффективность E = S / k * 100%. + +#### OMP - task_run + +Базовое время SEQ (k=1): 0.2356с. + +| Режим | Потоки (k) | Время, с | Ускорение | Эффективность | +| ----- | ---------- | -------- | --------- | ------------- | +| seq | 1 | 0.2356 | 1.00 | N/A | +| omp | 1 | 0.0420 | 5.61 | -- (*) | +| omp | 3 | 0.0430 | 5.48 | 182.5% | +| omp | 5 | 0.0447 | 5.27 | 105.4% | +| omp | 7 | 0.0420 | 5.61 | 80.1% | + +(*) При k=1 OMP уже использует все доступные системные ядра: нормирование эффективности на 1 поток +не применимо. + +#### OMP - task_pipeline + +Базовое время SEQ (k=1): 0.2516с. + +| Режим | Потоки (k) | Время, с | Ускорение | Эффективность | +| ----- | ---------- | -------- | --------- | ------------- | +| seq | 1 | 0.2516 | 1.00 | N/A | +| omp | 1 | 0.0480 | 5.24 | -- (*) | +| omp | 3 | 0.0523 | 4.81 | 160.4% | +| omp | 5 | 0.0468 | 5.37 | 107.5% | +| omp | 7 | 0.0468 | 5.37 | 76.8% | + +#### TBB - task_run + +Базовое время SEQ (k=1): 0.2356с. +TBB реагирует на PPC_NUM_THREADS: при k=1 работает однопоточно. + +| Режим | Потоки (k) | Время, с | Ускорение | Эффективность | +| ----- | ---------- | -------- | --------- | ------------- | +| seq | 1 | 0.2356 | 1.00 | N/A | +| tbb | 1 | 0.2365 | 1.00 | 99.6% | +| tbb | 3 | 0.0851 | 2.77 | 92.3% | +| tbb | 5 | 0.0490 | 4.81 | 96.1% | +| tbb | 7 | 0.0437 | 5.39 | 77.0% | + +#### TBB - task_pipeline + +Базовое время SEQ (k=1): 0.2516с. + +| Режим | Потоки (k) | Время, с | Ускорение | Эффективность | +| ----- | ---------- | -------- | --------- | ------------- | +| seq | 1 | 0.2516 | 1.00 | N/A | +| tbb | 1 | 0.2422 | 1.04 | ~100% | +| tbb | 3 | 0.0889 | 2.83 | 94.3% | +| tbb | 5 | 0.0540 | 4.66 | 93.2% | +| tbb | 7 | 0.0459 | 5.48 | 78.3% | + +#### STL - task_run + +Базовое время SEQ (k=1): 0.2356с. +STL аналогично TBB реагирует на PPC_NUM_THREADS: при k=1 работает однопоточно. + +| Режим | Потоки (k) | Время, с | Ускорение | Эффективность | +| ----- | ---------- | -------- | --------- | ------------- | +| seq | 1 | 0.2356 | 1.00 | N/A | +| stl | 1 | 0.2355 | 1.00 | 100.0% | +| stl | 3 | 0.0850 | 2.77 | 92.4% | +| stl | 5 | 0.0551 | 4.28 | 85.5% | +| stl | 7 | 0.0426 | 5.53 | 79.0% | + +#### STL - task_pipeline + +Базовое время SEQ (k=1): 0.2516с. + +| Режим | Потоки (k) | Время, с | Ускорение | Эффективность | +| ----- | ---------- | -------- | --------- | ------------- | +| seq | 1 | 0.2516 | 1.00 | N/A | +| stl | 1 | 0.2408 | 1.04 | ~100% | +| stl | 3 | 0.0910 | 2.77 | 92.2% | +| stl | 5 | 0.0566 | 4.45 | 88.9% | +| stl | 7 | 0.0477 | 5.27 | 75.3% | + +#### ALL - task_run + +Базовое время SEQ при mpiexec -n 1: 0.2344с. +При n=1 MPI-процессе весь внутренний OMP-параллелизм доступен без коммуникационных накладных расходов, +поэтому уже при n=1 достигается высокое ускорение. При увеличении n MPI-процессы конкурируют за ядра +одной машины, что ведет к уменьшению производительности после n=1. + +| Режим | Процессы (n) | Время, с | Ускорение | Эффективность | +| ----- | ------------ | -------- | --------- | ------------- | +| seq | 1 | 0.2344 | 1.00 | N/A | +| all | 1 | 0.0140 | 16.74 | -- (**) | +| all | 3 | 0.0151 | 15.52 | 517.4% | +| all | 5 | 0.0280 | 8.37 | 167.4% | +| all | 7 | 0.0507 | 4.62 | 66.0% | + +(**) При n=1 MPI+OMP задействует все ядра через OMP без MPI-накладных расходов; нормирование +эффективности на 1 MPI-процесс не применимо. + +#### ALL - task_pipeline + +Базовое время SEQ при mpiexec -n 1: 0.2530с. + +| Режим | Процессы (n) | Время, с | Ускорение | Эффективность | +| ----- | ------------ | -------- | --------- | ------------- | +| seq | 1 | 0.2530 | 1.00 | N/A | +| all | 1 | 0.0228 | 11.10 | -- (**) | +| all | 3 | 0.0233 | 10.86 | 362.1% | +| all | 5 | 0.0420 | 6.02 | 120.5% | +| all | 7 | 0.0567 | 4.46 | 63.8% | + +**Анализ результатов:** + +- **OMP** показывает стабильное ускорение ~5.3-5.6x вне зависимости от PPC_NUM_THREADS, поскольку + управляется системным OpenMP-планировщиком напрямую. +- **TBB** и **STL** корректно реагируют на PPC_NUM_THREADS. При 3 потоках дают ускорение ~2.77x + (эффективность ~92%), при 7 - ~5.4-5.5x (эффективность ~77-79%). Масштабируемость хорошая до + 5 потоков, затем начинает падать. +- **STL** при k=5 уступает TBB (4.28x против 4.81x) из-за статического разбиения строк; + при k=7 STL (5.53x) обгоняет TBB (5.39x) за счет устранения барьеров между шагами Фокса. +- **ALL (MPI+OMP)**: при n=1 лучший результат среди всех технологий. При + увеличении числа MPI-процессов на одной машине они делят ядра, что резко снижает пользу от + MPI-декомпозиции. При n=7 ускорение падает до 4.6x. + +--- + +## 8. Выводы + +В ходе лабораторной работы реализовано и исследовано параллельное умножение матриц +методом Фокса в пяти вариантах. Основные выводы по результатам замеров: + +- **SEQ** является корректным эталоном: время умножения матрицы 400x400 составляет ~0.235-0.237 с. +- **OMP** обеспечивает простую реализацию и стабильное ускорение ~5.3-5.6x за счёт + использования системного OpenMP-пула потоков, не зависящего от PPC_NUM_THREADS. +- **TBB** демонстрирует хорошую масштабируемость: 2.77x при 3 потоках, 4.81x при 5 и 5.39x при 7. + Динамический планировщик TBB устойчив к дисбалансу нагрузки при нечтном делении блочных строк. +- **STL** показывает сопоставимую с TBB масштабируемость. При 7 потоках немного превосходит TBB + (5.53x против 5.39x) за счет объединения всех шагов Фокса в теле одного потока; однако при 5 потоках + слегка уступает из-за менее гибкого статического разбиения. +- **ALL** показывает наилучшее время при n=1 (16.7x ускорение по task_run). На одном + вычислительном узле увеличение числа MPI-процессов сверх числа физических ядер ведет к конкуренции + за ресурсы и уменьшению производительности (4.6x при n=7). Данная технология выигрывает на + многоузловых кластерах. +- Общее ограничение: последовательный цикл по шагам step в OMP/TBB-версиях создает барьер после + каждого шага, что ограничивает масштабируемость. STL обходит это ограничение, объединяя все шаги + в одном потоке, но теряет гибкость балансировки при произвольном числе потоков. + +--- + +## 9. Источники + +1. Fox G.C., Otto S.W., Hey A.J.G. Matrix Algorithms on a Hypercube I: Matrix Multiplication // + Parallel Computing. 1987. +2. Гергель В.П. Теория и практика параллельных вычислений + 2007. +3. Корняков К.В., Мееров И.Б., Сысоев А.В. и др. Инструменты параллельного программирования для + систем с общей памятью. Н. Новгород: Изд-во ННГУ, 2010. + +--- + +## Приложение: ключевые фрагменты кода + +### Последовательная реализация: шаг Фокса + +```cpp +void FoxStep(const std::vector& a, const std::vector& b, + std::vector& c, int bs, int q, int step) { + int bsq = bs * bs; + for (int i = 0; i < q; i++) { + int k = (i + step) % q; + for (int j = 0; j < q; j++) { + MultiplyBlocks(a, ((i * q) + k) * bsq, + b, ((k * q) + j) * bsq, + c, ((i * q) + j) * bsq, bs); + } + } +} +``` + +### OMP: параллельный шаг Фокса + +```cpp +void FoxStepOmp(..., int step) { + int bsq = bs * bs; +#pragma omp parallel for default(none) shared(a, b, c, bs, q, step, bsq) schedule(static) + for (int i = 0; i < q; i++) { + int k = (i + step) % q; + for (int j = 0; j < q; j++) { + MultiplyBlocksLocal(a, ((i * q) + k) * bsq, + b, ((k * q) + j) * bsq, + c, ((i * q) + j) * bsq, bs); + } + } +} +``` + +### STL: рабочая функция потока + +```cpp +void FoxWorker(const std::vector& a, const std::vector& b, + std::vector& c, int bs, int q, + int row_begin, int row_end) { + int bsq = bs * bs; + for (int step = 0; step < q; step++) { + for (int i = row_begin; i < row_end; i++) { + int k = (i + step) % q; + for (int j = 0; j < q; j++) { + MultiplyBlocks(a, ((i * q) + k) * bsq, + b, ((k * q) + j) * bsq, + c, ((i * q) + j) * bsq, bs); + } + } + } +} +``` + +### MPI+OMP: распределение строк по процессам + +```cpp +void ComputeRowRange(int rank, int num_procs, int rows_per, int leftover, + int& row_start, int& row_count) { + if (rank < num_procs) { + row_start = (rank * rows_per) + std::min(rank, leftover); + row_count = rows_per + (rank < leftover ? 1 : 0); + } else { + row_start = 0; + row_count = 0; + } +} +``` diff --git a/tasks/sokolov_k_matrix_double_fox/tests/functional/main.cpp b/tasks/sokolov_k_matrix_double_fox/tests/functional/main.cpp index a813e3ad4..53e335b04 100644 --- a/tasks/sokolov_k_matrix_double_fox/tests/functional/main.cpp +++ b/tasks/sokolov_k_matrix_double_fox/tests/functional/main.cpp @@ -5,6 +5,7 @@ #include #include +#include "sokolov_k_matrix_double_fox/all/include/ops_all.hpp" #include "sokolov_k_matrix_double_fox/common/include/common.hpp" #include "sokolov_k_matrix_double_fox/omp/include/ops_omp.hpp" #include "sokolov_k_matrix_double_fox/seq/include/ops_seq.hpp" @@ -59,6 +60,7 @@ const std::array kTestParam = {std::make_tuple(1, "single_element" std::make_tuple(100, "100x100")}; const auto kTestTasksList = std::tuple_cat( + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_sokolov_k_matrix_double_fox), ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_sokolov_k_matrix_double_fox), ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_sokolov_k_matrix_double_fox), ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_sokolov_k_matrix_double_fox), diff --git a/tasks/sokolov_k_matrix_double_fox/tests/performance/main.cpp b/tasks/sokolov_k_matrix_double_fox/tests/performance/main.cpp index bb602fe64..a99266992 100644 --- a/tasks/sokolov_k_matrix_double_fox/tests/performance/main.cpp +++ b/tasks/sokolov_k_matrix_double_fox/tests/performance/main.cpp @@ -1,5 +1,6 @@ #include +#include "sokolov_k_matrix_double_fox/all/include/ops_all.hpp" #include "sokolov_k_matrix_double_fox/common/include/common.hpp" #include "sokolov_k_matrix_double_fox/omp/include/ops_omp.hpp" #include "sokolov_k_matrix_double_fox/seq/include/ops_seq.hpp" @@ -32,9 +33,10 @@ TEST_P(SokolovKMatrixDoubleFoxPerfTestsSeq, RunPerfModes) { namespace { -const auto kAllPerfTasks = ppc::util::MakeAllPerfTasks( - PPC_SETTINGS_sokolov_k_matrix_double_fox); +const auto kAllPerfTasks = + ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_sokolov_k_matrix_double_fox); const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks);