
在追求计算速度的过程中,仅仅“同时做很多事”是远远不够的。并行计算的真正挑战在于有效地协调工作,特别是当工作不均匀时。虽然简单的数据并行在处理重复性任务(如工厂流水线)方面表现出色,但在面对定义了科学和工程前沿的复杂、不规则问题时,它就会力不从心。这种被称为负载不均的低效率问题,会导致资源浪费和性能瓶颈,从而在我们解决这些挑战性问题的能力上造成了关键的差距。
本文将介绍基于任务的并行,这是一种更灵活、更强大的范式,旨在应对这种复杂性。通过将计算重构为一个由相互依赖的任务组成的网络,该模型开启了效率和性能的新高度。首先,在“原理与机制”一章中,我们将揭示这种方法背后的核心概念,探讨问题如何被表示为有向无环图(DAG),以及动态调度器如何将这种结构付诸实践。随后,“应用与跨学科联系”一章将展示这些原理如何应用于从天气预报到神经科学等领域以驾驭混乱,从而展示该模型深远的现实世界影响。
要真正欣赏并行计算的艺术,我们必须超越“同时做很多事”这一简单想法。真正的美在于理解如何组织工作以及哪些工作可以同时进行。并行计算的世界是广阔的,但我们可以通过探索两种伟大范式之间的根本区别来开始我们的旅程:数据并行和任务并行。
想象一个现代化的汽车工厂。提高生产速度最简单的方法是建立多条相同的装配线。每条生产线接收一个底盘,并对其执行完全相同的操作序列——安装轮子、安装发动机、装上车门——直到一辆成品车下线。这就是数据并行的本质。你有一个定义明确的单一过程(“内核”或“程序”),并将其同时应用于许多独立的数据片段(汽车)。
当工作是均匀的时,这个模型非常强大。在科学计算中,一个完美的例子出现在地球力学中使用的有限元方法。为了模拟地下的应力,科学家们将区域划分为成千上万个小的“单元”。计算一个单元刚度的过程在数学上与计算任何其他单元的过程完全相同。我们可以将每个单元分配给一个不同的计算工作者(比如,一个图形处理单元(GPU)上的线程),让它们并行执行相同的计算集。这是一个数据并行的梦想,一条完美同步的数字装配线。现代 GPU 是这方面的大师,它们采用一种称为单指令多线程(SIMT)的模型,其中成组的线程以锁步方式执行相同的指令,高效地处理均匀的数据。
但当工作不均匀时会发生什么呢?如果我们的工厂还必须制造定制车辆怎么办?一辆车可能需要天窗,另一辆需要特制发动机,第三辆则需要复杂的线束。一条僵化的装配线将会陷入停顿。标准车型生产线上的工人会很快完成工作,然后闲置下来,等待处理复杂定制任务的那个工人完成。这种负载不均的问题困扰着许多现实世界的模拟。
考虑对地震破裂进行建模。作用集中在断层线周围,而远处,地面几乎不动。为了高效地模拟这一点,我们使用自适应网格加密(AMR),仅在感兴趣的区域创建更精细的网格并进行更多的计算工作。如果我们将其视为一个简单的数据并行问题,那么分配给“安静”区域的核心会几乎立即完成它们的工作,然后等待。模拟的每一步的总时间都由最慢的工作者决定,即处理破裂最复杂部分的那个。这是经典的块同步并行(BSP)模型的弱点,在该模型中,所有工作者先计算,然后通信,再在一个全局屏障处等待所有人赶上进度才能继续。这种等待是浪费的时间,而屏障是由单一最重的任务决定的瓶颈。
正是在这里,一个更灵活、看似混乱但最终更强大的思想应运而生:基于任务的并行。
基于任务的并行不再将问题看作是由单一程序处理的数据集合,而是邀请我们将其看作是一系列带有依赖关系的任务的集合。一个任务只是一块工作——它可以是更新一个网格片,通过网络发送一条消息,或者计算一个物理量。依赖关系则是一条规则,规定一个任务必须在另一个任务完成后才能开始。
把它想象成烤蛋糕。你有一系列任务:预热烤箱、混合干性配料、混合湿性配料、将它们混合、倒入烤盘、烘烤,然后上霜。你不能在混合面糊之前烤蛋糕,也不能在蛋糕烤好之前给它上霜。但是你可以在混合配料的同时预热烤箱。
这个由任务和依赖关系组成的网络形成了一个称为有向无环图(DAG)的数学结构。“有向”是因为依赖关系有方向(任务A必须在任务B开始之前完成),“无环”是因为没有循环依赖(你不能有A依赖于B,而B又依赖于A的情况)。DAG 是我们计算的完整“配方”。它捕捉了问题的真实逻辑流程,揭示了所有并行的机会。对于我们的地震模拟,任务可能是“更新片A”、“更新片B”、“为A打包幽灵单元数据”以及“将数据从A发送到B”。那么“更新片B”任务将依赖于“将数据从A发送到B”任务。
一旦我们有了这个 DAG,我们就从锁步装配线的束缚中解放了出来。我们有了一个关于什么可以做的蓝图,并且我们可以让一个聪明的管理者来找出最好的执行方式。
基于任务的系统中的“聪明管理者”就是运行时调度器。它审视整个 DAG,并维护一个“就绪”任务池——这些任务的所有依赖都已满足。每当一个处理器核心空闲下来,调度器就简单地从池中分派一个就绪任务给它。这个简单的机制带来了深远的影响。
首先,它自动解决了负载均衡问题。如果一个核心被分配了一个长而困难的任务(比如计算一个具有复杂物理过程且高度加密的网格片),其他核心并不会等待。它们只是继续从池中取出更小的、就绪的任务。系统会动态地适应不规则的工作负载,让所有工作者尽可能地保持繁忙。
其次,也许更微妙的是,它允许系统隐藏延迟。“任务”可以是任何事情,包括“等待网络消息到达”。在传统的 BSP 模型中,所有处理器都将被迫等待通信完成。在基于任务的模型中,调度器看到核心1因等待数据而被阻塞。调度器不会让它闲置,而是会分配一个不依赖该数据的、不同的、就绪的计算任务给它。该核心在消息传输过程中执行有用的工作。通过将计算和通信都表示为同一个 DAG 中的任务,运行时可以巧妙地将它们交错执行,重叠两者,从而隐藏了原本会用于等待的时间。
实现这种动态调度器的一种流行且非常有效的策略是工作窃取。每个处理器维护自己的私有任务队列。它主要处理自己的任务。但如果其队列变空,它就变成一个“窃贼”,随机选择另一个处理器(“受害者”)来从中窃取一个任务。这种去中心化的方法优雅地将工作从繁忙的处理器重新分配到空闲的处理器,以很小的开销实现了动态负载均衡。
为了更精确地讨论这些并行算法的性能,我们需要两个基本概念:工作量(Work)和跨度(Span)。
这两个数字为我们在 个处理器上的并行执行时间 提供了基本限制:
基于任务的并行就是一场旨在最小化这两个约束的探索。总运行时间通常用一个组合式来近似,如 。调度器的工作就是实现一个尽可能接近这个理论最优值的运行时间。
这就引出了一个关键问题:任务粒度。我们的任务应该多大?让我们回到我们的数值天气预报模型,其中每个“任务”是大气中一个气柱的计算。
事实证明,对于许多不规则问题,通过细粒度任务减少跨度和改善负载均衡所带来的好处,远远超过了调度开销。分析表明,创建大量的小任务往往是释放大规模并行性的关键,它允许调度器有效地隐藏那些会削弱简单模型的不规则性。
基于任务的并行是银弹吗?不完全是。它的威力完全取决于 DAG 的结构。调度器只能利用问题本身固有的并行性。如果一个问题从根本上是顺序的,那么再巧妙的调度也无济于事。
考虑解压缩一个用 LZ 系列算法(比如 ZIP 文件中使用的那种)压缩的文件。这种格式通过用反向引用替换重复的数据序列来工作,这些反向引用本质上是像“从 500 字节前复制 100 字节”这样的指令。要解压第 1000 个字节,你可能需要知道第 500 个字节是什么。但要解压第 500 个字节,你可能需要第 200 个字节,依此类推。在最坏的情况下,整个文件可能是一条长长的依赖链,其中每个部分都依赖于紧邻其前的部分。
这个问题的 DAG 不是一个充满并行机会的宽阔、茂密的图;它是一条长长的直线。跨度()几乎等于总工作量()。可用并行性,可以看作是比率 ,接近于 1。这意味着这个问题是内在地顺序的。无论你投入多少处理器,你几乎都得不到任何加速,因为你被那条不可打破的依赖链所束缚。
这揭示了一个深刻的真理:并行编程的关键不仅仅是编写代码,还在于构建算法以使其具有较短的关键路径。对于像 LZ 解压缩这样的问题,这导致了一个有趣的实践权衡:程序员故意将问题分解为独立的块。反向引用不允许跨越块边界。这使得所有块可以并行解压缩,从而大大缩短了跨度。代价是什么?压缩率会略微变差,因为算法再也无法找到跨越那些人为边界的长匹配。这是一个绝佳的例子,展示了如何在一个维度(压缩大小)上牺牲一点最优性,以在另一个维度(并行速度)上获得巨大优势。
归根结底,基于任务的并行并非魔法。它是一个强大的透镜,通过它我们可以看到问题的真实结构。通过理解其原理——DAG、工作量与跨度的相互作用,以及动态调度的艺术——我们可以超越僵化的装配线,学会指挥现代高性能计算中那美丽而富有创造性的混乱。
既然我们已经探讨了基于任务的并行的原理和机制——“是什么”和“如何做”——那么让我们踏上一段旅程,去发现“为什么”。为什么这种计算模型如此深具实用价值?理解国际象棋的规则是一回事,而亲眼目睹一位大师如何运用这些规则来征服一个令人眼花缭乱的复杂棋盘则是另一回事。我们现在就要观看这位大师的表演。
现实世界,这个科学试图建模、工程试图控制的世界,很少是整洁有序的。它混乱、不规则、异步,而且异常复杂。简单的数据并行是一个强大的工具,但它就像一条装配线:在生产成千上万个相同物品时效率极高。而基于任务的并行则像一个由技艺精湛的工匠组成的工坊,每个人都身怀绝技,准备好在一个复杂、独一无二的项目上进行协作。它不回避不规则性,反而在其中茁壮成长。
科学领域中许多最具挑战性的问题都具有非均匀性的特点。待完成的工作并非均匀地分布在问题域中。一种僵化的、预先计划的分工注定是低效的,因为一些工作者会不堪重负,而另一些则闲坐着。基于任务的并行以其固有的动态性,提供了一个优雅的解决方案。
想象一下,你正在尝试预测整个地球的天气。你的超级计算机将地球划分为一个网格,每个处理器被分配一片地球区域进行模拟。问题在于,模拟一片晴空的计算成本远低于模拟一场伴有复杂云微物理过程的猛烈雷暴。如果你使用静态分区,分配给风暴带的处理器将会陷入困境,疯狂工作,而分配给平静沙漠的处理器将很快完成并等待。整个模拟只能以最慢、工作最繁重的处理器的速度进行。这是一个经典的负载不均问题,整体效率极差。
基于任务的并行提供了一个绝妙的解决方案。我们可以不分配固定的土地块,而是将模拟看作一个巨大的任务池,其中每个任务可能是对一小组网格列的计算。现在,一个在“平静”区域完成工作的处理器可以主动地从一个正在处理“风暴”区域的任务池中“窃取”工作。这种动态负载均衡,通常以“工作窃取”的形式实现,是任务模型的自然结果。这是一个协作系统,空闲的工作者会寻找工作,确保总计算负载被更均匀地分配。其结果是所有处理器几乎在同一时间完成工作,从而极大地提高了整个天气预报的效率和速度。
同样的驾驭不规则性的原理也适用于微观尺度。在分子动力学中,我们模拟构成从简单流体到复杂蛋白质等一切物质的原子之间错综复杂的舞蹈。主要的计算工作是计算相邻原子间的力。在任何现实系统中,原子的密度都不是均匀的。一些原子处于拥挤的环境中,必须与许多邻居相互作用,这代表了沉重的计算负载。另一些则处于邻居稀少的稀疏区域。将力的计算分解为大量独立的对相互作用任务,允许多核处理器在其线程间动态分配这种不规则的工作。
然而,这种自由引入了一个新的、微妙的挑战。如果一个处理原子 间作用力的线程和另一个处理原子 间作用力的线程都在同一时刻试图将它们的结果加到原子 的总作用力上,它们可能会造成“竞争条件”。一次更新可能会覆盖并抹去另一次更新。解决方案需要一种受控访问的形式,例如“原子操作”,它就像一个微观的旋转门,确保对原子 上的力的读-改-写周期是一个不可分割、不可中断的动作。我们在这里看到了一个根本性的权衡:任务模型的动态灵活性要求一种更复杂的方法来确保当多个任务汇集到同一数据片段时的正确性。
这一思想甚至延伸到了支撑如此多工程领域的抽象数学世界。考虑求解一个大规模线性方程组 的问题,这可能源于模拟一个复杂物体中的热流。矩阵 通常是“稀疏”的,意味着它的大多数元素都是零,这反映了物理相互作用通常是局部的这一事实。求解该系统的过程涉及的依赖关系并非简单或线性的,而是形成一个复杂、不规则的“消元树”。我们不能简单地将这棵树切成均匀的片来进行并行执行。但是我们可以定义一个“任务”来求解这棵树内一小组密集的变量(一个“超节点”)。一个任务只有当其在树中的“父”任务全部完成后才准备好执行。一个基于任务的运行时系统可以自动管理这个依赖图,一旦任务的依赖关系得到满足,就将它们分派给等待的处理器。这创造了一个动态的计算“波”,它流经整个树,自然地暴露了所有可用的并行性,而无需程序员手动编排复杂的调度。这是数学结构与并行执行的美妙结合。
也许任务并行最深远的应用在于它如何处理时间、延迟以及因果关系本身的概念。在某些问题中,通信延迟不仅仅是需要最小化的麻烦,而是一种可以被利用的资源。
让我们进入大脑的世界。模拟一个由数十亿神经元组成的网络是现代科学的重大挑战之一。每个神经元都不是一个简单的点,而是一个复杂的、分叉的树突树。通过这棵树传播的电信号由电缆方程控制,对单个神经元进行数值求解涉及一系列必须在树上上下传播的步骤——这是一个内在的串行过程,称为 Hines 算法。这意味着并行化模拟最有效的方法不是分割单个神经元,而是将整个神经元分配给不同的处理器。因此,任务就是单个神经元的更新。
现在是奇妙之处。当一个神经元“放电”时,它会向另一个神经元发送信号,但这个信号需要时间穿过突触间隙并沿着树突传播。存在一个最小的、非零的通信延迟 。这个物理事实对计算机科学家来说是一份礼物!这意味着处理器可以为一个持续时间为 的“前瞻”时间窗口模拟其本地神经元组,并且绝对确定没有新的信息可能从其他处理器到达以改变结果。这是一个“因果窗口”。当处理器忙于为这个窗口执行一批本地神经元更新任务时,在此期间产生的、注定在下一个时间窗口到达的脉冲消息已经在网络中传输。通信和计算被完美地重叠了。任务并行提供了理想的框架来管理一个待运行神经元任务的队列,并在这些前瞻期间执行它们,而网络则在为未来传递消息。我们把延迟从敌人变成了强大的盟友。
这种计算、时间与物理后果之间的联系并不仅限于模拟世界。在与现实世界交互的信息物理系统——机器人、无人机和自动驾驶汽车中,这是一个生死攸关的问题。这样一个系统的“大脑”是一个流水线:它感知环境(例如,过滤视频流),计算决策,并向其执行器发送命令。计算阶段通常是并行模式的混合:初始的传感器过滤可能是高度数据并行的,而决策阶段可能涉及几个可以作为独立任务并发运行的不同质的子问题(路径规划、对象识别、稳定性控制)。
从感知到行动的总时间是系统的端到端延迟。如果这个延迟太长,机器人的反应就会迟钝。更糟糕的是“抖动”——延迟中不可预测的变化。想象一下,你试图在手上平衡一根扫帚,但从你大脑到手部肌肉的信号被随机延迟了。你将不可避免地失败。对于机器人来说,这种失败可能意味着不稳定和崩溃。因此,分析我们的并行算法引入的延迟和抖动不仅仅是计算机科学中的一项练习;它是控制系统工程中安全分析的一个关键部分。它在算法的抽象性能与机器的物理稳定性之间建立了一种深刻而直接的联系。
从预测飓风、模拟蛋白质之舞,到求解抽象方程、模拟心智、保持机器人直立,我们看到了一个统一的思想在起作用。基于任务的并行是一种描述复杂工作的语言。它鼓励我们将问题分解,不是分解成僵硬、均匀的块,而是分解成其基本的、有意义的组成部分——任务——以及它们之间的依赖关系。它使我们能够陈述需要做什么,同时委托一个智能的运行时系统来协调执行的方式、时间和地点。
这是一个拥抱现实的混乱、不规则和异步本质的范式,而不是试图将其强行塞入一个过于简单、统一的盒子。这种灵活性是它的力量所在。这种适应性是它的美之所在。在一个科学和工程的前沿由日益增加的复杂性所定义的世界里,基于任务的并行是现代知识工具包中不可或缺的一部分。它就是我们指挥那美丽而混乱的计算交响乐的方式。