万卡GPU集群调度器:核心机制、选型与实战排坑
2026/9/11 6:20:21 网站建设 项目流程

先别急着聊算法和框架,咱们先把一个最现实的问题摆到桌面上:一万张 GPU 同时开工,到底是谁说了算?你说“项目经理说了算”?项目是虚构出来的概念,真正说了算的,是那个在训练场里默默排班、分卡、催进度、甚至轰人下机的系统——调度器。我干 AI Infra 这几年,见过太多团队把精力花在模型结构、数据 pipeline、加速卡选型上,结果集群规模一上千卡,所有人被同一个问题按在地上反复摩擦:任务排队了,但为什么排不上?卡明明空的,但作业就是起不来?优先级更高的任务来了,该不该把正在跑的训练掐掉?这一整套博弈,本质上就是调度器在做决策。它不写一行训练代码,但它决定了你的模型能不能按时训完、算力成本到底划不划算。这篇就是训练与调度主题的下半部分,专门聊这个“隐形老板”是怎么排班的。

重点会拆开几件事:万卡场景下调度器到底在解决什么难题,里面的核心机制是怎么设计的,主流的开源方案应该怎么选、怎么配,以及我在实际运维中踩过的那些坑。不管你是刚把训练从单卡挪到多卡的小团队,还是已经在维护千卡集群的 Infra 同学,这篇都值得看完。

1. 从单卡到万卡:调度器到底在解决什么问题

1.1 单卡像自建房,万卡像城市交通

先想想单卡训练是什么状态。你装好 PyTorch,配好 CUDA,torch.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-type=a100gpu-memory=80gnvidia.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 卡任务,调度器应该优先考虑把它放进同一个节点,其次是同一个机架里的不同节点,最差才是跨机架分布。这里的逻辑类似生活中的搬家:你肯定希望家具都在一个仓库里,而不是分五个仓库放着,搬起来累死人。

实操中,这种感知不一定要做得很精细,但至少要区分三个层级:

  1. 同节点、同 NVSwitch 域内:通信最快,优先分配。
  2. 同机架、走 RoCE 交换机:通信可接受,适合数据并行。
  3. 跨机架甚至跨机房:万不得已才用,一般用于超大模型训练或弹性任务。

调度器在打分时会给不同拓扑层级分配权重,能落在同一节点就尽量落在同一节点。这个优化不写进需求文档,但直接决定你的训练速度。

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(队列)、PodGroup(Pod 组)、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)为例,关键字段是minAvailable

apiVersion: 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: 8

minAvailable是火山调度器做组调度时的核心参数。这个字段含义是:只有当某个副本组能满足“最少这么多资源”时才分配,否则整个 PodGroup 不调度。如果你有 16 个 Worker,但集群只剩 4 个 Worker 的资源,请直接让它排队,不要硬启动半个集群。

这里有一个经验:不要只配置 requests 而不配置 limits,GPU 调度依赖两者一致,否则可能出现“调度按 8 卡走了,但 cgroup 隔离没跟上”的尴尬局面。另外,镜像最好预热到节点本地,避免调度完任务却卡在镜像拉取阶段,白白占着资源等下载。

3.3 用 Kueue 做多租户配额入口:和 Volcano 怎么配合

Volcano 解决的是“同一队列内怎么分资源”,但多团队、多项目场景下,你还需要一个上层机制来管理“谁可以用多少资源”。这就是 Kueue 的强项。

Kueue 的工作方式可以理解成“门卫+账本”:它先检查一个 Job 符不符合配额策略,符合了才放行进入调度;同时它会精确记录每个 Job 占了多少配额、什么时候释放。它不关心底层分发算法有多复杂,它只管“你能不能进门”。所以 Kueue 和 Volcano 并不是替代关系,而是可以层层叠加:

  1. 用户提交 Job。
  2. Kueue 检查配额,决定 Job 是否进入待调度队列。
  3. Volcano 在队列内执行具体的 Gang Scheduling、优先级排序、拓扑选择。
  4. 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 MIG(Multi-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确认实际空闲量
提示 “didn't 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 总是在等数据同步,利用率自然上不去。排查方法是看训练日志里的通信耗时占比,或者用nsysncu这类性能工具分析 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 上报 NotReady,Volcano 或 Kueue 会自动把该节点上的任务驱逐到其他节点。听起来很智能,但如果这个节点刚好承载了一个 64 卡分布式训练任务的 8 个 worker,那一次驱逐就会让整个任务中断。要是同一时刻有两三个节点都出问题,要重新调度的 pod 数量会瞬间暴涨,把 API Server 和调度器压垮。

应对这类问题,一方面是提升调度器自身的高可用能力,比如 etcd 备份、API Server 限流、调度器多副本;另一方面是控制爆炸半径,训练任务尽量做到“弹性容错”,也就是条件允许时使用 elastic training,某个 worker 挂了另外的 worker 可以等它重新加入,而不是整个任务重启。

说实话,万卡集群的运维核心已经不是“调度算法多精妙”,而是“在面对故障时,系统能不能优雅降级”。这一点,很多人是在真实线上事故里花了几百万电费才学会的。

5. 一些压箱底的经验

做了这么久 AI Infra,如果要给看到这里的朋友一句忠告,我会说:别急着给调度器加花哨功能,先把基础体验做到极致。

什么叫基础体验?第一,排队状态可视化,用户提交任务后能清晰看到自己在队列里的位置、预计等待时间,这能减少 90% 的“我的任务为什么还没跑”噪音;第二,配额透明化,每个团队能用的资源总量、已用量、剩余量都摆在明面上,争议会大幅减少;第三,被抢占时要有明确通知,别让用户第二天醒来才发现任务早就没了。

这三个功能技术上都不难,但价值远超一个复杂的抢占算法。调度器做得越花哨,线上出问题的概率越大。

还有一个小技巧:给调度系统加一个“调度决策日志”面板。每次调度器做决策时,把当时的输入(资源申请、集群状态)和输出(决策结果、原因)记录下来。问题排查时,翻这个日志比看任何监控图表都管用,它就像调度器的“黑匣子”,能帮你快速定位是哪一步出了问题。

从我个人的实践感受来说,调度器不是一个“配完就可以忘掉”的系统。集群规模一变,负载一复杂,它的行为就会跟着变化。保持观察、保持记录、保持敬畏,这大概是所有做算力调度的人最朴素的护身符。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询