ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AI训练调度器:从排队抢占到高效利用GPU集群的实践指南

AI训练调度器:从排队抢占到高效利用GPU集群的实践指南 去年年底有一次凌晨的线上事故我印象特别深。训练团队要起一个 8 卡微调任务推理团队要保住一个正在放量的 A/B 实验两边盯上同一批 GPU而集群里当时只剩 6 张卡。系统配的是先到先得后提交的任务只能排队于是两个团队在一个故障群里上演了真人版优先级抢占。最后的结果是两边 leader 各自找了更多人开会卡还是空着任务还是卡着。那段时间我每天都在处理类似的问题。单卡性能再强训练框架再顺当几百上千张 GPU 摆在你面前时真正决定训练场效率的早就不是某一台机器的算力而是那个不被大多数人注意的组件——调度器。这篇是 AI Infra 系列里训练与调度的下半部分。上一部分我们聊了训练环境、数据加载、单任务稳定性那些解决的是一只队伍能不能打的问题这次讲的是当你有几千甚至上万张 GPU 时到底怎么给训练任务排班谁先跑、谁后跑、跑多少卡、跑到一半被更高优先级的任务插队时能不能体面退场。调度器就是这个训练场的隐形老板它不说话但每一张卡的命运都捏在它手里。这个内容适合所有 AI Infra 工程师、ML Platform 工程师以及准备把训练规模从单机扩展到集群的团队。哪怕你暂时只有十几张卡尽早理解调度的底层逻辑也能帮你少踩很多资源看着够但任务就是跑不起来的坑。1. 从找个有卡的机器到把每张卡都排明白1.1 小团队手动排班够用为什么几千张卡就不行我刚做 AI Infra 那会儿团队的 GPU 资源少得可怜一个共享表格就能搞定排班谁要用卡在表格里写一行自己找一台空闲机器上去敲命令开始训练。卡不够的时候就靠人情关系好比调度器好使。这种方式在几十张卡、十几个人的规模下完全没问题因为大家互相认识任务也简单偶尔撞车开个会就解决了。但集群一旦涨到几百张卡、几十个团队共用手动排班就彻底崩了。首先是信息不对等没有人能实时知道哪台机器还有几张空卡其次是资源碎片化今天 A 任务占了 4 卡明天 B 任务占了 2 卡后天一个需要整机 8 卡的大任务来了发现所有节点都被残卡占着整个集群利用率看起来不低却连一个完整的 8 卡任务都凑不出来。更麻烦的是没有公平性保障同一个团队可能因为 leader 嗓门大就多拿资源另一个团队默默排三天队。调度器解决的就是这个谁先用卡的冲突问题。它的本质是一个多目标优化器在集群资源、任务优先级、组织配额、任务截止时间之间做权衡。一个设计良好的调度器能让每个任务在合理时间内得到资源同时把集群的整体吞吐推高。一个糟糕的调度器则会让你陷入GPU 利用率很高但任务全部在排队的诡异状态。所以调度器从来不是简单地把任务塞到有空位的机器上它是在为整个训练场的秩序负责。1.2 调度器管的不只是 GPU而是整台机器加通信拓扑很多人对调度器的理解是它负责分配显卡实际远没有这么简单。一个训练任务提交上来资源的请求维度至少包括GPU 卡数、GPU 型号、CPU 核数、内存大小、本地磁盘空间、是否独占整机某些场景下还要声明需要的网络带宽和 RDMA 资源。举个例子一个典型的 PyTorch DDP 训练任务资源声明大概是这样的8 张 A100 GPU64 个 CPU 核512GB 内存2TB 本地 NVMe 磁盘同时要求这 8 张卡必须在同一台物理机上。如果用 Kubernetes 的描述方式来写类似这样resources: requests: nvidia.com/gpu: 8 cpu: 64 memory: 512Gi ephemeral-storage: 2Ti limits: nvidia.com/gpu: 8 cpu: 64 memory: 512Gi ephemeral-storage: 2Ti nodeSelector: gpu-type: a100注意最后那个 nodeSelector它表示这个任务只接受 A100。别小看这一步很多时候任务调度不出去不是因为集群没卡而是有空闲的卡但型号不对或者驱动版本和任务需要的 CUDA 版本不兼容。调度器在做可行性检查时必须把型号、驱动、显存、CPU、内存、磁盘、网络这些因素全部过滤一遍筛出真正能跑这个任务的节点然后才进入打分环节。调度器评分时会考虑很多策略是尽量把任务堆在一起省节点binpack还是尽量打散降低故障影响spread任务之间有没有亲和或反亲和要求如果任务用了张量并行8 张卡是不是落在同一台机器的 NVLink 域内这些听起来像细节但每一个都直接影响训练性能和稳定性。一句话总结调度器排的不是一张卡而是一个任务在整棵树上的位置位置选错了后面所有训练优化都是白搭。2. 训练任务的三座大山Gang、优先级与抢占2.1 为什么多卡任务必须齐步走Gang Scheduling 的实现逻辑分布式训练天生有一个特点要么全部卡一起上要么一张都别上。一个 8 卡 DDP 任务如果调度器先给它分配了 4 张卡另外 4 张卡还空着等资源任务能先跑起来吗不能。因为 DDP 初始化阶段需要所有 rank 建立通信模型在每个 step 都要做 AllReduce只要有一个 rank 没起来整个任务就一直卡在初始化阶段直到超时。更糟糕的是那先分配到的 4 张卡会一直占着资源不动后面的任务也进不来造成一种死锁状态8 卡任务等剩下 4 卡4 卡资源被占着等任务启动新任务进不来集群堵成一锅粥。所以调度器必须支持 Gang Scheduling也就是满员才开车。以 Volcano 调度器为例它引入了一个叫 PodGroup 的概念一组 pod 组成一个 PodGroup声明最小成员数量和最小资源量。调度器在分配时只有发现某个节点的资源能够同时满足这个 PodGroup 的最小需求才会真正分配否则整个 PodGroup 保持 Pending 状态不占用任何资源。这里有个反直觉的点Gang 调度在某些场景下会主动降低资源利用率。比如一个任务需要 8 卡当前集群有 7 张空闲卡你可能会想先给它 7 张跑着等第 8 张空出来再补上。但在 DDP 场景下这个思路不成立因为 7/8 的任务没法做任何计算只会白等。所以宁可不调度也不要半调度这是训练任务和普通微服务调度最大的区别之一。2.2 优先级与抢占低优先级任务不是活该被杀死的调度器支持优先级这个大家都理解。但优先级怎么用里面门道很多。有些团队把优先级做成了高优任务可以无条件抢占低优任务结果高优任务一上线低优任务全部被杀尤其是那些已经跑了两天的训练任务checkpoint 没来得及保存进度直接归零比不调度更伤。训练任务其实有个天然优势几乎都有 checkpoint 机制。所以调度器在抢占训练任务时可以走优雅抢占流程先给目标任务发送 SIGTERM 信号让训练进程有机会把当前的模型权重保存下来然后退出释放显存最后调度器再把这批资源分配给高优任务。这个过程和推理服务的优雅下线是同一个思路只是对象从请求换成了训练进度。实际操作中我会把队列分成可抢占和不可抢占两类。可抢占队列通常跑的是实验性任务、超参数搜索、夜间的批量预训练它们挂了可以恢复损失可控不可抢占队列跑的是核心业务的微调、线上模型的增量训练这些任务一旦被杀影响的是业务指标。配置上类似这样apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: experiment spec: reclaimable: true weight: 10 capability: nvidia.com/gpu: 512 --- apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: production spec: reclaimable: false weight: 100 capability: nvidia.com/gpu: 256reclaimable 字段表示这个队列的资源是否可以被更高优先级队列回收。生产队列设为 false实验队列设为 true这样既能保证核心任务不受干扰又能让实验任务在资源空闲时把集群用满。另一个容易被忽略的点是抢占后的防抖机制。如果低优任务被抢占释放资源后调度器又立刻把它重新调起来然后又被抢就会形成抢占风暴。一个任务在几分钟内反复被杀、反复重启集群事件刷屏谁都没法干活。合理的做法是给被抢占的任务设置一个冷却时间比如被抢占后至少等待 10 分钟再重新调度给高优任务留出足够的稳定运行窗口。2.3 队头阻塞一次全组饿死的事故复盘优先级和高队列权重解决了一个问题但也可能制造新的问题最典型的就是队头阻塞。我经历过一次事故某个团队提了一个需要 1024 卡的超大预训练任务因为优先级高它排到了队列最前面。但这个任务要的资源太多集群暂时凑不齐于是它就一直卡在队首等。后面几十个 8 卡小任务虽然资源充足但因为队列排序规则是严格按优先级它们全被堵在后面没有一个能往前挪。结果就是集群一半资源空闲GPU 利用率不到 40%但所有任务都在等那个 1024 卡的大任务凑齐资源。这种状态持续了整整两天两个团队的 leader 都快打起来了。复盘后我们做了三件事。第一给队列设置 max 配额任何任务都不能占用超过集群总量某个比例的资源从机制上避免巨无霸任务堵死全队。第二限制每个队列的并行任务数避免一个团队同时提交几百个任务把其他团队彻底挤掉。第三引入优先级老化机制任务在队列里等得越久它的有效优先级越高这样既照顾了高优任务又不会让等待时间过长的小任务饿死。这里有一个更隐蔽的坑不同队列之间互相等资源也可能形成死锁。A 队列缺卡在等 B 队列释放B 队列也在等 A 队列释放两边都不退让。解决思路通常是给每个队列设置一个资源隔离的硬边界不能无限借用别人的资源。允许弹性借用是好事但一定要加上可回收的标记并且定义好回收顺序。3. 一张集群调度员的实操排班表3.1 队列和配额先用硬隔离把资源战争变成配额内博弈调度器上线的第一步永远不是调参而是定义队列和配额。队列本质上是资源的组织单元按团队分、按业务分、按优先级分都可以但一定要和组织的资源预算对应起来否则调度器再聪明也没法帮你解决资源分配矛盾。我个人偏好的做法是三层结构。第一层是物理资源池把 GPU 按型号划分成几个池子比如 A100 池、H800 池、L40S 池任务只能进匹配型号的池子。第二层是队列每个业务线一个队列队列声明自己的资源配额上限。第三层是命名空间和 Kubernetes 的 namespace 对齐配合 ResourceQuota 一起做硬隔离。举例来说训练团队 A 的资源配额是 512 张卡训练团队 B 是 256 张卡但 A 平时用不满B 有紧急任务需要 300 张卡时怎么办两种选择严格隔离B 超出配额的任务直接排队A 的卡空着就空着或者允许弹性借用B 可以在 A 不用的时候借 44 张卡一旦 A 有任务提交调度器触发回收B 的任务需要让出资源。这两种模式没有绝对好坏取决于团队的协作文化。如果团队之间信任度高弹性借用能显著提高集群整体利用率如果经常打架还是老实点用硬隔离。配额具体设多大不能靠拍脑袋。我通常会让团队导出一个月的任务历史数据统计每个任务的 GPU 卡数、运行时长、提交频率然后画出同时运行的 GPU 总数的时间线找到高峰期的聚合值再乘以一个 1.2 到 1.5 的系数作为配额基准。这个数据比任何人的直觉都可靠。3.2 Binpack 还是 Spread两个你不得不做的选择题资源分配策略里最经典的一对矛盾是 Binpack 和 Spread对应到实际场景就是任务来了是集中放在尽量少的节点上还是分散放在尽量多的节点上Binpack 策略的核心是把箱子尽量装满优点是能节省节点一台机器跑到 100% 再用下一台适合节省成本和资源治理缺点是一台机器上堆太多任务后故障影响面会变大单机宕机可能导致十几个任务同时失败。Spread 策略则相反任务分散在不同的节点上容错性好但会产生大量碎片可能出现每个节点都剩 2 卡但没有一个节点能跑 8 卡任务的尴尬局面。对 AI 训练场景我的建议是区分对待。大模型预训练、全参数微调这类多卡任务尽量用 Binpack把 8 张卡集中在一台机器上不仅能省节点还能充分利用机内 NVLink 的高速互联单卡或者 2 卡的小任务适合 Spread避免占住整机资源。Volcano 里可以通过修改调度策略实现给节点排序时使用 binpack.weight 或者 spread.weight 两个维度做权衡而不是一刀切。另外有一个经常被忽略的点多卡任务的节点连续性。8 卡任务如果落在 4 台不同的机器上每台机器 2 卡调度器会说资源满足但训练性能会因为跨机通信而大幅退化。所以训练任务的调度一定要尽可能把同一任务的卡放在尽可能少的节点上。调度器如果做不到这一点训练效率再高的框架也白搭。3.3 拓扑亲和当张量并行遇上 NVLink 互联前面提到多卡任务尽量落在同一个节点这里面的核心原因是通信拓扑。现代 GPU 服务器内部通过 NVLink 把多张卡连成高速互联带宽远高于跨服务器的网络带宽。如果一个任务使用了张量并行或专家并行每一步训练都要频繁和相邻的 rank 交换数据通信量随并行度线性增长。卡落在同一台机器的 NVLink 域内和跨机器走数据中心网络训练速度可能差 3 到 5 倍甚至直接 NCCL 超时报错。所以在生产集群里调度器光看有没有 8 张卡是不够的必须感知到这 8 张卡是不是在同一台机器上以及它们之间是否是高速互联。工程上的做法是给节点打上拓扑标签比如 node.kubernetes.io/nvlink-domain调度器在做可行性检查时加上一个约束。这里还有一层更深的问题多机多卡任务。比如一个 64 卡的任务需要 8 台 8 卡机器。调度器应该尽量把任务分配在同一个机架或者同一个 GPU PDU 下减少跨网络的跳数。高级的调度器甚至可以读取 NVIDIA DCGM 暴露的拓扑信息再配合 NCCL 的环境变量让训练任务自动选择最优的通信路径。这些能力不一定都来自调度器本身但调度器的排班决定了这个问题的下限。3.4 调度器选型对比Volcano、KubeScheduler、Slurm 还是自研我在不同规模的公司见过完全不同的调度器技术选型简单做个横向对比。调度器生态适合场景缺点Kubernetes 原生 KubeScheduler稳定、社区大但默认缺少 Gang 调度、队列级配额等 AI 特性单机多卡、少量 GPU 的容器调度训练任务落地体验差需要大量二次开发VolcanoK8s 扩展与 K8s 深度集成支持 Gang、队列、优先级、抢占是目前 AI 训练调度的主流方案以 Kubernetes 为底座的中大规模训练集群部分高级策略仍需二次开发文档质量一般Slurm超算/传统 HPC 生态调度大规模分布式任务非常成熟纯批处理、无容器化需求的超算集群不太适合在线服务混布容器支持相对弱自研调度器完美贴合业务但成本极高万卡级别、调度策略极其特殊的头部公司至少需要 2-3 名专职工程师维护迭代慢如果是新启动的 AI Infra 平台我的建议很直接Kubernetes Volcano 是性价比最高的起点不要一开始就想着自研。Volcano 确实有文档不完善、某些边界条件处理粗糙的问题但它已经覆盖了 90% 的常见场景。等你的集群规模到了几千张卡调度策略特殊到 Volccano 满足不了的时候再去考虑二次开发或自研。自研调度器的成本比大多数人想象的更高那是一个需要长期投入的方向。4. 调度器的体检报告这几个指标比 GPU 利用率更重要4.1 排队时间、队列深度、抢占次数比 GPU 利用率更值得盯很多团队看集群健康度只看 GPU 利用率利用率低了就觉得调度器不行。但调度器是否健康首先应该看任务排队时间。GPU 利用率高完全可能是大量低优先级小任务在跑而真正重要的高优任务在排队等资源。这种状态是虚假繁荣。我每次给调度器做体检会看四个核心指标任务排队等待时间重点关注 P50 和 P95。P95 如果超过一小时说明任务的 SLA 已经很难保障。各个队列的 pending 任务数和 pending 资源量。这个指标能直接反映队头阻塞问题哪个队列的 pending 卡数突然飙升说明有任务堵住了。抢占次数和被抢占任务的恢复成功率。抢占次数太多说明资源分配方式有问题或者弹性借用策略过于激进。调度失败原因分布。Unschedulable 的任务到底是因为资源不足、型号不匹配还是拓扑约束不满足每种原因的比例一看便知。监控这些指标不需要一开始就搭一套复杂的系统。最简单的做法是定时抓取 Volcano 调度器的 metrics 接口把关键指标接入 Prometheus再配 Grafana 面板。等你观察到P95 排队时间与任务完成成功率之间的关系你对调度器的调优就会变得有据可依。4.2 从调度失败事件反向定位问题一次定位排查的过程实际运维里任务调度不出去是最常见的事故。Kubernetes 里查看 Pod 事件最常见的报错是Unschedulable但具体原因千奇百怪。我总结过几个高频问题节点资源显示不足但明明用nvidia-smi看到有空闲 GPU。这种情况往往是调度器的可分配显存缓存没有及时更新或者节点上的 GPU 被某个进程占着但没有声明资源。节点打了 taint任务没有对应的 toleration。比如给训练节点专门打了dedicatedtraining的污点但任务声明里漏了 toleration调度器直接跳过。反亲和规则互相冲突。两个任务都设置了互相排斥的反亲和但资源又必须落在同一个节点上死锁了。nodeSelector 选不到节点。任务指定了某个型号的 GPU但这个型号的节点全部在下线维护。排查这类问题我的固定思路是先用kubectl describe pod看调度事件找到具体是哪个约束不满足然后缩小范围看节点标签和污点最后再到调度器日志里看这个 Pod 的打分记录。不要一开始就怀疑调度器有 bug90% 的调度失败都是资源声明和实际节点配置对不上。4.3 给调度器做灰度先小流量验证再全量切换调度器是全局组件改一个策略参数可能影响整个集群所有任务。这和改训练框架不同后者挂了最多影响单个任务调度器挂了全集群都会停摆。所以调度器变更必须走灰度线上验证通过后再全量。我自己的流程是先在测试集群把新策略跑三天把排队时间、完成率、抢占次数和旧策略做对比然后挑一个非核心队列比如实验队列先在它上面启用新策略观察一天确认没有异常后再推广到生产队列。每个策略都要做成可以动态开关的配置项出问题能一键回滚。调度器的回滚脚本应该和监控告警绑定一旦关键指标超过阈值自动恢复旧配置。这一点是很多从单机训练转向集群训练的团队最容易忽略的。他们觉得调度器只是一个分配器改改无所谓结果一个参数改砸了整个集群训练任务雪崩最后还得靠手动恢复。5. 落地一套调度排班体系我的实操清单5.1 第一步先约法三章把任务分类和 SLA 写清楚调度器上线前最重要的不是部署软件而是和各个业务团队达成共识。我每次都会拉着所有用 GPU 的团队开一次会确定三件事任务怎么分类核心训练、实验训练、批量推理、Fintune。不同分类对应不同的优先级和抢占策略。每个队列的 SLA核心训练任务排队时间不能超过 15 分钟实验任务可以等 2 小时批量任务可以等 4 小时。哪些任务必须启用 checkpoint不能保存进度的任务默认都不允许被抢占。这些约定写在 Wiki 上没用要落实到调度器的队列配置里。调度器不认人的面子只认配置。团队负责人可以在群里争资源但调度器只按照事先定义好的规则执行这样才能把资源战争从人情博弈变成规则博弈。5.2 第二步从最小可用开始逐步叠加策略很多人一上来就想把所有功能配上Gang、抢占、弹性配额、Binpack、复杂拓扑约束全开结果调度器自己先崩了。我的经验是分阶段推进。第一阶段只做资源调度部署 K8s Volcano定义几个基础队列配置好配额所有任务按队列分配资源不做抢占。先让整个流程跑起来观察排队和调度日志。第二阶段加优先级和抢占先把实验队列标为可抢占让高优任务能回收低优任务的资源同时确认被抢占的任务能通过 checkpoint 恢复。第三阶段加弹性配额和拓扑约束允许队列间借用空闲资源给多卡任务配置拓扑亲和让资源利用率进一步提升。每个阶段至少要稳定运行一到两周确认没有明显问题后再进入下一步。5.3 第三步故障预演和回滚是必须的调度器上线后我的最后一个建议是定期做故障预演。比如模拟核心队列的高优任务突然需要 500 卡集群只剩 300 卡的场景观察抢占机制能不能正常触发、低优任务能不能优雅退出、高优任务多久能跑起来。这种预演能在真正出大事之前把问题全暴露出来。更关键的是回滚预案。调度器配置变更的回滚不只是把 YAML 恢复还要确认恢复后集群状态是健康的。我在预演时遇到过这种情况回滚配置后所有任务重新排队结果把集群排队时间打到了历史最高。回滚不是万能的所以变更前的配置备份、回滚后的健康检查都要形成一套标准化流程。5.4 常见问题速查表最后附一份我日常排障用的速查表不一定覆盖所有场景但遇到问题可以先对号入座。问题现象可能原因处理建议任务一直 Pending事件显示资源不足节点 GPU 被未声明资源占住或调度器缓存未刷新kubectl describe node看可分配资源检查是否有进程占用重启 kubelet 刷新缓存高优任务抢占低优任务后低优任务反复被杀缺少抢占冷却时间给被抢占任务设置重调度最小间隔避免抢占风暴集群 GPU 利用率不低但核心任务排队严重低优小任务占满资源高优大任务凑不齐为队列设置最大并行任务数为核心任务保留资源池8 卡任务被分到多台机器训练速度极慢调度器未感知拓扑跨机通信成为瓶颈使用 Binpack 或拓扑亲和策略强制任务落在同一节点弹性借用资源后原队列任务无法回收资源回收策略没有配置 reclaimable检查队列配置被借用的队列需开启可回收标记最后再分享一点个人体会。我在前面加过很多次班处理调度事故后发现调度器上线这件事的技术难度其实只占四成剩下六成是组织协调怎么让业务团队接受排队规则怎么让老板理解 GPU 利用率不是越高越好怎么让每个任务在提交时就把优先级和 checkpoint 策略写清楚。这些事不做再好的调度器也只能在一个混乱的规则体系里挣扎。反过来如果规则定得清晰哪怕调度器本身配置朴素一点集群也能跑得相当顺。
返回列表