CS267 · 第 15 课

并行矩阵乘法

从 BLAS 层次结构到通信下界,再到现代并行矩阵乘法算法(SUMMA、2.5D、Strassen),理解如何在最小化通信的前提下实现高效并行稠密矩阵乘法。

授课:Jim Demmel 视频:YouTube 预计用时:90 分钟

历史背景与并行 motif

矩阵乘法是科学计算中最基础的 kernel 之一。从 1970 年代至今,稠密线性代数(Dense Linear Algebra, DLA)的发展史就是一部与存储层次结构(memory hierarchy)和通信开销博弈的历史。

DLA 软件时间线

LAPACK 支持 28 种矩阵类型,涵盖对称、三角、带状、三对角等结构。理解这些结构对算法选择至关重要。


BLAS 层次结构

基本线性代数子程序(BLAS)按数据重用能力分为三个层次。核心思想:Level k 的算法将 O(n^k) 操作作用于 O(n^k) 数据,从而在 cache 中实现高效重用。

Level操作例子数据/操作比Cache 友好性
1O(n)AXPY, DOT, SCALO(1)差(流式)
2O(n²)GEMV, GERO(n)一般
3O(n³)GEMM, SYMM, TRSMO(n²)好(可分块)

为什么 Level 3 如此重要?

现代处理器中 flop 速率远高于内存带宽。Level 3 的 O(n²) 数据重用意味着一旦数据进入 cache,就可以被 O(n) 次 flop 反复使用。因此尽可能将计算表达为 BLAS3 操作是性能优化的首要原则。


为什么要最小化通信?

在并行和异构系统中,通信(数据移动)而非算术计算成为瓶颈。执行时间模型为:

T = (#flops) · γ + (#words) · β + (#messages) · α

其中:

年份Peak Flop/sBandwidth (GB/s)α (μs)Flops/word
20053.2e106.455
20101.2e1112310
20173.0e131001300

指数级增长的鸿沟

峰值 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];

选择分块大小 b

理想情况下,每个分块的 3b² 个 double 能放入 L1 cache:3b²·8 ≤ 32KB ⇒ b ≈ 37。实际中还需考虑 TLB、寄存器压力,最优 b 通常由 auto-tuning 决定。


通信下界

在并行矩阵乘法中,我们关心两个下界:最少需要移动多少数据(words),以及最少需要发送多少消息(messages)。

Hong-Kung 下界(I/O 复杂度)

对于在大小为 M 的快速内存中计算 n×n 矩阵乘法,必须至少有:

#words moved ≥ Ω(n³ / √M)

这由矩阵乘法的算术复杂度(2n³ flops)与每个 word 最多贡献 O(√M) 次 flop 的事实导出。

Irony-Tiskin-Toledo 消息下界

在分布式内存系统中,假设有 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达到下界?
经典分块 GEMMO(n³/√M)O(1)words ✓, messages ✗ (串行)
2D SUMMAO(n³/(P√M))O(n²/√M·log P)接近(log P 因子)
2.5D 算法O(n³/(P√M))O(n³/(c·M^(3/2)))参数可调
StrassenO(n^log₂7 / √M)Ω(…)减少 flop,降低下界

并行数据布局

在将矩阵分布到 P 个处理器时,数据布局决定了通信模式。

1D 布局

2D 布局

处理器排列为 P_r × P_c 网格。矩阵 A 按行分布、B 按列分布、C 按 (i,j) 分布。这是 ScaLAPACK 等库的标准选择。

布局通信量负载均衡实现复杂度
1D BlockedO(n²)一般简单
1D CyclicO(n²)好中等
2D BlockedO(n²/√P)好中等
2D Block-CyclicO(n²/√P)最好复杂

并行矩阵-向量乘法

矩阵-向量乘法 y = Ax 是迭代方法(CG、GMRES)的核心。在 2D 布局下,每个处理器持有 A 的一个子块和 x 的对应部分:

  1. 在拥有 x 的处理器列内做 broadcast(或 reduce)
  2. 每个处理器计算本地 A_ij · x_j
  3. 在行内做 reduce 得到 y_i

BLAS2 的局限

GEMV 是 O(n²) 操作对 O(n²) 数据,数据重用仅 O(1)。因此在 cache 和分布式系统中都难以高效。这也是为什么迭代方法常需要矩阵-矩阵乘积(如 s-step CG、block Krylov)来提升效率。


1D 矩阵乘法:总线与环形网络

最简单的并行 matmul:每个处理器持有一行(或一列)的数据。

Naïve 总线算法

// 每个处理器 i 持有 A 的第 i 行,需要 B 的所有列
for each processor i (in parallel):
    broadcast A[i][:] to all processors
    compute C[i][:] = A[i][:] * B

问题:每次 broadcast 都使用整个总线带宽,无法流水线化。

Ring 旋转算法

// 环形拓扑:处理器 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

Ring 算法的优势

在环形中,每个链路仅传输 O(n²/P) 数据,且可以流水线化(overlap 通信与计算)。SUMMA 算法正是基于这种 outer-product 思想。


SUMMA 算法

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 的通信成本

SUMMA 的优势:简单、通用、易于实现,是 ScaLAPACK PDGEMM 的基础。


2.5D 矩阵乘法

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;

2.5D 的 scaling 特性

当 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 算法

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 的数值稳定性

Strassen 算法使用减法组合中间结果,导致误差界从经典 O(ε·n) 变为 O(ε·n^0.81·log n)。对于大多数问题是安全的,但在 LAPACK 中仅作为可选优化(xGEQRF 等)。

并行 Strassen 性能

并行 Strassen 的通信下界也降低:#words = Ω(n^log₂7 / √M)。这意味着 Strassen 不仅减少 flop,还减少通信量。然而递归开销和内存需求使其在实际中通常只在大规模问题(n > 10000)时才有优势。


ScaLAPACK PDGEMM

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

SLATE(Software for Linear Algebra Targeting Exascapes)是 ScaLAPACK 的现代替代品,专为 GPU 和异构系统设计,使用 task-based DAG 调度。


本课要点

通信是瓶颈

在现代并行系统中,通信(数据移动)的代价远超算术计算,且差距指数级增长。

BLAS3 优先

将尽可能多的计算表达为 Level 3 BLAS 操作,以获得最佳 cache 重用。

通用下界

#words = Ω(F/√M) 和 #messages = Ω(F/M^(3/2)) 适用于所有直接线性代数算法。

SUMMA

简单、通用、渐近接近下界的 2D 并行矩阵乘法算法。

2.5D 与 Strassen

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 数)是多少?