In this task, you will design and implement a parallel partitioning algorithm for matrix
 multiplication using a hybrid MPI and POSIX/OMP approach.
 a) Modify the parallel code from Task 2 such that:
 i)
 The root process uses POSIX/OMP threads to read each input matrix binary
 file into memory. You are required to investigate/research parallel file reading
 with binary files in C.
 ii)
 iii)
 Each MPI process will use POSIX/OMP threads to compute the matrix
 multiplication based on the partitioned method. In this context, you will use
 fewer MPI processes with one MPI process running threads to compute the
 parallel matrix multiplication. For instance, in task 2 above, if you have 4 CPU
 cores (or logical processes), you will execute mpirun with 4 processes. In task
 3, if you have the same 4 CPU cores, you can execute mpirun with 2
 processes, with each MPI process executing two POSIX/OMP threads to
 compute the parallel matrix multiplication. You can specify a fixed number of
 threads per MPI process. However, you should not fix the number of MPI
 processes in your design. Please refer to Figure 3 for a sample depiction of
 this method.
 The root process uses POSIX/OMP threads to write the matrix multiplication
 result into the binary file (i.e., parallel file write).
 b) Compile and execute the parallel code.
 i)
 Update the parallel time using Hybrid MPI-POSIX/OMP in Table 3 and
 compute the actual speed up.
 ii)
 Measure the time required to send and receive the messages between the
 root process and other processes and include it in Table 3 (i.e., MPI
 communication time).
 c) Compare and analyse the actual speed up versus the theoretical speed up. Include
 any observations on the differences between the theoretical and actual speed-ups
 and the impact of communication time on the speed-up. Analyse if the
 communication time in Table 3 is any better than the communication time in Table 2.
 Provide some reasoning here

this is the code I used:

// MatrixMul_MPI_Tile_Simple.c
// Parallel matrix multiplication using MPI with tile-based data distribution.

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
#include <time.h>

#define MASTER 0

int main(int argc, char *argv[])
{
    int rank, num_procs;
    int i, j, k;

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &num_procs);

    /* Clock information */
    struct timespec start, end, start_compute, end_compute;
    double time_taken, time_taken_compute;

    // Start overall timer
    if (rank == MASTER) {
        clock_gettime(CLOCK_MONOTONIC, &start);
        printf("Matrix Multiplication using MPI and Tile-Based Data Distribution - Start\n\n");
    }

    // Variables for matrix dimensions
    int rowA = 0, colA = 0;
    int rowB = 0, colB = 0;

    int *pMatrixA = NULL;
    int *pMatrixB = NULL;

    // Root process reads matrices A and B from binary files
    if (rank == MASTER) {
        // 1. Read Matrix A
        printf("Reading Matrix A - Start\n");
        FILE *pFileA = fopen("MA_1500x1500.bin", "rb");
        fread(&rowA, sizeof(int), 1, pFileA);
        fread(&colA, sizeof(int), 1, pFileA);

        pMatrixA = (int *)malloc(rowA * colA * sizeof(int));
        for (i = 0; i < rowA; i++){
            fread(&pMatrixA[i * colA], sizeof(int), colA, pFileA);
        }
        fclose(pFileA);
        printf("Reading Matrix A - Done\n");

        // 2. Read Matrix B
        printf("Reading Matrix B - Start\n");
        FILE *pFileB = fopen("MB_1500x1500.bin", "rb");
        fread(&rowB, sizeof(int), 1, pFileB);
        fread(&colB, sizeof(int), 1, pFileB);

        pMatrixB = (int *)malloc(rowB * colB * sizeof(int));
        for (i = 0; i < rowB; i++){
            fread(&pMatrixB[i * colB], sizeof(int), colB, pFileB);
        }
        fclose(pFileB);
        printf("Reading Matrix B - Done\n");

        if (colA != rowB) {
            printf("Matrix dimensions mismatch for multiplication!\n");
            MPI_Abort(MPI_COMM_WORLD, -1);
            exit(-1);
        }
    }

    // Broadcast dimensions to all processes
    MPI_Bcast(&rowA, 1, MPI_INT, MASTER, MPI_COMM_WORLD);
    MPI_Bcast(&colA, 1, MPI_INT, MASTER, MPI_COMM_WORLD);
    MPI_Bcast(&rowB, 1, MPI_INT, MASTER, MPI_COMM_WORLD);
    MPI_Bcast(&colB, 1, MPI_INT, MASTER, MPI_COMM_WORLD);

    int rowC = rowA;
    int colC = colB;

    // Determine the tile size (for simplicity, we'll divide rows of A and columns of B)
    int rows_per_proc = rowA / num_procs;
    int extra_rows = rowA % num_procs;

    int my_row_start = rank * rows_per_proc + (rank < extra_rows ? rank : extra_rows);
    int my_num_rows = rows_per_proc + (rank < extra_rows ? 1 : 0);

    // Allocate memory for local matrices
    int *local_A = (int *)malloc(my_num_rows * colA * sizeof(int));
    int *local_C = (int *)calloc(my_num_rows * colC, sizeof(unsigned long long));

    // Distribute A
    int *sendcounts_A = NULL;
    int *displs_A = NULL;
    if (rank == MASTER) {
        sendcounts_A = (int *)malloc(num_procs * sizeof(int));
        displs_A = (int *)malloc(num_procs * sizeof(int));

        int offset = 0;
        for (int i = 0; i < num_procs; i++) {
            int rows = rows_per_proc + (i < extra_rows ? 1 : 0);
            sendcounts_A[i] = rows * colA;
            displs_A[i] = offset * colA;
            offset += rows;
        }
    }

    MPI_Scatterv(pMatrixA, sendcounts_A, displs_A, MPI_INT, local_A, my_num_rows * colA, MPI_INT, MASTER, MPI_COMM_WORLD);

    // Broadcast B to all processes (since all processes need B)
    if (rank != MASTER) {
        pMatrixB = (int *)malloc(rowB * colB * sizeof(int));
    }
    MPI_Bcast(pMatrixB, rowB * colB, MPI_INT, MASTER, MPI_COMM_WORLD);

    // Start computation timer
    MPI_Barrier(MPI_COMM_WORLD);
    clock_gettime(CLOCK_MONOTONIC, &start_compute);

    // Local matrix multiplication
    for (i = 0; i < my_num_rows; i++){
        for (j = 0; j < colC; j++){
            unsigned long long sum = 0;
            for (k = 0; k < colA; k++){
                sum += (unsigned long long)local_A[i * colA + k] * (unsigned long long)pMatrixB[k * colB + j];
            }
            local_C[i * colC + j] = sum;
        }
    }

    // End computation timer
    clock_gettime(CLOCK_MONOTONIC, &end_compute);
    time_taken_compute = (end_compute.tv_sec - start_compute.tv_sec) + (end_compute.tv_nsec - start_compute.tv_nsec) / 1e9;

    if (rank == MASTER) {
        printf("Matrix Multiplication - Done\n");
        printf("Matrix Multiplication time: %f seconds\n", time_taken_compute);
    }

    // Gather the results back to the root process
    int *recvcounts_C = NULL;
    int *displs_C = NULL;
    unsigned long long *pMatrixC = NULL;
    if (rank == MASTER) {
        pMatrixC = (unsigned long long *)malloc(rowC * colC * sizeof(unsigned long long));
        recvcounts_C = (int *)malloc(num_procs * sizeof(int));
        displs_C = (int *)malloc(num_procs * sizeof(int));

        int offset = 0;
        for (int i = 0; i < num_procs; i++) {
            int rows = rows_per_proc + (i < extra_rows ? 1 : 0);
            recvcounts_C[i] = rows * colC;
            displs_C[i] = offset * colC;
            offset += rows;
        }
    }

    MPI_Gatherv(local_C, my_num_rows * colC, MPI_UNSIGNED_LONG_LONG, pMatrixC, recvcounts_C, displs_C, MPI_UNSIGNED_LONG_LONG, MASTER, MPI_COMM_WORLD);

    // Root process writes the result to binary file
    if (rank == MASTER) {
        // 4. Write results to a new file
        printf("Writing Resultant Matrix C to File - Start\n");
        FILE *pFileC = fopen("MC.bin", "wb");
        fwrite(&rowC, sizeof(int), 1, pFileC);
        fwrite(&colC, sizeof(int), 1, pFileC);
        for (i = 0; i < rowC; i++){
            fwrite(&pMatrixC[i * colC], sizeof(unsigned long long), colC, pFileC);
        }
        fclose(pFileC);
        printf("Write Resultant Matrix C to File - Done\n");

        clock_gettime(CLOCK_MONOTONIC, &end);
        time_taken = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
        printf("Overall time (Including read, multiplication and write)(s): %lf\n", time_taken);
    }

    // Clean up
    free(local_A);
    free(local_C);
    free(pMatrixB);
    if (rank == MASTER) {
        free(pMatrixA);
        free(pMatrixC);
        free(sendcounts_A);
        free(displs_A);
        free(recvcounts_C);
        free(displs_C);
    }

    if (rank == MASTER) {
        printf("Matrix Multiplication using MPI and Tile-Based Data Distribution - Done\n");
    }

    MPI_Finalize();
    return 0;
}