想象一家主打满汉全席的餐厅——豪庭餐厅。灶台数量有限,每桌宴席却要同时使用 3 到 8 个灶口才能开火;餐厅原本只有一个服务员,他一次只能端一道菜。于是常常出现这种局面:一道菜占着灶台,另外几道菜却因为灶口不够迟迟无法下锅,整桌客人只能干等。这就是分布式深度学习训练的真实写照:GPU 是金贵的灶台,一个训练任务往往要拆成几十个 Pod(菜),只有所有 Pod 都领到 GPU,任务才能真正开始算。 Kubernetes 原生调度器就像那个端菜的服务员,它只认识单个 Pod,不具备“整桌上菜”的全局视野。当任务需要 8 个 GPU,而集群只剩 6 个时,它先调度 6 个,剩下 2 个 Pod 无限 Pending;那 6 个 Pod 虽然占着 GPU,却因为没凑齐而不能执行——这就是 GPU 集群中著名的“死锁”与资源碎片化。于是,两个专门解决这类问题的调度官登场了:Volcano 先生和 YuniKorn 小姐。

岗位关联

掌握 GPU 集群调度器的权衡,是云原生 AI 平台架构师做出高性能、高可用设计决策的关键能力之一。

目录
  1. 可视化演示
  2. 调度背景
  3. Volcano调度器
  4. YuniKorn调度器
  5. 对比分析
  6. 多租户优化
  7. 生产实战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Volcano gang调度全Pod原子启动、YuniKorn队列公平分配多租户GPU池、三种调度器利用率对比,步进切换探索。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Volcano Gang调度:全有或全无 PodGroup所有Worker原子调度 资源不足时全部等待避免死锁 资源满足后同时启动训练 gang = all-or-nothing scheduling 分布式训练Worker必须同时启动

调度背景

想象一家主打满汉全席的餐厅——豪庭餐厅。灶台不多,每桌宴席却要同时使用 3 到 8 个灶口才能开火;餐厅原本只有一个服务员,他一次只能端一道菜。于是常常出现这种局面:一道菜占用着灶台,另外几道菜却因为灶口不够迟迟无法下锅,整桌客人只能干等。这是分布式深度学习训练的真实写照:GPU 是金贵的灶台,一个训练任务往往要拆成几十个 Pod(菜),只有所有 Pod 都领到 GPU,任务才能真正开始算。

Kubernetes 原生调度器就像那个端菜的服务员,它只认识单个 Pod,不具备“整桌上菜”的全局视野。当任务需要 8 个 GPU,而集群只剩 6 个时,它先调度 6 个,剩下 2 个 Pod 无限 Pending;那 6 个 Pod 虽然占着 GPU,却因为没凑齐而不能执行——这就是著名的“死锁”与资源碎片化。深度学习任务的资源画像很特殊:多卡同步训练需要组内所有 Pod 同时启动;训练时长通常以小时计,一旦被“卡死”,GPU 空转成本极高。理想情况下,训练耗时 T 与总计算量 Q 和并行算力 N×S 的关系是:

回到餐厅:要解决半桌菜的尴尬,必须有一位懂宴席的调度总控。GPU 集群调度器的任务,就是把零散的 Pod 组合成“一桌菜”来统一安排,这正是我们接下来要认识的调度官。

T=QN×S\displaystyle T = \frac{Q}{N \times S}
  1. 总工作量 QQ 表示整个训练的数据量与模型计算量,相当于宴席所有菜品的工作量总和。
    Q\displaystyle Q
  2. 并行算力 N×SN 卡并行时,理论上每秒能处理的工作量为单卡速度 S 的 N 倍。
    N×S\displaystyle N \times S
  3. 理想耗时 TT 是理想完成时间,说明加卡能缩短周期,但现实中因同步等待不可能无限加速,这正是需要 Gang 调度的重要原因。
    T=QN×S\displaystyle T=\frac{Q}{N\times S}

Volcano调度器

Volcano 先生是豪庭餐厅的“包席经理”。他接手后,立刻引入了一套新规矩:顾客下单时,不再一盘一盘算,而是把整桌菜登记成一份“宴席单”(Job),每道菜安排一个编号(Task),这些菜要凑成一个“菜组”(PodGroup)。

Volcano 是 Kubernetes 生态中的开源批量调度器,运行时会监听 PodGroup、Queue、Job 等 CRD。它的核心本领是 Gang scheduling:一个 PodGroup 里的所有 Pod 必须全部满足资源条件,调度器才允许它们同时被绑定;有任何一道菜没着落,整桌菜都按兵不动。这与默认调度器的“逐个试错”形成鲜明对比。

Volcano 也提供队列(Queue)模型来承载资源配额。每个任务属于一个队列,队列可以配置 capacity 和 share,用于控制团队之间的资源边界;同时通过插件机制支持优先级抢占、拓扑感知等功能。这套设计让 AI 平台能够把训练任务作为整体管理,而不是只盯着单个容器。

举个例子,一个分布式训练作业有 4 个 worker 和 2 个 PS,如果集群只剩 3 张 GPU,Volcano 会让整个作业一致等待;一旦凑齐 6 张卡,6 个 Pod 像宴席上的菜一样同时上桌,杜绝了半桌菜占灶台的现象。豪庭餐厅也终于能招待那些讲究“同进同出”的大客户了。

YuniKorn调度器

与包席经理不同,YuniKorn 小姐更像餐厅的“公平叫号总台”。散客大厅里顾客身份复杂:有大客户、普通食客、临时访客,大家都想用灶台,但灶台有限。她的职责不是等一桌菜齐,而是让所有顾客都能按规则公平地排到位子。

Apache YuniKorn 起家于 YARN 调度器,专为 Kubernetes 多租户场景设计。它最著名的能力是层级队列管理——租户可以建根队列,下面再挂子队列,像餐厅的大厅、包厢、VIP 区层层划分。调度时采用 Weighted Max-Min Fairness(加权最大最小公平):先给每个队列计算应得份额。

在此基础上,YuniKorn 还支持优先级抢占和弹性配额:队列空闲时,可以把 GPU 临时借给其他队列;当原租户任务回来时,再通过抢占回收,形成“高水位借用、低水位保障”的机制。这让多个团队共享一个 GPU 池时,既能互相救急,也不会被某一个大户永久占领。

包席经理管团队上桌,公平叫号台管客人排队——YuniKorn 的答卷,正是给多租户餐厅一份“大家都满意”的调度策略。

Si=wi∑j=1nwj×Rtotal\displaystyle S_i = \frac{w_i}{\sum_{j=1}^{n}w_j} \times R_{total}
  1. 队列权重w_i 表示租户 i 的权重,权重越高,理论上能分到越多 GPU。
    wi\displaystyle w_i
  2. 总权重Σw_j 是所有队列权重之和,用来把单个权重归一化。
    ∑j=1nwj\displaystyle \sum_{j=1}^{n} w_j
  3. GPU 总量R_total 是集群可分配的 GPU 总数,即所有灶台数量。
    Rtotal\displaystyle R_{total}
  4. 公平份额S_i 是租户 i 应得的资源份额,相当于按权重占比切分总灶台数。
    Si=wi∑j=1nwj×Rtotal\displaystyle S_i = \frac{w_i}{\sum_{j=1}^{n}w_j} \times R_{total}

对比分析

包席经理和公平叫号员各怀绝技,但餐厅老板最关心的是:到底该用谁?先看队列模型:Volcano 的关注点是任务,队列只是资源容器,结构相对扁平;YuniKorn 的立身之本就是多层级队列,可以严格表达“部门—项目—子任务”的评级体系。

再看 Gang 调度:Volcano 原生支持 PodGroup,是分布式训练的事实标准;YuniKorn 最初面向 YARN 的 MapReduce 场景,虽然后续版本也通过插件支持 Gang,但社区心智更偏向队列公平与抢占。公平分配策略也有差别:Volcano 主要通过队列容量和任务优先级竞争,YuniKorn 则按权重做 Max-Min 公平,并设计了抢占与弹性回收。

因此现实项目中常有三种姿势:单团队重训练任务,用 Volcano 一马当先;跨部门多租户治理,选 YuniKorn 做总调度;大型 AI 平台则把两者组合——Volcano 负责 PodGroup 级别的 Gang 调度,YuniKorn 负责多队列间的公平分配,各管一段。

这就像餐厅并设“包席部”和“散客叫号台”:整桌宴席找包席经理,散客排队找叫号员。两者不打架,反而让整个餐厅的灶台运转得更顺畅。

多租户优化

多租户 GPU 资源池,就像大餐厅把灶台分给不同顾客群体:有些是长期包席,有些是散客,有些是临时访客。如果每个群体都独占部分灶台,高峰时有人排队、低峰时空转一片;如果没有边界,大客户一来就把灶台全占走。

YuniKorn 的弹性配额是化解矛盾的关键:给每个租户设一个“最低保障”,在此之上允许共享空闲 GPU。这样某团队跑大批量训练时,可以借用其他队列的空闲卡;当原租户提交任务后,调度器再通过抢占把卡还回去,既保证公平,又提高利用率。Volcano 则可以在任务层面用 binpacking(紧密打包)和节点亲和把 Pod 集中到同一批 GPU 节点,减少跨节点碎片;再配合队列 capacity 的限制,避免单个租户无限膨胀。

实践中,平台通常用两把尺子评估效果:GPU 平均利用率和任务排队等待时间。引入弹性共享后,原本某部门夜间闲置的 30% 资源可以被其他团队利用;同时通过 PodGroup 排队机制,高峰期的任务也不会把集群资源占满而饿死小任务。

豪庭餐厅的灶台因此从“包间私有”走向“闲时共享、忙时归还”,客人更多了,翻台率也上去了。

生产实战

餐厅运营数周后,老板决定复盘:调度官们如何上岗,出了问题又怎么排查?部署阶段,Volcano 通常以 Helm 或 DaemonSet 方式安装一到两个 scheduler 实例,配置默认调度器为 volcano;YuniKorn 则通过 admission controller 拦截 Pod 创建,并把队列配置写入 ConfigMap。二者都能以 CRD 方式暴露队列和任务状态。

故障排查时,最常见的信号是任务一直 Pending。首先看队列残余容量是否足够;其次,用 kubectl get podgroup 看 PodGroup 是否满员;如果部分 Pod 显示 Running 但主进程迟迟不启动,多半是 Gang 条件未满足或 PS/worker 角色互相等待。YuniKorn 场景下,还要查队列状态与抢占事件。

某互联网公司 AI 平台复盘:训练集群有 40 台 A100 节点,早期用 K8s 默认调度器,Ray 任务频繁因部分 Pod 阻塞而超时;切换到 Volcano 后分布式训练任务的排队时间下降约 40%,同一集群支持的任务并发数提升 50%。随后接入 YuniKorn 做部门级队列治理,GPU 平均利用率从 45% 提升到 70% 以上。

选型上,没有银弹:优先看业务形态是“整桌宴席”还是“散客排队”,再用两个调度器的组合拳完成最终方案。豪庭餐厅的复盘结论很直接:好的调度,是让每一口灶台都在最需要它的时刻烧起来。

AI 应用场景

深度学习训练平台调度优化

阶段:模型训练与资源调度

解决的问题:传统 K8s 调度器将每个 worker 和 PS 视为独立 Pod,常出现部分 Pod 已占用 GPU 但训练无法开始(死锁)。Volcano 的 Gang 调度将整个训练作业作为 PodGroup 统一调度,显著降低等待与资源空转。

具体案例:某自动驾驶公司训练平台每天提交 200 多个多卡训练任务,切换到 Volcano 后,单任务排队时间从 15 分钟降到 5 分钟,A100 集群平均利用率提高 30%。

多租户 GPU 资源池治理

阶段:资源管理与多租户隔离

解决的问题:多部门共享 GPU 集群时,缺乏队列模型会造成“大户霸占、小户饿死”。YuniKorn 的层级队列与加权公平分配为各部门设定权重和配额,空闲资源可弹性借用。

具体案例:某集团 AI 中台将算法、数据、搜索三个部门划入 3 个队列,权重 2:2:1;周末大数据任务少,算法队借走空闲 GPU 跑训练,周一归还,月度 GPU 利用率从 38% 提升到 66%。

GPU 调度故障排查

阶段:运维监控与故障诊断

解决的问题:任务一直 Pending 或 Running 不工作,排查目标模糊。使用 Volcano/YuniKorn 后可通过 PodGroup 和队列状态快速判断是 Gang 未齐还是配额不足。

具体案例:某团队报障:8 卡训练任务有 6 卡已 Running,2 卡 Pending。接入 Volcano 后,PodGroup 显示未满员,调度器不再绑定前 6 个 Pod,资源得到释放,问题从 2 小时人工排查缩短到 10 分钟定位。

Python 演示

python65 行
#!/usr/bin/env python# -*- coding: utf-8 -*-"""GPU scheduler demo for gang scheduling and fair queue allocation."""from collections import deque  class GangScheduler:    def __init__(self, gpu_total):        self.gpu_total = gpu_total        self.pending = deque()        self.running = []     def submit(self, group_id, pod_count):        self.pending.append({"id": group_id, "pods": pod_count})     def tick(self):        allocated = 0        keep = deque()        for job in self.pending:            need = job["pods"]            if allocated + need <= self.gpu_total:                allocated += need                self.running.append(job)            else:                keep.append(job)        self.pending = keep     def show(self):        print("running:", [job["id"] for job in self.running])        print("pending:", [job["id"] for job in self.pending])  class FairShareScheduler:    def __init__(self, queues, total_gpus):        self.queues = queues        self.total_gpus = total_gpus     def allocate(self):        total_weight = sum(queue["weight"] for queue in self.queues)        result = {}        for queue in self.queues:            share = self.total_gpus * queue["weight"] / total_weight            result[queue["name"]] = min(share, queue["demand"])        return result  if __name__ == "__main__":    print("== Gang scheduling ==")    gang_scheduler = GangScheduler(8)    gang_scheduler.submit("job-a", 4)    gang_scheduler.submit("job-b", 3)    gang_scheduler.submit("job-c", 2)    gang_scheduler.tick()    gang_scheduler.show()     print("== Weighted fair queue ==")    queues = [        {"name": "cv", "weight": 2, "demand": 4},        {"name": "nlp", "weight": 1, "demand": 3},        {"name": "rec", "weight": 1, "demand": 2},    ]    fair_scheduler = FairShareScheduler(queues, 8)    result = fair_scheduler.allocate()    for name, share in result.items():        print(f"{name}: {share:.2f} GPU")
目的:用两个简单类分别模拟 Volcano 的“整组资源足够才调度”和 YuniKorn 的“按权重公平分配”,帮助理解两种调度器的核心逻辑。
  1. if allocated + need <= self.gpu_total:只有整个任务组的资源需求在当前轮次都被满足时才分配,体现 Gang 调度的精髓。
  2. self.pending = keep没有被分配的任务不丢弃,继续留在队列中等待下一轮调度。
  3. total_weight = sum(queue["weight"] for queue in self.queues)计算所有队列的权重总和,用来把权重归一化。
  4. share = self.total_gpus * queue["weight"] / total_weight按权重占比切分 GPU 总量,计算每个队列的理论份额。
  5. result[queue["name"]] = min(share, queue["demand"])实际分配不能超过队列需求,避免分配用不掉的资源。
  6. gang_scheduler.tick()触发一轮 Gang 调度,观察任务是否整组启动或继续等待。
  7. fair_scheduler.allocate()触发一轮加权公平分配,观察不同权重队列获得的 GPU 数量。

常见误区

误区Volcano 和 YuniKorn 都是 Kubernetes 调度器,功能重复,装多了会冲突,所以只能选一个。

正解它们其实是互补关系。Volcano 的重点在任务级 PodGroup 的 Gang 调度,YuniKorn 的重点在多租户队列的公平调度。生产中常组合部署:Volcano 负责训练任务整组上桌,YuniKorn 负责各租户之间排队分权,两个调度器可以共存协作。

为何常见:两者都出现在 K8s 调度领域,容易下意识认为是在竞争同一个生态位,忽略了它们解决的是不同层面的问题。

误区Gang 调度就是给 Pod 加 nodeAffinity 或 podAffinity,让它们跑到同一节点上,保证“在一起”。

正解亲和性只决定“位置希望”,Gang 调度解决“资源同步”。Gang 要求一组 Pod 的所有资源请求同时被满足,才会执行绑定;否则全部不调度。就算 Pod 都落在同一节点,只要资源不够,默认调度器仍会部分调度,造成死锁。

为何常见:看起来都和“把 Pod 放到一起”有关,初学者容易把两种机制混为一谈。

误区公平调度就是把 GPU 平均分给每个队列,比如 3 个队列各拿三分之一,谁也不多吃多占。

正解YuniKorn 用的是加权最大最小公平:先按权重算出理论份额,再在某个队列不用足时把剩余资源分给更需要的队列。“公平”指的是权重契约下的公平,不是简单的算术平均。

为何常见:“公平”的日常语义就是平均,忽略了调度器中的权重、需求与弹性机制。

误区给队列设置的 GPU 配额越大越好,这样任务不容易排队,用户体验最好。

正解配额过大但任务不饱和,会造成 GPU 空转;配额过小又会饿死其他重要任务。YuniKorn 的弹性配额和 Volcano 的 capacity 一起用,设定“保障值 + 可借出”,才能平衡隔离与利用率。

为何常见:直觉上资源越多越好,没有考虑多租户共享时的副作用,比如低峰期无人使用、高峰期相互争抢。

面试题

Q1. Volcano 的 Gang 调度主要用于解决什么问题?

单选题 · easy

  • A.提高单卡运算速度
  • B.保证整组 Pod 同时调度← 正确答案
  • C.降低容器日志开销
  • D.增加节点 GPU 数量

答案:B。Gang 调度要求同一 PodGroup 的所有 Pod 全部满足资源后才绑定,避免部分 Pod 先占用 GPU 导致任务卡死。单卡速度、日志开销和节点数量都不是调度器关心的。

Q2. YuniKorn 的多租户公平分配策略主要基于哪种思想?

单选题 · easy

  • A.先来先服务策略
  • B.加权最大最小公平← 正确答案
  • C.完全随机调度策略
  • D.固定队列轮流策略

答案:B。YuniKorn 使用 Weighted Max-Min Fairness,先按权重计算每个队列应得份额,再在队列需求不足时重新分配剩余资源,避免小任务被大任务饿死。

Q3. 以下哪一项最能描述 Kubernetes 默认调度器对多卡训练任务的常见问题?

单选题 · mid

  • A.单 Pod 请求过多 CPU
  • B.多卡任务部分调度后等待← 正确答案
  • C.所有节点均拥有 GPU
  • D.Pod 数量少于节点数

答案:B。默认调度器逐个调度 Pod,不会把同一训练任务当作整体看待。当资源不足以满足全部 Pod 时,一部分 Pod 先被调度占住资源,剩余 Pod 等待,形成死锁与碎片化。

Q4. Volcano 中使用哪个资源对象表示一组需要同时调度的 Pod?

单选题 · mid

  • A.Deployment
  • B.PodGroup← 正确答案
  • C.Service
  • D.Namespace

答案:B。PodGroup 是 Volcano 的 CRD,用来把一组 Pod 聚合为一个调度单元。Deployment、Service、Namespace 都不具备“同进同出”的调度语义。

Q5. YuniKorn 弹性配额的主要价值是什么?

单选题 · mid

  • A.固定资源上限防止借出
  • B.空闲资源借出提升利用率← 正确答案
  • C.减少集群队列数量
  • D.关闭所有任务优先级

答案:B。弹性配额允许队列在空闲时把 GPU 临时借给其他队列,提高整体利用率;当原租户提交任务后再抢占回收,兼顾隔离与共享。

Q6. 当集群剩余 GPU 不足以满足 PodGroup 全部 Pod 时,Volcano 会怎么做?

单选题 · hard

  • A.先调度可满足的一半
  • B.整个 PodGroup 全部等待← 正确答案
  • C.随机选择一个 Pod 调度
  • D.自动降级为单卡任务

答案:B。Gang 调度的核心是“全有或全无”。只要有一个 Pod 无法安放,整个 PodGroup 都不会被绑定,避免“部分资源被占但任务无法执行”的浪费。

Q7. 关于 Volcano 与 YuniKorn 的协同使用,正确的是?

单选题 · hard

  • A.二者只能二选一部署
  • B.Volcano 管公平,YuniKorn 管 Gang
  • C.Volcano 管 Gang,YuniKorn 管公平← 正确答案
  • D.两者调度结果完全相同

答案:C。Volcano 擅长 PodGroup 的 Gang 调度,YuniKorn 擅长多租户队列的公平分配。大型平台常将两者组合,各管一段,而不是二选一。

Q8. 以下哪些属于 YuniKorn 的核心特性?

多选题 · mid

  • A.支持层级队列管理← 正确答案
  • B.实现加权公平分配← 正确答案
  • C.支持优先级抢占← 正确答案
  • D.提供虚拟机快照

答案:ABC。层级队列、加权公平分配、优先级抢占都是 YuniKorn 的典型能力。虚拟机快照属于虚拟化平台功能,与调度器无关。

Q9. 关于 Gang 调度的副作用,以下描述合理的有?

多选题 · hard

  • A.可能造成低优先级饿死← 正确答案
  • B.组内等待增加时延← 正确答案
  • C.提高整组启动成功率← 正确答案
  • D.一定降低集群利用率

答案:ABC。Gang 调度让低优先级任务可能被高优任务抢占或排挤,组内等待也会增加调度时延,但其价值是提高整组启动成功率。“一定降低利用率”过于绝对,资源充足时反而能减少空转。

Q10. 在多租户 GPU 资源池中,兼顾隔离与共享的正确做法有哪些?

多选题 · extreme

  • A.队列强制独占不可借用
  • B.设置保障配额并允许弹性借用← 正确答案
  • C.通过抢占回收被占用资源← 正确答案
  • D.取消权重实现完全平均

答案:BC。先为每个队列设置最低保障配额,再允许空闲资源弹性借出,并通过抢占机制回收,是兼顾隔离与共享的成熟做法。强制独占会降低利用率,完全平均则会忽略权重与真实需求。