并行矩阵乘法(C++ mpi 并行实现)

Myth丶恋晨 2024-03-17 10:53 152阅读 0赞

矩阵乘法有2种思路,我最先想到的是第一种思路,但是时间、空间复杂度都比较高。后面参考了一些资料,实现了第二种思路。

一、思路1:按行、列分块

矩阵乘法有一个很好的性质,就是结果矩阵的每个元素是不互相依赖的,因此我们可以很好地实现并行。

假如想要计算的矩阵如下:

在这里插入图片描述

如果有8个进程,0号进程用于数据分发,其它进程用于计算,具体的分块如下图所示:

在这里插入图片描述

但是这种思路实现后会报错 Out of memory,具体原因后面分析。

在这里插入图片描述

思路1因为只传递了单行的数组,而二维vector单行数组是连续的,因此这里直接用二维vector表示矩阵就可以,代码如下:

  1. #include <stdio.h>
  2. #include <mpi.h>
  3. #include<iostream>
  4. #include<vector>
  5. #include <cstdlib>
  6. using namespace std;
  7. int main(int argc, char* argv[])
  8. {
  9. int myrank, processNum; // myrank: 进程号; processNum: 进程总数
  10. char processor_name[MPI_MAX_PROCESSOR_NAME];
  11. int namelen;
  12. MPI_Init(&argc, &argv);
  13. MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
  14. MPI_Comm_size(MPI_COMM_WORLD, &processNum);
  15. MPI_Get_processor_name(processor_name, &namelen); // 获取处理器名称
  16. double startTime, endTime; // 开始时间和结束时间
  17. // 如果是 0 号进程,随机初始化矩阵数据
  18. int row = 3000, middleColumn = 200, column = 300; // 矩阵 A 的行数、矩阵 A 的列数和 B 的行数、矩阵 B 的行数
  19. if (myrank == 0) {
  20. // 地主进程
  21. vector<vector<int>> A(row, vector<int>(middleColumn));
  22. vector<vector<int>> B(middleColumn, vector<int>(column));
  23. // 随机初始化矩阵 A 和 B
  24. for (int i = 0; i < row; i++) {
  25. for (int j = 0; j < middleColumn; j++) {
  26. A[i][j] = rand() % 10;
  27. }
  28. }
  29. for (int i = 0; i < middleColumn; i++) {
  30. for (int j = 0; j < column; j++) {
  31. B[i][j] = rand() % 10;
  32. }
  33. }
  34. // 打印矩阵 A 和 B
  35. // cout << "A:" << endl;
  36. // for (int i = 0; i < row; i++) {
  37. // cout << " ";
  38. // for (int j = 0; j < middleColumn; j++) {
  39. // cout << A[i][j] << " ";
  40. // }
  41. // cout << endl;
  42. //}
  43. // cout << "B:" << endl;
  44. // for (int i = 0; i < middleColumn; i++) {
  45. // cout << " ";
  46. // for (int j = 0; j < column; j++) {
  47. // cout << B[i][j] << " ";
  48. // }
  49. // cout << endl;
  50. // }
  51. startTime = MPI_Wtime(); // 开始计时
  52. int farmersProcessNum = processNum - 1; // 农民进程数
  53. // 分发矩阵 A 和 B
  54. for (int i = 0; i < row; i++) {
  55. for (int j = 0; j < column; j++) {
  56. int destProcess = (i * row + j) % farmersProcessNum + 1; // 目标进程号
  57. MPI_Send(A[i].data(), middleColumn, MPI_INT, destProcess, 0, MPI_COMM_WORLD);
  58. vector<int> BColumn(middleColumn);
  59. for (int k = 0; k < middleColumn; k++) {
  60. BColumn[k] = B[k][j];
  61. }
  62. MPI_Send(BColumn.data(), middleColumn, MPI_INT, destProcess, 0, MPI_COMM_WORLD);
  63. }
  64. }
  65. }
  66. else {
  67. // 农民进程 (进程号 > 0 的进程)
  68. int count = (row * column) / (processNum - 1); // 每个进程计算的数量
  69. int remainder = (row * column) % (processNum - 1); // 均分后多出来的待计算数
  70. if (myrank <= remainder) {
  71. count++;
  72. }
  73. for (int i = 0; i < count; i++) {
  74. // 接收矩阵 ARow 和 BColumn
  75. vector<int> ARow(middleColumn);
  76. vector<int> BColumn(middleColumn);
  77. MPI_Recv(ARow.data(), middleColumn, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
  78. MPI_Recv(BColumn.data(), middleColumn, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
  79. // 计算矩阵乘法
  80. int result = 0;
  81. for (int j = 0; j < middleColumn; j++) {
  82. result += ARow[j] * BColumn[j];
  83. }
  84. // 发送计算结果
  85. MPI_Send(&result, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);
  86. }
  87. }
  88. // 收集计算结果
  89. if (myrank == 0) {
  90. vector<vector<int>> C(row, vector<int>(column));
  91. for (int i = 0; i < row; i++) {
  92. for (int j = 0; j < column; j++) {
  93. // 计算农民进程的数量
  94. int farmersProcessNum = processNum - 1;
  95. // 计算接收的进程号
  96. int sourceProcess = (i * row + j) % farmersProcessNum + 1;
  97. MPI_Recv(&C[i][j], 1, MPI_INT, sourceProcess, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
  98. }
  99. }
  100. endTime = MPI_Wtime(); // 结束计时
  101. // 打印矩阵 C
  102. //cout << "C:" << endl;
  103. //for (int i = 0; i < row; i++) {
  104. // cout << " ";
  105. // for (int j = 0; j < column; j++) {
  106. // cout << C[i][j] << " ";
  107. // }
  108. // cout << endl;
  109. //}
  110. // 打印计算时间
  111. cout << "Time: " << endTime - startTime << "s" << endl;
  112. }
  113. MPI_Finalize();
  114. return 0;
  115. }

二、思路2:矩阵分块乘法

如果我们想要计算的矩阵如下:

在这里插入图片描述

同时,如果我们采用4个进程来计算这个矩阵乘法,0号进程用于数据分发,其它进程用于计算,如图:

在这里插入图片描述

1维数组表示2维数组

思路2的算法实现,由于我们进行MPI_Send发送数组的时候,必须提供一个连续地址的数组的首地址,而思路2不同于思路1只发送1行数据,它是要发送多行的。

这就要求我们不可以使用二维数组来实现,因为二维数组不同行的首尾地址并不是连续的。具体见这篇文章:https://blog.csdn.net/weixin_44560698/article/details/120566680.

所以,这里我们使用1维数组,表示2维数组,代码如下:

  1. #include<iostream>
  2. #include<vector>
  3. #include<mpi.h>
  4. using namespace std;
  5. // 输入:两个一维vector,表示矩阵,输出矩阵的row,middleColumn,column
  6. // 输出:一维vector,表示两个输入vector的乘积
  7. vector<int> matrixMultiplication(vector<int> A, vector<int> B, int row, int middleColumn, int column) {
  8. vector<int> res(row * column);
  9. for (int i = 0; i < row; i++) {
  10. for (int j = 0; j < column; j++) {
  11. int tmp = 0;
  12. for (int k = 0; k < middleColumn; k++) {
  13. // A[i][k] * B[k][j]
  14. tmp += A[i * middleColumn + k] * B[k * column + j];
  15. }
  16. // res[i][j] = tmp
  17. res[i * column + j] = tmp;
  18. }
  19. }
  20. return res;
  21. }
  22. int main(int argc, char* argv[])
  23. {
  24. int myrank, processNum;
  25. char processor_name[MPI_MAX_PROCESSOR_NAME];
  26. int namelen;
  27. MPI_Init(&argc, &argv);
  28. MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
  29. MPI_Comm_size(MPI_COMM_WORLD, &processNum);
  30. MPI_Get_processor_name(processor_name, &namelen);
  31. // 如果是 0 号进程,随机初始化矩阵数据
  32. int row = 3000, middleColumn = 2000, column = 3000;
  33. int farmerProcessNum = processNum - 1; // farmer 进程的数量
  34. int chunkSize = row / farmerProcessNum; // 每个 farmer 进程处理的行数
  35. int chunkSizeRemainder = row % farmerProcessNum; // 均分后的余数
  36. double startTime, endTime; // 记录开始和结束时间
  37. if (myrank == 0) {
  38. // 地主进程
  39. vector<int> A(row * middleColumn);
  40. vector<int> B(middleColumn * column);
  41. // 随机初始化矩阵 A 和 B
  42. for (int i = 0; i < row * middleColumn; i++) {
  43. A[i] = rand() % 10;
  44. }
  45. for (int i = 0; i < middleColumn * column; i++) {
  46. B[i] = rand() % 10;
  47. }
  48. // 打印矩阵 A 和 B
  49. //cout << "A:" << endl;
  50. //for (int i = 0; i < row; i++) {
  51. // cout << " ";
  52. // for (int j = 0; j < middleColumn; j++) {
  53. // cout << A[i * middleColumn + j] << " ";
  54. // }
  55. // cout << endl;
  56. //}
  57. //cout << "B:" << endl;
  58. //for (int i = 0; i < middleColumn; i++) {
  59. // cout << " ";
  60. // for (int j = 0; j < column; j++) {
  61. // cout << B[i * column + j] << " ";
  62. // }
  63. // cout << endl;
  64. //}
  65. // 记录程序开始时间
  66. startTime = MPI_Wtime();
  67. int startRow = 0, endRow = 0; // 每个 farmer 进程处理的起始行和结束行
  68. // 将矩阵 A 和 B 分发给其他进程
  69. for (int i = 1; i < processNum; i++) {
  70. // 计算下一个进程处理的结束行
  71. endRow = startRow + chunkSize - 1;
  72. if (i <= chunkSizeRemainder) {
  73. endRow++;
  74. }
  75. int tmpRow = endRow - startRow + 1;
  76. // 将矩阵 A 部分的数据分发给其他进程
  77. MPI_Send(A.data() + startRow * middleColumn, tmpRow * middleColumn, MPI_INT, i, 0, MPI_COMM_WORLD);
  78. // 将矩阵 B 所有的数据分发给其他进程
  79. MPI_Send(B.data(), middleColumn * column, MPI_INT, i, 0, MPI_COMM_WORLD);
  80. // 更新下一个进程处理的起始行
  81. startRow = endRow + 1;
  82. }
  83. }
  84. else {
  85. // 农民进程 (进程号 >= 1)
  86. // 计算行数
  87. int tmpRow = chunkSize;
  88. if (myrank <= chunkSizeRemainder) {
  89. tmpRow++;
  90. }
  91. // 计算列数
  92. int tmpColumn = column;
  93. // 计算中间列数
  94. int tmpMiddleColumn = middleColumn;
  95. // 接收矩阵 A 的数据
  96. vector<int> A(tmpRow * tmpMiddleColumn);
  97. MPI_Recv(A.data(), tmpRow * tmpMiddleColumn, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
  98. // 接收矩阵 B 的数据
  99. vector<int> B(tmpMiddleColumn * tmpColumn);
  100. MPI_Recv(B.data(), tmpMiddleColumn * tmpColumn, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
  101. // 计算矩阵乘法
  102. vector<int> res = matrixMultiplication(A, B, tmpRow, tmpMiddleColumn, tmpColumn);
  103. // 将计算结果返回给 0 号进程
  104. MPI_Send(res.data(), tmpRow * tmpColumn, MPI_INT, 0, 0, MPI_COMM_WORLD);
  105. }
  106. // 0 号进程接收其他进程的计算结果
  107. if (myrank == 0) {
  108. vector<int> res(row * column);
  109. int startRow = 0, endRow = 0; // 每个 farmer 进程处理的起始行和结束行
  110. for (int i = 1; i < processNum; i++) {
  111. // 计算下一个进程处理的结束行
  112. endRow = startRow + chunkSize - 1;
  113. if (i <= chunkSizeRemainder) {
  114. endRow++;
  115. }
  116. int tmpRow = endRow - startRow + 1;
  117. // 将矩阵 A 部分的数据分发给其他进程
  118. MPI_Recv(res.data() + startRow * column, tmpRow * column, MPI_INT, i, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
  119. // 更新下一个进程处理的起始行
  120. startRow = endRow + 1;
  121. }
  122. // 记录程序结束时间
  123. endTime = MPI_Wtime();
  124. // 打印计算结果
  125. //cout << "res:" << endl;
  126. //for (int i = 0; i < row; i++) {
  127. // cout << " ";
  128. // for (int j = 0; j < column; j++) {
  129. // cout << res[i * column + j] << " ";
  130. // }
  131. // cout << endl;
  132. //}
  133. // 打印计算时间
  134. cout << "Time: " << endTime - startTime << "s" << endl;
  135. }
  136. MPI_Finalize();
  137. return 0;
  138. }

三、算法性能对比

1. 时间复杂度

思路一 300 × 200 300\times 200 300×200 和 200 × 300 200 \times 300 200×300 相乘所耗费的时间为8.83s。

在这里插入图片描述

相同规模的矩阵相乘,思路2的时间是0.07s。

在这里插入图片描述

可以看到差距还是很大的。

我们再来看一下串行程序的运行时间:

在这里插入图片描述
串行程序的运行时间是0.321s,看来如果我们采用思路一的并行方法就真有点得不偿失了。

这里给出我写的串行代码:

  1. #include<iostream>
  2. #include<vector>
  3. using namespace std;
  4. // 输入:两个一维vector,表示矩阵,输出矩阵的row,middleColumn,column
  5. // 输出:一维vector,表示两个输入vector的乘积
  6. vector<int> matrixMultiplication(vector<int> A, vector<int> B, int row, int middleColumn, int column) {
  7. vector<int> res(row * column);
  8. for (int i = 0; i < row; i++) {
  9. for (int j = 0; j < column; j++) {
  10. int tmp = 0;
  11. for (int k = 0; k < middleColumn; k++) {
  12. // A[i][k] * B[k][j]
  13. tmp += A[i * middleColumn + k] * B[k * column + j];
  14. }
  15. // res[i][j] = tmp
  16. res[i * column + j] = tmp;
  17. }
  18. }
  19. return res;
  20. }
  21. int main() {
  22. // 矩阵 A 的行数、矩阵 A 的列数和 B 的行数、矩阵 B 的行数
  23. int row = 300, middleColumn = 200, column = 300;
  24. vector<int> A(row * middleColumn);
  25. vector<int> B(middleColumn * column);
  26. // 随机初始化矩阵 A 和 B
  27. for (int i = 0; i < row * middleColumn; i++) {
  28. A[i] = rand() % 10;
  29. }
  30. for (int i = 0; i < middleColumn * column; i++) {
  31. B[i] = rand() % 10;
  32. }
  33. // 记录开始和结束时间
  34. double startTime, endTime;
  35. startTime = clock();
  36. vector<int> res = matrixMultiplication(A, B, row, middleColumn, column);
  37. endTime = clock();
  38. cout << "Time: " << (endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
  39. // 输出 res
  40. //for (int i = 0; i < row; i++) {
  41. // for (int j = 0; j < column; j++) {
  42. // cout << res[i * column + j] << " ";
  43. // }
  44. // cout << endl;
  45. //}
  46. //cout << endl;
  47. }

我们将矩阵的大小扩大10倍,再来看:

思路1运行时间:

在这里插入图片描述

可以看到思路1直接out of memory了,其实是在意料之中的,具体原因可以看文章最后一部分 四、复杂度分析

思路2运行时间:

在这里插入图片描述

串行运行时间:

在这里插入图片描述

2. 空间复杂度

四、复杂度分析

首先,我们设:

r o w : 矩阵 A 行数 row:矩阵A行数 row:矩阵A行数

m i d d l e C o l u m n : 矩阵 A 列数和矩阵 B 行数 middleColumn:矩阵A列数和矩阵B行数 middleColumn:矩阵A列数和矩阵B行数

c o l u m n : 矩阵 B 列数 column:矩阵B列数 column:矩阵B列数

f a r m e r P r o c e s s N u m : 农民进程数 farmerProcessNum:农民进程数 farmerProcessNum:农民进程数

由于mpi主要是在并行程序之间进行通信。

对于思路1,是每次计算结果矩阵的一个元素,因此,它的总发送的数据量如下:

r o w × c o l u m n × m i d d l e C o l u m n 2 ( i n t ) row\times column \times middleColumn^2\quad (int) row×column×middleColumn2(int)

对于思路2,总发送的是A矩阵的所有+B矩阵的全部乘以进程数(因为每次都要发送B矩阵),如下:

r o w × m i d d l e C o l u m n + f a r m e r P r o c e s s N u m × m i d d l e C o l u m n × c o l u m n row\times middleColumn+farmerProcessNum\times middleColumn\times column row×middleColumn+farmerProcessNum×middleColumn×column

我们带入 r o w = 3000 , m i d d l e C o l u m n = 2000 , c o l u m n = 3000 row=3000,middleColumn=2000,column=3000 row=3000,middleColumn=2000,column=3000来计算一下上述思路1需要传输的内存:
r o w × c o l u m n × m i d d l e C o l u m n 2 ( i n t ) = 3000 × 3000 × 2000 × 2000 ( i n t ) = 36 0000 0000 0000 ( i n t ) = 4 × 36 0000 0000 0000 ( B y t e ) ≈ 131 ( T B ) \begin{aligned} &row\times column \times middleColumn^2\quad (int) \\ =&3000\times 3000 \times 2000\times 2000\quad (int) \\ =&36\quad 0000\quad 0000\quad 0000\quad (int) \\ =&4\times 36 \quad 0000\quad 0000\quad 0000\quad (Byte)\\ \approx&131 \quad (TB) \end{aligned} ===≈row×column×middleColumn2(int)3000×3000×2000×2000(int)36000000000000(int)4×36000000000000(Byte)131(TB)

具体转化结果如下:
在这里插入图片描述

传输的数据不管是传给哪个进程都是要放在内存里的,因此这种方式的空间数量级根本不可取= =。

再来看下思路2:

r o w × m i d d l e C o l u m n + f a r m e r P r o c e s s N u m × m i d d l e C o l u m n × c o l u m n ( i n t ) = 3000 × 2000 + 7 × 2000 × 3000 ( i n t ) = 4800 0000 ( i n t ) = 19200 0000 ( B y t e ) \begin{aligned} &row\times middleColumn+farmerProcessNum\times middleColumn\times column \quad (int) \\ =&3000\times 2000+7 \times 2000 \times 3000 \quad (int) \\ =&4800\quad 0000 \quad (int) \\ =&19200 \quad 0000 \quad (Byte) \end{aligned} ===row×middleColumn+farmerProcessNum×middleColumn×column(int)3000×2000+7×2000×3000(int)48000000(int)192000000(Byte)

在这里插入图片描述

可以看到 思路2 消耗的内存仅有 183MB ,与思路1131TB 的空间复杂度完全不在一个数量级。

结论:所以对于矩阵并行乘法,思路1是不可行的,至少也是应该是采用思路2来进行实现的。

发表评论

表情:
评论列表 (有 0 条评论,152人围观)

还没有评论,来说两句吧...

相关阅读