低复杂度DAG调度:异构计算中性能与效率的平衡之道
发布时间:2026/9/29 7:59:58 作者:尧图编辑部 阅读量:1,286

1. 为什么DAG调度是异构计算无法绕开的硬骨头第一次认真研究DAG有向无环图任务调度是我在搭建一套分布式推理服务的时候。当时要处理的任务拆成了十几个子任务子任务之间有严格的前后依赖一部分跑在CPU上一部分跑在GPU上还有几个轻量级的直接放到了FPGA上。最初我满脑子只想着算得快结果测试时发现整个流水线的吞吐量还不如单机版原因就是任务顺序没安排好GPU在等CPU的结果CPU在等磁盘IO的数据FPGA大部分时间在空转。这个问题的本质就是DAG任务调度——它解决的不是某个任务怎么算而是一堆有依赖关系的任务在多台性能各异的计算设备上怎么安排执行顺序和资源分配才能让整体完成时间最短。异构计算环境为什么更麻烦因为现实世界的计算资源几乎永远不是同质的。数据中心里有不同型号的CPU节点深度学习训练通常混着TPU、GPU和各种加速卡边缘计算场景更是CPU、GPU、NPU、FPGA混在一台设备里。这些处理器的算力不同、通信速度不同、存储层级也不同调度器不能假设任意任务丢给任意处理器都差不多。DAG调度通常被抽象成这样一个数学模型有一个任务集合任务之间有边边上的权值代表通信开销有一组处理器每个处理器上执行不同任务需要的时间不同处理器之间的通信开销也不同。调度器的目标就是找到一个任务到处理器的映射及执行顺序使总工期makespan也叫调度长度尽可能短。这个模型看起来简单但它背后的NP完全性在论文里反复被强调异构环境下的最优调度问题在绝大多数情况下没有多项式时间的精确解。所以这个研究方向从一开始就带着一种带着镣铐跳舞的气息。研究者不追求理论上绝对最优而是追求在有限时间内找到一个足够好的方案这就是标题里performance-effective和low-complexity两个词同时出现的原因。性能和复杂度在这个领域是一对天然的死对头。理解了这个背景再看这篇论文标题就能明白它想解决的痛点不是调度问题本身——这已经研究了几十年了——而是在保持调度质量的前提下把调度算法的时间复杂度和实现复杂度压下来让它真正能跑在线上。2. 性能与复杂度的博弈调度算法设计的核心矛盾2.1 最优解有多贵回溯法、单纯形法和它们的下场要理解为什么低复杂度难能可贵得先看看追求最优的代价。假设你手头只有20个任务、3台处理器任务依赖图稍微复杂一点用回溯法枚举所有合法的任务分配方式分支数会迅速膨胀。如果拿整数规划求解器跑光是把通信开销和处理器差异建模成约束条件就要花不少时间对于100个任务以上的DAG很多商业求解器会直接超时。我当年也天真地试着在项目里用OR-Tools去解一个中等规模的调度子问题结果两个小时没跑完而同样的任务用手写的启发式算法几十毫秒就给出了一个只差不到15%的次优解。那一刻我彻底明白了论文里用CPLEX跑小规模实验没问题真实系统里根本不可能这么玩——调度器本身就是流水线上的一个环节它的运行时间也是开销不考虑调度开销的调度算法只是纸上谈兵。2.2 two-level策略是怎么出现的正是因为精确求解不现实这个领域的主流做法退而求其次采用“任务优先级排序 处理器分配”的两阶段式启发式策略。第一阶段把DAG上的每个任务根据依赖关系和期望执行时间算出一个优先级分数然后用拓扑排序确定一个任务序。第二阶段按这个顺序把任务逐个取出在可用处理器里遍历打分挑一个预计完成时间最短的设备分配过去。这个思路本身不复杂但两个阶段的细节很讲究。多数经典算法在计算优先级时都会考虑“向上秩值upward rank”说白了就是从当前任务出发到出口任务沿最重路径的期望剩余工作量。要算出这个值需要在DAG上做一次自底向上的递归遍历每次遍历都要累加相邻边上的通信权值。这篇论文标题点出的低复杂度恰恰是在这个环节开始动手脚——如果一个算法要反复遍历整个图耗时必然随任务数增长如果能在一次遍历中同时算出优先级和分层的候选集复杂度的常数项就小很多。2.3 list scheduling稳定统治的理由List scheduling列表调度之所以是几乎所有高性能算法的底座除了理论上有不错的竞争比更关键的一点是它的工程复杂度极低。你不需要维护复杂的搜索树不需要反复求解局部子问题核心数据结构就两个一个按优先级排序的待调度队列一组记录设备可用时间的时间线。这种简单性直接带来了两个好处。第一错误率低几十行代码能写完核心逻辑不容易出现数据竞争和边界条件错误。第二可迁移性好同一个优先级排序逻辑换一套设备时间线模型就能从仿真环境挪到真实调度器里。我在自己的项目里做调度器选型时几乎没有犹豫就选了基于list scheduling的方案因为对一个持续迭代的系统来说算法模块的可理解性和维护成本同样是调度质量以外的重要指标。3. 从HEFT到PEFT经典算法的核心机制拆解3.1 HEFT为什么是绕不开的基准讨论异构DAG调度永远绕不开HEFTHeterogeneous Earliest Finish Time算法。十几年前提出的方法直到今天依然是衡量新算法的一把尺子大多数论文里对比对象都会带上它。HEFT的思路说白了就是我上面讲的两阶段策略的标准模板。第一阶段对每个任务计算upward rank作为优先级第二阶段按优先级逐个调度把任务放到能使其最早完成时间最小的处理器上。关键在于它不要求处理器上任务严格连续执行允许插入式调度——如果某个处理器上有空闲窗口而当前任务能在窗口内完成就直接塞进去。这个“插入”机制容易被新手忽略但它对调度质量的提升非常明显。因为在真实DAG里父子任务之间的通信约束经常导致下游处理器出现大量空闲期允许插入这些空闲窗口等于变相提高了设备的利用率。HEFT的功耗是O(E × P²)E是依赖边数P是处理器数。在节点数几百、处理器数十几的规模下跑一次只要几毫秒。对绝大多数应用场景这个复杂度完全够用所以它在工程里也被用得最多。3.2 PEFT换了个计算方式PEFTPredict Earliest Finish Time是在HEFT基础上优化优先级评估的算法。它的核心创新点是把优先级从一个单纯的路径长度估计改成对未来可能收益的“乐观估计”——不仅要看当前任务到终点的路径权重还要看在后续调度中可能产生的优化空间。这个改动的效果很直接在一些通信开销占比高CCR数值大的DAG上PEFT比HEFT能多跑出8%-15%的调度质量提升。代价是优先级计算阶段多了一轮矩阵形式的预估复杂度从HEFT的O(E × P²)变成O(E × P² V² × P)当任务数上千时这个V²项会让PEFT跑得明显更慢。到这里就能看出这篇论文标题里另一个关键词的深意。“Performance-effective and low-complexity”想表达的往往就是我能不能拿PEFT的质量付出接近HEFT的复杂度这本质上是在质量和开销之间重新寻找一个更好的甜点位而不是简单地往某一个极端靠。3.3 任务复制和聚类策略为何不是主角聊到这里顺便提一下任务复制task duplication和聚类clustering策略。任务复制的方法会把同一个任务复制到多个处理器上通过以空间换时间来避免通信开销聚类方法则倾向于把通信密集的小任务尽可能哪个在同一个处理器上从而把跨设备通信变成进程内调用或缓存访问。这两种方法在某些场景下表现非常好但它们的通病是实现复杂度偏高而且对DAG的结构很敏感。一个带循环依赖变体或者数据依赖不规则的DAG会让复制的收益迅速蒸发。所以这篇论文标题里既没提复制也没提聚类专注在list scheduling这个框架内做文章本身就是一种向“工程友好型”妥协的选择这种选择在工业界是有道理的——越简单的策略越容易塞进现有系统里。4. 评估一个调度算法到底行不行指标与实验设计4.1 不能只看调度长度调度长度比的意义很多刚接触这个方向的人会犯一个错误拿一个算法跑出来的调度总时长对比另一个算法的总时长谁短谁就好。这在单一DAG上没毛病但一旦要跨多个随机生成的工作负载比较算法就不科学了——因为不同DAG的规模、结构、通信比差异太大绝对时间根本比不出公平性。所以学术界一般会用调度长度比Schedule Length Ratio, SLR。SLR的定义是当前算法求得的调度长度除以整个DAG的关键路径下界。这里的关键路径下界不是真的拿一台无限快处理器算出来的而是假设通信开销为零、每一个关键路径上的任务都能无代价衔接计算出的理论最短时间。这样算出来的SLR永远大于等于1越接近1说明算法离理论极限越近。论文里要宣称performance-effective最重要的证据就是SLR在不同规模、不同通信计算比CCR的DAG集上比HEFT或PEFT低多少。如果只是比总执行时间样本差异会把结论搅浑审稿人一看就知道不靠谱。4.2 CCR和异构因子的作用为了公平地测试算法性能研究社区发展出了随机DAG生成器最常用的是DAgen。它能根据指定的参数生成任务数、依赖边密度、每两个任务间的通信量以及每个任务在各种处理器上的执行时间。这里有两个参数非常关键。一个是CCRCommunication to Computation Ratio指的是DAG上通信开销总和与计算开销总和的比值。CCR等于0.1时意味着这是一个计算密集型的DAG通信几乎可以忽略调度器主要任务就是尽量把计算负载分散开CCR等于10时通信成了大头调度器必须重点照顾数据局部性和减少跨设备传输。一个算法如果只在某个CCR区间有效那它的迁移价值就存疑。低复杂度算法如果能在CCR从0.1到10的范围内都保持较好的SLR才真正有说服力。另一个参数是异构因子heterogeneity factor用来控制同一任务在不同处理器上执行时间的离散程度。异构因子低各处理器算力接近调度难度相对小异构因子高比如同一任务在A设备上1毫秒、在B设备上50毫秒调度的收益空间就大得多。很多新算法在异构因子低的场景下表现不错一提高异构性就不行了——这种情况在论文里也常见需要特别当心。4.3 复杂度分析的正确写法一篇好的调度算法论文除了要有调度质量的实验复杂度分析部分也值得仔细抠。因为这部分直接回应low-complexity的承诺。工程上真正在意的是两件事第一算法跑一次的时间会不会成为系统瓶颈第二任务数从几百涨到几千时耗时是线性涨还是次方涨。比如一个基于HEFT框架改进的算法如果额外增加了一个O(V²)的预处理步骤在V500时可能无所谓但V5000时就要多出上亿次操作跑一次几百毫秒这在很多调度周期只有秒级的场景里是不可接受的。所以看我自己的经验评估一个调度算法前要先把它的实际耗时曲线跑一遍——不是只看Big O而是看常数同样的复杂度常数可能差出几十倍。5. 复现论文时我踩过的几个坑5.1 通信开销的建模假设有多理想化论文里的DAG通信权值往往被简化成任务完成后数据立刻进入网络传输传输时间固定且与通信双方负载无关。真实系统里通信时间受网络拥塞、资源竞争、数据结构序列化成本影响很大。尤其是GPU通信NVLink与PCIe的带宽差异、核函数启动开销都比论文里用几个固定权值要复杂得多。我复现文献级调度器时发现在一致性好的DAG上工程模型与论文模型差距有限但在通信密集型DAG上调度结果的实际收益比论文宣称的低不少。原因就是通信时间固定这个假设伤害了调度器的判断。所以如果你想把一个调度算法搬进自己的系统务必先做一次通信时间实测和建模。5.2 优先级计算的死循环和浮点误差list scheduling算法里容易出一个隐蔽的BUG计算upward rank时如果DAG不是严格的DAG比如存在环递归会死循环。真实业务的任务依赖极少是精雕细琢的DAG很多是运行时动态构造的依赖图中经常残留环或不可达节点。所以我在工程实现里总会在调度前加一次拓扑校验宁愿多花O(VE)的时间也不愿意让调度器在线上递归爆栈。另一个隐蔽问题是浮点误差。不同处理器的期望执行时间可能是小数CCR转换时再乘一个系数优先级排序时两个相近数字的差值会被浮点误差吃掉导致排序不稳定。我的做法是在优先级计算后额外加一个任务ID的次级排序键保证同分情况下的确定性输出这对可复现调试非常有帮助。5.3 为什么现实的DAG不是随机图另一个值得一提的坑是论文里用DAgen生成的随机DAG结构上往往比真实业务的依赖图“匀称”许多。真实的DAG常常有很深的链式通路外加少量汇合节点或者出现大量扇出扇入的宽胖结构。这两种形状对list scheduling算法的影响差异不小宽胖结构下任务复制或聚类策略会有更好收益链式结构下通信优化比计算并行更重要。因此在评估一个所谓performance-effective的调度算法时不要只看它在公共基准上的数据最好拿自己业务的依赖图灌进去跑一遍。如果有可能把几种典型形状的DAG做成测试集观察调度算法在不同拓扑特征下的表现稳定性。那种“某种形状特别快、换一种形状就崩”的算法多半是过拟合了特定结构用途有限。6. 低复杂度算法在真实系统里的定位6.1 调度器不是单机程序要考虑集成成本把一个调度算法搬进真实系统绝不只是写个类实现算法那么简单。它要对接任务描述模块、设备监控模块、失败重试机制和资源配额管理。算法逻辑越复杂对接成本越高。哪怕一个算法SLR只比另一个好3%如果它的实现需要引入一个新的图处理框架、维护一套额外的元数据团队很可能不买单。所以工业界长期活在够用就好的状态。很多大数据系统的调度器连HEFT都没用全用的是先来先服务加简单优先级队列。原因是业务场景里的DAG往往不大设备数量也不算多简单的策略在绝大多数流量下够用而复杂调度器的维护成本实在太高了。从这个角度看论文标题里那个low-complexity其实是很贴近现实需求的追求——它让更聪明的调度策略有机会被集成到真实系统中而不是永远停在论文里。6.2 静态调度与动态调度的边界还有一个常被忽略的问题这个标题里的调度策略默认是静态调度——所有任务在执行前已知DAG结构不变。但真实系统的任务运行时数据往往不完整执行时间也只能靠历史统计估计。也就是说完美的静态调度假设在现实中几乎不存在。好在静态调度算法可以为动态调度提供底层策略支撑。你可以把DAG调度器设计成多轮驱动每轮用低复杂度的静态调度算法为当前已知的任务子图生成一个调度计划当新任务动态到达时把新子图增量合并进现有计划重算。这种分轮静态调度的模式比设计一个全局动态调度器简单得多而且能直接复用论文里的算法和指标。这种工程化的思路其实也解释了为什么“低复杂度”的调度算法需求那么强烈——动态场景里调度器被调用的频率远高于纯静态场景每轮省下几毫秒积少成多就是非常可观的整体调度开销缩减。7. 结语从算法到选型的几点个人心得在这个方向钻研了几年我自己的感受是不要神化任何调度算法。无论HEFT、PEFT还是这篇标题里的低复杂度变体它们的共同目标是找到足够好的解而不是最好的解。选型时不妨按以下几个维度做个简单评估一调度器最长运行时间是否在系统的可接受范围内二算法对DAG结构和CCR的敏感度是否匹配你的业务负载三实现该算法的代码量和维护成本是否值得那几个百分点的调度质量提升。如果这几个维度都过线那这个算法就值得在真实系统里小流量试跑一段时间。等积累了足够多真实DAG样本后再决定要不要全量推广。我自己就是从闭眼抄PEFT到按业务数据重新评测转变之后才发现一个更简单的算法在自家负载上反而更好这正是调度领域最有意思的地方——脱离场景谈性能都是纸面上的热闹。