try ai
科普
编辑
分享
反馈
  • 并行计算

并行计算

SciencePedia玻尔百科
核心要点
  • 并行计算通过划分工作来加速任务,但其效率最终受限于处理器之间通信和同步的基本成本。
  • 问题的类型各不相同,从几乎随处理器数量线性扩展的“易并行”问题,到因基本数据依赖性而难以并行化的“内在串行”(P-完备)问题。
  • 现实世界的性能可能受到物理硬件限制,如内存带宽、缓存争用和热节流,有时更多的核心反而导致速度变慢。
  • 除了速度,并行计算还提供了一个强大的概念框架,用于理解复杂的去中心化系统,如市场、大脑和宇宙事件。

引言

在一个由数据和计算能力定义的时代,解决日益庞大和复杂问题的需求已将单处理器性能推向其物理极限。“仅仅制造更快的芯片”这种简单的解决方案已不再可行。这迫使计算领域发生范式转变,从单一的顺序思维模式转向由协调一致的处理器构成的“交响乐”。但这种“并行计算”究竟是如何运作的?除了速度之外,它在现实世界中又有哪些影响?本文旨在揭开并行计算世界的神秘面纱,探讨其理论前景与实践中通常混乱的现实之间的差距。在接下来的章节中,我们将首先探索其核心的“原理与机制”,深入研究什么样的问题适合并行处理、通信的关键作用以及内在串行任务的“坚不可摧之墙”。随后,我们将踏上一段旅程,探索其变革性的“应用与跨学科联系”,发现并行计算不仅解决了科学领域的重大挑战,还为理解从金融市场到人脑结构的一切事物提供了一个革命性的新框架。

原理与机制

既然我们已经瞥见了并行计算的前景,现在就让我们拉开帷幕,审视其引擎本身。它是如何工作的?什么样的问题适合由一组处理器来解决,又是什么让另一些问题顽固地抗拒并行化?如同任何宏伟的工程,并行计算有其核心原则、出人意料的技巧、难以逾越的极限和混乱的现实。从一个单一、缓慢的计算器到一首由协调核心演奏的交响乐,这段旅程是一个充满独创性与妥协的迷人故事。

“易并行”的梦想

想象一下,你有一项艰巨的任务:数清广阔海滩上每一粒沙子。单凭一己之力,这需要耗费一生。但如果你能雇佣一百万个助手呢?你可以将海滩分成一百万个小地块,分给每个助手,并告诉他们:“数清你地块里的沙子,然后向我报告。”当每个助手埋头苦干时,他们无需与任何其他助手交谈。他们的工作是完全独立的。

这就是​​易并行​​(embarrassingly parallel)问题的本质。这类问题似乎是为并行计算量身定做的,在这种情况下,“增加更多处理器”带来的“免费午餐”几乎是真实的。一个经典的例子是用蒙特卡洛方法估算 π\piπ。其思想是向一个完美内切一个圆的正方形内随机投掷飞镖。落在圆内的飞镖数与总投掷数的比率可以估算出两个区域的面积比,进而得出 π\piπ 的值。每一次投掷都是一个完全独立的事件,一次投掷对下一次没有任何影响。你可以让一个处理器投掷一百万次飞镖,也可以让一千个处理器各投掷一千次。在投掷飞镖阶段,处理器之间无需通信,它们只管工作。

对于这类问题,在理想情况下,性能增益非常简单。如果单个处理器完成 MMM 个任务需要时间 O(M)O(M)O(M),那么 PPP 个处理器原则上可以在 O(M/P)O(M/P)O(M/P) 的时间内完成工作。这被称为​​线性加速​​(linear speedup),是并行计算的“圣杯”。处理器数量加倍,时间减半。这是一个强大而直观的梦想。

协作的代价:通信与聚合

当然,海滩上的助手们最终必须报告他们各自的沙子计数,这样你才能将它们相加得到总数。这最后一步是通信和聚合的行为,是协作的代价。即使在最简单的并行任务中,独立工作的单元最终也必须合并它们的结果。

在计算中,这个聚合步骤通常是一个​​归约​​(reduction)操作,即使用一个二元运算符(如加法)将整个数值数组“归约”为一个单一的值。我们如何高效地对来自(比如说)1024个处理器的结果求和?一种天真的方法是让一个“主”处理器逐一调用其他1023个处理器来收集它们的结果。这种方法缓慢且串行,是一个浪费我们来之不易的并行收益的瓶颈。

一个远为优雅的解决方案是​​基于树的归约​​(tree-based reduction)。想象一个电话树。在第一轮中,处理器1将其结果加到处理器2的结果上,处理器3加到处理器4上,以此类推。我们仅用一个并行步骤就将部分和的数量减半。在下一轮中,第一轮的“胜出者”配对并重复此过程。这个过程持续进行,每个阶段活跃处理器的数量减半,直到一个处理器持有最终的总和。虽然对 PPP 个数字进行串行求和需要 P−1P-1P−1 步,但基于树的归约只需要 log⁡2(P)\log_2(P)log2​(P) 步。对于数千个处理器而言,这是一个巨大的差异。

这揭示了一个基本事实:并行解决问题的总时间是计算时间与通信时间之和。随着处理器数量 ppp 的增加,计算部分(Tcomp/pT_{comp}/pTcomp​/p)会变小,但通常依赖于 ppp 的通信部分可能会成为主导因素。真实加速比是一种权衡,由以下关系式捕获:S(p)=TserialTparallel=TcompTcompp+Tcomm(p)S(p) = \frac{T_{serial}}{T_{parallel}} = \frac{T_{comp}}{\frac{T_{comp}}{p} + T_{comm}(p)}S(p)=Tparallel​Tserial​​=pTcomp​​+Tcomm​(p)Tcomp​​。并行并非免费,它需要通信成本。

这里出现了一个微妙而深刻的问题。当我们以不同的顺序对浮点数求和时——并行归约与简单循环相比,不可避免地会这样做——我们通常会得到一个略有不同的答案!这是因为计算机算术的精度有限,且浮点加法并非完全满足​​结合律​​(associative)。(a+b)+c(a+b)+c(a+b)+c 的结果并不总是与 a+(b+c)a+(b+c)a+(b+c) 在比特层面上完全相同。这意味着,对于要求完美可复现性的科学工作,必须强制执行固定的归约顺序,为了确定性而牺牲一点性能。

解锁隐藏的并行性

那么,那些看起来不像可以划分为独立地块的海滩问题呢?如果任务之间似乎相互依赖怎么办?有时,一点数学上的巧思可以揭示看似不存在的并行性。

考虑一个长链操作,如 Pfinal=((P1⊕P2)⊕P3)⊕⋯⊕PNP_{\text{final}} = ((P_1 \oplus P_2) \oplus P_3) \oplus \dots \oplus P_NPfinal​=((P1​⊕P2​)⊕P3​)⊕⋯⊕PN​。这看起来是顽固的串行;在第二步完成之前,你无法计算第三步。这个计算的“深度”是 O(N)O(N)O(N)。但如果操作 ⊕\oplus⊕ 满足​​结合律​​(associative),比如加法或矩阵乘法呢?这意味着计算顺序无关紧要。我们可以重新排列括号!我们可以将其计算为一个“矮胖”的树形结构,而不是一个“瘦长”的链式结构:(P1⊕P2)(P_1 \oplus P_2)(P1​⊕P2​) 与 (P3⊕P4)(P_3 \oplus P_4)(P3​⊕P4​) 并行计算,然后将它们的结果合并。通过利用结合律,我们可以将一个深度为 O(N)O(N)O(N) 的计算转化为一个深度为 O(log⁡N)O(\log N)O(logN) 的计算,这是纯粹通过思维实现的奇迹般加速。

其他算法的并行性则以更微妙的方式内建于其结构中。Borůvka算法用于寻找最小生成树(一个经典的图问题),其工作过程是分阶段的。在每个阶段,它会识别出所有当前已连接的子图,或称为“组件”。然后,对于每个组件,它会同时找到连接该组件到另一个不同组件的最便宜的边。关键在于,每个组件都可以完全独立于其他组件执行此搜索。这并非易并行,因为阶段之间存在同步点,但在每个阶段内部,高度的并行性得以释放。

坚不可摧之墙:内在串行问题

尽管我们很聪明,但有些问题就是无法并行化。它们的核心存在一条不可打破的依赖链。最简单和最基本的例子是像 xt=g(xt−1)x_t = g(x_{t-1})xt​=g(xt−1​) 这样的递推关系。要计算时间 ttt 的状态,你必须知道时间 t−1t-1t−1 的状态。这就创建了一个​​依赖链​​(dependency chain)。从开始到结束所需的计算序列被称为​​关键路径​​(critical path)。无论你投入多少处理器,都无法缩短这条路径。求解时间将始终与链的长度 TTT 成正比。这就是为什么预测明天的天气依赖于了解今天的天气,或者为什么模拟股票价格的精确路径是一个循序渐进的过程。

这种“内在串行”的性质并不总是那么明显。它可以隐藏在算法的通信模式中。考虑两个计算化学问题。一个是蒙特卡洛模拟,属于易并行问题。另一个是密度泛函理论(DFT)计算,则性质迥异。在DFT计算的每一步,来自整个系统的信息——代表电子间复杂的量子相互作用——都必须被收集、转换(通常使用快速傅里叶变换),然后重新分发。这涉及到持续、大量的“全对全”(all-to-all)通信和全局同步。处理器们花在“交谈”上的时间比花在工作上的时间还多。

一个很好的具体例子来自求解大型线性方程组。一种称为​​全主元消去法​​(full pivoting)的技术通过在算法的每一步搜索整个剩余矩阵以找到最大元素,从而提供了出色的数值稳定性。在矩阵分布于数千个核心的并行机器上,这意味着每一步,所有上千个核心都必须停止,参与一次全局搜索,就“胜出者”达成一致,并等待数据相应地重新分配。这种全局同步成为一个致命的瓶颈,这就是为什么在实践中几乎总是使用更简单、“足够好”的策略,如​​部分主元消去法​​(partial pivoting)(它只搜索单列)。

计算机科学家为那些被认为是内在串行的问题起了一个正式的名字:​​P-完备​​(P-complete)。这些问题可以在串行机器上以多项式时间解决(它们属于​​P​​类),但它们似乎是“最难并行化”的问题。电路值问题(Circuit Value Problem)——确定一个逻辑电路的输出——是其典型例子。人们普遍认为,无论我们使用多少处理器,这些问题都无法在多对数时间内解决(即,它们不属于​​NC​​类)。为任何P-完备问题找到一个快速的并行算法将是一个革命性的突破,它将意味着所有P类问题都可以高效地并行化(P=NC),而大多数理论家认为这是错误的。

机器的现实:当更多核心意味着更多问题

到目前为止,我们一直将处理器和通信作为抽象实体来讨论。但真实的计算机是一个混乱的、资源有限的物理对象。正是在这里,并行加速的简洁理论常常撞上严酷现实的南墙。你可能会在8个核心上运行代码,发现它比在1个核心上快。受到鼓舞,你尝试了16个核心,结果发现它现在比8个核心时更慢了。这是怎么回事?

这种令人沮丧的现象称为负向扩展(negative scaling),可能由多种物理原因造成:

  • ​​内存带宽饱和:​​ CPU核心与主存(DRAM)之间的连接就像一条高速公路。如果8个核心已经造成了大量交通,再增加8个核心可能会导致完全拥堵。核心们大部分时间都在停顿,等待数据到达。

  • ​​缓存争用:​​ 核心共享资源,最主要的是末级缓存(LLC),这是一个小型、超快的内存库,用于存放常用数据。在8个核心的情况下,每个核心都能分到一块不错的缓存。增加到16个核心后,每个核心分到的就只有一半了。它们开始“颠簸”(thrash),不断地将彼此的数据踢出缓存,导致更多地往返于慢速主存。

  • ​​热节流:​​ 处理器会产生热量。16个核心全速运行比8个核心产生更多的热量和功耗。为避免熔毁,CPU的电源管理单元会介入,并告诉所有核心减速,降低它们的时钟频率。增加核心带来的增益被每个核心运行更慢的事实所抵消。

  • ​​NUMA效应:​​ 在高端工作站上,你可能会有两个独立的CPU,每个都有自己的本地内存。对于一个8核任务,操作系统可能会巧妙地将所有东西都保留在一个CPU上,这样所有的内存访问都是快速和本地的。对于一个16核任务,工作必须分配到两个CPU上。现在,一个CPU上的核心可能需要来自另一个CPU内存的数据,这需要通过互连进行一次慢得多的访问——这就是非一致性内存访问(NUMA)。

  • ​​同步多线程(SMT):​​ 像英特尔的超线程(Hyper-Threading)这样的技术,使单个物理核心在操作系统看来像是两个逻辑核心。如果你的CPU有8个物理核心,一个16线程的任务将在每个核心上放置两个线程。这些线程随后必须共享该核心的内部机制。对于计算密集型的科学代码,这常常导致它们互相干扰,两个线程一起运行的速度比单个线程独立运行时更慢。

机器中的幽灵:调试并行世界

最后,并行计算还有一个或许是最具人性的方面。编写一个正确的串行程序已经足够困难。编写一个正确的并行程序则是一个完全不同层级的挑战。

串行程序中的错误通常是确定性的。给定相同的输入,它每次都以相同的方式失败。你可以探查它,添加打印语句,并可靠地重现失败,直到找到原因。这些有时被称为“玻尔错误”(Bohr bugs)——坚实、可预测,就像行星模型一样。

然而,并行程序却被“海森堡错误”(Heisenbugs)所困扰——这些错误在你试图观察它们的那一刻似乎就改变了或消失了。由于操作系统和硬件在线程和消息的时序和调度上引入了微小、不可预测的变化,来自不同处理器的指令交错的确切顺序可能每次运行都不同。一个错误,比如两个线程在没有适当同步的情况下试图修改同一块内存的​​数据竞争​​(data race),可能只在数万亿种可能性中的一种特定的、“不幸的”交错中发生。

你运行代码一百次,它都完美无瑕。第一百零一次,它崩溃了。你试图添加日志语句来查看发生了什么。但打印到屏幕的行为是一个系统调用,它改变了时序——这种“探针效应”改变了交错顺序,于是错误消失了!就好像机器里的幽灵知道你在看着它。调试这些问题需要专门的工具和一种新的思维方式,你不仅仅是在调试单一的逻辑流,而是在调试许多逻辑流之间天文数字般复杂的相互作用。这就是并行世界的终极挑战和魅力所在。

应用与跨学科联系

在深入了解了并行计算的基本原理之后,我们可能会觉得它不过是一种工程技巧——一种让计算机运行更快的聪明方法。但这就像说望远镜只是让星星看起来更大的一种方式。一个新工具真正的魔力不在于更快地做旧事,而在于揭示全新的世界和新的思维方式。并行计算就是这样一种工具。它不仅使我们能够解决规模难以想象的问题,还为我们提供了一种强大的新语言和概念框架,用以描述宇宙、经济乃至我们自己心智的运作方式。

征服不可能:以暴力破解应对重大挑战

科学中的一些问题不仅仅是困难,而是规模庞大到骇人听闻。它们是如此之大,以至于没有任何一台计算机,无论多么强大,能够在其内存中容纳整个问题,更不用说在人的一生中解决它了。这些就是“重大挑战”(Grand Challenge)问题,它们是并行超级计算机的天然栖息地。

思考一下模拟两个黑洞碰撞的探索,这是数值相对论的基石。为了描述时空,我们必须将其分割成一个点网格,就像宇宙照片中的像素。但这是一张三维照片。如果我们决定将三个维度的“分辨率”都加倍以获得更清晰的图像,我们需要的不是两倍甚至四倍的内存,而是需要 2×2×2=82 \times 2 \times 2 = 82×2×2=8 倍的内存。所需内存随分辨率的立方 N3N^3N3 增长。此外,将模拟向前推进所需的时间步计算量也呈爆炸式增长,通常以更快的速度,如 N4N^4N4 扩展。对于现代研究所需的高分辨率网格,其巨大的数据量和计算量远远超出了任何单台机器的能力。这已不是等待更长时间的问题,而是根本不可能的问题。并行计算是唯一的出路。通过分解三维网格并将其分布到数千个处理器上,我们不仅聚合了它们的处理能力,同样关键的是,也聚合了它们的内存。每个处理器只持有宇宙的一小块,它们共同重建了数百万光年外发生的灾难性事件。

同样的故事也发生在其他领域。在材料科学中,模拟金属合金的凝固过程需要追踪微观晶体结构的演变。像相场建模这类依赖于类似网格计算的方法,也面临着同样的扩展挑战。但在这里我们学到了新的一课:仅仅划分问题是不够的。处理器之间必须通信,共享它们地块边界的信息。在使用快速傅里叶变换(FFT)等技术的复杂算法中,这种通信可能成为新的瓶颈。于是,并行编程的艺术变成了一场在计算与通信之间取得平衡的精妙舞蹈,需要设计巧妙的数据分解方式——如“笔式分解”(pencil decomposition)——来最大限度地减少处理器等待消息而非进行计算的时间。光速本身也成为算法设计中的一个限制因素。

众人拾柴火焰高:易并行问题

虽然有些问题像一个单一、巨大、相互关联的拼图,但另一些问题则更像一片满是石子的海滩,我们的目标是找到一颗特定的鹅卵石。我们不需要协调复杂的策略,只需要尽可能多的帮手,每个人搜索自己的一小片沙地。这些就是“易并行”问题,它们代表了并行计算最广泛和最成功的应用之一。

一个完美的例子是寻找一个特定的加密哈希值,这项任务是加密货币中“工作量证明”(proof-of-work)等技术的基础。问题很简单:找到一个数字,当它与给定的文本结合时,产生一个以(比如说)二十个零开头的哈希字符串。由于加密哈希函数的性质,没有巧妙的捷径。唯一的方法就是暴力破解:你必须一个一个地尝试每个数字,直到找到那个有效的。这个问题的妙处在于,每次猜测都是一个完全独立的计算。对数字5的检查与对数字5,000,000的检查毫无关系。因此,我们可以给我们的一百万个处理器分配不同的数字范围进行检查,它们可以同时工作而无需任何交流。第一个找到“金券”的人高喊“找到了!”,任务就完成了。

同样的原理也适用于探索数学模型的参数空间。在混沌理论中,著名的逻辑斯蒂映射(logistic map)的分岔图是通过对控制参数 rrr 的许多不同值运行模拟来生成的。每个 rrr 值都会产生一个完全独立的轨迹,可以在单独的处理器上计算。通过收集数千个并行计算的结果,一个美丽而复杂的结构从一系列本来看似孤立的数据点中浮现出来。这一策略是无数领域科学发现的主力,从药物发现(筛选数百万个候选分子)和金融建模(模拟数千种可能的市场情景)到搜寻地外智慧生命(分析数十亿个独立的无线电信号)。

错综复杂的依赖之舞

当任务不是独立的时会发生什么?如果问题一小部分的答案依赖于另一部分的答案怎么办?在这里,我们从简单的暴力破解转向了一场错综复杂、精心编排的数据之舞。

考虑生物信息学中比较两条长DNA链的挑战。Smith-Waterman算法是完成这项任务的标准工具,它构建一个大的二维矩阵,其中每个单元格 (i,j)(i,j)(i,j) 代表一个序列的前 iii 个字符与另一个序列的前 jjj 个字符的最佳比对得分。诀窍在于,单元格 (i,j)(i,j)(i,j) 的值依赖于其左侧、上方和左上角对角线邻居的值。你不能简单地一次性计算所有单元格。这种依赖性创造了一个计算的“波前”(wavefront)。你可以从计算第一个单元格 (1,1)(1,1)(1,1) 开始。一旦完成,你就可以并行计算它的邻居 (1,2)(1,2)(1,2) 和 (2,1)(2,1)(2,1)。当它们完成后,你就可以并行计算 (1,3)(1,3)(1,3)、(2,2)(2,2)(2,2) 和 (3,1)(3,1)(3,1)。计算像一道对角线波一样扫过整个矩阵。这种结构完美地适应了现代图形处理器(GPU)的架构,GPU包含数千个小型核心,可以同步地对不同数据点执行相同的操作,在单个并行步骤中处理矩阵的整个反对角线。

在复杂的工程模拟中,出现了另一种不同的舞蹈。在复合材料的多尺度分析中,工程师可能会构建一个大型结构(如飞机机翼)的“宏观”模型。但要理解机翼上每个点的材料属性,他们必须同时对该点材料的内部结构求解一个独立的“微观”模拟。这就创造了一个两级并行结构。“宏观”问题暂停,而数千个“微观”问题并行求解。但这里有一个转折:机翼的某些部分可能受力较小,行为简单(“弹性”响应),而其他部分可能接近断裂点,行为复杂(“塑性”响应)。塑性区域的微观模拟需要长得多的时间来求解。如果我们简单地给每个处理器分配固定数量的微观问题,一些处理器会很快完成并处于空闲状态,而另一些则被难题拖累。解决方案是一种名为​​动态负载均衡​​(dynamic load balancing)的巧妙策略。一个“主”处理器维护一个包含所有微观问题的队列。一旦一个“工作”处理器完成其当前任务,它就向主处理器请求一个新任务。这确保了所有处理器都保持忙碌,自然地适应了异构的工作负载,从而极大地加快了整体求解速度。

作为隐喻的并行性:科学的新透镜

也许并行计算最深远的影响不在于我们的机器,而在于我们的思想。它为我们提供了一套强大的类比和一种形式化语言,用以理解那些本身就是去中心化和并行的复杂系统。

经济学家 Friedrich Hayek 提出了“局部知识问题”:一个由数百万拥有各自独特局部信息的个体组成的国民经济,如何协调其活动以产生有效的结果?中央计划注定失败,因为没有任何单一实体能够收集和处理这海量分散的信息。Hayek 认为,市场解决了这个问题。使用并行计算的语言,我们可以使这个想法变得严谨。经济可以被看作一个大规模的分布式计算系统。每个公司或个人都是一个拥有私有、局部数据的“处理器”。价格体系充当了一种效率惊人、低带宽的通信协议。一个标量信号——价格——被广播出去。每个处理器(公司)随后利用其私有知识和这个公开价格,解决自己的局部优化问题(生产或消费多少)。这些并行计算的汇总结果被报告回来,价格随之调整,系统不断迭代。奇迹般地,这个分布式算法在从未集中局部知识的情况下,收敛到资源的全局最优配置。市场不是混乱,它是一台并行计算机。

这个透镜也可以转向内部,对准大脑。大脑没有中央CPU,它是一台大规模并行的机器。基底核(basal ganglia)是对于学习和行动至关重要的深层大脑结构,它包含多个在很大程度上相互隔离的处理环路。一个连接到运动皮层的环路,似乎专门用于学习程序性习惯——运动技能的“如何做”。另一个连接到边缘系统的环路,专门用于学习事物的激励价值——奖励的“价值几何”。这些环路并行运作,由不同的多巴胺信号调节。这种架构允许你同时学习骑自行车的肌肉记忆(一项程序性任务)和对某种冰淇淋口味的偏好(一项基于价值的任务),每个学习过程都在其专用通道中进行,互不干扰。看来,大脑在我们之前很久就发现了并行处理的效用。

最后,构建并行模拟这一行为本身,就迫使我们更深入地思考我们所模拟的现象。在一个基于主体的经济模型中,商业周期是由于主体根据公共新闻同步其预期而自然产生的,还是我们并行代码中用于让所有主体步调一致行动的“屏障同步”(barrier synchronization)造成的人为产物?并行计算的概念为提出这样的问题提供了词汇,而其技术——例如通过切换到异步更新方案来测试模型的鲁棒性——则提供了回答问题的方法。我们被迫将模型的结构与模拟的结构分离开来。从垂死恒星的核心到市场的逻辑,再到我们自己思想的架构,并行计算的原理提供的不仅仅是速度。它们提供了一种新的洞察力,揭示了信息处理方式和复杂系统自我组织方式中深层的结构统一性,无论是在我们构建的机器中,还是在我们努力理解的世界里。