
在一个从模拟宇宙事件到分析全球金融市场等科学和工业挑战都需要前所未有的计算能力的时代,并行计算是推动进步的重要引擎。我们已经构建了拥有数百万处理核心的机器,但释放其真正潜力并不仅仅是硬件问题。核心挑战在于问题本身的性质;有些问题可以毫不费力地被划分,而另一些则被复杂的依赖链所束缚。本文旨在解决如何设计能有效利用并行架构的算法这一根本问题。它深入探讨了决定任务如何并行的原则、我们面临的物理限制,以及为克服这些限制而发展的巧妙策略。
第一章“原理与机制”将探索并行性的谱系,从完美独立的任务到顽固的顺序性任务,并审视数据依赖和通信成本的关键作用。随后的“应用与跨学科联系”将展示这些核心思想不仅是抽象概念,更是解决金融、生物学、工程学和人工智能领域现实问题的强大工具。我们的旅程始于直面我们首先需要并行计算的根本原因:规模的暴政。
那么,我们拥有这些宏伟的机器,即拥有数千甚至数百万个处理核心并持续运转的超级计算机。但我们究竟如何让它们工作呢?这并不像把一个问题扔给一台百万核心的巨兽,并期望它能快一百万倍那么简单。事实证明,宇宙对于任务如何划分有着一些非常特殊的规则。并行计算的艺术与科学正是对这些规则的研究——这是一场深入问题本身结构的旅程。这是一个侦探故事,我们在此寻找独立的线索,揭示依赖的链条,并与通信的物理极限搏斗。
让我们从一个触及问题核心的问题开始:为什么我们不能只建造一台、单一的、快到无法想象的计算机?想象一下,尝试模拟宇宙中最剧烈的事件之一:两个黑洞的合并。为此,物理学家将时空切成一个巨大的三维网格,就像一个宇宙棋盘,然后一步步地计算每个点上引力的演化。
现在,假设我们的网格每条边有 个点。一个不错的模拟可能会使用 。我们需要在计算机内存中存储的点数不是 ,而是 ,也就是十亿个点。每个点都需要存储几个代表空间扭曲几何形状的数字。突然之间,你需要存储数百亿个数字。地球上没有一台计算机拥有那么大的内存。而这仅仅是为了保存一个时间快照!为了计算下一步,总操作数以类似的方式扩展,并且由于稳定性约束(著名的Courant–Friedrichs–Lewy条件),我们的时间步长必须随着网格变细而缩小。这导致总计算工作量可以以高达 的残酷方式扩展。
这就是我们所说的规模的暴政。问题不仅仅是在单台计算机上需要很长时间;它在物理上甚至无法开始计算。内存和计算需求远远超过了任何单一机器所能提供的。并行计算不是一种奢侈;它是我们唯一的出路。我们将网格分布在数千个处理器上,让每个处理器负责一个可管理的区块。我们不仅是在分担劳动;我们是在聚合一个集体的内存来容纳一个对于任何个体来说都过于庞大的问题。
一旦我们决定要划分一个问题,下一个问题是:如何划分?我们能把它切成一千块然后分发出去吗?有趣的是,答案完全取决于问题内在的性质。问题存在于一个谱系上,从完美独立到顽固交织。
在谱系的一端,我们有理想的情景。我们称这些任务为易并行(embarrassingly parallel),这是计算机科学家带着微笑使用的术语。它意味着问题可以分解为完全独立的子任务,这些子任务在最终阶段之前几乎不需要相互通信。
最简单的例子是反转一个数字数组的顺序。如果你有一个包含 个元素和 个处理器的数组,你可以简单地指示处理器 读取元素 并将其写入新位置 。每个处理器都可以同时执行此操作,而无需向任何其他处理器请求信息。整个工作在一个单一的、常数时间的步骤中完成。用复杂性理论的语言来说,这是一个属于NC⁰类的问题,即“最简单”的并行问题类别。
一个更现实的例子来自计算化学或物理学。想象一下,你想计算一种液体的平均属性,比如它的压力。一种方法是使用蒙特卡洛模拟。你生成数百万个不同的、随机的分子位置快照,并为每个快照计算压力。然后你对结果进行平均。这里的关键词是独立。一个快照的计算绝对不影响另一个快照的计算。你可以给一千个处理器一千个不同的起始种子,让它们同时运行各自的模拟。只有在最后,它们才需要通信,将它们的最终结果发送给一个主处理器进行平均。
在谱系的另一端,即更令人沮丧的一端,存在着似乎抗拒被分解的问题。它们的决定性特征是数据依赖:步骤B在步骤A完成之前无法开始。
一个经典而优美的例子是使用所谓的Horner方案来评估一个多项式。为了计算 ,你可以将其重写为嵌套形式:
这导出了一个简单而优雅的递推关系: 从 开始。 然后,对于从 到 的 ,计算 。 最终答案是 。
仔细观察这个递推关系。要得到 ,你需要 。要得到 ,你需要 。依此类推。你有一个从 一直延伸到 的不可打破的依赖链。你不能一次性计算所有的 值;你被迫一个接一个地做。这个算法有一个关键路径——最长的依赖操作链——其长度与多项式的阶数 成正比。这意味着无论你为单次评估投入多少处理器,所需时间总是受限于这个顺序链。该算法是内生顺序性的。
这种相同的依赖模式出现在许多地方。在求解线性方程组时,Gauss-Seidel方法使用来自同一迭代中分量 的最新值来更新解向量的第 个分量。同样,你有一个顺序链:在得到新的 之前,你无法计算 ;在得到新的 之前,无法计算 ,依此类推。这也是困扰许多强大预条件子(如不完全LU(ILU)分解)应用的恶魔。应用预条件子涉及求解三角系统,这意味着执行前向和后向替换——这两者都只是我们在Horner方法中看到的相同顺序依赖链的不同伪装。
大多数有趣的问题都处于这两个极端之间的混乱中间地带。在这里,算法设计者的技巧大放异彩。通常,算法中的一个微小改变就能对其并行性质产生巨大影响。
让我们重新审视求解线性系统的问题。我们看到Gauss-Seidel方法是顺序的。但如果我们稍微改变一下规则会怎样呢?在Jacobi方法中,为了计算迭代 的整个新解向量,我们严格只使用来自前一次迭代 的值。现在,每个分量 的更新规则只依赖于旧向量 。新向量的分量之间不再有任何依赖关系。 的所有 个分量都可以同时且独立地计算!我们打破了依赖链,创建了一个并行算法。我们付出的代价是Jacobi方法有时收敛得更慢(需要更多次迭代),但在并行计算的世界里,许多较慢的并行步骤可能比少数的顺序步骤要快得多。
这引出了一个关键的问题类别,它们不是易并行的,但仍然是高度可并行的。考虑我们前面例子中的密度泛函理论(DFT)计算。这是一个复杂的迭代过程,用于寻找材料的电子结构。与蒙特卡洛模拟不同,一个电子的状态与所有其他电子的状态密不可分。该算法需要像快速傅里叶变换(FFT)这样的操作来在实空间和倒易空间之间切换,或者需要正交化过程来保持电子波函数的良好行为。这些操作是集体性的。例如,一个并行的FFT需要一个“全对全”的通信步骤,其中每个处理器都必须将其本地数据的一部分发送给其他所有处理器。这是一个数据并行问题:我们可以分发数据,但处理器必须不断地通信和同步来更新全局状态。目标是设计这些通信模式,使其尽可能高效。
这使我们面临一个严酷的物理现实。到目前为止,我们讨论的都是抽象的依赖关系。但在真实的机器中,“通信”意味着通过电线发送电信号或光信号。这需要时间。一个处理器向几排机架外的另一个处理器请求数据,可能需要等待一段在计算术语中感觉像永恒的时间。通信成本,而非计算成本,通常是真正的瓶颈。
LU分解中主元选择策略的选择就是对此的一个绝佳说明,LU分解是求解稠密线性系统的标准方法。为了保持数值稳定性,必须交换行(可能还有列),以将可能的最大元素带到主元位置。完全主元法通过在每一步搜索整个剩余子矩阵来寻找最大元素,从而提供最佳的数值稳定性。在分布式内存的超级计算机上,这个子矩阵分布在所有处理器上。找到那个全局最大值需要一个全局同步:每个处理器都必须参与一次集体通信来找到获胜者。这会在分解的每一步都使整个计算停顿。
相比之下,部分主元法在数值上不那么稳健,但只在当前列中搜索主元。在典型的数据布局中,该列驻留在一列处理器上。通信现在被限制在一小部分处理器内,速度快得多。因此,尽管完全主元法在理论上更优越,但几乎没有大规模并行库使用它。全局通信的实际成本胜过了数值上的完美。
经过这次旅程,我们可能会问:有些问题是否就是根本上、不可约减地是顺序的?是我们还不够聪明,没能找到并行算法,还是有更深层次的原因?
复杂性理论用P-完备性的概念给出了一个深刻但不完整的答案。把P类看作所有在单台顺序计算机上能在多项式时间内解决的问题。把NC类(“Nick's Class”)看作是“可有效并行化”的问题——可以用多项式数量的处理器在多对数时间()内解决。如果一个问题在P类中,并且在形式上是P类中“最难”的问题,那么它就是P-完备的。电路值问题(给定一个逻辑电路和输入,输出是什么?)就是一个经典的例子。已经证明,如果任何一个P-完备问题能在NC中解决,那么P中的所有问题都能在NC中解决,这意味着 。这是一个巨大的论断,大多数理论家认为这是错误的。因此,一个P-完备问题在非常深刻的意义上被认为是“内生顺序性”的。为它找到一个大规模并行算法被认为极不可能,因为这将彻底改变我们对计算本身的理解。
最后,让我们回到现实。假设我们成功了。假设我们为DFT发明了一种革命性的新算法,它完美并行,使我们的主要计算速度提高了10倍。我们将其部署到我们发现新催化剂的自动化工作流程中。会发生什么?我们很快发现,我们的超级计算机大部分时间都在……等待。等待从磁盘读取数据,等待结果写入数据库,等待作业调度程序启动下一个任务。
这就是Amdahl定律的教训。一项任务的总加速比受限于该任务中无法被加速的部分。通过显著加速主要的计算核心,我们使得之前可以忽略不计的部分——数据I/O、文件解析、任务编排——成为了新的瓶颈。对性能的追求是一段持续的旅程。当我们征服一座山峰时,另一座先前隐藏在迷雾中的山峰便显现为我们的下一个挑战。理解这些原则——从黑洞的尺度,到方程中的依赖关系,再到工作流中数据移动的现实——是真正驾驭并行计算力量的关键。
现在我们已经探讨了并行计算的基本原理,你可能会问:“这一切都是为了什么?”这是一个合理的问题。物理学家的乐趣不仅在于揭示自然法则,还在于看到这些法则如何描绘我们周围的世界,从最宏伟的宇宙结构到最卑微的生物细胞。本着同样的精神,并行算法的真正美妙之处不在于数据依赖和同步的抽象规则,而在于它们解决实际问题的能力,以及更深刻地,为我们提供一个审视世界的新视角。
让我们踏上一段旅程,穿越几个看似不相关的领域——金融、生物学、工程学和人工智能——去看看同样的核心并行思想如何一次又一次地出现,揭示了复杂系统架构中惊人的一致性。
最简单也或许是最常见的并行形式,就是计算机科学家以其特有的不拘礼节的方式称之为“易并行”的。这个名字掩盖了它巨大的威力。它描述了任何可以被分解为大量彼此完全独立的较小任务的问题。总工作量仅仅是所有单个结果的集合。工人们之间不需要交谈;他们都可以埋头完成自己分配到的那部分工作。
考虑一下高风险的金融世界。一个核心任务是计算“风险价值”(VaR),这是一个衡量银行或投资基金在糟糕的一天可能损失多少钱的指标。一种常见的方法是通过历史模拟:你拿着你当前的投资组合,重演历史,计算在过去(比如说)一万个交易日中的每一天,你的损失会是多少。这些历史情景中的每一个都是一个完全独立的计算。1998年3月5日的损失与2007年10月19日的损失毫无关系。一台并行机器可以简单地给它的每个处理器分配一批日期来计算。这是一个完美的“易并行”任务,就像一支由办事员组成的军队,每人计算不同日期的结果。他们唯一需要聚集在一起的时候是在最后,将他们成千上万的损失数字汇总成一个单一的风险概况。
同样的模式也出现在重建生命之树的宏伟探索中。演化生物学家比较不同物种的DNA序列,以推断它们的演化关系。一个提议的演化树的可能性是通过独立分析DNA序列中的每个位点来计算的。某个特定基因中第57个位置的演化故事被认为与第58个位置的故事是独立的。由于基因组包含数百万或数十亿个位点,这个计算量是巨大的。然而,它也是易并行的。我们可以给每个处理器分配一组DNA位点,让它计算该组的可能性,然后在最后简单地将结果相乘。没有这种内在的并行性,让我们能够投入巨大的计算能力,现代系统基因组学将是不可能的。
即使是为喷气发动机或桥梁设计新材料也依赖于这一原则。在一个称为(平方有限元)的强大模拟技术中,工程师通过认识到大型材料由微观的异构小块组成来对其进行建模。为了理解整体的强度,他们在较大结构内的数百万个点上模拟一个微小的“代表性体积单元”(RVE)的响应。这些微观模拟中的每一个都是一个独立的任务。计算机实际上派遣了一支虚拟力学师军队,每人测试一小块材料在其局部条件下的表现,而整体的行为则从这些独立代理的集体工作中浮现出来。
当然,世界并非总是如此整齐地可分。许多问题更像是一场精心编排的舞蹈,而不是一屋子独立的工人。舞者们必须意识到彼此,协调他们的动作以创造一个连贯的整体。这就是数据并行的领域,我们在这里将数据本身划分给处理器,但计算需要通信和同步。
一个美丽的例子来自机器学习领域。-均值算法是发现数据中模式的主力军,用于从将客户划分为营销群体到识别星系中的星团等各种任务。该算法包括将每个数据点(例如,一个客户)分配给最近的簇“质心”,然后更新质心为新分配给它的点的均值。为了并行化这个过程,我们可以将数百万客户数据点分布在我们的处理器上。每个处理器可以将其本地客户分配给全局已知的质心(“map”阶段)。但要更新质心,每个人都必须做出贡献。每个处理器计算每个簇的向量部分和以及点的部分计数。然后,在一个“reduce”阶段,这些部分结果被通信和聚合,以计算下一次迭代的新的全局质心。这是一个局部工作后跟全局通信的节律性循环。
这种“map-reduce”模式是现代数据科学的核心。经济学家拟合复杂模型时,正是通过这种方式在包含数百万观测值的庞大数据集上计算似然函数的梯度。每个处理器在其数据切片上计算部分梯度,最终的归约步骤将它们相加以获得优化算法的最速下降方向。局部计算是并行的,但最终的归约步骤是一个必要的同步时刻,一个阻止完美扩展但对算法完整性至关重要的瓶颈。
当依赖关系更加错综复杂时会发生什么?有时,一个任务只有在它的直接邻居完成后才能开始。你不能简单地任意划分工作。这种情况催生了一种被称为波前的美丽计算模式。
想象一个网格,其中每个单元格的值取决于其北、西和西北方向邻居的值。你不能一次性计算整个网格。然而,你可以计算位置 的单元格。一旦完成,你就可以计算它的邻居 和 。而一旦那些完成,你就可以计算 、 和 。你看到这个模式了吗?可计算的单元格集合形成一个像波浪一样扫过网格的反斜线。这个波前上的所有单元格都可以并行计算。
这正是Smith-Waterman算法所面临的挑战,它是生物信息学的基石,用于寻找两个DNA或蛋白质序列之间的相似区域。比较矩阵中任何一点的对齐分数都取决于其邻居。通过沿着这些反斜线波前处理矩阵,我们可以释放并行处理器(如GPU)的力量,以显著加速寻找关键基因和蛋白质相似性的过程。
类似的想法出现在一个更抽象的场景中:计算对称矩阵的特征值,这是量子力学、振动分析和数据科学中的一项基本任务。并行Jacobi方法通过迭代地消去非对角元素来工作。一次“扫描”包括对矩阵应用一组旋转。关键是,只要它们作用于不相交的行和列集合,我们就可以同时执行多个旋转。一个精心设计的调度将所有非对角元素划分为多个阶段,其中每个阶段都包含一组可以并行执行的无冲突旋转。每个阶段就像一个波前,在移动到下一个阶段之前清理掉一组非对角元素。
到目前为止,我们讨论了如何并行化一个单一的大型计算。但如果问题不是一次计算,而是从一个令人难以置信的庞大可能性宇宙中找到一个单一的最优解呢?想想著名的旅行商问题(TSP):找到访问一组城市并返回起点的最短可能路线。即使对于数量不多的城市,可能的路线数量也比宇宙中的原子数量还多。蛮力检查是不可能的。
在这里,并行性提供了一种不同的策略:参数并行。我们不是划分数据,而是划分搜索。我们可以创建许多独立的搜索队伍,让它们同时探索解空间的不同区域。
岛屿模型遗传算法是这一思想的完美体现。我们创建几个“岛屿”,每个岛屿都有自己的候选解(路线)种群。每个岛屿独立地演化其种群,模仿自然选择以找到越来越好的路线。岛屿之间会周期性地通信,将它们最好的“移民”发送给邻居。这使得在一个岛屿上发现的好解决方案能够传播并影响其他岛屿的搜索,防止任何单一的种群陷入局部最优。这是一种美丽的并行启发式方法,它将独立探索与协作发现结合起来。
这把我们带到了最深刻的联系。并行计算不仅仅是我们为了解决问题而发明的一种工具。它似乎是自然界本身使用的一个基本原则。通过研究并行算法,我们获得了一套词汇和概念,用以理解构成我们世界的复杂、去中心化的系统。
我们已经看到方法如何将材料建模为并行、相互作用的微观域的集合。模型的结构本身是并行的,因为它所描述的现实是并行的。
但也许最引人注目的例子来自于深入观察活细胞内部。一个细胞必须根据来自多个途径的大量输入信号做出关键决策——分裂、分化、死亡。其中一些信号可能是嘈杂的、矛盾的,或者根本就是错误的。面对如此不确定性,细胞如何做出可靠的决策?
一个引人注目的模型使用分布式计算的语言来构建这个问题。我们可以将信号通路视为处理器,将决策复合体视为一个需要达成共识的系统。该模型提出,细胞使用一种法定人数规则(quorum rule),非常像容错计算机网络中使用的规则。只有当足够数量的通路投票“激活”时,才会做出“激活”转录的决定。这确保了少数错误的信号不会引发灾难性的错误。在分布式系统中保证“安全性”(不做出矛盾的决定)和“活性”(当证据明确时做出决定)的数学规则,结果成为理解生命逻辑本身的一个强大模型。
从证券交易所的交易大厅到细胞的核心,并行计算的原理无处不在。它是一种处理信息和管理复杂性的通用架构。通过学习它的语言,我们不仅能成为更好的工程师和科学家,能够构建更快的计算机和解决更大的问题,而且我们还对我们所居住的这个错综复杂、相互关联且美妙并行的世界有了更深的欣赏。