注:
(
1
1
1)加粗部分是个人认为比较重要的地方;
(
2
2
2)加粗且斜体部分是还没理解的部分;
(
3
3
3)部分术语保留了原文没有翻译,因为我觉得用原文的词更便于理解,
(
4
4
4)有的术语翻译在后面的括号里注明了原文对应的词,便于理解;
(
5
5
5)加中划线的词是感觉翻译得不好,没能理解原文的意思。
正文开始
我们介绍了高性能矩阵乘的基本原理,它是广泛使用的 G o t o B L A S GotoBLAS GotoBLAS 库的一部分,我们的设计和决策是通过不断细化具有多层次内存的架构模型来证明的,成果是一个 用于执行这个操作的 简单但有效的算法,在更多架构上的实现显示出可以实现接近峰值的性能。
Section1 Introduction
实现矩阵乘法以实现接近最优性能,需要结合微观层面的高性能 k e r n e l s kernels kernels 的周密的 e n g i n e e r i n g ( n . ) engineering(n.) engineering(n.),深入理解如何在宏观层面上分层操作( l a y e r e d layered layered),本文主要讨论宏观问题,即如何利用一个高性能 i n n e r − k e r n e l inner-kernel inner−kernel,而不是与该 i n n e r − k e r n e l inner-kernel inner−kernel 的 d e s i g n design design 和 e n g i n e e r i n g engineering engineering 相关的微观问题。
在 G u n n e l e t a l . [ 2001 ] Gunnel~et~al.[2001] Gunnel et al.[2001] 中提到了一种矩阵乘实现的分层( l a y e r e d layered layered)方法,该方法可以最优地分摊( a m o r t i z e amortize amortize) 在具有复杂的多级内存的架构上的 两个相邻内存层之间所需的数据移动(开销)。和该地区的其他工作一样 [ A g a r w a l e t a l . 1994 ; W h a l e y e t a l . 2001 ] [Agarwal~et~al. 1994; Whaley~et ~al. 2001] [Agarwal et al.1994;Whaley et al.2001], G u n n e l Gunnel Gunnel 等人用一个 “ i n n e r − k e r n e l inner-kernel inner−kernel” 来转换( c a s t cast cast)计算,该 “ i n n e r − k e r n e l inner-kernel inner−kernel” 用于计算 C : = A ~ B + C C := \widetilde{A}B + C C:=A B+C,其中 A ~ \widetilde{A} A 是一个规模为 m c ∗ k c m_c*k_c mc∗kc 的、以某种打包( p a c k e d packed packed)的格式连续存储的、适应( f i t i n fit~in fit in)于 c a c h e m e m o r y cache~memory cache memory 的矩阵。不幸的是,他们使用的内存层次结构模型至少在以下两方面是不现实的:
—— —— ——其假设该 i n n e r − k e r n e l inner-kernel inner−kernel 用于计算的 A ~ \widetilde{A} A 驻留在 L 1 c a c h e L_1~cache L1 cache 中。
—— —— ——其忽略了与 T r a n s l a t i o n L o o k − a s i d e B u f f e r ( T L B ) Translation~Look-aside~Buffer(TLB) Translation Look−aside Buffer(TLB) 相关的问题。
本文扩展了一个相关的技术报告$ [GotoandvandeGeijn~2002]$,该报告的观察结果如下:
—— —— ——浮点单元执行浮点操作的速率( f l o p s flops flops),浮点数从 L 2 c a c h e L_2~cache L2 cache 流( s t r e a m stream stream)到寄存器的速率,这两者的比率通常相对较小,这意味着 A ~ \widetilde{A} A 可以从 L 2 c a c h e L_2~cache L2 cache 中流出。
—— —— ——通常, T L B TLB TLB 能够寻址( a d d r e s s address address)的数据量大小( a m o u n t amount amount)是 A ~ \widetilde{A} A 大小( s i z e size size)的限制因素。
此外,本文观察到以下:
——
——
——事实上有
6
6
6 种
i
n
n
e
r
−
k
e
r
n
e
l
inner-kernel
inner−kernel 应被考虑用来构建(
b
u
i
l
d
i
n
g
building
building) 用于高性能矩阵乘的 块(
b
l
o
c
k
s
blocks
blocks),且已经证明,其中一种天然地(
i
n
h
e
r
e
n
t
l
y
inherently
inherently)比其他几个更优。(在
G
u
n
n
e
l
e
t
a
l
.
[
2001
,
2005
]
Gunnel~et~al. [2001, 2005]
Gunnel et al.[2001,2005]中,其中三个已被鉴别(
i
d
e
n
t
i
f
i
e
d
identified
identified))
对这些观察结果的仔细考虑是 D G E M M B a s i c L i n e a r A l g e b r a S u b p r o g r a m s ( B L A S ) DGEMM~Basic~Linear~Algebra~Subprograms(BLAS) DGEMM Basic Linear Algebra Subprograms(BLAS) 例程( r o u t i n e routine routine)实现的基础,该例程是广泛使用的 G o t o B L A S GotoBLAS GotoBLAS 库的一部分。
在图
1
1
1 中,我们预览了这些技术的效能。图中展示了我们的实现和供应商的实现(
I
n
t
e
l
’
s
M
K
L
(
8.1.1
)
a
n
d
I
B
M
’
s
E
S
S
L
(
4.2.0
)
l
i
b
r
a
r
i
e
s
Intel’s~MKL~(8.1.1)~and~IBM’s~ESSL~ (4.2.0)~libraries
Intel’s MKL (8.1.1) and IBM’s ESSL (4.2.0) libraries)分别在在因特尔
P
e
n
t
i
u
m
4
P
r
e
s
c
o
t
t
Pentium4~Prescott
Pentium4 Prescott 处理器、
I
B
M
P
o
w
e
r
5
IBM~Power~5
IBM Power 5 处理器以及因特尔
I
t
a
n
i
u
m
2
Itanium2
Itanium2 处理器上的性能。需要注意的是供应商实现采用了和本文所描述十分类似的技术。不能孤立地判断矩阵乘的性能,它通常是其他操作的基石,如
l
e
v
e
l
−
3
B
L
A
S
(
m
a
t
r
i
x
−
m
a
t
r
i
x
o
p
e
r
a
t
i
o
n
s
)
[
D
o
n
g
a
r
r
a
e
t
a
l
.
1990
;
K
a
g
s
t
r
o
m
e
t
a
l
.
1998
]
level-3~BLAS (matrix-matrix~operations) [Dongarra~et~al. 1990; Kagstrom~et~al. 1998]
level−3 BLAS(matrix−matrix operations)[Dongarra et al.1990;Kagstrom et al.1998] 和
L
A
P
A
C
K
[
A
n
d
e
r
s
o
n
e
t
a
l
.
1999
]
LAPACK [Anderson et al. 1999]
LAPACK[Andersonetal.1999]。本文所描述的技术是如何影响
l
e
v
e
l
−
3
B
L
A
S
level-3~BLAS
level−3 BLAS 的,在
G
o
t
o
a
n
d
v
a
n
d
e
G
e
i
j
n
[
2006
]
Goto~and~van~de~Geijn~[2006]
Goto and van de Geijn [2006]中讨论过。
本文尝试在高层次描述这些问题以便有更广泛的受众,低层次的问题仅在需要时介绍。在
S
e
c
t
i
o
n
2
Section~2
Section 2 我们介绍在本文剩余部分所使用的记号,在
S
e
c
t
i
o
n
2
Section~2
Section 2 介绍一种用于实现矩阵乘的分层方法(
l
a
y
e
r
e
d
a
p
p
r
o
a
c
h
layered~approach
layered approach),
S
e
c
t
i
o
n
4
Section~4
Section 4 讨论
i
n
n
e
r
−
k
e
r
n
e
l
s
inner-kernels
inner−kernels 的高性能实现,
S
e
c
t
i
o
n
5
Section~5
Section 5 给出 用于矩阵乘最常见情形的 实际算法,在
S
e
c
t
i
o
n
6
Section~6
Section 6 给出 在实践中用于 确定 为了优化性能而必须调整的 参数的更多细节,在
S
e
c
t
i
o
n
7
Section~7
Section 7 中给出在各种架构上使用高度调优的实现所获得的性能结果,
S
e
c
t
i
o
n
8
Section~8
Section 8 进行总结。
Section2 Notation
矩阵划分是描述矩阵乘算法的基础,给出一个 m × n m\times n m×n 的矩阵 X X X,我们仅考虑将 X X X 进行列分块和行分块:
若进行列分块,共分为
N
N
N 个列块,表示为
X
j
(
j
=
0
,
1
,
.
.
.
,
N
−
1
)
X_j(j=0,1,...,N-1)
Xj(j=0,1,...,N−1),每一个列块又有
n
b
n_b
nb 列(除了
X
N
−
1
X_{N-1}
XN−1,它可能不足
n
b
n_b
nb 列);若进行行分块,共分为
M
M
M 个行块,表示为
X
i
ˇ
(
i
=
0
,
1
,
.
.
.
,
M
−
1
)
\check{X_i}(i=0,1,...,M-1)
Xiˇ(i=0,1,...,M−1),每一个行块又有
m
b
m_b
mb 行(除了
X
ˇ
M
−
1
\check{X}_{M-1}
XˇM−1,它可能不足
m
b
m_b
mb 列)。
矩阵乘法的实现将由子矩阵的乘法组成,我们对这些计算进行了命名,如图 2 2 2, 3 3 3 所示。我们注意到这些特殊的形状经常作为 用于其他线性代数操作的算法 的一部分,例如 L A P A C K LAPACK LAPACK 支持的各种操作的计算大部分都可由 G E P P GEPP GEPP, G E M P GEMP GEMP 和 G E P M GEPM GEPM 来转换( c a s t cast cast),甚至给定单个密集线性代数操作,通常存在多种算法,每一个算法都能由这些 G E M M GEMM GEMM 乘法的不同情况来实现 [ B i e n t i n e s i e t a l ] [Bientinesi~et~al] [Bientinesi et al]。
Section3 A layered approach for GEMM
在图 4 4 4 中,我们展示了 G E M M GEMM GEMM 能够怎样分解为图 2 2 2 中所描述的特殊情形,通用的 G E M M GEMM GEMM 能够被分解为多个 G E P P GEPP GEPP, G E M P GEMP GEMP 或 G E P M GEPM GEPM 的调用, G E P P GEPP GEPP, G E M P GEMP GEMP 和 G E P M GEPM GEPM 又可被分解为多个 G E B P GEBP GEBP, G E P B GEPB GEPB 或 G E P D O T GEPDOT GEPDOT k e r n e l s kernels kernels 的调用。现在的想法是,如果这三个最低层的 k e r n e l s kernels kernels 能获得高性能,则 G E M M GEMM GEMM 的其它情形也能获得高性能。
在图 5 5 5,我们将图 4 4 4 中的顶部分支与一个三层嵌套循环联系起来
其中矩阵 A A A, B B B 和 C C C 按如下方法分为若干子矩阵:
C
=
{
C
11
C
12
⋯
C
1
N
C
21
C
22
⋯
C
2
N
⋮
⋮
⋱
⋮
C
M
1
C
M
2
⋯
C
M
N
}
,
A
=
{
A
11
A
12
⋯
A
1
K
A
21
A
22
⋯
A
2
K
⋮
⋮
⋱
⋮
A
M
1
A
M
2
⋯
A
M
K
}
,
B
=
{
B
11
B
12
⋯
B
1
N
B
21
B
22
⋯
B
2
N
⋮
⋮
⋱
⋮
B
K
1
B
K
2
⋯
B
K
N
}
C=\begin{Bmatrix} C_{11} & C_{12} & \cdots & C_{1N} \\ C_{21} & C_{22} & \cdots & C_{2N} \\ \vdots & \vdots & \ddots & \vdots \\ C_{M1} & C_{M2} & \cdots & C_{MN} \end{Bmatrix},~~A=\begin{Bmatrix} A_{11} & A_{12} & \cdots & A_{1K} \\ A_{21} & A_{22} & \cdots & A_{2K} \\ \vdots & \vdots & \ddots & \vdots \\ A_{M1} & A_{M2} & \cdots & A_{MK} \end{Bmatrix},~~B=\begin{Bmatrix} B_{11} & B_{12} & \cdots & B_{1N} \\ B_{21} & B_{22} & \cdots & B_{2N} \\ \vdots & \vdots & \ddots & \vdots \\ B_{K1} & B_{K2} & \cdots & B_{KN} \end{Bmatrix}
C=⎩
⎨
⎧C11C21⋮CM1C12C22⋮CM2⋯⋯⋱⋯C1NC2N⋮CMN⎭
⎬
⎫, A=⎩
⎨
⎧A11A21⋮AM1A12A22⋮AM2⋯⋯⋱⋯A1KA2K⋮AMK⎭
⎬
⎫, B=⎩
⎨
⎧B11B21⋮BK1B12B22⋮BK2⋯⋯⋱⋯B1NB2N⋮BKN⎭
⎬
⎫
其中
C
i
j
∈
R
m
c
×
n
r
C_{ij}\in\mathbb{R}^{m_c\times n_r}
Cij∈Rmc×nr,
A
i
p
∈
R
m
c
×
k
c
A_{ip}\in\mathbb{R}^{m_c\times k_c}
Aip∈Rmc×kc,
B
p
j
∈
R
k
c
×
n
r
B_{pj}\in\mathbb{R}^{k_c\times n_r}
Bpj∈Rkc×nr,块大小(
b
l
o
c
k
s
i
z
e
s
block~sizes
block sizes)
m
c
m_c
mc,
k
c
k_c
kc 和
n
r
n_r
nr 的具体取值将在后文具体讨论。
在
G
u
n
n
e
l
s
e
t
a
l
.
[
2001
]
Gunnels~et~al.[2001]
Gunnels et al.[2001] 中可以找到一个支持 本节中提到的对于一般方法的最优性主张 的理论,特别地,该论文支持这样的观察结果:如果内存层(
m
e
m
o
r
y
l
a
y
e
r
s
memory~layers
memory layers)之间的数据移动能被最优地摊销(
a
m
o
r
t
i
z
e
d
amortized
amortized),那么计算就应按照图
4
4
4 给出的决策树来转换(
c
a
s
t
cast
cast)。然而,本文是自成体系的(
s
e
l
f
−
c
o
n
t
a
i
n
e
d
self-contained
self−contained),因为我们表明该方法能很好地分摊(
a
m
o
r
t
i
z
e
d
amortized
amortized)这种开销(
o
v
e
r
h
e
a
d
overhead
overhead),因此最优性对我们的讨论并不重要。
Section4 high-performance GEBP,GEPB,GEPDOT (inner-kernels的高性能实现)
我们现在讨论用于 G E B P GEBP GEBP, G E P B GEPB GEPB 和 G E P D O T GEPDOT GEPDOT 高性能实现的技术,我们通过首先分析 使用内存层次结构( m e m o r y h i e r a r c h y memory~hierarchy memory hierarchy)的一个公认的朴素( n a i v e naive naive)模型在内存层之间移动数据的开销( c o s t cost cost) 来做这件事,在 S e c t i o n 4.2 Section~4.2 Section 4.2 我们为该模型添加了更多的实际细节,这为 S e c t i o n 5 Section~5 Section 5 中的 G E B P GEBP GEBP, G E P B GEPB GEPB 和 G E P D O T GEPDOT GEPDOT 的算法奠定了基础。
4.1 Basics
在图
6
6
6(左)我们描述了一个非常简单的多级内存模型,在
R
a
n
d
o
m
A
c
c
e
s
s
M
e
m
o
r
y
(
R
A
M
)
Random~Access~Memory(RAM)
Random Access Memory(RAM) 和寄存器组(
r
e
g
i
s
t
e
r
s
registers
registers)之间插入了一个缓存层(
c
a
c
h
e
m
e
m
o
r
y
cache~memory
cache memory),与
G
E
B
P
GEBP
GEBP,
G
E
P
B
GEPB
GEPB 和
G
E
P
D
O
T
GEPDOT
GEPDOT 高性能实现相关的顶级问题(
t
o
p
−
l
e
v
e
l
i
s
s
u
e
s
top-level~issues
top−level issues)可以用这个简化的体系结构来描述。
我们首先关注
G
E
B
P
GEBP
GEBP,其中
A
∈
R
m
c
×
k
c
A\in\mathbb{R}^{m_c\times k_c}
A∈Rmc×kc,
B
∈
R
k
c
×
n
B\in\mathbb{R}^{k_c\times n}
B∈Rkc×n,
C
∈
R
m
c
×
n
C\in\mathbb{R}^{m_c\times n}
C∈Rmc×n,且
B
,
C
B,C
B,C 进行列分块
B
=
(
B
0
∣
B
1
∣
⋯
∣
B
N
−
1
)
,
C
=
(
C
0
∣
C
1
∣
⋯
∣
C
N
−
1
)
B = (B_0\vert B_1\vert\cdots\vert B_{N-1}),~~~~~~~~~~~~~~~~C = (C_0\vert C_1\vert\cdots\vert C_{N-1})
B=(B0∣B1∣⋯∣BN−1), C=(C0∣C1∣⋯∣CN−1)
并做以下假设:
假设( a a a): A A A 的尺寸 m c m_c mc 和 k c k_c kc 足够小,使得 A A A、 B B B 的一个列分块里的 n r n_r nr 列(即 B j B_j Bj)和 C C C 的一个列分块里的 n r n_r nr 列(即 C j C_j Cj)三者能一起放进缓存中( f i t s i n t h e c a c h e fits~in~the~cache fits in the cache)。
假设( b b b):如果 A A A, C j C_j Cj 和 B j B_j Bj 都在缓存中,那 C j : = A B j + C j C_j := AB_j + C_j Cj:=ABj+Cj 就能以 CPU 峰值速率计算。
假设( c c c):如果 A A A 在缓存中,则其一直在缓存中,直到不需要为止。
在这些假设下,图 7 7 7 中的 G E B P GEBP GEBP 方法分摊了( a m o r t i z e s amortizes amortizes)了在主存( m a i n m e m o r y main~memory main memory)和缓存之间移动数据的开销:
具体计算如下:
加载(
l
o
a
d
load
load)
A
A
A 只需要一次,其数据移动开销为
m
c
k
c
m_ck_c
mckc;但
B
j
B_j
Bj 和
C
j
C_j
Cj 各有
N
−
1
N-1
N−1 个,故各需要加载
N
−
1
N-1
N−1 次,计算完还要把
C
j
C_j
Cj 存回(
s
t
o
r
e
store
store)主存,
C
j
C_j
Cj 又需要额外的
N
−
1
N-1
N−1 次,因此每一次(更新
C
j
C_j
Cj)的数据移动开销为
k
c
n
r
+
m
c
n
r
+
m
c
n
r
=
(
2
m
c
+
k
c
)
n
r
k_cn_r+m_cn_r+m_cn_r=(2m_c+k_c)n_r
kcnr+mcnr+mcnr=(2mc+kc)nr,共
N
−
1
N-1
N−1 次,再对
n
r
n_r
nr 累加求和,得
(
2
m
c
+
k
c
)
n
(2m_c+k_c)n
(2mc+kc)n。因此总的数据移动开销为
m
c
k
c
+
(
2
m
c
+
k
c
)
n
(
m
e
m
o
p
s
)
m_ck_c+(2m_c+k_c)n~(memops)
mckc+(2mc+kc)n (memops)
计算
A
B
j
AB_j
ABj 需要
m
c
k
c
n
r
m_ck_cn_r
mckcnr 次乘法,
m
c
(
k
c
−
1
)
n
r
m_c(k_c-1)n_r
mc(kc−1)nr 次加法,计算
C
j
:
=
A
B
j
+
C
j
C_j := AB_j + C_j
Cj:=ABj+Cj 需要
m
c
n
r
m_cn_r
mcnr 次加法,故更新一次
C
j
C_j
Cj 需要的总计算次数为
2
m
c
k
c
n
r
2m_ck_cn_r
2mckcnr,共更新
N
−
1
N-1
N−1 次,再对
n
r
n_r
nr 累加求和,得总的计算开销为
2
m
c
k
c
n
(
f
l
o
p
s
)
2m_ck_cn~(flops)
2mckcn (flops)
总计算开销/总数据移动开销的比率为
2
m
c
k
c
n
m
c
k
c
+
(
2
m
c
+
k
c
)
n
f
l
o
p
s
m
e
m
o
p
s
≈
2
m
c
k
c
n
(
2
m
c
+
k
c
)
n
f
l
o
p
s
m
e
m
o
p
s
w
h
e
n
k
c
≪
n
(
1
)
\frac{2m_ck_cn}{m_ck_c+(2m_c+k_c)n} \frac{flops}{memops}\approx\frac{2m_ck_cn}{(2m_c+k_c)n} \frac{flops}{memops}~~~~~~~when~k_c\ll n~~~~~~~(1)
mckc+(2mc+kc)n2mckcnmemopsflops≈(2mc+kc)n2mckcnmemopsflops when kc≪n (1)
即
2
m
c
k
c
2
m
c
+
k
c
(
2
)
\frac{2m_ck_c}{2m_c+k_c}~~~~~~~(2)
2mc+kc2mckc (2)
应在 “
m
c
k
c
m_ck_c
mckc 个浮点数(即矩阵
A
A
A)填充了缓存的大部分”这个约束条件和上述的假设(
a
a
a)-(
c
c
c)下 ,最大化(2)式比值。在
S
e
c
t
i
o
n
6.3
Section~6.3
Section 6.3 我们将看到,实践中还有其他影响
k
c
k_c
kc 选择的因素。然而,底线(
b
o
t
t
o
m
l
i
n
e
bottom~line
bottom line)是这样的:在简化的假设下,
A
A
A 应该占据尽可能多的缓存空间,而且应该是方阵(即
m
c
=
k
c
m_c=k_c
mc=kc),同时至少需要给
B
j
B_j
Bj 和
C
j
C_j
Cj 留出缓存空间。如果
m
c
=
k
c
≈
n
/
100
m_c=k_c\approx n/100
mc=kc≈n/100,那么即使数据移动操作(
m
e
m
o
p
s
memops
memops)比计算操作(
f
l
o
p
s
flops
flops)慢10倍,数据移动操作也仅给计算操作(
c
o
m
p
u
t
a
t
i
o
n
computation
computation)增加约10%的开销。
G E P B GEPB GEPB 和 G E P D O T GEPDOT GEPDOT 也可进行类似的分析。
4.2 Refinements
在讨论实践中的考虑时,我们仍关注 G E B P GEBP GEBP 的高性能实现,在本文的剩余部分,我们假设矩阵都是以列优先的方式存储。
4.2.1 Choosing the Cache Layer
图 6 6 6(右)描述了一个更加精确的内存层次结构,该图指出通常有多级缓存。
第一个问题: m c × k c m_c\times k_c mc×kc 的矩阵 A A A 应该放在哪一层缓存?(2)式表明(当然是在假设( a a a)-( c c c)下): m c × k c m_c\times k_c mc×kc 越大,在 R A M RAM RAM 和缓存之间移动数据的开销就被计算( c o m p u t a t i o n computation computation)分摊( a m o r t i z e d amortized amortized)得越好。这表明在(大致)满足假设( a a a)-( c c c)的前提下,矩阵 A A A 应加载进距离寄存器组最远的缓存层,因为它能容纳最多的数据。
L
1
L_1
L1 缓存本身就具备这样的特性:如果用它存储
A
A
A,
B
j
B_j
Bj 和
C
j
C_j
Cj,则满足假设(
a
a
a)-(
c
c
c),但
L
1
L_1
L1 缓存往往非常小。为了让
m
c
×
k
c
m_c\times k_c
mc×kc 很大,能否将
A
A
A 存在
L
2
L_2
L2 缓存中呢?分别记
R
c
o
m
p
R_{comp}
Rcomp 和
R
l
o
a
d
R_{load}
Rload 为CPU执行浮点操作的速率和将浮点数从
L
2
L_2
L2 缓存流(
s
t
r
e
a
m
stream
stream)到寄存器组的速率,同时假设
A
A
A 存在
L
2
L_2
L2 缓存中,而
B
j
,
C
j
B_j, C_j
Bj,Cj 存在
L
1
L_1
L1 缓存中,再进一步假设在
L
1
L_1
L1 缓存和寄存器组之间有足够的带宽,使得能顺利将
B
j
B_j
Bj 和
C
j
C_j
Cj 的元素(
e
l
e
m
e
n
t
s
elements
elements)从
L
1
L_1
L1 缓存加载到寄存器组。在
S
e
c
t
i
o
n
4.1
Section~4.1
Section 4.1 中,我们计算过完成一次
C
j
:
=
A
B
j
+
C
j
C_j := AB_j + C_j
Cj:=ABj+Cj 需要
2
m
c
k
c
n
r
2m_ck_cn_r
2mckcnr 次计算(
f
l
o
p
s
flops
flops),并且需要将
A
A
A 的
m
c
×
k
c
m_c\times k_c
mc×kc 个元素从
L
2
L_2
L2 缓存加载到寄存器组。为了将 把
A
A
A 的元素加载进寄存器组的开销 和计算的开销重叠(
o
v
e
r
l
a
p
overlap
overlap),也就是说前者应不大于后者,即
1
R
l
o
a
d
×
m
c
k
c
⩽
1
R
c
o
m
p
×
2
m
c
k
c
n
r
⇒
2
n
r
R
c
o
m
p
⩾
1
R
l
o
a
d
\frac{1}{R_{load}}\times m_ck_c \leqslant \frac{1}{R_{comp}} \times 2m_ck_cn_r \Rightarrow \frac{2n_r}{R_{comp}} \geqslant \frac{1}{R_{load}}
Rload1×mckc⩽Rcomp1×2mckcnr⇒Rcomp2nr⩾Rload1
即
n
r
≥
R
c
o
m
p
2
R
l
o
a
d
(
3
)
n_r \geq \frac{R_{comp}}{2R_{load}}~~~~~~~(3)
nr≥2RloadRcomp (3)
4.2.2 TLB Considerations
第二个体系结构方面的考虑和页面管理系统相关。在我们的讨论中,考虑一个典型的现代架构( a r c h i t e c t u r e architecture architecture)就足够了,其使用虚拟内存,这样可用内存的大小( s i z e size size)就不受物理内存大小的限制了:内存被划分为一些规定大小(通常是固定的)的页面( p a g e s pages pages)。页表( p a g e t a b l e page~table page table)将虚拟地址映射到物理地址并跟踪( t r a c k track track)一个页面是在内存里还是在磁盘上,问题在于页表本身也是放在内存里的,当进行虚拟地址向物理地址转换时,会产生额外的访存开销。为了克服这个问题,引入了一个更小的表 T r a n s l a t i o n L o o k − a s i d e B u f f e r ( T L B ) Translation~Look-aside~Buffer(TLB) Translation Look−aside Buffer(TLB) 用于存储最近使用( m o s t r e c e n t l y u s e d most~recently~used most recently used)的页面的信息,若一个虚拟地址能在 T L B TLB TLB 中找到,那么其转换就很快,若不能找到(发生 T L B m i s s TLB~miss TLB miss),就会去查找页表,并将查找的条目( e n t r i e s entries entries)从页表移到 T L B TLB TLB,换句话说, T L B TLB TLB 就是页表的一个缓存。最近有些架构已经引入了二级 T L B TLB TLB,其动机和引入 L 2 L_2 L2 缓存类似。
缓存不命中( c a c h e m i s s cache~miss cache miss)和 T L B m i s s TLB~miss TLB miss 之间最显著的差异在于前者不一定会暂停( s t a l l stall stall)CPU运行。通过使用算法预取技术( a l g o r i t h m i c p r e f e t c h i n g t e c h n i q u e s algorithmic~prefetching~techniques algorithmic prefetching techniques),可以容忍少量的缓存不命中,只要数据能够从它所在的内存中读取得足够快,并在需要计算时到达CPU。相比之下,一次 T L B m i s s TLB~miss TLB miss 就会造成CPU停顿,直到 T L B TLB TLB 更新新的地址。也就是说,预取可以掩盖( m a s k mask mask)缓存不命中,但不能掩盖 T L B m i s s TLB~miss TLB miss。
T L B TLB TLB 的存在意味着还需满足以下额外的假设:
假设( d d d): A A A 的尺寸 m c m_c mc 和 k c k_c kc 足够小,使得 A A A、 B B B 的一个列分块里的 n r n_r nr 列(即 B j B_j Bj)和 C C C 的一个列分块里的 n r n_r nr 列(即 C j C_j Cj)三者能同时被 T L B TLB TLB 寻址,从而在计算 C j : = A B j + C j C_j := AB_j + C_j Cj:=ABj+Cj 时没有 T L B m i s s TLB~miss TLB miss 发生。
假设( d d d):如果 A A A 能被 T L B TLB TLB 寻址,则其一直保留在那,直到不需要为止。
4.2.3 Packing
现在最重要的问题是,
A
A
A 通常是一个更大矩阵的子矩阵,因此在内存中不连续,这意味着对其寻址所需要的
T
L
B
TLB
TLB 条目(
e
n
t
r
i
e
s
entries
entries)远超(实际)
T
L
B
TLB
TLB 条目的最小值,解决方案是将
A
A
A 打包(
p
a
c
k
pack
pack)成一个连续的工作阵列(
w
o
r
k
a
r
r
a
y
work~array
work array)
A
~
\widetilde{A}
A
,然后再选择使得
A
~
,
B
j
\widetilde{A},B_j
A
,Bj 和
C
j
C_j
Cj 三者能一起放进
L
2
L_2
L2 缓存且都能被
T
L
B
TLB
TLB 寻址的参数
m
c
m_c
mc 和
k
c
k_c
kc。
Case 1 The TLB is the limitting factor
假设
T
L
B
TLB
TLB 有
T
T
T 个可用条目,记
T
A
~
,
T
B
j
T_{\widetilde{A}},~T_{B_j}
TA
, TBj 和
T
C
j
T_{C_j}
TCj 分别为
A
~
,
B
j
\widetilde{A},B_j
A
,Bj 和
C
j
C_j
Cj 专用的
T
L
B
TLB
TLB 条目的数量,则有
T
A
~
+
2
(
T
B
j
+
T
C
j
)
⩽
T
T_{\widetilde{A}}~+2(T_{B_j}+T_{C_j})\leqslant T
TA
+2(TBj+TCj)⩽T
引入因子
2
2
2 的原因如下:当
B
j
+
1
B_{j+1}
Bj+1 和
C
j
+
1
C_{j+1}
Cj+1 (注:除了
B
N
−
1
B_{N-1}
BN−1 和
C
N
−
1
C_{N-1}
CN−1,它们各有
n
r
n_r
nr 个列)第一次被寻址时(即访问的下一个块(
b
l
o
c
k
block
block)属于
B
j
+
1
B_{j+1}
Bj+1 和
C
j
+
1
C_{j+1}
Cj+1)时,用于寻址(
a
d
d
r
e
s
s
address
address)它们的
T
L
B
TLB
TLB 条目本应替换掉那些用于寻址
B
j
B_j
Bj 和
C
j
C_j
Cj 的条目,然而,完成计算
C
j
:
=
A
~
B
j
+
C
j
C_j := \widetilde{A}B_j + C_j
Cj:=A
Bj+Cj 后,一些和
A
~
\widetilde{A}
A
相关的
T
L
B
TLB
TLB 条目会成为最近最少使用的(
L
e
a
s
t
R
e
c
e
n
t
l
y
U
s
e
d
,
L
R
U
Least~Recently~Used,LRU
Least Recently Used,LRU),因此会被用于寻址
B
j
+
1
B_{j+1}
Bj+1 和
C
j
+
1
C_{j+1}
Cj+1 的条目替换掉。因子
2
2
2 允许和
B
j
B_j
Bj 和
C
j
C_j
Cj 相关的条目能够和与
A
~
\widetilde{A}
A
,
B
j
+
1
B_{j+1}
Bj+1 和
C
j
+
1
C_{j+1}
Cj+1 相关的条目共存,这样当
B
j
+
2
B_{j+2}
Bj+2 和
C
j
+
2
C_{j+2}
Cj+2 第一次被寻址时,和
B
j
B_j
Bj 和
C
j
C_j
Cj 相关的条目会成为最近最少使用的,从而被替换掉,这就避免了和
A
~
\widetilde{A}
A
相关的
T
L
B
TLB
TLB 条目被替换。
(个人理解:整个 S e c t i o n 4 Section~4 Section 4 讨论的都是 G E B P GEBP GEBP 的实现,对于 G E B P GEBP GEBP 操作来说, B j B_j Bj 和 C j C_j Cj 用了一次就不用了,但 A A A 会反复用到,因此应保证用于寻址 A A A 的 T L B TLB TLB 条目不被替换)
如果
A
→
A
~
A\rightarrow\widetilde{A}
A→A
的打包(
p
a
c
k
i
n
g
packing
packing)满足以下条件:打包完成后
A
~
\widetilde{A}
A
驻留在
L
2
L_2
L2 缓存中,且能够被
T
L
B
TLB
TLB 寻址,以为后续计算做准备,则其无需产生比 把
A
A
A 加载进
L
2
L_2
L2 缓存和
T
L
B
TLB
TLB 的开销还大的开销(
o
v
e
r
h
e
a
d
overhead
overhead)。访问
A
A
A 来完成打包过程的开销(
c
o
s
t
cost
cost)无需远大于将
A
A
A 移动到
L
2
L_2
L2 缓存的开销大,即使不进行
A
A
A 的打包,后者也是必要的。
G
E
B
P
GEBP
GEBP 操作是在
G
E
P
P
GEPP
GEPP 或者
G
E
P
M
GEPM
GEPM 的上下文(
c
o
n
t
e
x
t
context
context)执行的,对于
G
E
P
P
GEPP
GEPP 来说,会多次调用
G
E
B
P
GEBP
GEBP 操作,且每次都会重用矩阵
B
B
B,因此将
B
B
B 也打包(
p
a
c
k
pack
pack)成一个连续的工作阵列(
w
o
r
k
a
r
r
a
y
work~array
work array)
B
~
\widetilde{B}
B
是值得的,从而在进行
C
:
=
A
~
B
~
+
C
C := \widetilde{A}\widetilde{B} + C
C:=A
B
+C 计算时能够降低
T
B
j
~
T_{\widetilde{B_j}}
TBj
(用于
B
j
~
\widetilde{B_j}
Bj
的
T
L
B
TLB
TLB 条目)的值。
Case 2 The size of the L2 cache is the limiting factor
该情形也能进行类似的讨论,但由于当前的受限因素主要在于 T L B TLB TLB 能够寻址的内存大小(例如,当前一代的 P e n t i u m 4 Pentium4 Pentium4 上的 T L B TLB TLB 可以寻址约 256 K b y t e s 256Kbytes 256Kbytes,而 L 2 L_2 L2 缓存可以容纳 2 M b y t e s 2Mbytes 2Mbytes),我们不做具体细节的详细说明。
4.2.4 Accessing Data Contiguously
为了最高效地将数据移动到寄存器组,组织计算使得 在内存中连续(
c
o
n
s
e
c
u
t
i
v
e
consecutive
consecutive)的数据能够连续的操作使用 是很重要的,实现这一点的方法不仅是将
A
A
A 打包(
p
a
c
k
pack
pack)成一个连续的工作阵列(
w
o
r
k
a
r
r
a
y
work~array
work array)
A
~
\widetilde{A}
A
,还需要仔细安排(
a
r
r
a
n
g
e
arrange
arrange)它,详见
S
e
c
t
i
o
n
6
Section~6
Section 6。
4.2.5 Implementation of G E P B GEPB GEPB and G E P D O T GEPDOT GEPDOT
与 G E B P GEBP GEBP 的分析类似。
Section5 Practical Algorithms
在相对粗略地分析了这些方法之后,我们现在讨论图 4 4 4 中所有六个选项的实际算法,同时结合其他体系结构方面的考虑。
5.1 Implementing G E P P GEPP GEPP with G E B P GEBP GEBP
现在总结 S e c t i o n 4.2.1 − 4.2.4 Section~4.2.1-4.2.4 Section 4.2.1−4.2.4 的观察结果,即图 8 8 8 中的用 G E B P GEBP GEBP 来实现 G E P P GEPP GEPP。
打包( p a c k i n g packing packing)和计算( c o m p u t a t i o n computation computation)被安排( a r r a n g e arrange arrange)来最大化 A ~ \widetilde{A} A 的尺寸( s i z e size size):
在算法
G
E
P
P
_
V
A
R
1
GEPP\_VAR1
GEPP_VAR1 中将
B
B
B 打包成
B
~
\widetilde{B}
B
使得
B
j
B_j
Bj 通常只需要一个
T
L
B
TLB
TLB 条目,引进
B
j
+
1
B_{j+1}
Bj+1 需要第二个
T
L
B
TLB
TLB 条目,缓冲区
C
a
u
x
C_{aux}
Caux 仅需一个
T
L
B
TLB
TLB 条目,以及
C
j
C_j
Cj 所需的最多
n
r
n_r
nr 个条目(如果
C
j
C_j
Cj 的主要尺寸(
l
e
a
d
i
n
g
d
i
m
e
n
s
i
o
n
leading~dimension
leading dimension)很大,那就需要
n
r
n_r
nr 个),因此
T
A
~
T_{\widetilde{A}}
TA
至少为
T − ( n r + 3 ) T-(n_r+3) T−(nr+3),保守起见就取它。此外, C j C_j Cj 存储不连续没什么影响,因为进行 G E P P GEPP GEPP 操作时不会重复使用 C j C_j Cj。
(个人理解:要确定 A ~ \widetilde{A} A 能始终放在 L 2 L_2 L2 中的最大尺寸,它由留给 A ~ \widetilde{A} A 的 T L B TLB TLB 条目来确定,因为要保证不出现 T L B m i s s TLB~miss TLB miss)
一旦 B B B 和 A A A 分别被复制( c o p y copy copy)到 B ~ \widetilde{B} B 和 A ~ \widetilde{A} A 中,算法 G E B P _ O P T 1 GEBP\_OPT1 GEBP_OPT1 中的循环几乎能以浮点单元的峰值性能执行。
—— —— —— B B B 的打包是一个内存到内存( m e m o r y − t o − m e m o r y memory-to-memory memory−to−memory)的复制,其开销正比于 k c × n k_c\times n kc×n,该开销被分摊( a m o r t i z e d amortized amortized)到 2 m × n × k c 2m\times n\times k_c 2m×n×kc 的计算量(这是更新整个 C C C,也就是一次 G E P P GEPP GEPP 操作所需的计算量)里,**因此对每一个复制项( c o p i e d i t e m copied~item copied item,个人理解:一个复制项就是 B B B 的一个元素,即一个浮点数),有 O ( m ) O(m) O(m)的计算量去摊销。**这种打包操作破坏( d i s r u p t disrupt disrupt)了 T L B TLB TLB 之前的内容。
—— —— ——如果精心设计,从 A A A 到 A ~ \widetilde{A} A 的打包将 A A A 的数据从内存里重排( r e a r r a n g e rearrange rearrange)到一个 可能保留在 L 2 L_2 L2 缓存的 缓冲区,且能让 T L B TLB TLB 加载有用的条目。其开销正比于 m c × k c m_c\times k_c mc×kc,该开销被分摊( a m o r t i z e d amortized amortized)到 2 m c × k c × n 2m_c\times k_c\times n 2mc×kc×n 的计算量(这是更新一个 C j C_j Cj,也就是一次 G E B P GEBP GEBP 操作所需的计算量)里,因此对每一个复制项(理解同上)有 O ( n ) O(n) O(n) 的计算量去摊销。在实践中,此开销通常很小( l e s s e x p e n s i v e less~expensive less expensive)。
当 m m m 和 n n n 都很大,且 k k k 不太小时,该方法很适合用来实现 G E M M GEMM GEMM
5.2 Implementing G E P M GEPM GEPM with G E B P GEBP GEBP
在图 9 9 9 提出了一个类似的方案来用 G E B P GEBP GEBP 实现 G E P M GEPM GEPM,此时 C C C 会被重复更新,因此在把结果加回 C C C 前先用 C ~ = A B \widetilde{C}=AB C =AB 来累加是很值得的, B p ˇ \check{B_p} Bpˇ 没有重用,因此无需打包。 B j B_j Bj 需要至多 n r n_r nr 个 T L B TLB TLB 条目, B t e m p B_{temp} Btemp, C j C_j Cj 和 C j + 1 C_{j+1} Cj+1 各需要一个,因此 T A ~ T_{\widetilde{A}} TA 同样至少为 T − ( n r + 3 ) T-(n_r+3) T−(nr+3)。
5.3 Implementing G E P P GEPP GEPP with G E P B GEPB GEPB
图 10 10 10 展示了怎样用 G E P B GEPB GEPB 来实现 G E P P GEPP GEPP,算法 G E P P GEPP GEPP 中将 A A A 进行打包并转置以改善对其元素的连续访问,在算法 G E P B GEPB GEPB 中, B B B 也被打包了,并放在 L 2 L_2 L2 缓存,因此 T B ~ T_{\widetilde{B}} TB 是我们想要最大化的值。 C i C_i Ci 需要至多 n r n_r nr 个 T L B TLB TLB 条目, C a u x C_{aux} Caux, A i A_i Ai 和 A i + 1 A_{i+1} Ai+1 各需要一个,因此 T B ~ T_{\widetilde{B}} TB 至少为 T − ( n r + 3 ) T-(n_r+3) T−(nr+3)。
5.4 Implementing G E M P GEMP GEMP with G E P B GEPB GEPB
图 11 11 11 展示了怎样用 G E P B GEPB GEPB 来实现 G E M P GEMP GEMP,引入一个临时的 C ~ \widetilde{C} C 来累计 C ~ = ( A B ) T \widetilde{C}=(AB)^T C =(AB)T,且打包的副本 B ~ \widetilde{B} B 填充了 L 2 L_2 L2 缓存的大部分。 A ˇ i \check{A}_i Aˇi 需要至多 n r n_r nr 个 T L B TLB TLB 条目, C i C_i Ci, C i + 1 C_{i+1} Ci+1 和 A t e m p A_{temp} Atemp 各需要一个,因此 T B ~ T_{\widetilde{B}} TB 至少为 T − ( n r + 3 ) T-(n_r+3) T−(nr+3)。
5.5 用 G E P D O T GEPDOT GEPDOT 来实现 G E P M GEPM GEPM 和 G E M P GEMP GEMP
类似地, G E P M GEPM GEPM 和 G E M P GEMP GEMP 也可通过 G E P D O T GEPDOT GEPDOT 操作来实现。将 C C C 的一个块( b l o c k block block)放在 L 2 L_2 L2 缓存中,并通过将 A A A 的一些列和 B B B 的一些行来更新它。因为我们将在下面讨论这种方法可能较差,所以我们在这里不详细讨论。
5.6 讨论
图4展示了6种不同的 G E M M GEMM GEMM 实现方法,其中4种的细节分别在图8-11给出。我们现在论证以下事实:如果矩阵都按照列优先( c o l u m n − m a j o r o r d e r column-major~order column−major order)存储,那么图8中的方法(也就是对应图4决策树的最顶部分支,同时也在图5中给出)在实践中可能获得最好的性能。
我们首先关注的是从 L 2 L_2 L2 缓存中获得最好的带宽使用,注意基于 G E P D O T GEPDOT GEPDOT 实现的方法将 C C C 的一个块( b l o c k block block)放在 L 2 L_2 L2 缓存中,每次都要从内存流出( s t r e a m e d streamed streamed) A A A 的一些列和 B B B 的一些行用于相乘来更新 C C C 的块,(前提假设是 A A A 的一些列和 B B B 的一些行都太大了,不能放在 L 2 L_2 L2 缓存里),计算出的 C C C 又太大,不能放在寄存器,又得写回。因此该方法要求的 L 2 L_2 L2 和寄存器组之间的带宽是基于 G E B P GEBP GEBP 和 G E P B GEPB GEPB 实现的方法的两倍,首先舍弃。
比较图8和9的算法,主要区别在于:前者将 B B B 打包,并从主存中来回流动 C C C 的元素,后者从主存中流出 B B B,在一个中间的( t e m p o r a r y temporary temporary)缓冲区里计算 C C C,最后通过将此中间结果加回 C C C 来实现解包( u n p a c k unpack unpack)。在实践中,前者能够用计算来隐藏( h i d e hide hide)在主存来回移动 C C C 的元素的开销,但暴露了对 B B B 打包的开销,后者能够隐藏从内存取出 B B B 的开销,但暴露了对 C C C 解包的开销。 C C C 的解包是一个更复杂的操作,因此其开销比 B B B 的打包更大,这使得图8的算法比图9的更可取。图10和11的算法也可进行类似的比较。
现在我们要从图8和10中的算法选出最好的一个,二者表面上看似乎是对称的,因为 A A A 和 B B B 的作用刚好相反。注意前者每次访问 C C C 的一些列,后者每次访问 C C C 的一些行,如果矩阵按列优先存储,那按列访问矩阵块( b l o c k block block)是更可取的。因此图8的算法最优。
我们在本文后续关注图8的算法。
本小节( S e c t i o n 5.6 Section~5.6 Section 5.6)的结论是基于 “对 L 2 L_2 L2 缓存 b l o c k i n g blocking blocking 更具优势” 这一前提。
Section6 More Details Yet
现在,我们就“像 G E B P O P T 1 GEBP\ OPT1 GEBP OPT1 这样的 k e r n e l kernel kernel 是如何使用寄存器组的”这一问题提出一些最终的见解,然后再讨论在实践中如何选择参数。前文已说图8算法可能获得最好的性能,因此我们关注它。
6.1 Register Blocking
考虑图8中的 C a u x : = A ~ B j C_{aux}:=\widetilde{A}B_j Caux:=A Bj, A ~ , B j \widetilde{A},B_j A ,Bj 分别在 L 2 L_2 L2 和 L 1 L_1 L1 缓存,该操作可通过在寄存器组里完成 C a u x C_{aux} Caux 的尺寸为 m r × n r m_r\times n_r mr×nr 的子矩阵的 计算来实现。
这意味着在 C j C_j Cj 的计算过程中,没必要把 C a u x C_{aux} Caux 的这些子矩阵存在 L 1 L_1 L1 甚至 L 2 L_2 L2 缓存中:每一次计算完 C a u x C_{aux} Caux 的子矩阵,需要 m r n r m_rn_r mrnr 次 m e m o p s memops memops 将其从寄存器组( s t o r e store store)存到任一内存层,而计算一次子矩阵需要 2 m r n r k c 2m_rn_rk_c 2mrnrkc 次 f l o p s flops flops ,因此访存时间能被计算时间隐藏。后面我们将看到 k c k_c kc 一般取得相对较大。
此时我们更仔细地讨论一下将 A A A 打包成 A ~ \widetilde{A} A 是怎么实现的。在我们的实现中, A ~ \widetilde{A} A 的存储应满足以下条件:它的每一个尺寸为 m r × n r m_r\times n_r mr×nr 的子矩阵连续地存在内存里,每一个子矩阵本身又是按列优先存储的,这使得计算 C a u x : = A ~ B j C_{aux}:=\widetilde{A}B_j Caux:=A Bj 时访问 A ~ \widetilde{A} A 的元素是连续的。其他人的实现通常把 A ~ \widetilde{A} A 存储为 A A A 的转置,这在访问 A A A 时需要一个稍微复杂一些的方法。
6.2 Choosing m r × n r m_r\times n_r mr×nr
以下考虑影响到 m r × n r m_r\times n_r mr×nr 的选择:
—— —— ——通常用一半的可用寄存器来存储 C C C 的尺寸为 m r × n r m_r\times n_r mr×nr 的子矩阵,另一半用于 A ~ , B ~ \widetilde{A},~\widetilde{B} A , B 元素的预取。
——
——
——当
m
r
≈
n
r
m_r\approx n_r
mr≈nr 时,加载寄存器(
l
o
a
d
i
n
g
t
h
e
r
e
g
i
s
t
e
r
s
loading~the~registers
loading the registers)的开销的分摊方案是最优的。
—— —— ——如 S e c t i o n 4.2.1 Section~4.2.1 Section 4.2.1 提到的,从 L 2 L_2 L2 缓存中取出 A ~ \widetilde{A} A 的一个元素到寄存器的时间应不超过前一个元素的计算时间,因此有 n r ≥ R c o m p 2 R l o a d n_r \geq \frac{R_{comp}}{2R_{load}} nr≥2RloadRcomp, R c o m p R_{comp} Rcomp 和 R l o a d R_{load} Rload 对应图12中的 “ f l o p s / c y c l e s flops/cycles flops/cycles” 列和 “ S u s t a i n e d B a n d w i d t h Sustained~Bandwidth Sustained Bandwidth” 列(后者仅针对于 L 2 L_2 L2 缓存)。
寄存器短缺会限制算法 G E B P _ O P T 1 GEBP\_OPT1 GEBP_OPT1 的性能,因为会削弱 隐藏( h i d e hide hide)与到 L 2 L_2 L2 缓存带宽相关的约束的 能力
6.3 Choosing k c k_c kc
为了分摊 更新 C j C_j Cj 的 m r × n r m_r\times n_r mr×nr个元素 的开销, k c k_c kc 应越大越好。以下考虑影响到 k c k_c kc 的选择:
—— —— —— B j B_j Bj的元素被重用很多次,因此应保持在 L 1 L_1 L1 缓存中。此外,缓存的相联度和替换策略进一步限制了 B j B_j Bj 能占用 L 1 L_1 L1 缓存多少空间。实践中, B j B_j Bj 的 k c n r k_cn_r kcnr 个浮点数应只占据 L 1 L_1 L1 缓存空间的一半以下,以保证 B j B_j Bj 元素不会被 A ~ \widetilde{A} A 和 C a u x C_{aux} Caux 的元素驱逐。
—— —— —— A ~ \widetilde{A} A 空间( m c k c m_ck_c mckc个浮点数)应占据 L 2 L_2 L2 的相当大一部分。
在我们的实验中,最优选择是使得 k c k_c kc 个双精度浮点数占用半个页面,此选择通常也满足其他约束以及本文之外的其他体系结构的约束。
6.4 Choosing m c m_c mc
前面已经讨论过, m c × k c m_c\times k_c mc×kc 规模的矩阵 A ~ \widetilde{A} A 应占用以下二者中较小者 的相当大一部分:(1)能被 T L B TLB TLB 寻址到的内存;(2) L 2 L_2 L2 缓存。
实际上,它还会被 L 2 L_2 L2 缓存的相联度和替换策略进一步影响。在实践中, m c m_c mc 通常选择为使得 A ~ \widetilde{A} A 仅占用上述二者中较小者一半的 值。
Section7 Experiments
本节展示用前几节描述的技术来实现的 D G E M M B L A S DGEMM~BLAS DGEMM BLAS 例程( r o u t i n e routine routine)获得的性能。
7.1 Algorithm Chosen
在典型的架构上实现 S e c t i o n 5 Section~5 Section 5 中讨论的所有算法是一项艰难的工作,因为之前讨论了图8中的算法可能获得最优性能,因此我们实现了它。 G E B P _ O P T 1 GEBP\_OPT1 GEBP_OPT1 算法针对不同的体系结构使用不同的汇编代码实现, A → A ~ A\rightarrow\widetilde{A} A→A 和 B → B ~ B\rightarrow\widetilde{B} B→B 的例程采用 C 语言实现,因为编译器似乎能优化这些操作。
7.2 Parameters
图12展示了一些典型架构的物理和算法参数,并不是所有参数都在本文分析的考虑范围之内,全部列出只是为了完整性起见。
以下几个参数需要额外解释以下:
Duplicate
该参数指出矩阵 B B B 的元素是否被复制( d u p l i c a t e d duplicated duplicated)为 B B B 的打包的一部分。为了利用好 P e t i u m 4 ( N o r t h w o o d ) Petium4(Northwood) Petium4(Northwood) 和 O p t e r o n Opteron Opteron 处理器上的 S S E 2 SSE2 SSE2 指令,这是很有必要的。尽管 C o r e 2 W o o d c r e s t Core~2~Woodcrest Core 2 Woodcrest 拥有 S S E 3 SSE3 SSE3 指令集,但复制指令( i n s t r u c t i o n s f o r d u p l i c a t i o n instructions~for~duplication instructions for duplication)由乘法单元发出( i s s u e issue issue)且必须使用和 N o r t h w o o d Northwood Northwood 架构相同的技术。
Sustained Bandwidth
观察到的( o b s e r v e d observed observed)从指定( i n d i c a t e d indicated indicated)内存层(如果是 L 2 L_2 L2 缓存,那就是 R l o a d R_{load} Rload )存器组的持续带宽,单位 d o u b l e s / c y c l e doubles/cycle doubles/cycle。
Covered Area
能被 T L B TLB TLB 寻址的内存大小;有些架构有一个(慢得多的) l e v e l 2 T L B level~2~TLB level 2 TLB,其相对于 L 1 T L B L_1~TLB L1 TLB 的作用就和 L 2 L_2 L2 缓存相对于 L 1 L_1 L1 缓存的作用一样。是否通过 L 1 T L B L_1~TLB L1 TLB 或 L 2 T L B L_2~TLB L2 TLB 的条目数量来限制 A ~ \widetilde{A} A 的大小( s i z e size size),这取决于 A → A ~ A\rightarrow\widetilde{A} A→A 和 B → B ~ B\rightarrow\widetilde{B} B→B 的开销。
A ~ ( K b y t e s ) \widetilde{A}(Kbytes) A (Kbytes)
为 A ~ \widetilde{A} A 留出的内存空间。
7.3 Focus on the Intel Pentium 4 Prescott Processor (3.6 GHz, 64bit)
等式( 3 3 3)指出:为了用计算 A ~ \widetilde{A} A 元素预取的开销,需满足 n r ≥ R c o m p 2 R l o a d n_r \geq \frac{R_{comp}}{2R_{load}} nr≥2RloadRcomp。对于该架构, R c o m p = 2 R_{comp}=2 Rcomp=2, R l o a d = 1.03 R_{load}=1.03 Rload=1.03,故 n r ≥ 2 / ( 2 × 1.03 ) ≈ 0.97 n_r \geq 2/(2\times1.03)\approx0.97 nr≥2/(2×1.03)≈0.97。同样,对于 E M 64 T EM64T EM64T 架构,它与之前的唯一区别是寄存器数量由 8 8 8 个增加到 16 16 16 个。每个寄存器能够存储 2 2 2 个双精度浮点数,取 m r × n r = 4 × 4 m_r\times n_r=4\times 4 mr×nr=4×4,因此其中 8 8 8 个寄存器用于存储这 16 16 16 个双精度浮点。
k c k_c kc的选择很复杂,因为在该架构中,对于计算 A ~ \widetilde{A} A 的列和 B ~ \widetilde{B} B 的列的内积的循环,其索引的更新应尽可能避免(为什么?)。因此该循环被完全展开(循环展开可以减少循环索引的更新),但这会导致循环的代码量增大,对应生成的指令就增多,使得将指令存储在指令缓存中成为一个问题。这把 k c k_c kc 限制在了 196 196 196( k c k_c kc 再增大,指令缓存就放不下了),略低于 S e c t i o n 6.3 Section~6.3 Section 6.3 中论述的 256 256 256( k c k_c kc 个双精度浮点数占用半个页面)。
图 13 13 13 展示了不同的 m c m_c mc 和 k c k_c kc 组合对 D G E M M DGEMM DGEMM 性能的影响,该架构是 “ A ~ \widetilde{A} A 应被 T L B TLB TLB 寻址” 这条规则的例外,因为在该架构中, T L B m i s s TLB~miss TLB miss 的成本比其他架构更低。当 A ~ \widetilde{A} A 被选择为填充 L 2 L_2 L2 缓存一半大小时,性能略高于选择其为 T L B TLB TLB 能寻址内存大小的一半。
P e n t i u m 4 Pentium4 Pentium4 的 N o r t h w o o d Northwood Northwood 版本依靠 S S E 2 SSE2 SSE2 指令来每周期计算 2 2 2 个 f l o p flop flop,该指令要求复制 B B B 中的条目,这种复制是被并入 B → B ~ B\rightarrow\widetilde{B} B→B 过程的一种数据移动,而 P r e s c o t t Prescott Prescott 子架构所支持的 S S E 3 SSE3 SSE3 指令在 B → B ~ B\rightarrow\widetilde{B} B→B 时无需这种复制。
图 14 14 14 展示了在 P e n t i u m 4 Pentium4 Pentium4 架构上用该方法获得的性能(注: m m m, n n n, k k k 是大矩阵的尺寸):
—— —— ——顶部的标签为 K e r n e l Kernel Kernel 的曲线对应 k e r n e l kernel kernel 例程(即 G E B P _ O P T 1 GEBP\_OPT1 GEBP_OPT1)的性能。
—— —— ——稍低的标签为 d g e m m dgemm dgemm 的曲线对应用 G E P P GEPP GEPP 操作序列实现的 D G E M M DGEMM DGEMM 例程的性能,其中 G E P P GEPP GEPP 操作使用图8的算法实现的。
—— —— ——底部的两条曲线对应 A → A ~ A\rightarrow\widetilde{A} A→A 和 B → B ~ B\rightarrow\widetilde{B} B→B 例程所耗时间的占比。
打包( p a c k i n g packing packing)操作带来的开销几乎解释了从 K e r n e l Kernel Kernel 曲线到 d g e m m dgemm dgemm 曲线下降的原因
图 15 15 15 的两幅图像(第二张好像缺失了)研究了 m m m 和 n n n 变化对实现性能的影响:
上面一张是 n = k = 2000 n=k=2000 n=k=2000 时, m m m 变化引起的四个指标的变化曲线,当 m m m 较小时,就像 G E P M GEPM GEPM 操作一样, B → B ~ B\rightarrow\widetilde{B} B→B 没有被足够的计算分摊( a m o r t i z e d amortized amortized),从而导致相对较低的性能,一种解决方案可能是跳过 B → B ~ B\rightarrow\widetilde{B} B→B 的过程,另一种解决方案可能是实现图 9 9 9 中的算法;类似地,下面一张是 m = k = 2000 m=k=2000 m=k=2000 时, n n n 变化引起的四个指标的变化曲线,当 n n n 较小时,就像 G E M P GEMP GEMP 操作一样, A → A ~ A\rightarrow\widetilde{A} A→A 没有被足够的计算分摊,从而导致相对较低的性能,一种解决方案可能是跳过 A → A ~ A\rightarrow\widetilde{A} A→A 的过程(这将要求 G E M P GEMP GEMP 操作由 A X P Y AXPY AXPY 操作实现,而不是内积),另一种解决方案可能是实现图 11 11 11 中的算法。
7.4 其他架构
对于剩余的架构,我们简要地讨论如何选择参数,并在图 16 − 20 16-20 16−20 展示其性能
AMD Opteron processor (2.2GHz, 64bit)
n r ≥ 2 / ( 2 × 0.71 ) ≈ 1.4 n_r \geq 2/(2\times0.71)\approx1.4 nr≥2/(2×0.71)≈1.4;经观察,在寄存器中存储 C C C 的条目的最佳选择是 m r × n r = 4 × 4 m_r\times n_r=4\times 4 mr×nr=4×4。
不像 P e n t i u m 4 Pentium4 Pentium4 那样,计算 A ~ \widetilde{A} A 的列和 B ~ \widetilde{B} B 的列的内积的内层循环没有必要展开, L 1 L_1 L1 的大小也不是一个问题。因此, k c k_c kc 可以取为使得 B ~ \widetilde{B} B 的一列( B ~ \widetilde{B} B 的一列就是 k c k_c kc 个双精度浮点数)占据半页的值: k c = 256 k_c=256 kc=256,取 m c × k c = 384 × 256 m_c\times k_c=384\times 256 mc×kc=384×256 可以使得 A ~ \widetilde{A} A 填充 T L B TLB TLB 可寻址空间的约三分之一。
最新的 O p t e r o n Opteron Opteron 架构支持 S S E 3 SSE3 SSE3 指令,我们注意到复制 B ~ \widetilde{B} B 的元素仍是有益的,这增加了 B → B ~ B\rightarrow\widetilde{B} B→B 的开销,使性能降低了约 3%。
该架构的性能展示如图 16 16 16 所示。
Intel Itanium2 processor (1.5GHz)
对于浮点数计算,该架构天然忽略了 L 1 L_1 L1数据缓存和 L 1 T L B L_1 TLB L1TLB,从而 I t a n i u m Itanium Itanium 的 L 2 L_2 L2 缓存和 L 3 L_3 L3 缓存扮演了其他架构的 L 1 L_1 L1 缓存和 L 2 L_2 L2 缓存的角色,并且只有 L 2 T L B L_2TLB L2TLB 是有意义的。因此 n r ≥ 4 / ( 2 × 2.0 ) = 1.0 n_r \geq 4/(2\times2.0)=1.0 nr≥4/(2×2.0)=1.0。由于有足够的可用寄存器( 128 128 128 个),取 m r × n r = 8 × 8 m_r\times n_r=8\times 8 mr×nr=8×8(占 32 32 32 个寄存器)。尽管 k c k_c kc 的(理论)最佳选择是 1 K 1K 1K( 1 K 1K 1K 的双精度浮点刚好占据半页),但在实践中 k c = 128 k_c=128 kc=128 的性能几乎一样好。
该架构有许多使得优化变得容易的特点:大量的寄存器,缓存和寄存器之间非常好的带宽,以及没有乱序执行。
该架构的性能展示如图 17 17 17 所示。
IBM POWRE5 processor (1.9GHz)
对于该架构, n r ≥ 4 / ( 2 × 0.93 ) ≈ 2.15 n_r \geq 4/(2\times0.93)\approx2.15 nr≥4/(2×0.93)≈2.15, m r × n r = 4 × 4 m_r\times n_r=4\times 4 mr×nr=4×4。该架构有一个 D − E R A T D-ERAT D−ERAT,相当于(其他架构的) L 1 T L B L_1~TLB L1 TLB,它的 T L B TLB TLB 相当于(其他架构的) L 2 T L B L_2~TLB L2 TLB。 k c = 256 k_c=256 kc=256 使得 B ~ \widetilde{B} B 的一列刚好占据一个页的一半,选择 m c × k c = 256 × 256 m_c\times k_c=256\times 256 mc×kc=256×256 可以使得 A ~ \widetilde{A} A 填充 T L B TLB TLB 可寻址空间的约四分之一。这是一种折中方案: T L B TLB TLB 的速度相对较慢。通过将 A ~ \widetilde{A} A 的内存占用空间控制在 D − E R A T D-ERAT D−ERAT 可寻址的大小,可以获得更好的性能。
该架构的性能展示如图 18 18 18 所示。
PowerPC440 FP2 processor (700MHz)
对于该架构, n r ≥ 4 / ( 2 ∗ 0.75 ) ≈ 2.7 n_r \geq 4/(2*0.75)\approx2.7 nr≥4/(2∗0.75)≈2.7, m r × n r = 8 × 4 m_r\times n_r=8\times 4 mr×nr=8×4。该架构有一个额外的复杂点:将 B ~ \widetilde{B} B 和 A ~ \widetilde{A} A 的元素从 L 1 L_1 L1 缓存和 L 2 L_2 L2 缓存移动到寄存器所需的组合带宽使得总带宽饱和,这意味着将 C C C 的元素加载进寄存器不能被计算重叠( o v e r l a p p e d overlapped overlapped),从而 k c k_c kc 应该取得非常大,以便在尽可能多的计算中分摊掉这个暴露的开销。选择 m c × k c = 128 × 3 K m_c\times k_c=128\times 3K mc×kc=128×3K,这使得 A ~ \widetilde{A} A 填充了 L 2 L_2 L2 缓存的 3 / 4 3/4 3/4。
到缓存的带宽缺乏、采用 F I F O FIFO FIFO 策略的 L 1 L_1 L1 缓存和乱序执行的指令,使得对该架构的优化变得困难。由于页面大小较大,因此 T L B TLB TLB 的可寻址空间很大。
需要注意的是, I B M IBM IBM 使用了类似于本文中讨论的技术来实现它们的矩阵乘法。
该架构的性能展示如图 19 19 19 所示。
Core 2 Woodcrest (2.66GHz) processor
在本文的最终版修订时, C o r e 2 W o o d c r e s t Core~2~Woodcrest Core 2 Woodcrest 最近发布,因此该架构的性能指标特别有趣。
对于该架构, n r ≥ 4 / ( 2 × 1.0 ) = 2.7 n_r \geq 4/(2\times 1.0)=2.7 nr≥4/(2×1.0)=2.7, m r × n r = 4 × 4 m_r\times n_r=4\times 4 mr×nr=4×4。对于 P r e s c o t t Prescott Prescott 架构(为什么又提 P r e s c o t t Prescott Prescott 架构?),有 16 16 16 个可用寄存器,每个能存储 2 2 2 个双精度浮点数,其中一半( 8 8 8 个)用于存储 C C C 的 m r × n r m_r\times n_r mr×nr 个条目( e n t r i e s entries entries)。 A ~ \widetilde{A} A 的占用空间等于 T L B TLB TLB 能寻址( c o v e r e d covered covered)的内存空间。
该架构的性能展示如图 20 20 20 所示。
Section8 Conclusion
我们对影响高性能矩阵乘性能设计的高层次问题(宏观层面, h i g h l e v e l i s s u e s high~level~issues high level issues)给出了系统的分析,这些见解被整合到一个能在大量不同架构上取得极高性能的实现中。
几乎所有目前 属于 L A P A C K LAPACK LAPACK一部分 的例程 [ A n d e r s o n e t a l . 1999 ] [Anderson et al. 1999] [Andersonetal.1999] 都会执行大量的 G E P P GEPP GEPP、 G E M P GEMP GEMP 或 G E P M GEPM GEPM 操作。类似地,重要的 B L A S k e r n e l s BLAS~kernels BLAS kernels 也能用这三种 G E M M GEMM GEMM 的特殊情形来描述 [ K a g s t r o m e t a l . 1998 ] [Kagstrom~et~al.1998] [Kagstrom et al.1998]。我们最近与 F L A M E FLAME FLAME 项目相关的研究表明:几乎所有这些例程都是 用 大量 G E P P GEPP GEPP 来进行计算的 算法变体。当与 基于本文中的见解实现的 矩阵乘法例程进行接口时,这些可选择的算法变体将获得非常好的性能。
其中一个不能 用大量的 G E P P GEPP GEPP 来重定义 的操作是 Q R QR QR 分解。在 Q R QR QR 分解中,大约一半的计算可用 G E P P GEPP GEPP 来描述,而另一半固有地需要 G E M P GEMP GEMP 或 G E P M GEPM GEPM 操作。而且 p a n e l panel panel 必须天然地取得很窄,因为它越宽,就必须执行更多的额外计算。这表明有必要进一步研究 这些 G E M M GEMM GEMM 的特殊情形的 高性能实现。
代码:http://www.tacc.utexas.edu/resources/software。
正文结束
以上均为个人理解,难免有理解错误或不到位的地方,欢迎指正。