并行与分布式系统中的并行独立任务调度:基于有向无环图(DAG)的关键路径调度算法
题目描述
在并行计算中,我们常常需要执行一组具有依赖关系的任务。这些任务和它们的依赖关系可以被建模为一个有向无环图(Directed Acyclic Graph, DAG),其中节点代表任务,有向边(u→v)代表任务u必须在任务v开始之前完成。每个任务有一个已知的执行时间(权重)。我们的目标是在一个拥有P个相同处理器的系统上,将这些任务调度到处理器上执行,使得总的完成时间(调度长度或Makespan)最小。这是一个NP难问题,因此我们寻求高效的启发式算法。关键路径调度(Critical Path Scheduling)是一种经典的、基于任务优先级列表的启发式调度策略。本题要求你理解并讲解如何设计一个并行的关键路径调度算法,以加速调度列表的生成过程,并分析其优势与挑战。
解题过程循序渐进讲解
第一步:理解问题与基本概念
- 任务图(DAG):G = (V, E),其中|V| = n个任务,|E| = m条依赖边。每个任务v_i有一个执行时间(权重)t(v_i)。
- 处理器模型:我们有P个相同的、可并行的处理器。每个处理器一次只能执行一个任务,每个任务必须在一个处理器上不中断地执行完成。
- 调度目标:将每个任务分配到一个开始时间s(v_i)和一个处理器上,满足:
- 依赖约束:对于每条边(u→v),有 s(v) ≥ s(u) + t(u)。
- 处理器约束:在任一时刻,一个处理器上最多运行一个任务。
- 最小化最大完成时间:Makespan = max_{v_i ∈ V} [s(v_i) + t(v_i)]。
- 关键路径(Critical Path, CP):
- 在一个任务图中,从入度为0的源节点到出度为0的汇节点的最长加权路径称为关键路径。
- 该路径的长度(路径上所有任务执行时间之和)是在无限多处理器(即只考虑依赖,不考虑资源竞争)情况下的最短可能完成时间,因此是Makespan的一个理论下界。
- 关键路径上的任务如果被延迟,会直接导致整个Makespan的延迟,因此它们具有最高的调度优先级。
第二步:串行关键路径调度(列表调度的一种)
关键路径调度是列表调度(List Scheduling)算法家族中的一员,它通过一个特定的优先级规则来生成任务执行列表。
- 计算优先级:为每个任务v计算一个优先级值。最常用的是从节点v到汇节点的最长路径长度,称为b-level(bottom level)。b-level(v) = t(v) + max_{(v→w)∈E} b-level(w)。可以通过从汇节点开始的逆拓扑序动态规划计算。
- 生成优先级列表:将任务按照优先级(b-level)非递增排序。b-level越大,说明该任务在关键路径上或接近关键路径,优先级越高。
- 调度循环:
- 维护一个“就绪任务”集合R,包含所有前置任务都已被调度的任务。
- 只要有任务未调度,就循环:
a. 从R中选择优先级最高的任务v。
b. 将v分配给最早可用的处理器(即能使v最早开始执行的那个处理器)。
c. 从R中移除v,并将v的所有直接后继中,因v调度完成而变得就绪的任务加入R。
这个串行算法的时间复杂度主要在排序O(n log n)和调度循环中维护就绪集与处理器状态(通常用二叉堆,O(n log n))。它的结果是一个有效的调度,且Makespan不会差于理论下界的2倍(近似比有保证)。
第三步:并行化关键路径调度的动机与挑战
- 动机:对于大规模任务图(n很大),计算b-level和调度循环本身可能成为瓶颈。并行化可以加速调度器的决策过程,尤其适合需要在线调度或任务图动态生成的场景。
- 挑战:
- 依赖计算:b-level的计算是典型的动态规划,存在后向依赖(计算v需要其后继w的b-level)。需要设计并行算法来求解DAG所有节点的b-level。
- 列表生成:对任务按b-level排序是一个全局操作,虽然可以并行排序,但需要所有b-level值已知。
- 调度决策:调度循环本质是顺序的,因为分配决策(选择任务、选择处理器)会影响后续任务的可用时间。核心挑战在于如何并行地做出多个不冲突的调度决策。
第四步:并行化设计——分阶段并行调度算法
一个有效的思路是将调度过程阶段化,在每个阶段并行地调度一批独立的任务。
步骤1:并行计算b-level
- 思想:计算b-level等价于在反转边的DAG上求从汇节点到各节点的最长路径。可以使用并行松弛(Parallel Relaxation) 或基于拓扑层的并行扫描。
- 基于拓扑排序的并行算法:
- 并行计算DAG的拓扑序(例如,使用并行队列进行并行BFS,计算每个节点的拓扑层)。
- 在反转的DAG中,节点按逆拓扑序处理。初始化所有节点的b-level为其自身的执行时间t(v)。
- 按逆拓扑序顺序处理每一层(层内并行):
- 对于层L中的每个节点u(并行处理),检查其所有前驱节点p(注意:在原DAG中,p是u的后继。在反转DAG中,p是u的前驱)。
- 对于每个前驱p,执行松弛操作:
b-level(p) = max(b-level(p), t(p) + b-level(u))。
- 由于按逆拓扑序处理,当处理节点u时,其所有后继(在反转DAG中是前驱)的b-level已经被其所有后继更新完毕,因此计算是正确的。
- 并行性:在每一层内部,对节点的松弛操作可以完全并行。
步骤2:并行生成带优先级的就绪任务列表
- 目标:不一定是生成一个全局排序的列表,而是维护一个始终可高效获取最高优先级就绪任务的数据结构。我们可以使用并行优先级队列。
- 初始化:将所有入度为0的源节点插入优先级队列,优先级为b-level。
- 数据结构:并行优先级队列(如并行堆)需要支持并行的
insert和delete-max操作。这可以通过锁或无锁编程实现,但可能存在争用。
步骤3:并行调度决策——基于时间窗口的批处理
这是最关键的创新点。串行算法一次调度一个任务,我们改为一次调度一批任务。
- 时间窗口估计:维护一个全局的“当前时间”指针T_current。估计一个合理的时间窗口[T_current, T_current + Δ]。Δ的选择可以是一个启发式,如平均任务执行时间。
- 批处理构建:
- 并行地从优先级队列中取出当前就绪的、优先级最高的一批任务。由于处理器数量为P,我们最多可以尝试选取P个任务。
- 冲突检测:检查这批任务之间的隐含依赖。即使两个任务A和B在DAG中没有直接边相连,如果它们共享一个共同的未完成的前驱,它们也不能在同一批中被调度。这需要快速检查。一种方法是维护每个任务的“最早开始时间”(EST),EST(v) = max_{(u→v)} (完成时间(u))。只有EST(v) <= T_current + Δ的任务才可能在本窗口调度。
- 对候选任务集,尝试并行分配处理器。这可以建模为一个二分图匹配问题:任务集合 vs 处理器集合。如果某个处理器在该时间窗口内空闲,则存在边。目标是找到最大匹配。可以使用并行二分图匹配算法(如并行贪心匹配)。
- 提交与更新:
- 将成功匹配的(任务,处理器,开始时间)对作为调度决策提交。开始时间可以设为max(处理器可用时间, 任务EST)。
- 并行更新:
a. 处理器状态:更新被占用处理器的下一个可用时间。
b. 任务状态:标记任务为已调度,计算其完成时间。
c. 就绪集更新:对于每个刚被调度的任务v,并行地检查它的每个直接后继w。通过对w的未完成前驱计数器进行原子减1操作。如果计数器减到0,则将w插入优先级队列。 - 将T_current推进到下一个“有意义”的时间点(例如,所有处理器中最早的空闲时间,或下一个任务的最早开始时间)。
步骤4:迭代循环
重复步骤3,直到所有任务都被调度。每次迭代生成一个批次的调度决策。
第五步:算法总结与复杂度分析
- 算法流程总结:
- 并行计算所有任务的b-level(优先级)。
- 初始化并行优先级队列,放入所有入度为0的任务。
- While (有任务未调度):
- 确定当前时间窗口。
- 从队列中并行提取一批高优先级、EST符合条件的候选任务。
- 并行求解候选任务与空闲处理器的二分图匹配,决定本批次被调度的任务。
- 并行提交调度决策,更新处理器时间、任务状态,并将新就绪的任务插入队列。
- 优势:
- 将顺序的调度决策过程转化为并行的批决策过程,利用了多核/分布式环境。
- 保留了关键路径调度的优先级思想,有助于获得较短的Makespan。
- 更新操作(递减计数器、插入队列)可以高度并行。
- 挑战与注意事项:
- 窗口大小Δ:Δ太小会导致批次太多,并行度不足;Δ太大会引入不准确的估计,可能使调度决策劣化。可能需要自适应调整。
- 数据结构争用:并行优先级队列和共享计数器的争用可能成为瓶颈,需要使用高效的并发数据结构。
- 负载均衡:在批处理匹配阶段,需要确保工作负载在参与计算的线程/进程间均衡。
- 与完全在线调度的区别:此算法仍需要预先知道任务图和执行时间,是一种“离线”或“静态”调度的并行化,但比纯串行算法快得多。
通过这种“并行计算优先级” + “基于时间窗口的批处理匹配”的设计,我们实现了一个并行化的关键路径调度器,能够在多核机器或分布式内存系统上,高效地为大规模DAG任务图生成高质量的调度方案。