通信是瓶颈
在现代并行系统中,通信(数据移动)的代价远超算术计算,且差距指数级增长。
CS267 · 第 15 课
从 BLAS 层次结构到通信下界,再到现代并行矩阵乘法算法(SUMMA、2.5D、Strassen),理解如何在最小化通信的前提下实现高效并行稠密矩阵乘法。
矩阵乘法是科学计算中最基础的 kernel 之一。从 1970 年代至今,稠密线性代数(Dense Linear Algebra, DLA)的发展史就是一部与存储层次结构(memory hierarchy)和通信开销博弈的历史。
LAPACK 支持 28 种矩阵类型,涵盖对称、三角、带状、三对角等结构。理解这些结构对算法选择至关重要。
基本线性代数子程序(BLAS)按数据重用能力分为三个层次。核心思想:Level k 的算法将 O(n^k) 操作作用于 O(n^k) 数据,从而在 cache 中实现高效重用。
| Level | 操作 | 例子 | 数据/操作比 | Cache 友好性 |
|---|---|---|---|---|
| 1 | O(n) | AXPY, DOT, SCAL | O(1) | 差(流式) |
| 2 | O(n²) | GEMV, GER | O(n) | 一般 |
| 3 | O(n³) | GEMM, SYMM, TRSM | O(n²) | 好(可分块) |
现代处理器中 flop 速率远高于内存带宽。Level 3 的 O(n²) 数据重用意味着一旦数据进入 cache,就可以被 O(n) 次 flop 反复使用。因此尽可能将计算表达为 BLAS3 操作是性能优化的首要原则。
在并行和异构系统中,通信(数据移动)而非算术计算成为瓶颈。执行时间模型为:
T = (#flops) · γ + (#words) · β + (#messages) · α
其中:
| 年份 | Peak Flop/s | Bandwidth (GB/s) | α (μs) | Flops/word |
|---|---|---|---|---|
| 2005 | 3.2e10 | 6.4 | 5 | 5 |
| 2010 | 1.2e11 | 12 | 3 | 10 |
| 2017 | 3.0e13 | 100 | 1 | 300 |
峰值 flop 速率每 ≈ 2.5 年翻 10×,而带宽和延迟仅每 ≈ 5 年翻 10×。这意味着通信开销相对于计算正指数级增长。未来 exascale 系统上,最小化通信比最小化 flop 更重要。
通信还消耗能量:移动 1 bit 的能量约是执行 1 flop 的 100–1000 倍。因此最小化通信同时优化能耗。
经典的 cache-oblivious 分块 GEMM 算法:将 A、B、C 分块为大小为 b×b 的子矩阵,使得 3b² 数据能放入 cache。
// Blocked GEMM: C = A * B + C
// A: m x k, B: k x n, C: m x n
for (ii = 0; ii < m; ii += b)
for (jj = 0; jj < n; jj += b)
for (pp = 0; pp < k; pp += b)
// Micro-kernel: C[ii:ii+b][jj:jj+b] += A[ii:ii+b][pp:pp+b] * B[pp:pp+b][jj:jj+b]
for (i = ii; i < min(ii+b, m); i++)
for (j = jj; j < min(jj+b, n); j++)
for (p = pp; p < min(pp+b, k); p++)
C[i][j] += A[i][p] * B[p][j];
理想情况下,每个分块的 3b² 个 double 能放入 L1 cache:3b²·8 ≤ 32KB ⇒ b ≈ 37。实际中还需考虑 TLB、寄存器压力,最优 b 通常由 auto-tuning 决定。
在并行矩阵乘法中,我们关心两个下界:最少需要移动多少数据(words),以及最少需要发送多少消息(messages)。
对于在大小为 M 的快速内存中计算 n×n 矩阵乘法,必须至少有:
#words moved ≥ Ω(n³ / √M)
这由矩阵乘法的算术复杂度(2n³ flops)与每个 word 最多贡献 O(√M) 次 flop 的事实导出。
在分布式内存系统中,假设有 P 个处理器,每个有大小为 M 的本地内存,则:
#messages ≥ Ω(n³ / M^(3/2))
Irony、Tiskin 和 Toledo(以及 Demmel 等人的后续工作)证明,对于任意直接 LA 算法(包括 LU、QR、特征值等),在大小为 M 的 fast memory 中完成 flop 数为 F 的计算,必须:
#words = Ω(F / √M)
#messages = Ω(F / M^(3/2))
这些下界是通用的——任何算法都无法突破。
存在一系列算法能够渐近达到上述下界:
| 算法 | #words | #messages | 达到下界? |
|---|---|---|---|
| 经典分块 GEMM | O(n³/√M) | O(1) | words ✓, messages ✗ (串行) |
| 2D SUMMA | O(n³/(P√M)) | O(n²/√M·log P) | 接近(log P 因子) |
| 2.5D 算法 | O(n³/(P√M)) | O(n³/(c·M^(3/2))) | 参数可调 |
| Strassen | O(n^log₂7 / √M) | Ω(…) | 减少 flop,降低下界 |
在将矩阵分布到 P 个处理器时,数据布局决定了通信模式。
处理器排列为 P_r × P_c 网格。矩阵 A 按行分布、B 按列分布、C 按 (i,j) 分布。这是 ScaLAPACK 等库的标准选择。
| 布局 | 通信量 | 负载均衡 | 实现复杂度 |
|---|---|---|---|
| 1D Blocked | O(n²) | 一般 | 简单 |
| 1D Cyclic | O(n²) | 好 | 中等 |
| 2D Blocked | O(n²/√P) | 好 | 中等 |
| 2D Block-Cyclic | O(n²/√P) | 最好 | 复杂 |
矩阵-向量乘法 y = Ax 是迭代方法(CG、GMRES)的核心。在 2D 布局下,每个处理器持有 A 的一个子块和 x 的对应部分:
GEMV 是 O(n²) 操作对 O(n²) 数据,数据重用仅 O(1)。因此在 cache 和分布式系统中都难以高效。这也是为什么迭代方法常需要矩阵-矩阵乘积(如 s-step CG、block Krylov)来提升效率。
最简单的并行 matmul:每个处理器持有一行(或一列)的数据。
// 每个处理器 i 持有 A 的第 i 行,需要 B 的所有列
for each processor i (in parallel):
broadcast A[i][:] to all processors
compute C[i][:] = A[i][:] * B
问题:每次 broadcast 都使用整个总线带宽,无法流水线化。
// 环形拓扑:处理器 i 将数据传给 (i+1) mod P
for step = 0 to P-1:
each processor i:
send A_local to processor (i+1) mod P
C_local += A_local * B_panel
receive new A_local from (i-1) mod P
在环形中,每个链路仅传输 O(n²/P) 数据,且可以流水线化(overlap 通信与计算)。SUMMA 算法正是基于这种 outer-product 思想。
Scalable Universal Matrix Multiplication Algorithm (SUMMA) 是 2D 并行 GEMM 的经典算法,基于 outer product 形式:
// SUMMA: C = A * B, A: m x k, B: k x n
// 处理器网格: P_r x P_c
for (p = 0; p < k; p++) {
// 拥有 A[:, p] 的处理器列 broadcast 到本列所有处理器
if (I_own_column_p) broadcast A_col to my row;
// 拥有 B[p, :] 的处理器行 broadcast 到本行所有处理器
if (I_own_row_p) broadcast B_row to my col;
// 每个处理器更新本地 C 块
C_local += A_col * B_row; // outer product
}
SUMMA 的优势:简单、通用、易于实现,是 ScaLAPACK PDGEMM 的基础。
2.5D 算法通过复制数据到 c 层来减少通信,在内存和通信之间取得平衡:
// 2.5D GEMM: 使用 c 份数据副本
// 将 P 个处理器组织为 c 个 P^(1/3) x P^(1/3) 的层
for each layer l in [0, c):
// 每层执行独立的 SUMMA 子问题
C_l += A_l * B_l;
// 跨层 reduce 结果
Reduce(C_0, C_1, ..., C_{c-1}) -> C;
当 c = P^(1/3) 时,2.5D 算法实现完美强扩展(perfect strong scaling):在 P 个处理器上使用 c 份副本,通信量与 P=1 时相同!代价是使用 c 倍内存。
#words = O(n³ / (P·√M)) · (c^(1/2))
#messages = O(n³ / M^(3/2)) · (1/c)
Strassen(1969)发现矩阵乘法可以用 7 次乘法而非 8 次完成:
// 将 A, B, C 分为 2x2 块
// 计算 7 个中间矩阵:
M1 = (A11 + A22) * (B11 + B22)
M2 = (A21 + A22) * B11
M3 = A11 * (B12 - B22)
M4 = A22 * (B21 - B11)
M5 = (A11 + A12) * B22
M6 = (A21 - A11) * (B11 + B12)
M7 = (A12 - A22) * (B21 + B22)
// 组合结果:
C11 = M1 + M4 - M5 + M7
C12 = M3 + M5
C21 = M2 + M4
C22 = M1 - M2 + M3 + M6
递归应用 Strassen:
T(n) = 7·T(n/2) + O(n²) ⇒ T(n) = O(n^log₂7) ≈ O(n^2.81)
比经典 O(n³) 算法减少约 13% 的 flop 数(当 n 大时)。
Strassen 算法使用减法组合中间结果,导致误差界从经典 O(ε·n) 变为 O(ε·n^0.81·log n)。对于大多数问题是安全的,但在 LAPACK 中仅作为可选优化(xGEQRF 等)。
并行 Strassen 的通信下界也降低:#words = Ω(n^log₂7 / √M)。这意味着 Strassen 不仅减少 flop,还减少通信量。然而递归开销和内存需求使其在实际中通常只在大规模问题(n > 10000)时才有优势。
ScaLAPACK 的 PDGEMM 实现了基于 SUMMA 的分布式 GEMM:
// ScaLAPACK PDGEMM 调用示例
pdgemm_("N", "N", &m, &n, &k, &alpha,
A, &ia, &ja, descA,
B, &ib, &jb, descB,
&beta,
C, &ic, &jc, descC);
SLATE(Software for Linear Algebra Targeting Exascapes)是 ScaLAPACK 的现代替代品,专为 GPU 和异构系统设计,使用 task-based DAG 调度。
在现代并行系统中,通信(数据移动)的代价远超算术计算,且差距指数级增长。
将尽可能多的计算表达为 Level 3 BLAS 操作,以获得最佳 cache 重用。
#words = Ω(F/√M) 和 #messages = Ω(F/M^(3/2)) 适用于所有直接线性代数算法。
简单、通用、渐近接近下界的 2D 并行矩阵乘法算法。
2.5D 通过数据复制换取通信减少;Strassen 通过减少 flop 降低下界。
问题 1:在时间模型 T = flops·γ + words/β + messages·α 中,哪个参数随时间增长最快(相对于其他)?
问题 2:根据通信下界,在大小为 M 的快速内存中完成 F 次 flop 的计算,最少需要移动多少 words?
问题 3:SUMMA 算法基于矩阵乘法的哪种数学形式?
问题 4:2.5D 矩阵乘法算法中,当副本数 c = P^(1/3) 时,可以实现什么性质?
问题 5:Strassen 算法的渐近复杂度(flop 数)是多少?