
先别急着聊算法和框架咱们先把一个最现实的问题摆到桌面上一万张 GPU 同时开工到底是谁说了算你说“项目经理说了算”项目是虚构出来的概念真正说了算的是那个在训练场里默默排班、分卡、催进度、甚至轰人下机的系统——调度器。我干 AI Infra 这几年见过太多团队把精力花在模型结构、数据 pipeline、加速卡选型上结果集群规模一上千卡所有人被同一个问题按在地上反复摩擦任务排队了但为什么排不上卡明明空的但作业就是起不来优先级更高的任务来了该不该把正在跑的训练掐掉这一整套博弈本质上就是调度器在做决策。它不写一行训练代码但它决定了你的模型能不能按时训完、算力成本到底划不划算。这篇就是训练与调度主题的下半部分专门聊这个“隐形老板”是怎么排班的。重点会拆开几件事万卡场景下调度器到底在解决什么难题里面的核心机制是怎么设计的主流的开源方案应该怎么选、怎么配以及我在实际运维中踩过的那些坑。不管你是刚把训练从单卡挪到多卡的小团队还是已经在维护千卡集群的 Infra 同学这篇都值得看完。1. 从单卡到万卡调度器到底在解决什么问题1.1 单卡像自建房万卡像城市交通先想想单卡训练是什么状态。你装好 PyTorch配好 CUDAtorch.cuda.is_available()返回 True然后python train.py跑起来完事。这就像自建房地是你的墙是你砌的水电你自己接没人和你抢。但到了集群环境一切都不一样了。一万张 GPU 不属于任何一个人它是公共资源。你要在别人也在用的时候去申请一个“8 卡 A100”的租户这个申请怎么审批、怎么分配、怎么保证其他人不受影响就是调度器在管。而且比城市交通更麻烦的是GPU 请求不像汽车过红绿灯那么整齐有人要 1 张卡有人要 64 张卡有人要跑 3 小时有人要跑 3 天还有人申请时不写时长准备“先占着再说”。这还不是最头疼的。训练任务和普通 CPU 任务有个本质区别它往往是“全有或全无”。一个需要 8 张卡的数据并行任务你分给它 7 张它不但跑不快反而根本起不来因为分布式通信组初始化就缺一个 rank。这种特性意味着调度器不能简单地“有卡就分”需要站在更高的维度统筹全局。1.2 训练和推理对调度的要求根本不是一回事很多刚接触集群的人会把训练作业和推理服务混在一起讨论但这俩对调度系统的诉求可以说是两个极端。推理服务是常驻的7x24 小时都在延迟敏感用户点一下、发个请求几百毫秒内就得有响应。它对调度器的要求是稳服务不能因为资源不够就被赶走扩容缩容要平滑最好能精确预测流量。训练任务是短则几小时、长则数周的批处理作业它对单次响应时间不敏感但对“连续性”极其敏感。原因很简单一个训练任务如果跑到第 10 个小时被调度器掐断哪怕有 checkpoint重跑也需要时间如果没有 checkpoint前面 10 小时直接从“有效算力”变成“无效电费”。所以调度器在策略上必须区分这两类负载。对推理服务偏重稳定性和隔离性对训练任务偏重连续性和公平性。这个差异直接决定了后面要讲的队列设计、优先级策略和抢占逻辑。千万别用一套规则硬套两种场景那是灾难的开始。1.3 没有调度器的集群本质上是个资源黑市我在交流时经常听到有人问不搞调度器行不行大家约定好谁用哪几张卡不就行了小规模确实可以。五人团队、二十张卡拉个共享表格谁要用填一下名字勉强能跑。但规模上去之后这种“君子协定”会迅速崩盘。首先是资源碎片化。有人申请了 8 卡任务跑完只释放了 6 卡剩下 2 卡因为太小没人用就永远空着。时间一长集群里全是这种边角料碎片大任务进不来小任务不够用。其次是“占坑”心理。如果资源不用抢那默认就是谁先占谁用。很多团队会为了避免“以后要用没得用”提前把卡占住哪怕当前任务根本用不满。结果就是真实利用率低得可怜但表面上“卡全被占了”。最搞笑的是分布式死锁。两个任务各需要 8 张卡集群里刚好只有 16 张卡每个人都先占了 8 张中的一部分比如任务 A 占了 4 张、任务 B 占了 4 张然后双方都在等对方释放……事情就僵在那里谁也无法开始。这种问题靠人工协调协调到崩溃也解不开。调度器的存在就是为了用一套“有规则、可度量、能强制执行”的机制把资源黑市变成有序市场。它不一定是最公平的但一定是全局可预期的。2. 调度器的核心工作逻辑从描述一张卡到安排一万张卡2.1 资源描述GPU 不是只有一个“张数”维度很多人理解资源调度觉得就是把数量管好8 卡、16 卡、32 卡就这么简单。真到了生产环境一张 GPU 在调度器眼里的属性多到吓人。算力类型A100、H100、L40S、昇腾 910B、显存大小40G、80G、显存带宽、卡间互联方式NVLink 还是 PCIe、物理位置在哪个节点、哪个 NUMA 域、连在哪颗 CPU 上、是否支持 MIG 拆分、固件版本、健康状态……每一项都可能成为任务能否运行的硬约束。举个例子一个模型并行训练任务对卡间通信带宽极其敏感它最好把 8 卡落在同一个节点上并且走 NVLink如果调度器不关心拓扑把 8 张卡分布在 4 台机器上训练时的 allreduce 通信时间会成倍增长最终 GPU 利用率可能从 90% 掉到 40%这是实打实的效率损失。所以调度器的第一步工作是先把每张卡的“画像”描述清楚。在 Kubernetes 生态里通常会给节点打上标签比如gpu-typea100、gpu-memory80g、nvidia.com/gpu.product这类字段在 Slurm 里则通过 node feature 和 partition 概念来表达。这一步做得粗糙后面所有决策都会跟着变形。2.2 全有或全无Gang Scheduling 为什么是刚需分布式训练任务有一个让调度算法设计者痛不欲生的特征要么一次性把所有需要的资源都拿到要么一个都别给。这就是工业界常说的 Gang Scheduling捆绑调度、组调度。你可能会说既然需要 8 卡那就等 8 卡都空闲了再一起分配不就行了理论上是对的但实现起来有个棘手的问题如果调度到一个任务需要 8 卡而当时只剩 6 卡你是继续等剩下的 2 卡还是先把 6 卡派出去如果先把 6 卡派出去任务 A 启动失败、等待第 7 张卡同时因为占了 6 张卡不肯释放任务 B也需要 8 卡只剩下 2 卡可等两个任务互相锁死。这就是前面说的资源死锁在调度层面的正式版本。Gang Scheduling 的解法很简单粗暴如果一个任务的minAvailable得不到满足就一个资源都不分配给它宁可让它继续排队也不要造成部分占用。这个策略牺牲了一点资源利用率但换来了系统整体的活锁避免在训练场景下是非常划算的。Volcano 里的minAvailable字段、Kueue 里的queueing策略都是围绕这个思路实现的。第一次配调度器的人最容易在这一点上犯错觉得“先给一点卡让任务跑起来后面再等”是人性化设计结果把整个集群搅成一锅粥。2.3 拓扑感知把通信延迟“算”进调度决策在单机八卡时代NVLink 把卡和卡之间连成一张高速网通信不再是瓶颈。但集群训练跑的是分布式节点和节点之间要靠 RoCE 或者 InfiniBand 来通信。这里的带宽、延迟和同一节点内的 NVLink 完全不是一个量级。所以调度器需要有“拓扑感知”能力把任务分到“通信代价最小”的位置上去。具体来说一个 8 卡任务调度器应该优先考虑把它放进同一个节点其次是同一个机架里的不同节点最差才是跨机架分布。这里的逻辑类似生活中的搬家你肯定希望家具都在一个仓库里而不是分五个仓库放着搬起来累死人。实操中这种感知不一定要做得很精细但至少要区分三个层级同节点、同 NVSwitch 域内通信最快优先分配。同机架、走 RoCE 交换机通信可接受适合数据并行。跨机架甚至跨机房万不得已才用一般用于超大模型训练或弹性任务。调度器在打分时会给不同拓扑层级分配权重能落在同一节点就尽量落在同一节点。这个优化不写进需求文档但直接决定你的训练速度。2.4 排队策略优先级、配额和公平性的三重博弈资源永远不够所以排队是常态。问题在于谁排前面谁排后面凭什么最简单的策略是 FIFO先到先得。但纯 FIFO 一定会出问题一个低优先级的测试任务可能占住 1000 卡跑一周而真正重要的高优任务只能排队等一个周末过去。所以生产环境的调度器必须有优先级和配额机制。优先级解决“重要任务插队”的问题高优任务可以先于低优任务获得资源甚至可以把低优任务占用的资源抢占过来。配额解决“用户之间争抢”的问题每个团队、每个项目分多少个卡时GPU 小时用完了就得等谁也不许跨组抢资源。这两者通常叠加使用再加一点权重公平排队比如 DRF、主资源公平算法兼顾多租户场景下的公平性。但这套设计里有个极其容易踩坑的点抢占。训练任务不是普通进程你把它掐了它前面几十个小时的算力就全浪费了。所以抢占策略一定要配合 checkpoint 机制而且最好在抢占前给被抢占任务一个“优雅退出”的宽限期让框架有机会存下当前状态。否则频繁抢占的结果就是高优任务一直在跑低优任务永远在从头再来整体集群的“有效产出”反而非常低。这一点我在很多团队里见过前期只配了抢占没配 checkpoint 兜底结果低优任务每天醒来都在重新训练第二天问我为什么进度条一直不动。你说糟心不糟心。3. 主流调度方案选型与实操配置3.1 方案全景Kubernetes 系、Slurm 系、自研系聊完原理来点实际的。现在业界做 GPU 集群调度主流方案我大致分成三类方案适合规模优势劣势Kubernetes Volcano中小到大规模云原生生态好资源抽象灵活支持 Gang Scheduling、队列、优先级学习曲线陡调度器本身性能有上限Kubernetes Kueue中大规模多租户明显配额管理能力强和 K8s 原生机制结合好适合做资源“门禁”底层调度能力弱通常要和 Volcano 搭配Slurm超算/科研机构调度算法成熟擅长批处理支持复杂作业依赖对容器和云原生支持差GPU 资源管理比较粗糙自研调度内核超大规模接近万卡完全控制可以深度优化开发维护成本极高不适合小团队如果你团队已经重度使用 Kubernetes那么 Volcano 几乎是不二之选如果你在超算中心、科研机构Slurm 还是祖宗级方案但想把它和容器、GPU 设备插件深度融合也要做不少改造。而万卡规模的互联网公司大多走在第三条路上基于社区方案做深度定制或者干脆把调度器拆成多层入口层管配额、核心层管算法、设备层管插件。另外提醒一句别把“海豚调度器”这类工作流调度和资源调度混为一谈。海豚负责编排的是 DAG 任务流比如“先跑数据预处理再跑训练再跑评估”它不负责分配 GPU 卡真正发卡的是 Volcano、Kueue、Slurm 这类资源调度器。两者是不同层面的东西经常有人搞混问的问题驴唇不对马嘴。3.2 实操示例用 Volcano 调度一个分布式训练任务Volcano 是目前 Kubernetes 生态里比较成熟的批处理调度器对 AI 训练场景支持得比较好。它引入了几个核心概念Queue队列、PodGroupPod 组、Gang Scheduling组调度。这里给你一个最小可复用的参考。先部署 Volcano假设已经有 K8s 集群GPU 设备插件已装好kubectl apply -f https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development.yaml然后创建一个队列表示一个资源池apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: train-queue spec: weight: 1 capability: nvidia.com/gpu: 64这里的关键点有两个。第一capability限定了这个队列能用的 GPU 总量是 64 张避免一个组把集群全部吃光第二weight决定了多个队列抢资源时的权重配比权重越大相同资源紧张程度下越容易优先获得分配。接下来创建用于训练的任务。以常见 PyTorchJob需要先安装 Kubeflow 的 training operator为例关键字段是minAvailableapiVersion: kubeflow.org/v1 kind: PyTorchJob metadata: name: pytorch-gpu-job spec: pytorchReplicaSpecs: Master: replicas: 1 template: spec: schedulerName: volcano containers: - name: pytorch image: your-registry/train-image:latest resources: requests: nvidia.com/gpu: 8 limits: nvidia.com/gpu: 8 Worker: replicas: 8 template: spec: schedulerName: volcano containers: - name: pytorch image: your-registry/train-image:latest resources: requests: nvidia.com/gpu: 8 limits: nvidia.com/gpu: 8minAvailable是火山调度器做组调度时的核心参数。这个字段含义是只有当某个副本组能满足“最少这么多资源”时才分配否则整个 PodGroup 不调度。如果你有 16 个 Worker但集群只剩 4 个 Worker 的资源请直接让它排队不要硬启动半个集群。这里有一个经验不要只配置 requests 而不配置 limitsGPU 调度依赖两者一致否则可能出现“调度按 8 卡走了但 cgroup 隔离没跟上”的尴尬局面。另外镜像最好预热到节点本地避免调度完任务却卡在镜像拉取阶段白白占着资源等下载。3.3 用 Kueue 做多租户配额入口和 Volcano 怎么配合Volcano 解决的是“同一队列内怎么分资源”但多团队、多项目场景下你还需要一个上层机制来管理“谁可以用多少资源”。这就是 Kueue 的强项。Kueue 的工作方式可以理解成“门卫账本”它先检查一个 Job 符不符合配额策略符合了才放行进入调度同时它会精确记录每个 Job 占了多少配额、什么时候释放。它不关心底层分发算法有多复杂它只管“你能不能进门”。所以 Kueue 和 Volcano 并不是替代关系而是可以层层叠加用户提交 Job。Kueue 检查配额决定 Job 是否进入待调度队列。Volcano 在队列内执行具体的 Gang Scheduling、优先级排序、拓扑选择。K8s 默认调度器兜底处理普通非训练工作负载。一个配置 Kueue ClusterQueue 的简版示意apiVersion: kueue.x-k8s.io/v1beta1 kind: ClusterQueue metadata: name: team-a-cluster-queue spec: namespaceSelector: {} resourceGroups: - coveredResources: [nvidia.com/gpu] flavors: - name: a100-80g resources: - name: nvidia.com/gpu nominalQuota: 32这套方案的好处是你可以在一个集群里给不同团队划清界限团队 A 最多用 32 张 A100团队 B 最多用 16 张谁也别想偷偷超用。而且 Kueue 支持队列排队、抢占、停止和弹性配合 K8s 的 namespace 隔离可以在混乱的多租户环境中理出一条清晰的管理线。我在实际部署中比较推荐“Kueue 做入口 Volcano 做内核”的模式这比单独上一个大而全的自研调度器要稳妥得多。好用的调度器从来不是一次性写完的而是把“资源门禁”、“队列内策略”、“设备分配”分层解耦每一层都可以独立演进。3.4 显存粒度隔离别把一张 A100 当一台“大电脑”做调度除了管“卡数”还要管“显存粒度”。很多人上来就问我们能不能把一个 80G 的 A100 按显存切成很多份同一时刻多个用户共用可以但有代价。方案一是用 NVIDIA MIGMulti-Instance GPU把一张物理 GPU 切分成多个独立的 GPU 实例每个实例有自己的显存和算力硬件隔离级别高。但 MIG 的限制很多不是所有卡都支持切分粒度固定而且一旦切分一张卡能跑的最大模型也就受限了。方案二是靠软件层面的显存隔离比如设备插件 显存配额。但你得想清楚GPU 的算力是没法像显存一样细粒度隔离的。你把 80G 显存切成 8 份每个用户分 10G看起来皆大欢喜可真跑起来单个用户计算时会把 GPU 的 SM 全占满其他用户的运行时间被严重拖慢比显存不足还难受。经验之谈除非你有非常明确的细粒度推理场景否则训练任务老老实实按整卡分配就行不要为了“显得资源多”去硬拆卡。整卡调度不仅简单而且能避免大量相互干扰的争吵。4. 常见问题与排查技巧实录4.1 任务一直 Pending到底卡在哪一环这是我在群里被问得最多的问题。训练任务提交后一直排队、Pending看不到资源也不见报错。遇到这种情况不要瞎猜按顺序排查这几项现象可能原因排查手段一直 Pending看事件提示 “0/8 nodes available”资源不足kubectl describe pod看具体提示配合kubectl top nodes确认实际空闲量提示 “didnt match node selector”节点标签不匹配检查节点标签kubectl get nodes --show-labels确认 GPU 型号对吗提示 “pod group not ready”Gang Scheduling 的 minAvailable 没满足查 Volcano PodGroup 状态kubectl get podgroup事件里没有任何调度相关日志调度器没启用确认 Pod 里schedulerName写的是不是 volcano别让默认调度器截胡镜像拉不出来镜像仓库/网络问题看 Pod events 的拉取阶段耗时提前做镜像预热我给团队培训时总强调一句话先看 event再看 metrics最后看代码。80% 的调度问题在kubectl describe pod的 Events 字段里就能找到答案很多同学上来就翻日志效率反而低。4.2 GPU 利用率上不去是调度的问题还是训练的问题有时候任务确实跑起来了但 GPU 利用率很低监控面板上一片蓝色看起来像资源没充分利用。这时候问题可能出在两个层面。调度层面的问题是拓扑感知没做好。比如一个 8 卡任务被拆到 4 台机器上跨机 allreduce 成了瓶颈GPU 总是在等数据同步利用率自然上不去。排查方法是看训练日志里的通信耗时占比或者用nsys、ncu这类性能工具分析 kernel 执行和通信重叠的部分。训练层面的问题也很常见最典型的是数据加载太慢。GPU 计算一秒钟的事CPU 预处理数据要三秒钟那 GPU 就有三分之二时间是闲着的。很多新团队在单卡上跑没问题是因为内存里直接放数据到了多机多卡数据从网络文件系统拉取延迟一下子暴露出来调度器再聪明也救不了数据管线。这种时候先别急着怪调度器先看看显存占用、SM 占用率、数据加载时间这几个指标再决定优化方向。调度器负责“把任务放到合适的地方”训练效率负责“把放进去的任务跑满”两者缺一不可。4.3 被抢占的任务总是白跑checkpoint 到底该怎么配优先级高的任务来了低优先级任务必须让位这是调度器的正常行为。但暴露出一个经常被忽视的问题低优先级任务的 checkpoint 频率太低被抢之后恢复成本高得离谱。我见过最夸张的例子checkpoint 每 10 小时存一次任务在第 9 小时被抢占重新排队后又从头训练。表面上看调度策略很合理实际上集群的“有效产出”极低因为大量算力都花在了重复计算上。解决思路不是取消抢占而是给被抢占任务一个优雅退出机制。具体操作上可以给 Pod 加 preStop hook在收到 SIGTERM 信号后先完成一次 checkpoint 再退出同时把抢占时的“宽限期”调大一点比如默认 30 秒改成 300 秒让框架有充足时间保存模型状态。这个经验在很多团队里实测非常有效算是花小钱办大事的典型。4.4 用户侧环境问题从“教用户装 PyTorch”到“统一镜像”另一个常见现象是用户提交训练任务后环境起不来。要么 CUDA 版本不匹配要么少了某个 Python 依赖要么torch.cuda.is_available()返回 False。这些问题的根源往往不是调度器而是“每个人都在自建环境”这件事本身。在集群场景下正确的做法是平台方提供标准化镜像把 CUDA、cuDNN、PyTorch、常用库全部固化在镜像里。用户只需要基于基础镜像做小增量修改比如“我的代码需要 numpy 2.x”而不是从零开始折腾驱动和 CUDA。我之前在线上环境里发现一个节点的/usr/local/cuda里版本五花八门不同用户各装一套最后谁都跑不顺。后来改成统一镜像 环境模块化的思路这种问题直接消失。这背后的逻辑很简单调度器能保证你的任务有资源但保证不了你镜像里的 CUDA 和驱动的兼容性。资源调度与环境维护是两件事不要混为一谈。4.5 万卡集群真正怕的是“单点故障”规模到了一万张卡你最先要担心的已经不是“资源不够”而是“一个节点的故障会不会引发雪崩”。比如某台节点上的 GPU 温度过高触发了内核驱动重置kubelet 上报 NotReadyVolcano 或 Kueue 会自动把该节点上的任务驱逐到其他节点。听起来很智能但如果这个节点刚好承载了一个 64 卡分布式训练任务的 8 个 worker那一次驱逐就会让整个任务中断。要是同一时刻有两三个节点都出问题要重新调度的 pod 数量会瞬间暴涨把 API Server 和调度器压垮。应对这类问题一方面是提升调度器自身的高可用能力比如 etcd 备份、API Server 限流、调度器多副本另一方面是控制爆炸半径训练任务尽量做到“弹性容错”也就是条件允许时使用 elastic training某个 worker 挂了另外的 worker 可以等它重新加入而不是整个任务重启。说实话万卡集群的运维核心已经不是“调度算法多精妙”而是“在面对故障时系统能不能优雅降级”。这一点很多人是在真实线上事故里花了几百万电费才学会的。5. 一些压箱底的经验做了这么久 AI Infra如果要给看到这里的朋友一句忠告我会说别急着给调度器加花哨功能先把基础体验做到极致。什么叫基础体验第一排队状态可视化用户提交任务后能清晰看到自己在队列里的位置、预计等待时间这能减少 90% 的“我的任务为什么还没跑”噪音第二配额透明化每个团队能用的资源总量、已用量、剩余量都摆在明面上争议会大幅减少第三被抢占时要有明确通知别让用户第二天醒来才发现任务早就没了。这三个功能技术上都不难但价值远超一个复杂的抢占算法。调度器做得越花哨线上出问题的概率越大。还有一个小技巧给调度系统加一个“调度决策日志”面板。每次调度器做决策时把当时的输入资源申请、集群状态和输出决策结果、原因记录下来。问题排查时翻这个日志比看任何监控图表都管用它就像调度器的“黑匣子”能帮你快速定位是哪一步出了问题。从我个人的实践感受来说调度器不是一个“配完就可以忘掉”的系统。集群规模一变负载一复杂它的行为就会跟着变化。保持观察、保持记录、保持敬畏这大概是所有做算力调度的人最朴素的护身符。