利用MPI实现Cannon算法并行矩阵乘法

C a n o n   Canon\, Canon算法是并行矩阵乘法的经典算法,将多个处理器排列成二维网格,采用二维块划分的方法将矩阵分给不同的处理器计算各自的局部结果,以此来加速计算。在本文中,为方便起见,示例程序中的矩阵均为 n   n\, n阶方阵,处理器的数量为2的幂次,确保每个矩阵得到的局部矩阵的元素个数相同。

一、二维矩阵串行乘法

两个 n   n\, n维方阵的乘法 A × B = C   A\times B=C\, A×B=C的串行计算公式为:
C i j = ∑ k = 0 n − 1 A i k ⋅ B k j C_{ij} = \sum_{k=0}^{n-1} A_{ik} \cdot B_{kj} Cij=k=0n1AikBkj程序可以表示为:

void Multiply(double* A, double* B, double* C, int n)
{
	for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++)
			for (int k = 0; k < n; k++)
				C[i * n + j] += A[i * n + k] * B[j + k * n];
}

程序将二维矩阵用一维矩阵线性展开,用一维数组来模拟二维数组。

二、Cannon算法

并行化二维矩阵乘法的一种思想是二维块划分方法,将 p   p\, p 个进程( p   p\, p为完全平方数)排列成 p × p   \sqrt[]{p}\times\sqrt[]{p}\, p ×p 二维网格,然后将矩阵 A 、 B 、 C   A、B、C\, ABC都分成 p × p   \sqrt[]{p}\times\sqrt[]{p}\, p ×p 块,均匀分布在网格上,矩阵第 ( i , j )   (i,j)\, (i,j)个子块分别记为 A i j   A_{ij}\, Aij B i j   B_{ij}\, Bij C i j   C_{ij}\, Cij,存储在二维进程网格上坐标为 ( i , j )   (i,j)\, (i,j)的进程 P i j   P_{ij}\, Pij上。计算 C i j   C_{ij}\, Cij时要将 A i k   A_{ik}\, Aik(第 i   i\, i行进程上的所有 A   A\, A的子块)和 B k j   B_{kj}\, Bkj(第 j   j\, j列进程上的所有 B   B\, B的子块)都收集到进程 P i j   P_{ij}\, Pij上,再计算 C i j   C_{ij}\, Cij,公式可以表达为:
C i j = ∑ k = 0 p − 1 A i k ⋅ B k j C_{ij} = \sum_{k=0}^{\sqrt[]{p}-1} A_{ik} \cdot B_{kj} Cij=k=0p 1AikBkj如下图所示:
二维块划分

然而每一个进程都重复收集 A i k   A_{ik}\, Aik B k j   B_{kj}\, Bkj会造成空间的大量浪费,时间效率也比较低,于是有了更高效的 C a n o n   Canon\, Canon算法,其表达式为:
C i j = ∑ k = 0 p − 1 A i , ( i + j + k ) % p ⋅ B ( i + j + k ) % p , j C_{ij} = \sum_{k=0}^{\sqrt[]{p}-1} A_{i,(i+j+k)\%\sqrt[]{p}} \cdot B_{(i+j+k)\%\sqrt[]{p},j} Cij=k=0p 1Ai,(i+j+k)%p B(i+j+k)%p ,j C a n o n   Canon\, Canon算法基本思想是,每一个进程只存储 A   A\, A B   B\, B C   C\, C矩阵的一个子块,本地相对应的 A   A\, A B   B\, B子块相乘后将结果累加到本地 C   C\, C子块上,然后再与其他进程交换 A   A\, A B   B\, B子块,继续相乘将结果累加到本地 C   C\, C子块上。但是要保证对应的 A   A\, A B   B\, B子块相乘,不能错位,我们注意到,在最开始, P i j   P_{ij}\, Pij上的 A   A\, A为所在行的第 j   j\, j个, B   B\, B为所在列的第 i   i\, i个, A   A\, A B   B\, B子块并不对应,因此将一行上的 A   A\, A子块循环向左移动 i   i\, i格,一列上的 B   B\, B子块循环向上移动 j   j\, j格,这样 A   A\, A B   B\, B子块就能对应了,以后每执行一次计算,每行所有 A   A\, A子块循环向左移动1格,每列上的 B   B\, B子块循环向上移动1格, A   A\, A B   B\, B子块相乘的结果累加在本地的 C   C\, C子块上。
排列

算法的个步骤表示如下:

1、第一次重排列

k = 0 k=0 k=0时, A i , ( i + j ) % p A_{i,(i+j)\%\sqrt[]{p}} Ai,(i+j)%p 并不处于 P i j   P_{ij}\, Pij上,需要对齐,于是 A i , ( i + j ) % p A_{i,(i+j)\%\sqrt[]{p}} Ai,(i+j)%p 传送到 P i j   P_{ij}\, Pij上,具体的实现方式是,二维网格上每一行的进程都将 A   A\, A子块循环向左移位,第 i   i\, i行的所有进程将 A   A\, A子块循环向左移位 i   i\, i个单位;同时 B ( i + j ) % p , j B_{(i+j)\%\sqrt[]{p},j} B(i+j)%p ,j并不处于 P i j   P_{ij}\, Pij上, B ( i + j ) % p , j B_{(i+j)\%\sqrt[]{p},j} B(i+j)%p ,j传送到 P i j   P_{ij}\, Pij上,第 j   j\, j列的所有进程将 B   B\, B子块循环向上移位 j   j\, j个单位,如下图所示:
初始排列
得到的第一次重排列后的矩阵排列为:
第一次重排列后
每个进程得到初次重排列后的 A   A\, A B   B\, B子块后,将 A   A\, A B   B\, B子块相乘的结果累加在本地的 C   C\, C子块上。

2、后续重排列

以后每进行一次计算,每行进程的 A   A\, A子块都循环向左移动一个单位,每列进程的 B   B\, B子块都循环的向上移动一个单位,如下图所示, A   A\, A B   B\, B子块相乘的结果累加在本地的 C   C\, C子块上,该步骤重复 p − 1 , \sqrt[]{p}-1, p 1,次。
重排列
最后进程 P i j   P_{ij}\, Pij上能够得到本地的结果 C i j   C_{ij}\, Cij

二、程序实现

1、创建二维进程拓扑结构

创建进程二维拓扑结构的函数为:

dims[0] = dims[1] = sqrt(comm_size);
periods[0] = periods[1] = 1;
MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 1, &comm_2d);

c o m m _ s i z e   comm\_size\, comm_size为进程的总数量, d i m s [ 2 ]   dims[2]\, dims[2]数组表示二维拓扑结构中每一维的大小, p e r i o d [ 2 ]   period[2]\, period[2]全部设置成1,表示拓扑结构的第 i   i\, i维有环绕接。这样我们得到了新的进程通讯器 c o m m _ 2 d   comm\_2d\, comm_2d。由于每一个进程都会被分配一个进程号以及进程坐标,从进程号获取进程坐标的函数如下:

MPI_Cart_coords(comm_2d, myrank, 2, mycoords);

m y r a n k   myrank\, myrank是进程序号, m y c o o r d s   mycoords\, mycoords是大小为2的一维数组。

2、输入输出矩阵

输入输出矩阵均为 8 × 8   8\times8\, 8×8矩阵, A   A\, A B   B\, B矩阵均为正交矩阵,且 B = A T   B=A^{T}\, B=AT A   A\, A矩阵为:
A矩阵
计算结果应该可以得到一个单位矩阵。

3、主程序

每个进程保存的本地矩阵子块分别为 l o c a l _ A   local\_A\, local_A l o c a l _ B   local\_B\, local_B l o c a l _ C   local\_C\, local_C,方便起见,进程的数量设为1、4、16、64这4种情况中的一种。

#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<math.h>
#include<windows.h>
#include<mpi.h>

#define pi acos(-1)

int myrank, comm_size, srcrank, dstrank;
int dims[2], periods[2], mycoords[2], coords[2];
int n = 8, local_n;
MPI_Comm comm_2d;

void Multiply(double* A, double* B, double* C, int n);
void GenerateData(MPI_Comm comm_2d, double* local_A, double* local_B);
void Cannon(MPI_Comm comm_2d, double* local_A, double* local_B, double* local_C);
void GatherResult(MPI_Comm comm_2d, double* local_C);

int main()
{
	double* local_A, * local_B, * local_C;

	MPI_Init(NULL, NULL);
	MPI_Comm_size(MPI_COMM_WORLD, &comm_size);
	MPI_Comm_rank(MPI_COMM_WORLD, &myrank);

	if (myrank == 0) printf("Number of Process: %d\n", comm_size);

	double start = MPI_Wtime();
	
	dims[0] = dims[1] = sqrt(comm_size);
	periods[0] = periods[1] = 1;
	MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 1, &comm_2d);

	MPI_Comm_rank(comm_2d, &myrank);
	MPI_Cart_coords(comm_2d, myrank, 2, mycoords);

	local_n = n / dims[0];
	local_A = (double*)malloc(sizeof(double) * local_n * local_n);
	local_B = (double*)malloc(sizeof(double) * local_n * local_n);
	local_C = (double*)malloc(sizeof(double) * local_n * local_n);
	memset(local_A, 0, sizeof(double) * local_n * local_n);
	memset(local_B, 0, sizeof(double) * local_n * local_n);
	memset(local_C, 0, sizeof(double) * local_n * local_n);
	
	GenerateData(comm_2d, local_A, local_B);
	Cannon(comm_2d, local_A, local_B, local_C);
	GatherResult(comm_2d, local_C);

	double end = MPI_Wtime();
	if(myrank == 0) printf("Running time: %.2f seconds...\n", end - start);

	MPI_Comm_free(&comm_2d);
	MPI_Finalize();
	return 0;
}
4、Cannon算法主函数
void Cannon(MPI_Comm comm_2d, double* local_A, double* local_B, double* local_C)
{
	int uprank, downrank, leftrank, rightrank;
	MPI_Status status;

	//计算左边一格的(目标)进程序号leftrank,和右边一格的(源)进程序号rightrank
	coords[0] = mycoords[0];
	coords[1] = (mycoords[1] - 1) % dims[1];
	MPI_Cart_rank(comm_2d, coords, &leftrank);
	coords[1] = (mycoords[1] + 1) % dims[1];
	MPI_Cart_rank(comm_2d, coords, &rightrank);
	
	//计算向上一格的(目标)进程序号uprank,和向下一格的(源)进程序号downrank
	coords[0] = (mycoords[0] - 1) % dims[0];
	coords[1] = mycoords[1];
	MPI_Cart_rank(comm_2d, coords, &uprank);
	coords[0] = (mycoords[0] + 1) % dims[0];
	MPI_Cart_rank(comm_2d, coords, &downrank);

	//A矩阵第一次重排列
	coords[0] = mycoords[0];
	coords[1] = (mycoords[1] - mycoords[0]) % dims[1];
	MPI_Cart_rank(comm_2d, coords, &dstrank);
	coords[1] = (mycoords[1] + mycoords[0]) % dims[1];
	MPI_Cart_rank(comm_2d, coords, &srcrank);
	if (myrank != dstrank)
	{
		MPI_Sendrecv_replace(local_A, local_n * local_n, MPI_DOUBLE, dstrank, 0, srcrank, 0, comm_2d, &status);
	}

	//B矩阵第一次重排列
	coords[1] = mycoords[1];
	coords[0] = (mycoords[0] - mycoords[1]) % dims[0];
	MPI_Cart_rank(comm_2d, coords, &dstrank);
	coords[0] = (mycoords[0] + mycoords[1]) % dims[0];
	MPI_Cart_rank(comm_2d, coords, &srcrank);
	if (myrank != dstrank)
	{
		MPI_Sendrecv_replace(local_B, local_n * local_n, MPI_DOUBLE, dstrank, 0, srcrank, 0, comm_2d, &status);
	}

	Multiply(local_A, local_B, local_C, local_n);

	for (int time = 0; time < dims[0] - 1; time++)
	{
		//local_A循环往左滚动一格
		MPI_Sendrecv_replace(local_A, local_n * local_n, MPI_DOUBLE, leftrank, 0, rightrank, 0, comm_2d, &status);
		
		//local_B循环往上滚动一个
		MPI_Sendrecv_replace(local_B, local_n * local_n, MPI_DOUBLE, uprank, 0, downrank, 0, comm_2d, &status);

		Multiply(local_A, local_B, local_C, local_n);
	}
}

void Multiply(double* A, double* B, double* C, int n)
{
	for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++)
			for (int k = 0; k < n; k++)
			{
				C[i * n + j] += A[i * n + k] * B[j + k * n];
				Sleep(10);
			}
}

其中 M P I _ C a r t _ r a n k   MPI\_Cart\_rank\, MPI_Cart_rank用于将进程坐标转换为进程号, M P I _ S e n d r e c v _ r e p l a c e   MPI\_Sendrecv\_replace\, MPI_Sendrecv_replace函数可以视为 M P I _ S e n d MPI\_Send MPI_Send以及 M P I _ R e c v   MPI\_Recv\, MPI_Recv函数的组合,用于在一个绕接环中,每一个进程向目标进程 d s t r a n k dstrank dstrank发送数据,并接受来自 s r c r a n k srcrank srcrank源进程的数据,并且在收发数据中所有进程使用的都是同一个缓存。使用该函数可以实现 A A A B B B子块的循环移位。

5、生成数据并分发到各进程的函数

0号进程将 A A A B B B矩阵的数据放入 b u f f e r A bufferA bufferA b u f f e r B bufferB bufferB中再发送给对应进程的 l o c a l _ A local\_A local_A l o c a l _ B local\_B local_B中。

void GenerateData(MPI_Comm comm_2d, double* local_A, double* local_B)
{
	if (mycoords[0] == 0 && mycoords[1] == 0)			//0号进程生成和发送数据
	{
		double A[8 * 8] = {
		cos(pi / 6), -sin(pi / 6), 0, 0 , 0, 0, 0, 0,
		sin(pi / 6), cos(pi / 6), 0, 0 , 0, 0, 0, 0,
		0, 0, cos(pi / 4), -sin(pi / 4), 0, 0, 0, 0,
		0, 0, sin(pi / 4), cos(pi / 4), 0, 0, 0, 0,
		0, 0, 0, 0, cos(pi / 6), -sin(pi / 6), 0, 0,
		0, 0, 0, 0, sin(pi / 6), cos(pi / 6), 0, 0,
		0, 0, 0, 0, 0, 0, cos(pi / 4),  -sin(pi / 4),
		0, 0, 0, 0, 0, 0, sin(pi / 4), cos(pi / 4),
		};

		double* B = (double*)malloc(sizeof(double) * n * n);
		memset(B, 0, sizeof(double) * n * n);
		for (int i = 0; i < n; i++)
			for (int j = 0; j < n; j++)
				B[j * n + i] = A[i * n + j];

		for (int i = 0; i < local_n; i++)
			for (int j = 0; j < local_n; j++)
			{
				local_A[i * local_n + j] = A[i * n + j];
				local_B[i * local_n + j] = B[i * n + j];
			}

		for (int row = 0; row < dims[0]; row++)
			for (int col = 0; col < dims[1]; col++)
			{
				if (row == 0 && col == 0) continue;		//0号进程
				//offset是坐标为(row,col)进程对应的A、B子块的第一个元素在A、B矩阵中的下标
				int offset = row * dims[1] * local_n * local_n + col * local_n;
				double* bufferA = (double*)malloc(sizeof(double) * local_n * local_n);
				double* bufferB = (double*)malloc(sizeof(double) * local_n * local_n);

				for (int i = 0; i < local_n; i++)
					for (int j = 0; j < local_n; j++)
					{
						bufferA[i * local_n + j] = A[offset + i * n + j];
						bufferB[i * local_n + j] = B[offset + i * n + j];
					}

				coords[0] = row;
				coords[1] = col;
				MPI_Cart_rank(comm_2d, coords, &dstrank);
				MPI_Send(bufferA, local_n * local_n, MPI_DOUBLE, dstrank, 0, comm_2d);
				MPI_Send(bufferB, local_n * local_n, MPI_DOUBLE, dstrank, 1, comm_2d);
				free(bufferA);
				free(bufferB);
			}
		free(B);
	}
	else
	{
		MPI_Recv(local_A, local_n * local_n, MPI_DOUBLE, 0, 0, comm_2d, MPI_STATUS_IGNORE);
		MPI_Recv(local_B, local_n * local_n, MPI_DOUBLE, 0, 1, comm_2d, MPI_STATUS_IGNORE);
	}
}
6、收集结果到0号进程的函数

所有进程将计算结果(本地 C C C子块的数据)放入 b u f f e r C bufferC bufferC中发送给0号进程,0号进程收集 b u f f e r C bufferC bufferC中的数据放入 C C C矩阵的对应位置中。

void GatherResult(MPI_Comm comm_2d, double* local_C)
{
	int otherrank;
	MPI_Status status;

	if (coords[0] == 0 && coords[1] == 0)
	{
		double* C = (double*)malloc(sizeof(double) * n * n);
		memset(C, 0, sizeof(double) * n * n);
		double* bufferC = (double*)malloc(sizeof(double) * local_n * local_n);
		memset(bufferC, 0, sizeof(double) * local_n * local_n);
		for (int row = 0; row < dims[0]; row++)
		{
			for (int col = 0; col < dims[1]; col++)
			{
				if (row == 0 && col == 0)
				{
					for (int i = 0; i < local_n; i++)
						for (int j = 0; j < local_n; j++)
							C[i * n + j] = local_C[i * local_n + j];
					continue;
				}
				coords[0] = row;
				coords[1] = col;
				MPI_Cart_rank(comm_2d, coords, &otherrank);
				MPI_Recv(bufferC, local_n * local_n, MPI_DOUBLE, otherrank, 0, comm_2d, &status);
				int offset = row * dims[1] * local_n * local_n + col * local_n;

				for (int i = 0; i < local_n; i++)
					for (int j = 0; j < local_n; j++)
						C[offset + i * n + j] = bufferC[i * local_n + j];
					
			}
		}
		free(bufferC);
		for (int i = 0; i < n; i++)
		{
			for (int j = 0; j < n; j++)
				printf("%.2f ", C[i * n + j]);
			printf("\n");
		}
	}
	else
	{
		MPI_Send(local_C, local_n * local_n, MPI_DOUBLE, 0, 0, comm_2d);
	}
}

三、实现结果

程序在 V S 2019 VS2019 VS2019上运行,可以看到随着进程数量的增加, 0 0 0号进程的运行时间明显减少(显示器上显示的执行时间是 0 0 0号进程的执行时间)。但是当进程增加到原来的 n n n倍, 0 0 0号进程的运行时间并不为原来的 1 n \frac{1}{n} n1,这一方面是因为 0 0 0号进程需要与更多的进程点对点发送 A A A B B B矩阵的数据,另一个重要原因是我的电脑为 I n t e l Intel Intel 8 8 8 C P U CPU CPU,最多只能有 8 8 8个进程同时运行,因此会有 64 − 8 = 56 64-8=56 648=56个进程会在等待队列和就绪队列上等待被 C P U CPU CPU调度,影响总程序运行时间。但是多个进程确实明显加速了矩阵乘法。
在这里插入图片描述

  • 6
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
MPI矩阵乘法的两种实现方法分别为普通的矩阵乘法和Cannon算法。下面分别介绍这两种方法的实现步骤。 1. 普通的矩阵乘法 普通的矩阵乘法算法是最基本的矩阵乘法算法,也是最容易理解和实现算法。其实现步骤如下: - 从文件中读入两个矩阵A和B,将其分配给各个进程。 - 对于进程i,将A的第i行分配给该进程,B的第i列分配给该进程。 - 每个进程计算该进程所分配到的A和B的部分矩阵的乘积。最终得到C的一部分。 - 将各个进程计算得到的部分C矩阵合并,得到最终的C矩阵。 2. Cannon算法 Cannon算法是一种分治算法,它可以将矩阵乘法的计算任务分配给多个进程,使得计算速度更快。其实现步骤如下: - 从文件中读入两个矩阵A和B,将其分配给各个进程。 - 对于进程i和j,将A的第i行分配给进程(i+j) mod p,将B的第j列分配给进程(i+j) mod p。 - 每个进程计算该进程所分配到的A和B的部分矩阵的乘积。最终得到C的一部分。 - 依次进行p-1轮的通信和计算,每轮将A向左移动一列,B向上移动一行,然后计算所得的矩阵乘积的部分矩阵,再将所得的部分矩阵累加到最终的C矩阵上。 总体来说,普通的矩阵乘法比较容易理解和实现,但是当矩阵规模较大时,其计算速度会变慢。而Cannon算法虽然相对复杂一些,但是可以将计算任务分配给多个进程,从而提高计算效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楚国令尹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值