十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

异构硬件下深度学习计算图的多目标全局调度实践

异构硬件下深度学习计算图的多目标全局调度实践 我们团队之前接过一个挺头疼的项目一台机器上同时挂着GPU、NPU、还有一堆CPU要跑一个多模态的深度学习模型。按理说这种异构堆叠性能应该很猛可实际一跑发现大部分时间都在等数据传输有的加速卡利用率不到30%。一个算子一个算子地调今天换一下切分策略明天改一下算子实现始终顾头不顾尾。后来我们把目光从“单算子优化”挪到了“整张计算图的全局调度”上自己攒了一套面向异构硬件的深度学习图多目标调度框架名字就叫HeteroOpt。这篇文章把整个设计与落地过程说透包括为什么单点优化不够用、图全局搜索空间怎么建模、多目标怎么权衡以及真正在工程里接入时会踩到哪些坑。HeteroOpt要解决的不是“某一个op能在哪块卡上跑得最快”而是在一张包含几十上百个算子的深度学习计算图里把每个算子分配到哪个设备、以什么顺序执行、中间结果怎么传输、显存/内存怎么复用整体考虑起来让最终系统的端到端时延、内存峰值、能耗等目标尽可能同时逼近最优。它在学术界对应的搜索词是编译优化里的Graph Scheduling / Op Placement在工业界的同类东西包括XLA的自动并行、TensorFlow的Placer、以及一些AI芯片厂商自带的编译器。但HeteroOpt的定位更偏向“硬件异构度高、同时需要兼顾多个性能目标、且希望框架本身不绑定死某一套硬件SDK”的场景。1. 为什么要做“图全局”调度而不是继续抠单个算子1.1 异构硬件多了以后算子级优化开始失灵以前做深度学习部署大多数时候是单GPU环境。那时候优化手段很直接把算子换成推理引擎里更快的kernel开启算子融合调一下batch size。这些方法都在“单算子”或者“相邻几个算子”的范围内打转在单卡上确实有效。但到了异构环境之后就变味了——机器上不只有GPU可能有专门做矩阵乘的NPU有擅长稀疏计算的加速卡有负责数据预处理的CPU。同一个算子放在不同硬件上的执行时间差异很大比如卷积在GPU上很猛但某个轻量的op在NPU上反而有更低的启动开销LayerNorm这种内存密集型算子放到CPU上跑有时比在GPU上还快因为它不涉及太多并行核心利用率的问题。一旦一个模型被切分成多个部分分布到不同设备上算子级的局部最优就不等于全局最优了。你先让卷积在NPU上快了两倍但卷积的输出需要从NPU显存拷到GPU显存供下一个算子使用一次跨设备拷贝可能就是几毫秒。如果这个拷贝的代价比省下来的执行时间还要多那么单算子级别的“优化”反而是负优化。这就是HeteroOpt切入的角度——把整张图当作一个整体来做调度决策而不是一个算子一个算子地单独找最优点。1.2 计算图调度问题的本质决策空间巨大且互相牵扯深度学习模型落到框架里通常会被表示成一张有向无环图DAG。每个节点是一个算子每条边代表张量依赖。调度的核心是给每个节点决定三件事放在哪个设备上执行、与其他节点之间采用什么执行顺序、跨设备产生的中间张量走哪条传输路径。这三个决策听起来简单组合起来就是一个爆炸性的搜索空间。假设一张图有100个算子可用设备有4种仅“放置”这一个维度的组合数就是4的100次方。再加上顺序和传输路径的选择全量枚举根本不可能。多目标情况下更麻烦如果你想同时优化时延、显存峰值、能耗这三个目标经常是矛盾的。比如把算子均匀铺到多个设备上能缩短时延但跨设备拷贝增加能耗上来了又比如把数据都塞进同一块显存里减少传输显存峰值可能爆了。所以这里解决的不只是一个搜索问题而是一个多目标优化问题——最终产出的是一个帕累托前沿上的方案集合按实际场景约束去选。1.3 为什么现在才需要“多目标调度”早些年训练和推理的目标很单一模型能跑起来、速度不慢就行。现在不行了约束条件越来越多在线推理服务要求时延P99稳定不能为了峰值吞吐牺牲尾延迟边缘设备显存/内存有限可能需要牺牲一点速度换取内存峰值可控大型数据中心和AI集群开始关注功耗希望同样任务耗电更少训练场景里可能还要求分布式数据并行策略和调度方案兼容单一目标优化很难覆盖这些场景框架层做多目标调度就能用一个统一的搜索框架去兼容不同部署诉求。这也是HeteroOpt在设计时和早期论文里的启发式放置算法最大的区别它并不输出一个孤立的“最优方案”而是输出一组可以按场景挑选的候选解。我在实际测试中还发现一个现象同一个模型在不同batch size下最优调度差异很大。batch1时为了减少传输开销算子尽量放同一设备batch32时计算密集度上来了拆分到多个设备并行可能更划算。这个变量维度用静态手写规则几乎不可能覆盖必须靠自动搜索。2. HeteroOpt 的核心设计思路与架构拆解2.1 整体架构解析层、搜索层、执行层三层分离HeteroOpt整体上分了三层每层职责非常清晰前端图解析层负责把不同框架的模型统一转换成内部计算图表示。这里关键不是简单地把PyTorch模型转成ONNX而是要把算子粒度做归一化比如把PyTorch里的torch.addmm和TensorFlow里的MatMul BiasAdd归一到同一类标准算子。这一步做不干净后面所有调度规则和cost model都白搭。调度搜索层这是框架的核心负责在计算图上搜索多目标候选调度方案。它内部包含一个性能预测器cost model和一个搜索策略。搜索策略根据预测出的多组目标值迭代式地输出一批非支配解。后端执行适配层把调度方案翻译成不同硬件平台的执行动作包括算子加载、流/队列创建、数据传输等。这一层做得越抽象HeteroOpt能适配的新硬件就越多。我在设计过程中发现绝大多数调度框架死在“适配层做太厚”这件事上。每接入一个新硬件就要写一套设备管理器还经常和设备SDK版本强绑定。HeteroOpt的做法是只做“图级调度”把真正的kernel执行交给底层的推理引擎或者框架原生runtime来处理。调度层只负责回答“这个op放哪个设备”和“什么时候启动”不负责“怎么在设备上把kernel跑得最快”。2.2 搜索空间如何建模HeteroOpt把一次调度方案编码成一组向量常见的形式是为每个算子分配一个设备ID可选设备列表来自拓扑扫描执行顺序则通过一个拓扑序数组来表示所有调度方案都必须满足DAG的依赖约束。为了让搜索空间更好探索不会直接用设备ID硬编码而是对硬件做抽象分组。比如有两张GPU一张是A100一张是消费级卡在算子放置视角下它们可能是两个不同的“设备槽位”但如果两者计算能力差异太大自动搜索经常会生成把所有大算子都丢到A100的方案设备实际上没有很好的并行。这种情况下要引入一个“并行粒度”维度允许把图切分成多个子图同一个设备上可以串行执行多个子图不同设备间子图可以并行。子图概念的加入让搜索空间不再只到“算子级”还能做“算子集群级”的粗粒度决策减少搜索压力。每个候选方案还需要额外携带一份执行计划包括算子启动顺序和跨设备通信原语插入位置。HeteroOpt内部把这些统一叫作ScheduleCandidate。有了这样统一的表示cost model和搜索策略才能在上面做迭代。2.3 多目标搜索策略遗传算法 代价模型近似评估多目标搜索的经典方法有很多比如NSGA-II、MOEA/D、基于贝叶斯优化的多目标版本。HeteroOpt在实现上并没有固定死某一个算法而是采用了一个基础骨架随机初始化一批合法的调度候选满足DAG依赖约束用cost model算出每个候选在时延、内存、能耗等目标上的表现对候选做基于帕累托支配关系的排序通过选择、交叉、变异生成新一代候选重复若干代输出最终帕累托前沿这里最核心的技巧在于不能直接在真实硬件上评估每个候选。一次真实评估往往包含完整的模型加载、预热、多次推理/训练一次可能要几秒到几分钟。搜索过程中可能要评估数千个候选全量真实评估根本等不起。所以必须依赖一个cost model来做快速预估只在搜索收敛后选取少数几个头部候选到真实硬件做验证。cost model的建模是一个混合方案单算子的设备执行时间通过离线benchmark建立查找表数据传输时间通过设备拓扑和带宽矩阵估算多个算子之间的相互影响比如同设备争抢带宽、缓存颠簸等用一个轻量级修正因子叠加这套模型绝对不算精确但好处是评估一个候选的时间在毫秒级能做大规模搜索。实际测试下来cost model预测出的帕累托前沿和真实硬件验证后的前沿排序一致性大概保持在80%以上这在自动调度场景里已经足够用了。2.4 多目标之间的冲突与取舍逻辑HeteroOpt默认优化三个目标端到端时延、内存/显存峰值、总能耗。目标之间冲突非常明显。比如显存峰值优化最直接的做法是让所有算子尽量串行同时复用同一块显存buffer。但这会直接拉长执行时间。反过来如果想把时延压到最低就得让没有依赖关系的算子在不同设备上并行跨设备中间结果会同时驻留在多块显存里显存峰值立刻上去了。能耗这个目标又不一样它不是简单地等于“执行时间×功耗”。异构设备的功耗差异很大NPU跑某些算子时的能效比可能比GPU高好几倍即便执行时间比GPU慢一点系统总能耗也可能更低。HeteroOpt处理这些冲突的方式是不把多目标压成一个加权指标而是把所有目标都保留下来用多目标进化算法产生帕累托前沿。最后用户根据实际场景选点。比如边缘部署选内存适中的点云端在线服务选时延优先的点。这个设计在实际使用中非常合理。2.5 为什么不直接选“搜索空间最大”的全局最优方案理论上搜索空间越大越可能找到全局最优但工程里要控制搜索代价。HeteroOpt提供的解决方案是“分层搜索”第一层粗粒度算子集群划分和设备放置先确定哪些算子捆在一起去哪个设备第二层集群内部细粒度算子排序和内存规划第三层跨设备通信计划的微调比如选择传输时机、合并小张量这种分层方式显著缩小了搜索空间。一个 200 个算子的模型如果直接做算子级全空间多目标搜索可能要几十分钟才能收敛用分层搜索通常几分钟内就能得到一个足够好的帕累托前沿。这也是HeteroOpt这类框架工程可用性的关键指标优化效果好不算本事优化时间短且效果稳定才是能落地的本事。3. HeteroOpt 实操落地从接入到跑通一个真实模型3.1 接入方式PyTorch模型怎么进HeteroOptHeteroOpt的接入方式对已有训练推理代码很友好。核心思路是框架不要求你把模型代码重写一遍而是通过标准的图捕获机制拿到计算图然后再出调度方案最后回注到执行流程里。以PyTorch为例用torch.jit.trace或者torch.export拿到的Graph会通过图解析层转换成内部IR。在内部IR上做算子归一化时有个非常关键的细节——不同框架算子融合粒度不一致。比如PyTorch中nn.Conv2d单独是一个节点但在某些推理引擎里它已经被融合成了ConvBiasReLU。如果调度层的cost model只挂了标准粒度算子遇到融合算子会出现查不到表的情况。HeteroOpt的解法是支持按子图匹配做模式替换事先定义一组常见的融合模式在解析阶段就把它显式标注出来。简化以后的接入代码骨架大概是这样的import heteroopt as hopt # 拿到计算图 graph hopt.trace_from_pytorch(model, example_inputs) # 设备拓扑抽象假设有 GPU0 GPU1 NPU0 CPU0 devices hopt.DeviceTopology.from_yaml(devices.yaml) # 定义优化目标与权重范围 objectives hopt.MultiObjective( latency_weight0.5, memory_weight0.3, energy_weight0.2, ) # 搜索调度方案 result hopt.search( graphgraph, devicesdevices, objectivesobjectives, max_iterations500, search_strategynsga2, ) # 选一个方案并接入后端 plan result.select(pareto_pointlatency) hopt.patch_with_pytorch(model, plan)中间如果用的是推理引擎比如TensorRT或者自研NPU的runtimeHeteroOpt就不再尝试自己造轮子去执行算子而是把调度方案转为对应后端可以加载的图结构。这就要提到下一小节的关键配置。3.2 设备拓扑描述一个必须写清楚的配置文件HeteroOpt的所有调度都基于“设备之间通信要花多少时间”这个假设设备拓扑文件写不准调度结果基本就是瞎猜。实测中我见过有人把设备间带宽配置填成理论峰值结果调度方案同时把数据分发到两块卡实际拷数据时发现走的是PCIe而非内部互联性能崩得厉害。一个简化的拓扑文件长这样devices: - id: gpu0 type: cuda compute_capability: 8.0 memory_capacity_mb: 24576 peak_power_w: 300 - id: npu0 type: npu memory_capacity_mb: 32768 peak_power_w: 160 links: - src: gpu0 dst: npu0 bandwidth_gbps: 32 # 实际测量值不要填理论值 latency_us: 20 - src: gpu0 dst: cpu0 bandwidth_gbps: 16 latency_us: 30这些带宽和延迟数值绝对建议拿真实设备用一次大buffer拷贝实测而不是看规格书。我在项目里就遇到过一个情况规格书写NPU和GPU之间走PCIe 4.0 x16理论带宽32GB/s实际跑到14GB/s就上不去了因为DMA描述符开销和中断处理占了一大块。如果按理论值建模搜索出来的调度方案大概率会倾向于频繁跨设备传输真实跑起来反而更慢。3.3 模拟一次完整的调度优化运行实际运行流程可以分成四个步骤前置分析Profiling先在每台设备上运行一次模型记录每个算子在不同设备上的执行时间、内存分配量、功耗曲线。这些数据会落成一份JSON缓存文件。第二次优化时如果模型没变直接读缓存可以省掉大量benchmark时间。候选生成搜索算法随机初始化一批合法调度方案。这里有个实现细节初始种群的多样性比质量重要。如果初始候选全是“全放GPU0”的朴素方案后面很难跳出局部最优。所以初始化时会强制让方案覆盖不同的设备数量、不同的算子分配倾向。目标评估每个候选都会通过cost model快速算出一组多目标性能预估值。评估完后会做帕累托非支配排序。在实现里排序算法并不复杂——一个O(n²)的支配关系判断就够了更复杂的算法有时候反而因为数值精度问题产生噪声。方案产出与真实验证搜索收敛后取前沿上若干个代表性方案在实际设备上跑一遍更新cost model的修正因子。最后选择一个方案交给运行时执行。真实执行阶段HeteroOpt不会一次性把所有算子都加载进设备再执行而是按调度计划依次下发。这里有一个坑有些硬件后端不支持动态形状如果模型输入尺寸变化需要回退到“安全模式”即调度结果只用于固定shape的模型。支持动态shape的调度方案目前还是一个比较开放的课题通常做法是分桶处理为几个典型shape各优化一次运行时按输入尺寸查表。3.4 用案例看收益一个简化版对比我们用一个小型CV模型做过对比。模型主体是ResNet风格的网络放在一台GPUNPU混合机器上。三种配置结果大致如下配置端到端时延ms显存峰值MB功耗W说明全放GPU012.84200172均衡但无优化手动拆图到GPUNPU10.25100155有的算子放NPU省了能耗跨设备拷贝增加了显存HeteroOpt时延优先解8.65400168(理论) 有效缩短时延HeteroOpt内存优先解11.93700143牺牲少量时延显存和功耗都降低这张表不是用来证明数值多漂亮核心要传达的是不同目标约束下调度方案的走向差异非常大。如果你让一个手写规则的人去优化通常只会盯着时延最后得到的方案几乎不会自动跑到“内存优先解”这种形态。HeteroOpt的价值就在这里——跑一次搜索把各种权衡下的方案都摆在面前去做取舍的是人。3.5 一些提高实际收益的经验技巧调度粒度可以结合算子“落盘”策略来优化。很多框架里跨设备传输是同步阻塞的HeteroOpt支持异步预取和计算-通信overlap关键做法是把叶子节点算子提前发起数据传输让它在等待时执行其他设备上的算子。这个能力在搜索空间里表现为额外的fifo_depth参数控制同一时刻允许有多少个跨设备传输在飞行。值设太大显存峰值飙升设太小传输延迟藏不住。实测下来一个经验值是在4到8之间具体取决于设备间带宽和数据大小。另一个经验是图解析阶段最好过滤掉“参数初始化节点”和“形状推导节点”这些op只是图上的噪音对调度没有实际意义不剔除的话cost model会被这些无谓节点干扰。4. 落地过程中的常见问题与排查技巧4.1 搜索出的方案在真实设备上反而更慢这是所有调度框架都会遇到的头号问题。原因几乎都是cost model预估和真实运行差值太大。排查时不要一上来就怀疑搜索算法先做下面几步检查设备拓扑里的带宽和延迟参数是否是实测值检查cost model有没有把kernel launch的开销算进去检查搜索时用的“单算子耗时表”是不是在当前机型上基准测得不能拿另一台机器的数据直接复制解决手段有三种第一采集更多真实样本回填到cost model里做在线校准第二搜索时加入鲁棒性约束比如对cost model预测方差比较高的候选方案做惩罚第三收敛后不直接选“预测最优”方案而是选“预测前沿中段且与当前部署物理距离较小”的方案降低风险。4.2 跨设备传输带宽利用率低实际测试中出现过一个现象明明带宽写的是32GB/s但调度方案里大量数据传输实际只跑到8GB/s。原因往往出在张量碎片化上——一个小tensor传输占比过高时启动传输的原语开销就盖过了传输本身时间。处理这种问题一是开启传输合并把多个小张量打包成一个buffer做一次传输二是修改调度候选生成逻辑给“跳变式”跨设备方案加一个惩罚项让搜索算法更倾向保持子图连续性减少跨设备切边次数。4.3 内存峰值优化了的方案实际跑的时候竟然OOM这种情况大多数时候不是搜索的问题而是算子执行框架内部有额外的workspace内存需求。比如某些kernel在GPU上需要临时workspace buffer大小和输入尺寸直接相关这部分内存在图级IR里根本看不出来。你把显存峰值预估小了实际执行自然爆。HeteroOpt的做法是在cost model里给每个算子加一个workspace_memory_mb字段数值来自算子库侧真实测量。搜索完成后落地执行前还建议加一层显存预分配保护——预留10%左右的裕量缓冲这个你自己掂量如果搜出来的方案显存已经顶到上限附近就尽量选内存更宽裕的帕累托解。4.4 多次优化搜索结果不稳定进化算法天生带随机性两次搜索出来可能给出不同方案。如果这是你第一次接触这种事不用慌正常的。但工程上如果结果差异太大会影响可复现性和上线流程。常规处理办法固定随机种子增大初始种群数量增加迭代次数保留历史搜索结果作为下一轮搜索的初始种群之一HeteroOpt内部支持把上一次搜索产出的帕累托前沿序列化成文件追加进下一轮搜索的初始种群这样后续搜索会沿着之前的搜索结果继续演进不会每次都从零开始。4.5 排查问题速查表现象可能原因建议操作搜索时间过长初始种群设备分配过于分散提高设备拓扑约束先限制候选设备数量搜索时间过短但结果差迭代次数不够未充分探索调大迭代次数或交叉变异概率选出的时延优先解显存超限显存峰值目标权重压得太低同时设定显存硬约束条件而不是只改权重结果和真实执行差太多cost model参数陈旧在线校准或重新benchmark某一块设备一直空闲该设备类型在cost model里没有算子耗时表检查profile是否覆盖了该设备上的全部算子同一模型在不同Shape下最优方案不一致动态Shape导致的调度不稳定分桶优化每个典型Shape生成一次调度方案最后再分享一个我在实际测试里养成的习惯每次跑完HeteroOpt搜索不要只盯着一个最优方案看把整个帕累托前沿导出来按目标维度做个可视化。多目标调度的本质就是取舍人眼扫一遍前沿分布往往能发现比算法自动选点更好的方案——比如某个方案虽然时延比最优解慢了3%但显存降低了15%而线上指标对显存更敏感。这个层面上的判断调度框架再自动也替代不了人。
返回列表