矩阵并行计算
一、矩阵卷帘存储
串行计算中无需考虑数据在处理中怎样存储,但是在并行计算中需要进行关注,以便进行负载均衡等操作。
卷帘存储:矩阵A,矩阵中的元素为
A
i
j
A_{ij}
Aij,处理机阵列
p
×
q
p\times q
p×q,小块
A
i
j
A_{ij}
Aij存放在处理器
P
k
l
P_{kl}
Pkl处(k = i mod p, l = j mod q)。
如下图所示:
二、矩阵向量并行乘法
y = A x , A ∈ R m n , x ∈ R n \quad \quad y=A x, \quad A\in\mathbb{R}^{mn},\ x\in\mathbb{R}^n y=Ax,A∈Rmn, x∈Rn
2.1 串行算法
2.1.1 i-j实现
矩阵A的一行乘以向量x,外层循环i循环一次确定结果y中的一个值,内层循环j负责控制遍历矩阵A的一行,x的每一行(即为每一个元素)。
void matvec_i_j(float *y, float **A, float *x, int m, int n)
{
float *arr = (float*)calloc(m, sizeof(float));
memset(arr, 0, m);
for (int i = 0;i < m;i++)
for (int j = 0;j < n;j++)
arr[i] += A[i][j] * x[j];
printf("Multiply the matrix by the vector: \n");
for (int i = 0;i < m;i++)
printf("%f \n", arr[i]);
}
2.1.2 j-i实现
矩阵A的一列乘向量x的一个值,即为外层循环j循环一次计算出结果y中每个值的第j个分量,再对每次算出的值相加即可。
void matvec_j_i(float *y, float **A, float *x, int m, int n)
{
float *arr = (float *)calloc(m, sizeof(float));
memset(arr, 0, m);
for (int j = 0; j<n; j++){
for (int i = 0; i<m; i++){
arr[i] += A[i][j] * x[j];
}
}
printf("Multiply the matrix by the vector j-i: \n");
for (int i = 0; i<m; i++)
printf("%f \n", arr[i]);
}
二者运行结果相同,如下图所示:
2.2 并行算法
2.2.1 行分块划分矩阵
将矩阵 A 按行划分成如下的行块子矩阵
A
=
[
A
0
T
,
A
1
T
,
…
,
A
p
−
1
T
]
T
\quad\quad\quad\quad\quad\quad A=\left[A_{0}^{T}, A_{1}^{T}, \ldots, A_{p-1}^{T}\right]^{T}
A=[A0T,A1T,…,Ap−1T]T
则:
A
x
=
[
A
0
T
,
A
1
T
,
…
,
A
p
−
1
T
]
T
x
=
[
(
A
0
x
)
T
,
(
A
1
x
)
T
,
…
,
(
A
p
−
1
x
)
T
]
\quad\quad \begin{aligned} A x &=\left[A_{0}^{T}, A_{1}^{T}, \ldots, A_{p-1}^{T}\right]^{T} x \\ &=\left[\left(A_{0} x\right)^{T},\left(A_{1} x\right)^{T}, \ldots,\left(A_{p-1} x\right)^{T}\right] \end{aligned}
Ax=[A0T,A1T,…,Ap−1T]Tx=[(A0x)T,(A1x)T,…,(Ap−1x)T]
将 A i A_i Ai存放在结点 P i P_i Pi中,每个结点计算 A i x A_ix Aix,最后调用 MPI_GATHER 或 MPI_GATHERV 即可。
2.2.2 列分块划分矩阵
将矩阵 A 按列划分,并对 x 也做相应的划分
A
=
[
A
0
,
A
1
,
…
,
A
p
−
1
]
,
x
=
(
x
0
T
,
x
1
T
,
…
,
x
p
−
1
T
)
T
\quad\quad A=\left[A_{0}, A_{1}, \ldots, A_{p-1}\right], \ x=\left(x_{0}^{T}, x_{1}^{T}, \ldots, x_{p-1}^{T}\right)^{T}
A=[A0,A1,…,Ap−1], x=(x0T,x1T,…,xp−1T)T
则有结果为:
A
x
=
∑
i
=
0
p
−
1
A
i
x
i
T
\quad\quad\quad\quad A x=\sum_{i=0}^{p-1} A_{i} x_{i}^{T}
Ax=∑i=0p−1AixiT
将
A
i
A_i
Ai 和
x
i
x_i
xi 存放在结点
P
i
P_i
Pi 中,每个结点计算
A
i
x
i
T
A_i\ x_i^T
Ai xiT,最后调用 MPI_REDUCE 或 MPI_ALLREDUCE 即可。
三、矩阵乘并行计算
本节要考虑的计算问题为:
C
=
A
×
B
C = A\times B
C=A×B
3.1 串行矩阵乘法
3.1.1i-j-k实现
矩阵A的行和矩阵B的列相乘相加即可。
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
for (int r=0;r<k;r++)
arr[i][j] += A[i][r] * B[r][j];
}
}
3.2 矩阵并行分块乘法
3.2.1 行列分块
矩阵A按行进行划分,矩阵B按列进行划分。
A
=
[
A
0
T
A
1
T
⋯
A
p
−
1
T
]
T
,
B
=
[
B
0
B
1
⋯
B
p
−
1
]
A=\left[\begin{array}{llll}A_{0}^{T} & A_{1}^{T} & \cdots & A_{p-1}^{T}\end{array}\right]^{T}, \quad B=\left[\begin{array}{llll}B_{0} & B_{1} & \cdots & B_{p-1}\end{array}\right]
A=[A0TA1T⋯Ap−1T]T,B=[B0B1⋯Bp−1]
第一轮计算后得到
C
i
j
=
A
i
×
B
j
C_{ij}=A_i\times B_j
Cij=Ai×Bj
然后第i号处理器将其所有的矩阵B的列块传递至第i-1号处理器中,再次进行计算。B的列块循环传送p-1次后得到C的最终结果。此时编号为i的处理器中存储有子矩阵
C
i
0
,
C
i
1
,
.
.
.
,
C
i
(
p
−
1
)
C_{i0},C_{i1},...,C_{i(p-1)}
Ci0,Ci1,...,Ci(p−1)。
循环过程如下图所示,第一次计算时为图(a),传递后为图(b)。
其实整个计算的过程是从对角线开始斜向上进行计算的,计算过程如下图所示:
for i=0 to p-1
j=(i+myid) mod p
Cj=A*B
src = (myid+1) mod p
dest = (myid-1+p) mod p
if (i!=p-1)
send(B,dest)
recv(B,src)
end if
end for
本算法中,Cj = Cmyid, j ,A = Amyid ,B 在处理器中每次循环向前移动一个处理器,即每次交换一个子矩阵数据块,共交换 p-1 次。
3.2.2 行行分块算法
将矩阵A、B均按行进行划分,如下:
A
=
[
A
0
T
,
A
1
T
,
…
,
A
p
−
1
T
]
T
,
B
=
[
B
0
T
,
B
1
T
,
…
,
B
p
−
1
T
]
A=\left[A_{0}^{T}, A_{1}^{T}, \ldots, A_{p-1}^{T}\right]^{T}, \quad B=\left[B_{0}^{T}, B_{1}^{T}, \ldots, B_{p-1}^{T}\right]
A=[A0T,A1T,…,Ap−1T]T,B=[B0T,B1T,…,Bp−1T]
再对矩阵A的每一行再次划分,划分为单个元素:
A
i
=
[
A
i
0
,
A
i
1
,
…
,
A
i
,
p
−
1
]
,
i
=
0
,
1
,
…
,
p
−
1
A_{i}=\left[A_{i 0}, A_{i 1}, \ldots, A_{i, p-1}\right], \quad i=0,1, \ldots, p-1
Ai=[Ai0,Ai1,…,Ai,p−1],i=0,1,…,p−1
要计算出矩阵C的第一行,需要用矩阵A中的第一行去乘矩阵B的每一列,也就是
C
i
=
A
i
×
B
C_i = A_i\times B
Ci=Ai×B
此时真正需要相乘的是矩阵A的第一行的第j个元素和矩阵B的第j行,每次相乘会得到矩阵
C
i
j
C_{ij}
Cij的一部分,j++依次计算即可得到矩阵C的第i行的值,即为 :
C
i
=
∑
j
=
0
p
−
1
A
i
j
×
B
j
C_i=\sum_{j=0}^{p-1} A_{ij} \times B_{j}
Ci=j=0∑p−1Aij×Bj
mp1 ≡ (myid+1) mod p, mm1 ≡ (myid−1 + p) mod p
for i = 0 to p − 1 do
l ≡ (i+myid) mod p
C = C + Al × B
if i ≠ p − 1, send(B, mm1), recv(B, mp1)
end{for}
数据交换方式与3.2.1相同,不同的只是计算C的方式。
3.2.3 列列分块
矩阵A和矩阵B均按列进行分块,C也按列进行划分。
与3.2.2类似此处对B的一列再次进行划分,划分为单个元素与A相乘,计算方法如下:
C
j
=
A
∗
B
j
=
∑
i
=
0
p
−
1
A
i
B
i
j
C_{j}=A * B_{j}=\sum_{i=0}^{p-1} A_{i} B_{i j}
Cj=A∗Bj=i=0∑p−1AiBij
此时唯一不同在于要进行交换的是A矩阵,不是B矩阵。A矩阵在每次循环后,发送
A
j
A_j
Aj至上一个处理器。
mp1 ≡ (myid+1) mod p, mm1 ≡ (myid−1 + p) mod p
for i = 0 to p − 1 do
l ≡ (i+myid) mod p
C = C + A × Bl
if i ≠ p − 1, send(A, mm1), recv(A, mp1)
end{for}
3.2.4 列行分块
矩阵A按列分块,矩阵B按行进行分块.
A
=
[
A
0
A
1
⋯
A
p
−
1
]
,
B
i
=
[
B
i
0
B
i
1
…
B
i
,
p
−
1
]
A=\left[\begin{array}{llll}A_{0} & A_{1} & \cdots & A_{p-1}\end{array}\right], \quad B_{i}=\left[\begin{array}{llll}B_{i 0} & B_{i 1} & \ldots & B_{i, p-1}\end{array}\right]
A=[A0A1⋯Ap−1],Bi=[Bi0Bi1…Bi,p−1]
此时计算C时按照下式进行:
C
=
A
∗
B
=
∑
i
=
0
p
−
1
A
i
B
i
C=A * B=\sum_{i=0}^{p-1} A_{i} B_{i}
C=A∗B=i=0∑p−1AiBi
每次计算都可以算出C的一个子矩阵,一共算出P个与C相同大小的子矩阵,再进行相加即可得到C。
假设C也是按列存放在处理器中的则有将B的一行再次进行了划分(按行存放与3.2.2类似):
C
j
=
∑
i
=
0
p
−
1
A
i
B
i
j
C_{j}=\sum_{i=0}^{p-1} A_{i} B_{i j}
Cj=i=0∑p−1AiBij
C = A × Bmyid
for i = 1 to p − 1 do
l ≡ (i+myid) mod p, k ≡ (p − i+myid) mod p
T = A × Bl
send(T, l), recv(T, k)
C = C + T
end{for}
3.3 Cannon乘法
Cannon乘法和并行分块乘法不同,不是仅仅让 B 矩阵的各列块循环移动,而是有目的地让 A 的各行块以及 B 的各列块皆施行循环移位,从而实现对 C 的子块的计算。
将矩阵 A 和 B 分成 p 个方块
A
i
j
A_{ij}
Aij和
B
i
j
B_{ij}
Bij
(
0
≤
i
,
j
≤
p
−
1
)
(0 \leq i, j \leq \sqrt{p}-1)
(0≤i,j≤p−1),每块大小为
⌈
n
/
p
⌉
×
⌈
n
/
p
⌉
\lceil n / \sqrt{p}\rceil \times\lceil n / \sqrt{p}\rceil
⌈n/p⌉×⌈n/p⌉,并将它们分配给
p
×
p
\sqrt{p}\times \sqrt{p}
p×p 个处理器
(
P
00
,
P
01
,
…
,
P
p
−
1
p
−
1
)
\left(P_{00}, P_{01}, \ldots, P \sqrt{p}-1 \sqrt{p}-1\right)
(P00,P01,…,Pp−1p−1)。
算法执行分三步:
整个计算过程如下: