HPC

HPC lab1 MPI并行矩阵乘法

Posted by RogersLuo's Blog on October 9, 2024

高性能计算程序设计(1) 秋季2024

课程名称:高性能计算程序设计 批改人:

| 实验 | 高性能计算程序设计基础(0) | 专业(方向) | 信息与计算科学 | | —– | ————————— | ———— | —————— | | 学号 | 22336173 | 姓名 | 罗弘杰 | | Email | 3133974071@qq.com | 完成日期 | 10/09 |

通过MPI实现通用矩阵乘法

通过MPI点对点通信的方式实现通用矩阵乘法(Lab1),MPI并行进程(rank size)从1增加至8,矩阵规模从512增加至2048.

通用矩阵乘法(GEMM)通常定义为:

\[C = AB\] \[C_{m,n} = \sum_{n = 1}^{N}{A_{m,n}B_{n,k}}\]

输入:M , N, K三个整数(512 ~2048)

问题描述:随机生成M*N和N*K的两个矩阵A,B,对这两个矩阵做乘法得到矩阵C.

输出:A,B,C三个矩阵以及矩阵计算的时间

基于MPI的通用矩阵乘法优化

分别采用MPI点对点通信和MPI集合通信实现矩阵乘法中的进程之间通信,并比较两种实现方式的性能。如有余力,可以进一步尝试用mpi_type_create_struct聚合MPI进程内变量后通信。

点对点通信:mp_v1.cpp

0
1
2
3
4
5
6
7
//#0进程发送数据
    for (int i = 0; i < comm_sz - 1; i++) { // 分配矩阵A的行
        begin_Arow = i * avg_rows;
        end_Arow = (i + 1 == comm_sz - 1) ? M : (i + 1) * avg_rows;
        MPI_Send(&end_Arow, 1, MPI_INT, i + 1, 10, MPI_COMM_WORLD);
        MPI_Send(&A[begin_Arow * N], (end_Arow - begin_Arow) * N, MPI_FLOAT, i + 1, 1, MPI_COMM_WORLD);
        MPI_Send(B, N * K, MPI_FLOAT, i + 1, 2, MPI_COMM_WORLD); // 发送整个B矩阵
    }
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
else { // 其他进程接收数据并计算
        MPI_Recv(&end_Arow, 1, MPI_INT, 0, 10, MPI_COMM_WORLD, &status);
        begin_Arow = avg_rows * (my_rank - 1);

        float *localA = (float *)malloc((end_Arow - begin_Arow) * N * sizeof(float));
        float *localB = (float *)malloc(N * K * sizeof(float));
        float *localC = (float *)malloc((end_Arow - begin_Arow) * K * sizeof(float));
    	//创建本地矩阵空间
        MPI_Recv(localA, (end_Arow - begin_Arow) * N, MPI_FLOAT, 0, 1, MPI_COMM_WORLD, &status);
        MPI_Recv(localB, N * K, MPI_FLOAT, 0, 2, MPI_COMM_WORLD, &status);

        matrix_multiply_s(end_Arow - begin_Arow, N, K, localA, localB, localC); // 计算

        MPI_Send(localC, (end_Arow - begin_Arow) * K, MPI_FLOAT, 0, 3, MPI_COMM_WORLD); // 发送结果

        free(localA);
        free(localB);
        free(localC);
    }

集合通信:mp_v2.cpp

​不同于点对点通信中,0号不计算只负责管理信息发送接受,集合通信中0号进程也要负责计算,使用scatter,bcast,gather等集合通信函数实现信息传递;

​不同于点对点,集合通信要求发送指针和接受指针是两个进程中都创建了的。

0
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
    float *A = (float *)malloc(M * N * sizeof(float));
    float *localA = (float *)malloc(split_num * N * sizeof(float));
    float *B = (float *)malloc(N * K * sizeof(float));
    float *localC = (float *)malloc(split_num * K * sizeof(float));
    float *C = (float *)malloc(M * K * sizeof(float));
    double start ,end; //以上是所有进程都要创建的变量
	...
    else{ // 0号进程负责分配任务


        MPI_Bcast(B, N * K, MPI_FLOAT, 0,  MPI_COMM_WORLD); // 发送整个B矩阵
        MPI_Scatter(A, split_num*N, MPI_FLOAT, localA, split_num*N, MPI_FLOAT, 0, MPI_COMM_WORLD);
        
        matrix_multiply_s(split_num, N, K, localA, B, localC); // 计算

        MPI_Gather(localC, split_num * K, MPI_FLOAT, C, split_num * K, MPI_FLOAT, 0, MPI_COMM_WORLD);
        if(my_rank ==0 ){
            double end = MPI_Wtime();
            printf("computing time: %.8lf\n", end-start);
            // printf("%f,%f,%f,%f", C[0], C[K-1], C[(M-1)*K],C[(M-1)*K+K-1]);
        }

        free(A);
        free(B);
        free(C);
        free(localC);
        free(localA);

    } 

性能比较:

​用8核心计算4096矩阵乘法:

点对点通信:

image-20241009104851529

集合通信:

image-20241009104707962

​优化后的矩阵乘法计算时间更少。

将 “实验0” 改造成矩阵乘法库函数

将Lab0的单进程矩阵乘法改造为一个标准的库函数 matrix_multiply(函数实现文件和函数头文件),输入参数为三个完整定义矩阵(A,B,C),定义方式没有具体要求,可以是二维矩阵,也可以是struct等。在Linux系统中将此函数编译为.so文件,由其他程序调用。

0
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
37
38
39
40
41
42
43
44
//matrix_multiply_s.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>


/*
    输入三个矩阵的维度,以及三个矩阵的二维矩阵,输出计算时间
*/

extern void matrix_multiply_s(int M, int N, int K, float* A, float* B, float* C);



// matrix_multiply_s.cpp
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void matrix_multiply_s(int M, int N, int K, float* A, float* B, float* C) {
/*
    输入三个矩阵的维度,以及三个矩阵的指针,输出计算时间
*/
    for (int i = 0; i < M; ++i) {
        for (int j = 0; j < K; ++j) {
            C[i*M+j] = 0;
        }
    }
    clock_t start, end;
    start = clock();

    for (int i = 0; i < M; i++) {
        for (int k = 0; k < N; k++){
            for (int j = 0; j < K; j++){  
                C[i*K+j] += A[i*N+k] * B[k*K+j];
            }
        }
    }
    end = clock();
    printf("C Matrix Multiplying in sequence of %d*%d and %d*%d use time of %f seconds\n", M, N, N, K, (double)(end - start) / CLOCKS_PER_SEC);

    return ;
}

编译为共享库, libmm.so,并复制到本地的第三方库路径

0
1
2
g++ -c -fPIC matrix_multiply_s.cpp 
g++ -shared -o libmm.so matrix_multiply_s.o
sudo cp libmm.so /usr/local/lib

构造MPI版本矩阵乘法加速比和并行效率表

参考下图,分别构造MPI版本的标准矩阵乘法和优化后矩阵乘法的加速比和并行效率表格。并分类讨论两种矩阵乘法分别在强扩展和弱扩展情况下的扩展性。

加速比=串行计算时间/并行时间

并行效率=加速比/核心数

表一:使用点对点通信得出的加速比、并行效率、计算时间

注意到:linux下的mpi程序运行时,需要在命令行中指定核心数,并且核心数目是小于等于物理核心数目的,否则会出现错误。可以使用–use-hwthread-cpus参数来指定使用的硬件的线程数, 或者使用–oversubscribe参数来使用更多进程.

Comm_size (num of processes) Order of Matrix (Speedups/Parallel_ Efficiency/Seconds)        
  128 256 512 1024 2048
1 0.00839543 0.04692694 0.43021884 3.94523671 28.39599224
2 1.39/0.695/0.006037 0.8938/0.4469/0.05250491 1.0585/0.5293/0.40643450 1.0497/0.5248/3.75847875 1.0553/0.5276/26.90824521
4 1.72/0.43/0.00489500 1.7735/0.4434/0.02645970 2.8156/0.7039/0.15279611 2.1797/0.5449/1.80998449 1.8288/0.4572/15.52743378
8 2.08/0.26/0.00403109 1.8052/0.2257/0.02599474 3.7905/0.4738/0.11349913 3.6631/0.4579/1.07702992 3.5946/0.4493/7.89957483
16 0.9570/0.0598/0.00877253 1.6398/0.1025/0.02861832 3.9502/0.2469/0.05893468 9.0532/0.5658/0.43578183 8.8971/0.5561/3.19160100

表二:使用集合通信得出的加速比、并行效率、计算时间

Comm_size (num of processes) Order of Matrix (Speedups/Parallel_ Efficiency/Seconds)        
  128 256 512 1024 2048
1 0.00839543 0.04692694 0.37274516 3.94523671 28.39599224
2 2.5291/1.2646/0.00295841 1.1819/0.5909/0.03970544 2.0352/1.0176/0.18314764 2.1280/1.0640/1.85398334 2.0434/1.0217/13.89635362
4 3.0287/0.7572/0.00247196 3.7555/0.9389/0.01249551 3.4087/0.8522/0.10935211 2.7355/0.6839/1.44225968 3.0725/0.7681/9.24201751
8 3.7759/0.4720/0.00708678 2.4674/0.3084/0.01901847 3.6371/0.4546/0.10248491 4.2989/0.5374/0.91772155 4.3048/0.5381/6.59630985
16 5.0051/0.3128/0.0017 3.7750/0.2359/0.0124 7.0536/0.4408/0.0591 10.4621/0.6539/0.3771 9.4553/0.5910/3.0032

在并行计算中,强扩展性和弱扩展性是衡量并行算法性能的两个重要概念:

  1. 强扩展性:指在固定的问题规模下,随着处理器数量的增加,程序的运行时间减少的程度。理想情况下,处理器数量翻倍时,运行时间应减半。如果能够实现这一目标,则称为具有良好的强扩展性。在这里应该竖着看表格,关注任务量一定,核心数增加一倍,运行时间是否线性减少!
  2. 弱扩展性:指在处理器数量增加的同时,问题规模也随之增加,要求每个处理器所处理的工作量保持不变。在这种情况下,程序的运行时间应保持相对稳定。理想情况下,增加处理器数量的同时,处理时间不应显著增加。在这里应该按照对角线斜着看表格,关注在核心数目增加,任务量对应增加时,计算时间是否能保持不变,并行效率保持在合理水平

强扩展性:

​同比两个表格的对应每一列可以看到,优化后的矩阵乘法加速比更高,并行效率更好。说明优化后的矩阵乘法通信消耗更少,在强扩展性的工况下,扩展性更好,也就是增加核心数有更好收益。

弱扩展性:

​斜着看两个表格(表格涂黑的数据),在核心数乘2的时候,任务量乘2, 优化后的矩阵乘法同样加速比更高,并行效率也更好,说明在弱扩展性的工况下,优化后的矩阵乘法在增加核心数时,有更好收益。

Creative Commons License本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.