diff --git a/tasks/goriacheva_k_strassen_algorithm/common/include/common.hpp b/tasks/goriacheva_k_strassen_algorithm/common/include/common.hpp new file mode 100644 index 0000000000..3b8c1cd7ff --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/common/include/common.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "task/include/task.hpp" + +namespace goriacheva_k_strassen_algorithm { + +using Matrix = std::vector>; + +struct InType { + Matrix a; + Matrix b; +}; + +using OutType = Matrix; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +inline std::vector Flatten(const Matrix &m) { + std::vector buf; + buf.reserve(m.size() * m.size()); + for (const auto &row : m) { + buf.insert(buf.end(), row.begin(), row.end()); + } + return buf; +} + +inline Matrix UnFlatten(const std::vector &buf, std::size_t n) { + Matrix m(n, std::vector(n)); + for (std::size_t i = 0; i < n; ++i) { + for (std::size_t j = 0; j < n; ++j) { + m[i][j] = buf[(i * n) + j]; + } + } + return m; +} + +inline bool IsSquare(const Matrix &m) { + if (m.empty()) { + return false; + } + const std::size_t n = m.size(); + return std::ranges::all_of(m, [n](const auto &r) { return r.size() == n; }); +} + +inline std::size_t NextPowerOfTwo(std::size_t n) { + std::size_t p = 1; + while (p < n) { + p <<= 1; + } + return p; +} + +inline Matrix PadMatrix(const Matrix &a, std::size_t new_size) { + Matrix r(new_size, std::vector(new_size, 0.0)); + for (std::size_t i = 0; i < a.size(); ++i) { + for (std::size_t j = 0; j < a.size(); ++j) { + r[i][j] = a[i][j]; + } + } + return r; +} + +inline Matrix CropMatrix(const Matrix &a, std::size_t size) { + Matrix r(size, std::vector(size)); + for (std::size_t i = 0; i < size; ++i) { + for (std::size_t j = 0; j < size; ++j) { + r[i][j] = a[i][j]; + } + } + return r; +} + +inline Matrix Add(const Matrix &a, const Matrix &b) { + std::size_t n = a.size(); + Matrix c(n, std::vector(n)); + for (std::size_t i = 0; i < n; ++i) { + for (std::size_t j = 0; j < n; ++j) { + c[i][j] = a[i][j] + b[i][j]; + } + } + return c; +} + +inline Matrix Sub(const Matrix &a, const Matrix &b) { + std::size_t n = a.size(); + Matrix c(n, std::vector(n)); + for (std::size_t i = 0; i < n; ++i) { + for (std::size_t j = 0; j < n; ++j) { + c[i][j] = a[i][j] - b[i][j]; + } + } + return c; +} + +inline Matrix NaiveMultiply(const Matrix &a, const Matrix &b) { + std::size_t n = a.size(); + Matrix c(n, std::vector(n, 0.0)); + for (std::size_t i = 0; i < n; ++i) { + for (std::size_t k = 0; k < n; ++k) { + for (std::size_t j = 0; j < n; ++j) { + c[i][j] += a[i][k] * b[k][j]; + } + } + } + return c; +} + +inline Matrix Strassen(const Matrix &a, const Matrix &b, std::size_t threshold = 128) // NOLINT(misc-no-recursion) +{ + std::size_t n = a.size(); + if (n <= threshold) { + return NaiveMultiply(a, b); + } + + std::size_t k = n / 2; + + Matrix a11(k, std::vector(k)); + Matrix a12(k, std::vector(k)); + Matrix a21(k, std::vector(k)); + Matrix a22(k, std::vector(k)); + Matrix b11(k, std::vector(k)); + Matrix b12(k, std::vector(k)); + Matrix b21(k, std::vector(k)); + Matrix b22(k, std::vector(k)); + + for (std::size_t i = 0; i < k; ++i) { + for (std::size_t j = 0; j < k; ++j) { + a11[i][j] = a[i][j]; + a12[i][j] = a[i][j + k]; + a21[i][j] = a[i + k][j]; + a22[i][j] = a[i + k][j + k]; + b11[i][j] = b[i][j]; + b12[i][j] = b[i][j + k]; + b21[i][j] = b[i + k][j]; + b22[i][j] = b[i + k][j + k]; + } + } + + Matrix m1 = Strassen(Add(a11, a22), Add(b11, b22), threshold); + Matrix m2 = Strassen(Add(a21, a22), b11, threshold); + Matrix m3 = Strassen(a11, Sub(b12, b22), threshold); + Matrix m4 = Strassen(a22, Sub(b21, b11), threshold); + Matrix m5 = Strassen(Add(a11, a12), b22, threshold); + Matrix m6 = Strassen(Sub(a21, a11), Add(b11, b12), threshold); + Matrix m7 = Strassen(Sub(a12, a22), Add(b21, b22), threshold); + + Matrix c(n, std::vector(n)); + for (std::size_t i = 0; i < k; ++i) { + for (std::size_t j = 0; j < k; ++j) { + c[i][j] = m1[i][j] + m4[i][j] - m5[i][j] + m7[i][j]; + c[i][j + k] = m3[i][j] + m5[i][j]; + c[i + k][j] = m2[i][j] + m4[i][j]; + c[i + k][j + k] = m1[i][j] - m2[i][j] + m3[i][j] + m6[i][j]; + } + } + + return c; +} + +} // namespace goriacheva_k_strassen_algorithm diff --git a/tasks/goriacheva_k_strassen_algorithm/data/efficiency.png b/tasks/goriacheva_k_strassen_algorithm/data/efficiency.png new file mode 100644 index 0000000000..97ddfa08ec Binary files /dev/null and b/tasks/goriacheva_k_strassen_algorithm/data/efficiency.png differ diff --git a/tasks/goriacheva_k_strassen_algorithm/data/speedup.png b/tasks/goriacheva_k_strassen_algorithm/data/speedup.png new file mode 100644 index 0000000000..c1f8947357 Binary files /dev/null and b/tasks/goriacheva_k_strassen_algorithm/data/speedup.png differ diff --git a/tasks/goriacheva_k_strassen_algorithm/data/tests.json b/tasks/goriacheva_k_strassen_algorithm/data/tests.json new file mode 100644 index 0000000000..f290a4c054 --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/data/tests.json @@ -0,0 +1,72 @@ +[ + { "name": "1x1_single", "input": { "A": [[7.0]], "B": [[3.0]] }, "result": [[21.0]] }, + { "name": "2x2_simple", "input": { "A": [[1.0,2.0],[3.0,4.0]], "B": [[5.0,6.0],[7.0,8.0]] }, "result": [[19.0,22.0],[43.0,50.0]] }, + { "name": "2x2_zeros", "input": { "A": [[0.0,0.0],[0.0,0.0]], "B": [[1.0,2.0],[3.0,4.0]] }, "result": [[0.0,0.0],[0.0,0.0]] }, + { "name": "2x2_identity", "input": { "A": [[1.0,0.0],[0.0,1.0]], "B": [[2.0,3.0],[4.0,5.0]] }, "result": [[2.0,3.0],[4.0,5.0]] }, + { "name": "3x3_random", "input": { "A": [[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0,9.0]], "B": [[9.0,8.0,7.0],[6.0,5.0,4.0],[3.0,2.0,1.0]] }, "result": [[30.0,24.0,18.0],[84.0,69.0,54.0],[138.0,114.0,90.0]] }, + { "name": "3x3_identity", "input": { "A": [[1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0]], "B": [[3.0,2.0,1.0],[6.0,5.0,4.0],[9.0,8.0,7.0]] }, "result": [[3.0,2.0,1.0],[6.0,5.0,4.0],[9.0,8.0,7.0]] }, + { "name": "3x3_diagonal", "input": { "A": [[1.0,0.0,0.0],[0.0,2.0,0.0],[0.0,0.0,3.0]], "B": [[4.0,0.0,0.0],[0.0,5.0,0.0],[0.0,0.0,6.0]] }, "result": [[4.0,0.0,0.0],[0.0,10.0,0.0],[0.0,0.0,18.0]] }, + { "name": "2x2_negatives", "input": { "A": [[-1.0,2.0],[3.0,-4.0]], "B": [[5.0,-6.0],[-7.0,8.0]] }, "result": [[-19.0,22.0],[43.0,-50.0]] }, + { "name": "3x3_repeats", "input": { "A": [[2.0,2.0,2.0],[2.0,2.0,2.0],[2.0,2.0,2.0]], "B": [[1.0,2.0,3.0],[1.0,2.0,3.0],[1.0,2.0,3.0]] }, "result": [[6.0,12.0,18.0],[6.0,12.0,18.0],[6.0,12.0,18.0]] }, + { "name": "3x3_fractional", "input": { "A": [[1.5,2.0,3.5],[4.0,5.0,6.5],[7.5,8.0,9.5]], "B": [[0.5,1.0,1.5],[2.0,2.5,3.0],[3.5,4.0,4.5]] }, "result": [[17.0,20.5,24.0],[34.75,42.5,50.25],[53.0,65.5,78.0]] }, + { "name": "4x4_simple", "input": { "A": [[1.0,2.0,3.0,4.0],[5.0,6.0,7.0,8.0],[9.0,10.0,11.0,12.0],[13.0,14.0,15.0,16.0]], "B": [[16.0,15.0,14.0,13.0],[12.0,11.0,10.0,9.0],[8.0,7.0,6.0,5.0],[4.0,3.0,2.0,1.0]] }, "result": [[80.0,70.0,60.0,50.0],[240.0,214.0,188.0,162.0],[400.0,358.0,316.0,274.0],[560.0,502.0,444.0,386.0]] }, + { "name": "4x4_identity", "input": { "A": [[1.0,0.0,0.0,0.0],[0.0,1.0,0.0,0.0],[0.0,0.0,1.0,0.0],[0.0,0.0,0.0,1.0]], "B": [[1.0,2.0,3.0,4.0],[5.0,6.0,7.0,8.0],[9.0,10.0,11.0,12.0],[13.0,14.0,15.0,16.0]] }, "result": [[1.0,2.0,3.0,4.0],[5.0,6.0,7.0,8.0],[9.0,10.0,11.0,12.0],[13.0,14.0,15.0,16.0]] }, + { "name": "4x4_negatives", "input": { "A": [[-1.0,2.0,-3.0,4.0],[5.0,-6.0,7.0,-8.0],[-9.0,10.0,-11.0,12.0],[13.0,-14.0,15.0,-16.0]], "B": [[16.0,-15.0,14.0,-13.0],[-12.0,11.0,-10.0,9.0],[8.0,-7.0,6.0,-5.0],[-4.0,3.0,-2.0,1.0]] }, "result": [[-80.0,70.0,-60.0,50.0],[240.0,-214.0,188.0,-162.0],[-400.0,358.0,-316.0,274.0],[560.0,-502.0,444.0,-386.0]] }, + { "name": "4x4_repeats", "input": { "A": [[1.0,1.0,1.0,1.0],[1.0,1.0,1.0,1.0],[1.0,1.0,1.0,1.0],[1.0,1.0,1.0,1.0]], "B": [[2.0,2.0,2.0,2.0],[2.0,2.0,2.0,2.0],[2.0,2.0,2.0,2.0],[2.0,2.0,2.0,2.0]] }, "result": [[8.0,8.0,8.0,8.0],[8.0,8.0,8.0,8.0],[8.0,8.0,8.0,8.0],[8.0,8.0,8.0,8.0]] }, + { "name": "4x4_fractional", "input": { "A": [[1.5, 2.0, 3.0, 4.5],[5.5, 6.5, 7.0, 8.0],[9.0, 10.5, 11.5, 12.0],[13.0, 14.5, 15.0, 16.5]], "B": [[0.5, 1.0, 1.5, 2.0],[2.5, 3.0, 3.5, 4.0],[4.5, 5.0, 5.5, 6.0],[6.5, 7.0, 7.5, 8.0]] }, "result": [[48.5, 54.0, 59.5, 65.0],[102.5, 116.0, 129.5, 143.0],[160.5, 182.0, 203.5, 225.0],[217.5, 247.0, 276.5, 306.0]] }, + { "name": "5x5_identity", "input": { "A": [[1.0,0.0,0.0,0.0,0.0],[0.0,1.0,0.0,0.0,0.0],[0.0,0.0,1.0,0.0,0.0],[0.0,0.0,0.0,1.0,0.0],[0.0,0.0,0.0,0.0,1.0]], "B": [[1.0,2.0,3.0,4.0,5.0],[6.0,7.0,8.0,9.0,10.0],[11.0,12.0,13.0,14.0,15.0],[16.0,17.0,18.0,19.0,20.0],[21.0,22.0,23.0,24.0,25.0]] }, "result": [[1.0,2.0,3.0,4.0,5.0],[6.0,7.0,8.0,9.0,10.0],[11.0,12.0,13.0,14.0,15.0],[16.0,17.0,18.0,19.0,20.0],[21.0,22.0,23.0,24.0,25.0]] }, +{ + "name": "6x6_identity", + "input": { + "A": [[1,0,0,0,0,0], + [0,1,0,0,0,0], + [0,0,1,0,0,0], + [0,0,0,1,0,0], + [0,0,0,0,1,0], + [0,0,0,0,0,1]], + "B": [[1,2,3,4,5,6], + [7,8,9,10,11,12], + [13,14,15,16,17,18], + [19,20,21,22,23,24], + [25,26,27,28,29,30], + [31,32,33,34,35,36]] + }, + "result": [[1,2,3,4,5,6], + [7,8,9,10,11,12], + [13,14,15,16,17,18], + [19,20,21,22,23,24], + [25,26,27,28,29,30], + [31,32,33,34,35,36]] +}, +{ + "name": "8x8_mixed", + "input": { + "A": [[1,2,3,4,5,6,7,8], + [8,7,6,5,4,3,2,1], + [1,3,5,7,9,11,13,15], + [2,4,6,8,10,12,14,16], + [1,1,1,1,1,1,1,1], + [2,2,2,2,2,2,2,2], + [3,3,3,3,3,3,3,3], + [4,4,4,4,4,4,4,4]], + "B": [[1,0,0,0,0,0,0,0], + [0,1,0,0,0,0,0,0], + [0,0,1,0,0,0,0,0], + [0,0,0,1,0,0,0,0], + [0,0,0,0,1,0,0,0], + [0,0,0,0,0,1,0,0], + [0,0,0,0,0,0,1,0], + [0,0,0,0,0,0,0,1]] + }, + "result": [[1,2,3,4,5,6,7,8], + [8,7,6,5,4,3,2,1], + [1,3,5,7,9,11,13,15], + [2,4,6,8,10,12,14,16], + [1,1,1,1,1,1,1,1], + [2,2,2,2,2,2,2,2], + [3,3,3,3,3,3,3,3], + [4,4,4,4,4,4,4,4]] +} + + +] diff --git a/tasks/goriacheva_k_strassen_algorithm/data/time_vs_processes.png b/tasks/goriacheva_k_strassen_algorithm/data/time_vs_processes.png new file mode 100644 index 0000000000..989ddf099c Binary files /dev/null and b/tasks/goriacheva_k_strassen_algorithm/data/time_vs_processes.png differ diff --git a/tasks/goriacheva_k_strassen_algorithm/info.json b/tasks/goriacheva_k_strassen_algorithm/info.json new file mode 100644 index 0000000000..cbe1e07bf7 --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Ксения", + "last_name": "Горячева", + "middle_name": "Александровна", + "group_number": "3823Б1ФИ2", + "task_number": "3" + } +} diff --git a/tasks/goriacheva_k_strassen_algorithm/mpi/include/ops_mpi.hpp b/tasks/goriacheva_k_strassen_algorithm/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..4d6595016d --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/mpi/include/ops_mpi.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "goriacheva_k_strassen_algorithm/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace goriacheva_k_strassen_algorithm { + +class GoriachevaKStrassenAlgorithmMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + + explicit GoriachevaKStrassenAlgorithmMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + InType input_matrices_; + OutType result_matrix_; + + static Matrix MpiStrassenTop(const Matrix &a, const Matrix &b); +}; + +} // namespace goriacheva_k_strassen_algorithm diff --git a/tasks/goriacheva_k_strassen_algorithm/mpi/src/ops_mpi.cpp b/tasks/goriacheva_k_strassen_algorithm/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..33719e9b34 --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/mpi/src/ops_mpi.cpp @@ -0,0 +1,208 @@ +#include "goriacheva_k_strassen_algorithm/mpi/include/ops_mpi.hpp" + +#include + +#include +#include +#include + +#include "goriacheva_k_strassen_algorithm/common/include/common.hpp" + +namespace goriacheva_k_strassen_algorithm { + +namespace { + +struct Blocks { + Matrix a11; + Matrix a12; + Matrix a21; + Matrix a22; + Matrix b11; + Matrix b12; + Matrix b21; + Matrix b22; +}; + +Blocks SplitMatrices(const Matrix &a, const Matrix &b, std::size_t k) { + Blocks blk{.a11 = Matrix(k, std::vector(k)), + .a12 = Matrix(k, std::vector(k)), + .a21 = Matrix(k, std::vector(k)), + .a22 = Matrix(k, std::vector(k)), + .b11 = Matrix(k, std::vector(k)), + .b12 = Matrix(k, std::vector(k)), + .b21 = Matrix(k, std::vector(k)), + .b22 = Matrix(k, std::vector(k))}; + + for (std::size_t i = 0; i < k; ++i) { + for (std::size_t j = 0; j < k; ++j) { + blk.a11[i][j] = a[i][j]; + blk.a12[i][j] = a[i][j + k]; + blk.a21[i][j] = a[i + k][j]; + blk.a22[i][j] = a[i + k][j + k]; + + blk.b11[i][j] = b[i][j]; + blk.b12[i][j] = b[i][j + k]; + blk.b21[i][j] = b[i + k][j]; + blk.b22[i][j] = b[i + k][j + k]; + } + } + + return blk; +} + +Matrix ComputeMi(int task_id, const Blocks &blk) { + switch (task_id) { + case 0: + return Strassen(Add(blk.a11, blk.a22), Add(blk.b11, blk.b22)); + case 1: + return Strassen(Add(blk.a21, blk.a22), blk.b11); + case 2: + return Strassen(blk.a11, Sub(blk.b12, blk.b22)); + case 3: + return Strassen(blk.a22, Sub(blk.b21, blk.b11)); + case 4: + return Strassen(Add(blk.a11, blk.a12), blk.b22); + case 5: + return Strassen(Sub(blk.a21, blk.a11), Add(blk.b11, blk.b12)); + case 6: + return Strassen(Sub(blk.a12, blk.a22), Add(blk.b21, blk.b22)); + default: + return {}; + } +} + +void ComputeMissingTasks(std::vector &m, int start_task, const Blocks &blk) { + for (int task = start_task; task < 7; ++task) { + m[task] = ComputeMi(task, blk); + } +} + +Matrix AssembleResult(const std::vector &m, std::size_t k) { + std::size_t n = 2 * k; + Matrix c(n, std::vector(n)); + + for (std::size_t i = 0; i < k; ++i) { + for (std::size_t j = 0; j < k; ++j) { + c[i][j] = m[0][i][j] + m[3][i][j] - m[4][i][j] + m[6][i][j]; + c[i][j + k] = m[2][i][j] + m[4][i][j]; + c[i + k][j] = m[1][i][j] + m[3][i][j]; + c[i + k][j + k] = m[0][i][j] - m[1][i][j] + m[2][i][j] + m[5][i][j]; + } + } + + return c; +} + +} // namespace + +GoriachevaKStrassenAlgorithmMPI::GoriachevaKStrassenAlgorithmMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; +} + +bool GoriachevaKStrassenAlgorithmMPI::ValidationImpl() { + return IsSquare(GetInput().a) && IsSquare(GetInput().b) && GetInput().a.size() == GetInput().b.size(); +} + +bool GoriachevaKStrassenAlgorithmMPI::PreProcessingImpl() { + input_matrices_ = GetInput(); + return true; +} + +Matrix GoriachevaKStrassenAlgorithmMPI::MpiStrassenTop(const Matrix &a, const Matrix &b) { + int rank = 0; + int size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + + std::size_t n = a.size(); + if (size == 1 || n <= 1) { + return Strassen(a, b); + } + + std::size_t k = n / 2; + auto blocks = SplitMatrices(a, b, k); + + int num_tasks = std::min(7, size); + int task_id = rank % num_tasks; + + MPI_Comm subcomm = MPI_COMM_NULL; + MPI_Comm_split(MPI_COMM_WORLD, task_id, rank, &subcomm); + + int sub_rank = 0; + MPI_Comm_rank(subcomm, &sub_rank); + + Matrix mi; + if (sub_rank == 0) { + mi = ComputeMi(task_id, blocks); + } + + MPI_Comm_free(&subcomm); + + std::vector m(7); + + if (rank == 0) { + if (task_id < num_tasks && sub_rank == 0) { + m[task_id] = mi; + } + + for (int i = 1; i < num_tasks; ++i) { + int tid = 0; + std::vector buf(k * k); + MPI_Status status; + + MPI_Recv(&tid, 1, MPI_INT, MPI_ANY_SOURCE, 0, MPI_COMM_WORLD, &status); + MPI_Recv(buf.data(), static_cast(k * k), MPI_DOUBLE, status.MPI_SOURCE, 1, MPI_COMM_WORLD, + MPI_STATUS_IGNORE); + + m[tid] = UnFlatten(buf, k); + } + + ComputeMissingTasks(m, num_tasks, blocks); + } else if (sub_rank == 0) { + auto buf = Flatten(mi); + MPI_Send(&task_id, 1, MPI_INT, 0, 0, MPI_COMM_WORLD); + MPI_Send(buf.data(), static_cast(k * k), MPI_DOUBLE, 0, 1, MPI_COMM_WORLD); + } + + Matrix c; + std::vector flat_c; + + if (rank == 0) { + c = AssembleResult(m, k); + flat_c = Flatten(c); + } else { + flat_c.resize(n * n); + } + + MPI_Bcast(flat_c.data(), static_cast(n * n), MPI_DOUBLE, 0, MPI_COMM_WORLD); + + if (rank != 0) { + c = UnFlatten(flat_c, n); + } + + return c; +} + +bool GoriachevaKStrassenAlgorithmMPI::RunImpl() { + const auto &a = input_matrices_.a; + const auto &b = input_matrices_.b; + + std::size_t n = a.size(); + std::size_t m = NextPowerOfTwo(n); + + Matrix a_pad = (n == m) ? a : PadMatrix(a, m); + Matrix b_pad = (n == m) ? b : PadMatrix(b, m); + + Matrix c_pad = MpiStrassenTop(a_pad, b_pad); + result_matrix_ = (n == m) ? c_pad : CropMatrix(c_pad, n); + + return true; +} + +bool GoriachevaKStrassenAlgorithmMPI::PostProcessingImpl() { + GetOutput() = result_matrix_; + return true; +} + +} // namespace goriacheva_k_strassen_algorithm diff --git a/tasks/goriacheva_k_strassen_algorithm/report.md b/tasks/goriacheva_k_strassen_algorithm/report.md new file mode 100644 index 0000000000..0a0b94ef50 --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/report.md @@ -0,0 +1,198 @@ +# Умножение плотных матриц. Элементы типа double. Алгоритм Штрассена. +(последовательная и MPI-реализации, алгоритм Штрассена) + +Студентка: Горячева Ксения Александровна, группа 3823Б1ФИ2 +Технологии: SEQ, MPI +Вариант: №3 + +--- + +## 1. Введение +Задача умножения матриц является одной из базовых и наиболее часто встречающихся задач линейной алгебры и численных методов. Она широко применяется в научных вычислениях, компьютерной графике, машинном обучении и обработке сигналов. + +Классический алгоритм умножения квадратных матриц имеет вычислительную сложность +O(n³), что делает его неэффективным для матриц большого размера. Одним из способов оптимизации является использование алгоритма Штрассена, который уменьшает асимптотическую сложность до O(n^{log₂7}) ≈ O(n^{2.81}) за счёт сокращения количества умножений. + +Целью данной работы является реализация и исследование последовательной и параллельной (MPI) версий алгоритма Штрассена для умножения плотных квадратных матриц с элементами типа double. + +--- + +## 2. Постановка задачи + +Входные данные: +Две квадратные матрицы A и B размера n × n, элементы типа double. + +Выходные данные: +Матрица C = A × B размера n × n. + +Ограничения и допущения: +* матрицы являются квадратными и одинакового размера; +* размер матриц может не быть степенью двойки; +* элементы матриц имеют тип double; +* результаты SEQ и MPI реализаций должны совпадать с точностью 1e-6. + +--- + +## 3. Последовательная реализация (SEQ) + +### 3.1 Общая идея + +Последовательная версия основана на рекурсивной реализации алгоритма Штрассена. +Если размер матрицы превышает заданный порог (threshold = 128), матрица разбивается на 4 подматрицы, после чего вычисляются 7 вспомогательных матриц вместо 8, как в классическом блочном алгоритме. +Для малых размеров используется наивное умножение, так как оно эффективнее из-за меньших накладных расходов. + +--- + +### 3.2 Дополнение матриц + +Алгоритм Штрассена требует, чтобы размер матрицы был степенью двойки. +Если это не так, матрицы дополняются нулями до ближайшей степени двойки: +m = NextPowerOfTwo(n) +После вычислений результат обрезается до исходного размера. + +--- + +### 3.3 Наивное умножение + +При достижении порога рекурсии используется стандартный алгоритм: +for i + for k + for j + C[i][j] += A[i][k] * B[k][j] + +--- + +### 3.4 Сложность + +* по времени: + * асимптотически — O(n^{log₂7}) + * для малых n — O(n³) +* по памяти: + * O(n²) с учётом временных подматриц + +--- + +## 4. Параллельная реализация (MPI) + +### 4.1 Общая схема + +MPI-реализация использует параллелизм верхнего уровня алгоритма Штрассена. +Семь независимых произведений (M₁ … M₇) распределяются между MPI-процессами. +Если количество процессов меньше 7, оставшиеся произведения вычисляются на процессе с рангом 0. + +--- + +### 4.2 Разбиение вычислений + +Для матриц размера n × n: +* матрицы A и B разбиваются на подматрицы A₁₁, A₁₂, A₂₁, A₂₂ и аналогично для B; +* каждому процессу назначается вычисление одного из Mi; +* используется MPI_Comm_split для формирования подкоммуникаторов. + +--- + +### 4.3 Обмен данными + +* локальные результаты Mi сериализуются в одномерный буфер (Flatten); +* передаются на процесс 0 с помощью MPI_Send; +* процесс 0 собирает все Mi и формирует итоговую матрицу C; +* результат рассылается всем процессам через MPI_Bcast. + +--- + +### 4.4 Особенности реализации + +* параллелизация выполняется только на верхнем уровне рекурсии; +* дальнейшие рекурсивные вызовы выполняются локально; +* при числе процессов > 7 избыточные процессы не участвуют в вычислениях. + +--- + +### 4.5 Сложность + +* вычислительная сложность — как у SEQ версии; +* ускорение достигается за счёт параллельного вычисления независимых Mi; +* накладные расходы связаны с: + * созданием подкоммуникаторов; + * передачей данных; + * синхронизацией процессов. + +--- + +## 5. Тестирование корректности + +Для проверки корректности был реализован набор функциональных тестов, включающий: +* матрицы размеров от 1×1 до 5×5; +* единичные матрицы; +* нулевые матрицы; +* матрицы с отрицательными элементами; +* дробные значения; +* повторяющиеся элементы; +* матрицы, размер которых не является степенью двойки. + +Всего выполнено 36 функциональных теста, каждый из которых запускался: +* для SEQ версии; +* для MPI версии. + +Все тесты успешно пройдены. + +--- + +## 6. Экспериментальное исследование производительности + +Измерения проводились на матрицах размера 256 × 256. + +| Число процессов (P) |Время SEQ (сек) |Время MPI (сек) | Ускорение (S) | Эффективность (E) | +|---------------------|-----------------|----------------|---------------|-------------------| +| 1 | 0.260 | 0.262 | 0.99 | 0.99 | +| 2 | 0.269 | 0.227 | 1.18 | 0.59 | +| 4 | 0.339 | 0.170 | 1.99 | 0.50 | +| 8 | 0.546 | 0.096 | 5.69 | 0.71 | +| 16 | 1.217 | 0.155 | 7.85 | 0.49 | +| 32 | 2.168 | 0.362 | 5.99 | 0.19 | + +Графики производительности + +![График времени выполнения](data/time_vs_processes.png) +*Рис. 1. Зависимость времени выполнения от числа процессов.* + +![График ускорения](data/speedup.png) +*Рис. 2. Зависимость ускорения от числа процессов.* + +![График эффективности](data/efficiency.png) +*Рис. 3. Зависимость эффективности от числа процессов.* + +--- + +## 7. Анализ результатов + +При 1 процессе ускорение близко к 1, что подтверждает корректность измерений. +Максимальное ускорение достигается при 16 процессах (≈ 7.85), однако эффективность при этом уже снижается. +Наилучшая эффективность наблюдается при 8 процессах (≈ 0.71), что соответствует числу независимых подзадач алгоритма Штрассена. +При дальнейшем увеличении числа процессов эффективность резко падает из-за накладных расходов MPI и недоиспользования процессов. + +--- + +## 8. Выводы + +В ходе работы были реализованы и исследованы последовательная и параллельная версии алгоритма Штрассена для умножения плотных матриц. + +Основные выводы: +* алгоритм Штрассена корректно работает для матриц произвольного размера; +* MPI-реализация демонстрирует значительное ускорение по сравнению с SEQ; +* наилучшая эффективность достигается при количестве процессов, близком к числу независимых подзадач (7); +* при большом числе процессов накладные расходы начинают доминировать. + +Разработанная реализация подтверждает эффективность комбинирования алгоритмической оптимизации (Штрассен) и параллельных вычислений (MPI) для задач линейной алгебры. + +--- + +## 9. Используемые источники + +1. Parallel Programming Course - [https://learning-process.github.io/parallel_programming_course/ru/](https://learning-process.github.io/parallel_programming_course/ru/) +2. Parallel Programming 2025-2026 Video-Records - [https://disk.yandex.ru/d/NvHFyhOJCQU65w](https://disk.yandex.ru/d/NvHFyhOJCQU65w) +3. Open MPI: Documentation — [https://www.open-mpi.org/doc/](https://www.open-mpi.org/doc/) +4. Matrix Multiplication - [https://www.geeksforgeeks.org/dsa/Strassens-matrix-multiplication/](https://www.geeksforgeeks.org/dsa/Strassens-matrix-multiplication/) +5. Александр Охотин. Математические основы алгоритмов, осень 2025 г. Лекция 5. Структуры данных в алгоритме Дейкстры. Пути между всеми парами вершин в графе: алгоритм Варшалла. Пути между всеми парами вершин в графе с весами: алгоритм Флойда–Варшалла. Достижимость в графе и умножение матриц. Алгоритм Штрассена быстрого умножения матриц - [https://users.math-cs.spbu.ru/~okhotin/teaching/algorithms1_2025/okhotin_algorithms1_2025_l5.pdf](https://users.math-cs.spbu.ru/~okhotin/teaching/algorithms1_2025/okhotin_algorithms1_2025_l5.pdf) + +--- diff --git a/tasks/goriacheva_k_strassen_algorithm/seq/include/ops_seq.hpp b/tasks/goriacheva_k_strassen_algorithm/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..94a68d2958 --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/seq/include/ops_seq.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "goriacheva_k_strassen_algorithm/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace goriacheva_k_strassen_algorithm { + +class GoriachevaKStrassenAlgorithmSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit GoriachevaKStrassenAlgorithmSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + InType input_matrices_; + OutType result_matrix_; +}; + +} // namespace goriacheva_k_strassen_algorithm diff --git a/tasks/goriacheva_k_strassen_algorithm/seq/src/ops_seq.cpp b/tasks/goriacheva_k_strassen_algorithm/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..f9fee144f0 --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/seq/src/ops_seq.cpp @@ -0,0 +1,44 @@ +#include "goriacheva_k_strassen_algorithm/seq/include/ops_seq.hpp" + +#include + +#include "goriacheva_k_strassen_algorithm/common/include/common.hpp" + +namespace goriacheva_k_strassen_algorithm { + +GoriachevaKStrassenAlgorithmSEQ::GoriachevaKStrassenAlgorithmSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; +} + +bool GoriachevaKStrassenAlgorithmSEQ::ValidationImpl() { + return IsSquare(GetInput().a) && IsSquare(GetInput().b) && GetInput().a.size() == GetInput().b.size(); +} + +bool GoriachevaKStrassenAlgorithmSEQ::PreProcessingImpl() { + input_matrices_ = GetInput(); + return true; +} + +bool GoriachevaKStrassenAlgorithmSEQ::RunImpl() { + const auto &a = input_matrices_.a; + const auto &b = input_matrices_.b; + + std::size_t n = a.size(); + std::size_t m = NextPowerOfTwo(n); + + Matrix a_pad = (n == m) ? a : PadMatrix(a, m); + Matrix b_pad = (n == m) ? b : PadMatrix(b, m); + + Matrix c_pad = Strassen(a_pad, b_pad); + result_matrix_ = (n == m) ? c_pad : CropMatrix(c_pad, n); + + return true; +} + +bool GoriachevaKStrassenAlgorithmSEQ::PostProcessingImpl() { + GetOutput() = result_matrix_; + return true; +} + +} // namespace goriacheva_k_strassen_algorithm diff --git a/tasks/goriacheva_k_strassen_algorithm/settings.json b/tasks/goriacheva_k_strassen_algorithm/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/goriacheva_k_strassen_algorithm/tests/.clang-tidy b/tasks/goriacheva_k_strassen_algorithm/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/tests/.clang-tidy @@ -0,0 +1,13 @@ +InheritParentConfig: true + +Checks: > + -modernize-loop-convert, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-non-const-global-variables, + -misc-use-anonymous-namespace, + -modernize-use-std-print, + -modernize-type-traits + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 50 # Relaxed for tests diff --git a/tasks/goriacheva_k_strassen_algorithm/tests/functional/main.cpp b/tasks/goriacheva_k_strassen_algorithm/tests/functional/main.cpp new file mode 100644 index 0000000000..d8fe06d2fa --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/tests/functional/main.cpp @@ -0,0 +1,115 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "goriacheva_k_strassen_algorithm/common/include/common.hpp" +#include "goriacheva_k_strassen_algorithm/mpi/include/ops_mpi.hpp" +#include "goriacheva_k_strassen_algorithm/seq/include/ops_seq.hpp" +#include "task/include/task.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace goriacheva_k_strassen_algorithm { + +using FuncParam = ppc::util::FuncTestParam; + +class GoriachevaKStrassenAlgorithmFuncTests : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const testing::TestParamInfo &info) { + return std::get(ppc::util::GTestParamIndex::kNameTest)>(info.param); + } + + protected: + void SetUp() override { + const auto ¶ms = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + input_ = std::get<0>(params); + expected_ = std::get<1>(params); + } + + InType GetTestInputData() final { + return input_; + } + + bool CheckTestOutputData(OutType &output_data) final { + if (output_data.size() != expected_.size()) { + return false; + } + + for (std::size_t i = 0; i < expected_.size(); ++i) { + if (output_data[i].size() != expected_[i].size()) { + return false; + } + for (std::size_t j = 0; j < expected_[i].size(); ++j) { + if (std::fabs(output_data[i][j] - expected_[i][j]) > 1e-6) { + return false; + } + } + } + return true; + } + + private: + InType input_; + OutType expected_; +}; + +namespace { + +std::vector LoadTestParams() { + const std::string path = ppc::util::GetAbsoluteTaskPath(PPC_ID_goriacheva_k_strassen_algorithm, "tests.json"); + + std::ifstream fin(path); + if (!fin.is_open()) { + throw std::runtime_error("Cannot open tests.json"); + } + + nlohmann::json j; + fin >> j; + + std::vector cases; + cases.reserve(j.size() * 2); + + const std::string settings_path = PPC_SETTINGS_goriacheva_k_strassen_algorithm; + + const std::string mpi_suffix = + ppc::task::GetStringTaskType(GoriachevaKStrassenAlgorithmMPI::GetStaticTypeOfTask(), settings_path); + + const std::string seq_suffix = + ppc::task::GetStringTaskType(GoriachevaKStrassenAlgorithmSEQ::GetStaticTypeOfTask(), settings_path); + + for (const auto &item : j) { + InType input; + input.a = item.at("input").at("A").get>>(); + input.b = item.at("input").at("B").get>>(); + + OutType expected = item.at("result").get(); + + TestType tc{input, expected, item.at("name").get()}; + + std::string mpi_name = std::get<2>(tc) + "_" + mpi_suffix; + cases.emplace_back(ppc::task::TaskGetter, mpi_name, tc); + + std::string seq_name = std::get<2>(tc) + "_" + seq_suffix; + cases.emplace_back(ppc::task::TaskGetter, seq_name, tc); + } + + return cases; +} + +const std::vector kFuncParams = LoadTestParams(); + +TEST_P(GoriachevaKStrassenAlgorithmFuncTests, MatrixMultiplication) { + ExecuteTest(GetParam()); +} + +INSTANTIATE_TEST_SUITE_P(GoriachevaKStrassenAlgorithmFunctionalTests, GoriachevaKStrassenAlgorithmFuncTests, + testing::ValuesIn(kFuncParams), GoriachevaKStrassenAlgorithmFuncTests::PrintTestParam); + +} // namespace +} // namespace goriacheva_k_strassen_algorithm diff --git a/tasks/goriacheva_k_strassen_algorithm/tests/performance/main.cpp b/tasks/goriacheva_k_strassen_algorithm/tests/performance/main.cpp new file mode 100644 index 0000000000..4938843b1e --- /dev/null +++ b/tasks/goriacheva_k_strassen_algorithm/tests/performance/main.cpp @@ -0,0 +1,60 @@ +#include + +#include +#include +#include + +#include "goriacheva_k_strassen_algorithm/common/include/common.hpp" +#include "goriacheva_k_strassen_algorithm/mpi/include/ops_mpi.hpp" +#include "goriacheva_k_strassen_algorithm/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace goriacheva_k_strassen_algorithm { + +class GoriachevaKStrassenAlgorithmPerfTests : public ppc::util::BaseRunPerfTests { + public: + static constexpr std::size_t kMatrixSize = 256; + + protected: + void SetUp() override { + input_.a.assign(kMatrixSize, std::vector(kMatrixSize, 1.0)); + input_.b.assign(kMatrixSize, std::vector(kMatrixSize, 2.0)); + } + + InType GetTestInputData() final { + return input_; + } + + bool CheckTestOutputData(OutType & /*output_data*/) final { + return true; + } + + private: + InType input_; +}; + +TEST_P(GoriachevaKStrassenAlgorithmPerfTests, RunPerfModes) { + ExecuteTest(GetParam()); +} + +namespace { + +auto BuildPerfParams() { + const auto all_tasks = + ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_goriacheva_k_strassen_algorithm); + + return ppc::util::TupleToGTestValues(all_tasks); +} + +const auto kPerfParams = BuildPerfParams(); + +std::string MakePerfName(const ::testing::TestParamInfo &info) { + return GoriachevaKStrassenAlgorithmPerfTests::CustomPerfTestName(info); +} + +INSTANTIATE_TEST_SUITE_P(GoriachevaKStrassenAlgorithmPerfomanceTests, GoriachevaKStrassenAlgorithmPerfTests, + kPerfParams, MakePerfName); + +} // namespace +} // namespace goriacheva_k_strassen_algorithm