CS267 · 第 10 课

数据并行算法

数据并行是并行计算中最基础也最强大的范式——通过统一操作作用于大量数据,我们可以设计出 span 极低、可高效映射到 GPU、SIMD、SMP 等硬件的算法。

授课:Aydin Buluc 视频:YouTube 预计用时:90 分钟

什么是数据并行?

数据并行(data parallelism)是指对一组数据元素执行相同的操作。它是并行计算中最自然的并行来源,因为数据元素之间通常相互独立。

数据并行的层次

数据并行可以从简单到复杂分为多个层次:

归约(Reduce)与扫描(Scan)

数据并行中最核心的两个全局操作是归约和扫描:

Scan 是"并行排序器"

许多看似需要串行依赖的问题,都可以通过 scan 并行化。一旦你学会识别 scan 模式,就能把大量算法转化为并行版本。

理想化代价模型:Span 与 Depth

为了分析并行算法的效率,我们使用两个关键度量:

在 P 个处理器上的执行时间满足:

T(P) ≥ max(W/P, S)

并行度(parallelism)定义为 W / S。算法能达到的最大加速比受限于并行度。

Brent 定理

若一个并行算法的 work 为 W、span 为 S,则在 P 个处理器上的运行时间 T(P) ≤ W/P + S。这意味着只要 P ≤ W/S,就能获得接近线性的加速。

Blelloch 并行前缀算法

Blelloch(1990)提出的并行 prefix 算法是一个经典的 work-efficient 并行算法:

// Blelloch parallel prefix (exclusive scan)
// Phase 1: up-sweep (reduce)
for d = 0 to log(n)-1:
  parallel for k = 0 to n-1 by 2^(d+1):
    a[k + 2^(d+1) - 1] += a[k + 2^d - 1]

// Phase 2: down-sweep
a[n-1] = 0  // exclusive scan: set identity
for d = log(n)-1 down to 0:
  parallel for k = 0 to n-1 by 2^(d+1):
    t = a[k + 2^d - 1]
    a[k + 2^d - 1] = a[k + 2^(d+1) - 1]
    a[k + 2^(d+1) - 1] += t

Work 与 Span

Blelloch 算法的 work = O(n),span = O(log n)。虽然它比 naive 的 O(n log n) 版本做了更多操作,但它是 work-efficient 的——总操作数与串行算法同阶,这是并行算法设计的重要目标。

归约的下界证明

对基于二元算子 ⊕ 的归约,我们证明其 span 的下界为 Ω(log n)。

证明思路

假设 ⊕ 满足结合律但不满足交换律(最一般情况)。归约必须产生一个长度为 n-1 的二叉树。任何二叉树在 n 个叶节点上的高度至少为 ⌈log₂ n⌉。因此,任何并行归约算法的 span 至少为 Ω(log n)。

这意味着 Blelloch 算法达到了最优 span。

扫描的应用

扫描看似简单,却能解决大量问题。以下是一些典型应用:

1. 流压缩(Stream Compression)

给定数组 A 和布尔掩码 M,保留 M[i]==true 的元素并紧凑排列。

// 对掩码做 exclusive scan,得到输出下标
scan(M, out_idx)  // out_idx[i] = 前面 true 的个数
parallel for i:
  if M[i]: B[out_idx[i]] = A[i]

2. 基数排序(Radix Sort)

基数排序的每一轮都可以用 scan 实现:统计每个桶的元素个数,用 scan 确定每个桶的起始偏移,再用 scatter 把元素放到正确位置。

// 对 k 位 key 的数组做 radix sort(1 位为例)
// 1. 分离 0-bit 和 1-bit 的 mask
mask0 = (key & 1) == 0
// 2. 对 mask0 做 exclusive scan,得到 0 桶的输出位置
scan(mask0, pos0)
// 3. 1 桶的起始位置 = 总 0 个数
total0 = reduce(mask0)
// 4. 1 桶的输出位置
pos1 = (key & 1) * (total0 + pos1_raw) + ...
// 5. scatter 到输出数组

3. 链表排名(List Ranking / Pointer Doubling)

给定一个链表,计算每个节点到尾部的距离。串行是 O(n),并行用 pointer doubling 可达 O(log n)。

// Pointer doubling for list ranking
// next[i] = 下一个节点的下标,-1 表示尾
// dist[i] = 到 next[i] 的距离(初始为 1,尾节点为 0)
while any next[i] != -1:
  parallel for i:
    if next[i] != -1:
      dist[i] += dist[next[i]]
      next[i] = next[next[next[i]]]
      next[i] = next[next[i]]

Pointer Doubling 的精髓

每一步把每个节点的"下一个"指针向前跳两步,同时累加距离。经过 O(log n) 步后,所有节点都指向尾部,距离即为答案。

4. 斐波那契数列(Fibonacci via Matrix Prefix)

斐波那契递推可以写成矩阵乘法:

[F(n+1)]   [1 1]^n   [F(1)]
[ F(n) ] = [1 0]   × [F(0)]

因此 F(n) 就是矩阵 [[1,1],[1,0]] 的 n 次幂。多个 n 对应的值可以通过矩阵乘法并行前缀一次性算出。

5. n 位整数加法(Parallel Prefix Addition)

超大规模整数加法可以通过并行前缀进位(carry-lookahead)实现 O(log n) span。核心思想:

// Parallel prefix addition ( Brent-Kung / Sklansky style )
for i in 0..n-1:
  g[i] = a[i] & b[i]
  p[i] = a[i] ^ b[i]
// (g, p) 定义一个二元算子:
// (g2,p2) ∘ (g1,p1) = (g2 | (p2 & g1), p2 & p1)
scan on (g, p) with operator ∘ → carry[i]
sum[i] = p[i] ^ carry[i]

6. 词法分析(Lexical Analysis)

在编译器中,需要判断每个字符是否是 token 的起始位置。这可以通过 scan 实现:

// is_start[i] = 1 当且仅当 A[i] 是 token 的第一个字符
// 规则:A[i] 与 A[i-1] 属于不同字符类
is_start[0] = 1
parallel for i = 1..n-1:
  is_start[i] = (class(A[i]) != class(A[i-1]))
// 如果需要给每个 token 编号,对 is_start 做 exclusive scan

7. 矩阵求逆(Triangular / Dense Matrix Inversion)

三角矩阵的求逆可以通过分块 + 并行前缀实现。对于稠密矩阵,LU 分解中的多个步骤都可以识别出 scan 模式。

8. 分段扫描(Segmented Scan)

给定一个数组和一组分段标志,在每个分段内独立做 scan。例如:

A     = [1, 2, 3 | 4, 5 | 6, 7, 8]
flag  = [1, 0, 0 | 1, 0 | 1, 0, 0]
result= [1, 3, 6 | 4, 9 | 6, 13, 21]

分段 scan 可以通过扩展扫描算子(在分段边界处重置)来实现。

映射到硬件

数据并行算法需要在具体硬件上执行。主要目标平台:

硬件并行模型适合的数据并行模式
SIMD / 向量机同一指令作用于多个数据元素规则的一元/二元操作、连续访存
GPUSIMT,大量线程,内存层次丰富大规模规则 stencil、reduce、scan
SMP / MPP多核共享内存或分布式内存分块数据并行,MPI + OpenMP 混合

SIMD 映射

SIMD(Single Instruction, Multiple Data)是最直接的数据并行硬件。一条向量指令同时对 V 个元素执行相同操作。SIMD 要求数据对齐且连续,不支持不规则的 scatter/gather(或效率很低)。

GPU 映射

GPU 通过 SIMT 实现数据并行:

SMP / MPP 映射

在共享内存多处理器(SMP)和大规模并行处理器(MPP)上:

算法设计的意义

数据并行算法的研究告诉我们:只要识别出问题中的 scan/reduce 模式,就能设计出 work-efficient 且 span 最优的并行算法。这种"模式识别"能力是并行程序员的核心技能。


本课要点

数据并行

对大量数据执行相同操作,是最自然的并行来源。

Reduce 与 Scan

全局归约与并行前缀是数据并行中最核心的两个算子。

Span 与 Work

并行算法的两个基本度量;Brent 定理连接理想模型与现实执行。

Blelloch 算法

Work-efficient、span O(log n) 的并行前缀算法,达到归约下界。

应用广泛

从流压缩、基数排序到链表排名、大整数加法,scan 模式无处不在。


知识检查

问题 1:下列哪项不是数据并行的典型模式?

问题 2:并行归约(reduce)的 span 下界是什么?

问题 3:Blelloch 并行前缀算法的 work 和 span 分别是多少?

问题 4:在流压缩(stream compression)问题中,exclusive scan 的作用是什么?

问题 5:Pointer doubling 算法用于 list ranking 时,每轮迭代中每个节点做什么?