并行与分布式系统中的并行前缀和(扫描)算法:Kogge-Stone并行前缀扫描算法
题目描述
我们有一个包含 \(n\) 个元素的数组 \(A[0 \dots n-1]\),希望计算其前缀和(也称为扫描运算)。具体来说,要计算数组 \(B\),使得:
\[B[i] = A[0] \oplus A[1] \oplus \dots \oplus A[i] \]
其中 \(\oplus\) 是一个满足结合律的二元运算符,如加法、乘法、取最大值、逻辑与/或等。
在并行计算中,我们希望利用多处理器同时计算这些前缀和,以减少总时间。Kogge-Stone 算法是一种经典的工作最优并行前缀和算法,它在拥有 \(p\) 个处理器的并行系统上,仅需 \(O(\log n)\) 时间步完成计算,并且总的操作量(工作负载)为 \(O(n)\),与串行算法相同。该算法特别适合于硬件实现(如 GPU、FPGA)或SIMD(单指令多数据) 架构。
解题过程循序渐进讲解
步骤1:问题理解与串行算法回顾
首先,理解前缀和的计算依赖关系:
- 串行算法依次计算:
\(B[0] = A[0]\)
\(B[1] = B[0] \oplus A[1]\)
\(B[2] = B[1] \oplus A[2]\)
...
这需要 \(O(n)\) 时间,但每一步都依赖于前一步的结果,因此天然是串行的。
我们的目标是设计一个并行算法,打破这种依赖链。
步骤2:并行前缀和的基本思想——倍增法
并行前缀和的核心思想是倍增法(doubling),也称为递归倍增。其基本操作是:在每一步,每个位置 \(i\) 尝试从距离它 \(2^k\) 的位置获取部分和,并更新自己的值。
定义:设 \(S_k(i)\) 表示第 \(k\) 步后,位置 \(i\) 存储的值为从 \(i\) 向前(向左)长度为 \(2^k\) 的区间和(若区间越界则取实际范围)。
更形式化地:
初始:\(S_0(i) = A[i]\)。
迭代:对于 \(k = 1, 2, \dots, \lceil \log_2 n \rceil\),执行:
\[S_k(i) = S_{k-1}(i) \oplus S_{k-1}(i - 2^{k-1}) \quad \text{(如果 } i - 2^{k-1} \ge 0 \text{)} \]
否则 \(S_k(i) = S_{k-1}(i)\)。
经过 \(\lceil \log_2 n \rceil\) 步后,\(S_{\log n}(i)\) 就是从 \(A[0]\) 到 \(A[i]\) 的前缀和 \(B[i]\)。
为什么正确?
因为每一步将覆盖区间长度翻倍。第 \(k\) 步后,位置 \(i\) 的值是区间 \([i-2^k+1, i]\) 的和(如果下标从0开始)。当 \(2^k > i\) 时,覆盖区间就包括了下标0,从而得到完整前缀和。
步骤3:Kogge-Stone 算法的具体模式
Kogge-Stone 算法是上述倍增法的一种具体实现模式,其特点是:
- 在每一步 \(k\),所有位置 \(i\)(只要 \(i \ge 2^{k-1}\))都并行地执行更新。
- 更新规则:\(S[i] = S[i] \oplus S[i - 2^{k-1}]\)。
- 算法在 逻辑上 需要 \(\log_2 n\) 步,每一步的并行度接近 \(n\)(除了越界的位置)。
伪代码(以共享内存并行模型为例,假设数组大小为 \(n\),且 \(n\) 是2的幂以简化):
输入:数组 A[0..n-1]
输出:前缀和数组 B[0..n-1]
1. 将 A 复制到 B 中 // 初始化:B[i] = A[i]
2. for k = 1 to log₂(n) do // 倍增步
3. parallel for i = 2^(k-1) to n-1 do
4. B[i] = B[i] ⊕ B[i - 2^(k-1)]
5. end parallel for
6. end for
步骤4:举例说明(n=8,运算符为加法)
初始数组 A: [3, 1, 4, 1, 5, 9, 2, 6]
第1步(k=1,偏移量=1):
- i=1: B[1] = B[1] + B[0] → 1+3=4
- i=2: B[2] = 4 + 1 = 5
- i=3: B[3] = 1 + 4 = 5
- i=4: B[4] = 5 + 1 = 6
- i=5: B[5] = 9 + 5 = 14
- i=6: B[6] = 2 + 9 = 11
- i=7: B[7] = 6 + 2 = 8
此时 B: [3, 4, 5, 5, 6, 14, 11, 8]
解释:B[i] 现在是 A[i-1] + A[i](对于 i≥1),即相邻两元素和。
第2步(k=2,偏移量=2):
- i=2: B[2] = B[2] + B[0] → 5+3=8
- i=3: B[3] = 5 + 4 = 9
- i=4: B[4] = 6 + 5 = 11
- i=5: B[5] = 14 + 5 = 19
- i=6: B[6] = 11 + 6 = 17
- i=7: B[7] = 8 + 14 = 22
此时 B: [3, 4, 8, 9, 11, 19, 17, 22]
解释:B[i] 现在是 A[i-3] + A[i-2] + A[i-1] + A[i](对于 i≥3),即连续4个元素和(对于 i≥3),但对 i=2 是前3个和。
第3步(k=3,偏移量=4):
- i=4: B[4] = B[4] + B[0] → 11+3=14
- i=5: B[5] = 19 + 4 = 23
- i=6: B[6] = 17 + 8 = 25
- i=7: B[7] = 22 + 9 = 31
最终 B: [3, 4, 8, 9, 14, 23, 25, 31]
这正是前缀和:[3, 3+1=4, 3+1+4=8, 3+1+4+1=9, 3+1+4+1+5=14, 3+1+4+1+5+9=23, 3+1+4+1+5+9+2=25, 3+1+4+1+5+9+2+6=31]。
步骤5:算法复杂度分析
- 时间:共有 \(\lceil \log_2 n \rceil\) 步,每一步所有可并行操作同时进行(假设足够多处理器)。所以并行时间为 \(O(\log n)\)。
- 工作总量:每一步大约有 \(n\) 个操作,总操作数约为 \(n \log n\)?仔细看:第 \(k\) 步的操作数约为 \(n - 2^{k-1}\),总操作数为 \(\sum_{k=1}^{\log n} (n - 2^{k-1}) = n \log n - (n-1) = O(n \log n)\)。这比串行的 \(O(n)\) 多了一个对数因子,因此不是工作最优的。
但是,Kogge-Stone 算法在实际硬件(如 GPU)中,因为其规则的通信模式(每个线程固定从距离为 \(2^{k-1}\) 的邻居读取),非常适合硬件实现,且当 \(n\) 不大时,对数因子影响不大。若要工作最优(总操作量 \(O(n)\)),可以使用 Brent–Kung 或 Ladner–Fischer 等变种,它们在理论上更优,但可能通信模式更复杂。
步骤6:在分布式内存模型中的考虑
在分布式内存系统(如 MPI)中,数组元素分布在不同处理器上。Kogge-Stone 算法需要全局通信,每一步都需要远距离数据交换。
通常做法:
- 每个处理器先计算本地前缀和。
- 然后进行 \(\log P\) 步的处理器间通信,交换部分和并更新。
- 最后调整本地结果。
这实际上是 并行前缀和的两阶段算法,Kogge-Stone 常用于处理器内部的计算,而处理器间则用树状或超立方体通信。
步骤7:扩展性与实际应用
- 任意结合运算符:只要运算符满足结合律,算法就适用。例如,加法、乘法、最大值、最小值、逻辑与/或、字符串连接等。
- SIMD 友好:Kogge-Stone 的规则内存访问模式非常适合 SIMD 指令集(如 SSE、AVX、GPU 线程束)。
- 硬件实现:在电路设计中,Kogge-Stone 是构建快速加法器(进位选择加法器)的基础结构。
- 局限性:需要 \(O(n \log n)\) 次操作,对于极大 \(n\) 可能不如工作最优算法高效。但在实际硬件中,常因规则性而胜出。
总结
Kogge-Stone 并行前缀和算法通过倍增策略,在 \(\log n\) 步内完成所有前缀和的计算。其核心是每一步将每个元素与其前方固定距离的元素进行结合操作,从而逐步累加覆盖范围。虽然总操作量略高于最优,但其规则的并行模式使其在硬件和高并行度架构中极具实用价值。理解该算法有助于掌握并行计算中递归倍增这一通用技巧。