基于超算平台的矩阵乘法MPI并行实验

基于超算平台的矩阵乘法MPI并行实验

一、实验目的

  • 熟悉超算环境,掌握在超算上如何运行作业
  • 熟悉MPI编程环境,掌握MPI编程基本函数及MPI的相关通信函数用法;
  • 将通用矩阵乘法转为MPI并行实现,减少大型矩阵乘法计算所需的时间,进一步加深MPI的使用与理解

二、实践平台与环境:

本实验在国家超级计算郑州中心先进计算平台上进行,郑州中心于2020 年建成并投入运行,理论峰值计算能力达到 100PFlops,存储裸容量 100PB,网络系统带宽 200Gb/s,网络延迟微秒级;同时配备国产安全可靠的云计算平台、高性能计算集群管理调度平台、人工智能平台以及专业的在线运维平台,提供类型多样的先进计算服务;采用绿色节能的浸没式相变液冷冷却技术,PUE 值小于 1.04。

  • 系统平台:超算平台(郑州超算中心)

  • 加载的module:

    compiler/devtoolset/7.3.1

    mpi/hpcx/2.4.1/gcc-7.3.1

三、实验方案

(一)问题描述

通过MPI并行对两个大型矩阵A(MxN),B(NxK)做乘法得到矩阵C(MxK)

(二)方案描述

将矩阵A按行“平均”分配给所有进程,将整个矩阵B分配给所有进程,最终每个进程都将获得A的部分行和矩阵B,然后每个进程将自己所获得的A矩阵的行与B矩阵进行矩阵乘法运算,最终将每个进程所计算的结果进行组装,获得最终的矩阵C。

1、矩阵A的分配。由于矩阵A的行数可能不能够被总的进程数目整除,为尽可能的做到“平均”,且一次性将A矩阵的行数全部分给所有的进程,需要先计算每个进程分配的行数, 可以采用以下的方式计算每个进程分配的行数:

/*
    M:矩阵A的总的行数
    world_size: 总的进程数目
    lines: 数组,存放每个进程需要分配的行数
*/
int *lines = new int[world_size];
int equal_rows = M / world_size;
int remain_lines = M % world_size;
for (int i = 0; i < world_size; i++)
{
    int rows = equal_rows;
    if (remain_lines != 0)
    {
        rows++;
        remain_lines--;
    }
    lines[i] = rows;
}

然后可以采用集合通信的方式实现分配,这里采用MPI_Scatterv;MPI_Scatterv的语法如下:

其中两个关键的参数:

因此需要计算发送到每个进程的元素数,和要发送每个进程数据相对于矩阵A中元素的偏移,这里计算的方式就比较简单,不在过多叙述:

// 为每个rank分配发送数据相对于矩阵A的起始位置scatter_info.displs[i],
// 发送数据的个数scatter_info.displs[i]
DataInfo scatter_info = DataInfo(world_size);
int tmp = 0;
for (int i = 0; i < world_size; i++)
{
    scatter_info.cnts[i] = lines[i] * N;
    scatter_info.displs[i] = tmp;
    tmp += scatter_info.cnts[i];
}
double *split_A = new double[lines[rank] * N]; // 每个进程获得矩阵A的部分数据
MPI_Scatterv(Matrix_A, scatter_info.cnts, scatter_info.displs, MPI_DOUBLE,
                split_A, lines[rank] * N, MPI_DOUBLE, 0, MPI_COMM_WORLD);

2、矩阵B的分配。矩阵B的分配可以采用MPI_Bcast函数广播到每一个进程,其实现过程如下:

MPI_Bcast(Matrix_B, N * K, MPI_DOUBLE, 0, MPI_COMM_WORLD);

3、每个进程进行矩阵计算。

void AxB(double *A, double *B, int m, int n, int k, double *C) // A(mxn) B(nxk)
{
    for (int i = 0; i < m; i++)     // A 行数
        for (int j = 0; j < k; j++) // B 列数
        {
            double temp = 0;
            for (int p = 0; p < n; p++) // B 行数
            {
                temp += A[i * n + p] * B[p * k + j];
            }
            C[i * k + j] = temp;
        }
}
double *split_A = new double[lines[rank] * N]; // 每个进程获得矩阵A的部分数据
MPI_Scatterv(Matrix_A, scatter_info.cnts, scatter_info.displs, MPI_DOUBLE,
                split_A, lines[rank] * N, MPI_DOUBLE, 0, MPI_COMM_WORLD);

double *resultbuff = new double[lines[rank] * K]; // 存放每个进程的计算结果

AxB(split_A, Matrix_B, lines[rank], N, K, resultbuff);

4、结果的收集。由于每一个进程计算的结果长度不一致,因此采用MPI_Gatherv函数对结果进行组合,MPI_Gatherv的语法如下:

参数的使用与MPI_Scatterv类似,需要指定从每个进程获取的元素个数,以及从每个进程获取的元素需要存放的位置等。

DataInfo gatherinfo = DataInfo(world_size);
tmp = 0;
for (int i = 0; i < world_size; i++)
{
    gatherinfo.displs[i] = tmp;
    gatherinfo.cnts[i] = lines[i] * K;
    tmp += gatherinfo.cnts[i];
}
MPI_Gatherv(resultbuff, lines[rank] * K, MPI_DOUBLE, Matrix_C, gatherinfo.cnts,
            gatherinfo.displs, MPI_DOUBLE, 0, MPI_COMM_WORLD);

四、实验步骤与结果分析

1、加载并行计算环境

2、编译源文件

3、编写sbatch脚本 param.sh

4、运行脚本。

sbatch param.sh

5、实验时间分析。

依次修改脚本中param.sh中mpirun -np 16 t3 中的进程数16为8、4、2、1进行实验,其运算所需的时间分别为0.4s、0.7s、1.3s、2.9s、4.5s,可以看出当进程数目从16减为8、4、2、1时,计算所用的时间差不多增加为原来的二倍,说明并行进程数可以加快计算的速度。

在这里插入图片描述

6、计算结果准确度分析。当使用两个进程来计算两个小矩阵的成绩,从结果可以看出,采用并行计算的结果和不采用并行计算的结果相同。

五、总结

一直以来,我就对并行计算有很大的兴趣,通过分布式计算这门课,让我了解到了mpi并行技术,于是就按照教程[2]动手尝试做了一些简单的例子,最终用mpi实现了下矩阵的并行运算,并整理了这份课程报告,从中加深了我对mpi并行计算的理解,同时呢,对于C++编程又重新的熟悉了以下,另外也熟悉了超算的使用,相信这门课对我以后的学习具有很大的帮助!

六、参考文献

[1] Microsoft MPI:https://learn.microsoft.com/zh-cn/message-passing-interface/microsoft-mpi

[2] MPI Tutorial: https://mpitutorial.com/tutorials/mpi-hello-world/zh_cn/

七、完整代码

#include <stdio.h>
#include "mpi.h"
#include <stdlib.h>
#include <iostream>

struct DataInfo
{
    int *displs;
    int *cnts;
    DataInfo(int world_size)
    {
        this->displs = new int[world_size]();
        this->cnts = new int[world_size]();
    }
    ~DataInfo()
    {
        delete[] this->displs;
        delete[] this->cnts;
    }
};

void AxB(double *A, double *B, int m, int n, int k, double *C) // A(mxn) B(nxk)
{
    for (int i = 0; i < m; i++)     // A 行数
        for (int j = 0; j < k; j++) // B 列数
        {
            double temp = 0;
            for (int p = 0; p < n; p++) // B 行数
            {
                temp += A[i * n + p] * B[p * k + j];
            }
            C[i * k + j] = temp;
        }
}

void PrintMatrix(double *matrix, int m, int n, char *name)
{
    printf("%s:\n", name);
    for (int i = 0; i < m; i++)
    {
        for (int j = 0; j < n; j++)
        {
            printf("%f", matrix[i * n + j]);
        }
        printf("\n");
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    int M = 1000;
    int N = 1100;
    int K = 900;
    if (argc > 0) // 测试计算精度,用小矩阵
    {
        M = 5;
        N = 4;
        K = 3;
    }
    double *Matrix_A = new double[M * N]; // 矩阵A MxN
    double *Matrix_B = new double[N * K]; // 矩阵B NxK
    double *Matrix_C = new double[M * K]; // 结果矩阵

    int rank;               // process id number
    int world_size;         // number of process
    MPI_Init(&argc, &argv); // parallel init
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);
    double start, stop; // 计时时间

    /*
        M:矩阵A的总的行数
        world_size: 总的进程数目
        lines: 数组,存放每个进程需要分配的行数
    */
    int *lines = new int[world_size];
    int equal_rows = M / world_size;
    int remain_lines = M % world_size;
    for (int i = 0; i < world_size; i++)
    {
        int rows = equal_rows;
        if (remain_lines != 0)
        {
            rows++;
            remain_lines--;
        }
        lines[i] = rows;
    }

    if (rank == 0)
    {
        // 数组A,B赋值
        start = MPI_Wtime();
        for (int i = 0; i < M; i++)
        {
            for (int j = 0; j < N; j++)
                Matrix_A[i * N + j] = i + 1;
        }
        for (int i = 0; i < N; i++)
        {
            for (int j = 0; j < K; j++)
                Matrix_B[i * K + j] = j + 1;
        }
    }

    MPI_Bcast(Matrix_B, N * K, MPI_DOUBLE, 0, MPI_COMM_WORLD);

    // 为每个rank分配发送数据相对于矩阵A的起始位置scatter_info.displs[i],
    // 发送数据的个数scatter_info.displs[i]
    DataInfo scatter_info = DataInfo(world_size);
    int tmp = 0;
    for (int i = 0; i < world_size; i++)
    {
        scatter_info.cnts[i] = lines[i] * N;
        scatter_info.displs[i] = tmp;
        tmp += scatter_info.cnts[i];
    }
    double *split_A = new double[lines[rank] * N]; // 每个进程获得矩阵A的部分数据
    MPI_Scatterv(Matrix_A, scatter_info.cnts, scatter_info.displs, MPI_DOUBLE,
                 split_A, lines[rank] * N, MPI_DOUBLE, 0, MPI_COMM_WORLD);

    double *resultbuff = new double[lines[rank] * K]; // 存放每个进程的计算结果

    AxB(split_A, Matrix_B, lines[rank], N, K, resultbuff);

    DataInfo gatherinfo = DataInfo(world_size);
    tmp = 0;
    for (int i = 0; i < world_size; i++)
    {
        gatherinfo.displs[i] = tmp;
        gatherinfo.cnts[i] = lines[i] * K;
        tmp += gatherinfo.cnts[i];
    }
    MPI_Gatherv(resultbuff, lines[rank] * K, MPI_DOUBLE, Matrix_C, gatherinfo.cnts,
                gatherinfo.displs, MPI_DOUBLE, 0, MPI_COMM_WORLD);
    if (rank == 0 && argc > 0)
    {
        PrintMatrix(Matrix_A, M, N, "A");
        PrintMatrix(Matrix_B, N, K, "B");
        printf("mpi total processes: %d", world_size);
        PrintMatrix(Matrix_C, M, K, "mpi AXB");
        double *resultc = new double[M * K];
        AxB(Matrix_A, Matrix_B, M, N, K, resultc);
        PrintMatrix(resultc, M, K, "AXB");
        delete[] resultc;
    }

    delete[] split_A;
    delete[] resultbuff;
    delete[] Matrix_A;
    delete[] Matrix_B;
    delete[] Matrix_C;
    stop = MPI_Wtime();
    if (rank == 0 && argc == 0)
    {
        printf("consume time: %f s\n", stop - start);
    }
    MPI_Finalize();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值