清晨的景区大巴调度中心,十几辆大巴整齐停在发车区。广播响起:“8 号旅行团,请到 3 号门集合!”导游举起名单,在车前清点人数。他完全可以先让先到的几个人上车坐着,可他偏不——规矩是:一个旅行团必须全员到齐、同时上车,缺一个人,全团都在车下等着,车也不发。听起来有点死板?但在分布式世界里,这正是救命的原则。那个旅行团,就是一个分布式训练任务;每一位团员,就是集群里的一个并行进程;大巴座位,就是宝贵的 GPU 资源。如果让先来的进程先跑起来,它们会发现队友迟迟不出现,只能在集合通信中干等,直到超时崩溃。于是调度器(导游)选择了 All-or-Nothing:要么全员上车,整车发车;要么无人上车,全员等待。这篇文章,就带你听懂调度大厅里的全部门道。

岗位关联

理解 Gang Scheduling,是每一位 AI 平台架构师和分布式训练工程师的必修课——它决定了你的千卡集群是高效满载,还是每天都在空转与重启中烧钱。

目录
  1. 可视化演示
  2. All-or-Nothing:整组资源的原子分配
  3. 排队与抢占:等待区里的博弈
  4. 资源碎片:凑不齐的座位
  5. 分布式训练为何必须整组发车
  6. 主流调度器的 Gang 支持对比
  7. 生产案例复盘:千卡训练的上车记
  8. 旋钮调优:等待与利用率的平衡
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示Gang Scheduling要求一组任务必须全部同时获得资源才能启动,展示排队、资源匹配与all-or-nothing调度语义,避免资源碎片。

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

Gang调度 任务组 资源槽 已启动 全部任务同时获得资源才启动

All-or-Nothing:整组资源的原子分配

景区的清晨,调度中心广播响起:“8 号旅行团请到 3 号门集合。”导游拿着名单站在车前清点人数——他可以先让先到的几个人上车坐着吗?不行。大巴的规矩是:一个旅行团必须全员到齐、同时上车,缺任何一个人,全团都在车下等着。这就是 Gang Scheduling 最核心的一条规则:All-or-Nothing,要么全上,要么全等。

在分布式系统里,“旅行团”是一个并行任务组(gang),每位团员是一个并行进程。它们要跑的是需要集体协作的计算,比如分布式训练。调度器(导游)手里攥着集群资源(大巴座位),只有整组任务所需的资源能一次性、同时分配到位时,才允许这一组启动。

这个语义也叫资源原子性(resource atomicity):调度器要么同时把 n 个任务全部放到资源上,要么一个都不放,绝不出现“先启动 7 个、缺 1 个”的中间状态。为什么这么决绝?因为训练程序启动时会建立全局通信,部分启动会让已启动的进程阻塞在初始化等待上,直到网络超时崩溃,反而浪费更多资源。因此 gang 任务在调度器眼里是“不可分割”的。

正因为导游坚持“名单上画满勾才发车”,才避免了车开出去却发现人没到齐的尴尬。在集群里,这份坚持换来的是分布式程序启动后不会因为“缺人通信”而整段崩掉。

S(G)={1,∀i∈G:Ai=1(全员已取得资源)0,否则整组继续等待\displaystyle S(G) = \begin{cases} 1, & \forall i \in G : A_i = 1 \text{(全员已取得资源)}\\ 0, & \text{否则整组继续等待} \end{cases}
通俗解释:S(G) 就是导游的“是否发车”按钮——名单上每个人都拿到车票,按钮按下发车;只要有一人没票,按钮就锁死,全团原地等待。
  1. 发车决策 S(G)调度器对旅行团 G 做出的“是否启动”决策,1 表示发车,0 表示继续等。
    S(G)\displaystyle S(G)
  2. 全员点名 ∀i∈G把旅行团成员逐个检查,一个都不能漏。
    ∀i∈G\displaystyle \forall i \in G
  3. 车票到手 A_i=1第 i 个任务获得所需资源,标志位置 1。
    Ai=1\displaystyle A_i = 1
  4. 分段决策所有成员都为 1 时输出 1;只要有人是 0,整体输出 0。
    {1全员就绪0有人缺失\displaystyle \begin{cases} 1 & \text{全员就绪}\\ 0 & \text{有人缺失}\end{cases}

排队与抢占:等待区里的博弈

游客服务中心前有一排长椅,那是“等待区”。旅行团凑不齐座位时,导游就带大家在此排队:先来的团排前面,后来的团自动往后排,调度中心保证先来后到。可偶尔会来一个 VIP 考察团(高优先级任务),调度中心就会协调:把散客请下车腾出座位,先让 VIP 团发车。

排队对应调度器的队列(queue),先进先出(FIFO)是最朴素的公平策略。但纯公平会让紧急的大团等太久,于是引入优先级(priority)。当高优组需要资源时,系统可以抢占(preemption)低优任务占用的资源——挂起或驱逐低优任务腾出位置。与此同时,回填(backfill)机制发挥着精妙作用:在队首大团还没凑齐资源时,让排后面的小任务(散客)使用零散空位,前提是不耽误队首大团预定的发车时间。

具体来说,回填允许散客任务 j 插入,需要满足两个条件:它的资源需求不超过当前空闲资源;它的预计运行时间不超过队首旅行团的预计剩余等待时间。这样既不推迟大团发车,又能利用本来会浪费的空位,是调度大厅里“螺蛳壳里做道场”的智慧。

等待区、VIP 插队、散客补位——调度中心用这套组合拳,在“公平”与“效率”之间找到了平衡点。

Start(j)  ⟺  (rj≤Rfree)∧(dj≤Thead)\displaystyle \text{Start}(j) \iff \left(r_j \le R_{\text{free}}\right) \wedge \left(d_j \le T_{\text{head}}\right)
通俗解释:散客能不能“插队上车”?一要车里确实有他坐的空位,二要他的行程比前面旅行团继续等待的时间还短——绝不耽误正主。
  1. 回填许可 Start(j)散客任务 j 获得回填上车的许可。
    Start(j)\displaystyle \text{Start}(j)
  2. 有空位 r_j≤R_free散客需要的座位数不超过剩余空位数。
    rj≤Rfree\displaystyle r_j \le R_{\text{free}}
  3. 不误正主 d_j≤T_head散客预计占用时长不超过队首团的剩余等待时间。
    dj≤Thead\displaystyle d_j \le T_{\text{head}}
  4. 逻辑与 ∧两个条件缺一不可,同时成立才放行。
    (rj≤Rfree)∧(dj≤Thead)\displaystyle \left(r_j \le R_{\text{free}}\right) \wedge \left(d_j \le T_{\text{head}}\right)

资源碎片:凑不齐的座位

中午,调度中心接到一个 8 人旅行团。调度员一看屏幕:4 辆大巴各剩 2 个空位。8 个空位总数够,但每辆车都坐不下 8 人——这就是“碎片”。座位总数不少,却凑不出一整车的空位。

把整组任务塞进一台或几台机器时,资源碎片(fragmentation)成为头号敌人。单任务按需占用部分 GPU,导致每台节点都剩“零头”,零头无法满足 gang 的整体需求。于是集群总利用率看着不低,大任务却永远等不到资源——这正是队首阻塞的根源。

常见应对手段有四种:其一,bin-packing 打包,调度器优先把散客任务集中压到少量节点上,把整节点“留白”给大团;其二,按节点对齐放置,让 gang 的一次分配尽量落在一块完整区域;其三,弹性 gang,允许任务组在等待中缩减成员规模(如 16 卡降到 8 卡)来适配碎片;其四,错峰整理,在低峰期集中迁移任务、合并空闲资源。

调度员们逐渐学会了“聚拢散客、留白整车”的整理术,让零碎的座位也能重新拼成大团的完整车厢。

ηfrag=1−Cfreemax⁡Rfree\displaystyle \eta_{\text{frag}} = 1 - \frac{C_{\text{free}}^{\max}}{R_{\text{free}}}
通俗解释:碎片率等于 1 减去“最大连续空块占空位总数的比例”。如果空位连成一大片,碎片率趋近 0;如果全是东零西碎的小块,碎片率趋近 1。
  1. 总空位 R_free集群当前空闲座位总数。
    Rfree\displaystyle R_{\text{free}}
  2. 最大连续块 C_max同一辆车上最长的连续空位段长度。
    Cfreemax⁡\displaystyle C_{\text{free}}^{\max}
  3. 可利用率比值最大连续块占空闲总量的比例,衡量资源“能被大团直接整块利用”的程度。
    Cfreemax⁡Rfree\displaystyle \frac{C_{\text{free}}^{\max}}{R_{\text{free}}}
  4. 求补得碎片率1 减去上述比例,越接近 1 说明资源被切得越碎。
    1−Cfreemax⁡Rfree\displaystyle 1 - \frac{C_{\text{free}}^{\max}}{R_{\text{free}}}

分布式训练为何必须整组发车

为什么旅行团非得整团坐同一辆车?因为车上配了对讲机(通信网络),导游要实时跟每个团员确认位置。如果团里有人坐上另一辆车,对讲机就联系不上;有人没上车,导游喊话也没人应答。分布式训练正是如此:所有并行 worker 就像戴着同一频道对讲机的团员。

在 MPI 或 PyTorch DDP 中,程序一启动就要执行集合通信(collective communication):MPI_Init 要求所有进程同时入场,DDP 的 init_process_group 需要所有 rank 在超时时间内建立连接,而训练中每一轮迭代结束都要做 all-reduce 梯度同步。任何一个 worker 缺席或迟到,其余 worker 全部阻塞等待,直到通信超时抛出异常,整组任务宣告失败并重启。

这就是“掉队者效应”(straggler effect):一轮迭代的耗时被最慢的 worker 决定。Gang Scheduling 通过“整组同时启动”消除了最常见的掉队来源——“有人还没被调度上”;而一旦运行起来,每轮的时间构成可以用公式衡量。

正因为“一圈人都要对讲机呼应”,分布式训练才如此依赖 Gang Scheduling 的整组发车——少一个人,整趟车都是哑的。

Titer=max⁡i∈[1,N]ticompute+tallreduce\displaystyle T_{\text{iter}} = \max_{i \in [1,N]} t_i^{\text{compute}} + t_{\text{allreduce}}
通俗解释:团队协作的速度由最慢的人决定——一轮训练的总时间等于所有 worker 中计算最久的那一个,再加上全员喊一遍口号(梯度同步)的通信时间。
  1. 队友耗时 t_i第 i 个 worker 完成一轮前向加反向计算的耗时。
    ticompute\displaystyle t_i^{\text{compute}}
  2. 取最慢 max对全部 worker 的计算耗时取最大值,即拖后腿的那个人。
    max⁡i∈[1,N]\displaystyle \max_{i \in [1,N]}
  3. 对讲机时间 t_allreduce全员同步梯度(all-reduce)的通信耗时。
    tallreduce\displaystyle t_{\text{allreduce}}
  4. 串行相加计算与通信按轮串行,相加得到一轮迭代的墙钟时间。
    max⁡i∈[1,N]ticompute+tallreduce\displaystyle \max_{i \in [1,N]} t_i^{\text{compute}} + t_{\text{allreduce}}

主流调度器的 Gang 支持对比

不同景区用不同的管理软件:老牌景区用班车时刻表系统(Slurm),互联网园区用智能预约系统(Kubernetes),专门接待大团的景区用定制版“包车系统”(Volcano、Ray)。它们都能管大巴,但对“整团发车”的支持力度差别很大。

Slurm 是超算标配,通过 salloc 或 sbatch 的 --ntasks 一次性分配一组计算节点,天然支持整组并发启动;Kubernetes 默认调度器按 Pod 逐个调度,不支持“整组原子分配”,需要借助 Volcano 的 PodGroup 或 Ray 的 Placement Group 这类协同调度组件。Volcano 引入 PodGroup 自定义资源,声明 minMember 表示“组内最少成员数”,只有就绪成员数达标才允许整个 PodGroup 启动。

选型时看三点:是否原生支持 gang(Slurm 是,K8s 默认否);是否支持优先级抢占(Volcano 支持 preemption);是否支持回填(backfill)。云原生 AI 平台大多选 Volcano 来补足 K8s 的整组调度能力,这也是当前生产环境最主流的技术栈组合。

选对调度中心的管理系统,旅行团的发车体验天差地别——大团多的场景,务必选择带“包车协议”的调度器。

start  ⟺  nready≥minMember\displaystyle \text{start} \iff n_{\text{ready}} \ge \text{minMember}
通俗解释:Volcano 的包车规则——到停车场报到的成员数达到 minMember 就触发发车,否则整团继续等,绝不先走一部分。
  1. 已报到人数 n_readyPodGroup 中资源已就绪的成员数量。
    nready\displaystyle n_{\text{ready}}
  2. 最少成员数 minMemberVolcano 配置中声明的组成员数量下限。
    minMember\displaystyle \text{minMember}
  3. 达标判断 ≥就绪人数达到下限即允许启动。
    nready≥minMember\displaystyle n_{\text{ready}} \ge \text{minMember}
  4. 充要条件 ⇔满足则启动、不满足绝不启动,二者互为充要条件。
    start  ⟺  nready≥minMember\displaystyle \text{start} \iff n_{\text{ready}} \ge \text{minMember}

生产案例复盘:千卡训练的上车记

某年“五一”黄金周,调度中心接到一个 512 人的超大型旅行团(千卡大模型预训练)。如果按老办法“来多少座发多少车”,这 512 人可能在停车场耗上一整天。调度中心提前做了三件事:分区规划、错峰发车、实时监控。

大模型预训练通常需要成百上千张 GPU 同时就位。实践中,平台会把集群划分成多个资源分区(partition/queue),每个分区配置独立的配额与优先级;超大 gang 任务放在专门的大分区,避免与小任务互相踩踏。同时启用回填,让零散小任务填充等待期的空隙,提高 GPU 利用率。

某 AIGC 公司 512 卡 A100 集群的复盘数据显示:使用 Volcano PodGroup 声明 minMember=512 后,任务不再“部分启动→通信超时→反复重启”,启动成功率从 68% 升到 97%;配合回填与小任务错峰,集群利用率从 61% 提升到 78%。在故障排查中,“任务进入运行态却迟迟无法完成”这类问题的第一排查项,就是确认任务是否真正同时获得了全部资源。

复盘报告贴在调度中心墙上:一次成功的团建(训练),靠的是提前规划、整组发车和实时盯盘。

Util=∑i=1NticomputeN×Twall×100%\displaystyle \text{Util} = \frac{\sum_{i=1}^{N} t_i^{\text{compute}}}{N \times T_{\text{wall}}} \times 100\%
通俗解释:利用率等于所有卡真正干活的时间除以所有卡从上班到下班的总时间。等待、空转、重启都不算“干活”。
  1. 有效算力时长求和把每张 GPU 上真正执行计算的时间累加起来。
    ∑i=1Nticompute\displaystyle \sum_{i=1}^{N} t_i^{\text{compute}}
  2. 理论上限 N×T_wallN 张卡乘以墙钟时间,即所有卡同时跑满的上限。
    N×Twall\displaystyle N \times T_{\text{wall}}
  3. 取比值有效计算时间占理论上限的比例,反映资源真实产出。
    ∑i=1NticomputeN×Twall\displaystyle \frac{\sum_{i=1}^{N} t_i^{\text{compute}}}{N \times T_{\text{wall}}}
  4. 换算百分比乘以 100% 得到直观的利用率读数。
    ×100%\displaystyle \times 100\%

旋钮调优:等待与利用率的平衡

调度中心想把运营再升级,就要调一堆旋钮:等待区设多大?最多等多久?几点发车最划算?配置调得好,旅行团和散客双赢;调得差,要么团等太久退票,要么车空着烧油。

核心可调参数有三类。其一是超时参数:例如 PyTorch DDP 的初始化超时(默认约 30 分钟)、NCCL 的通信超时,还有 gang 等待资源的时间上限;其二是队列配额:每个团队或租户可同时占用的 GPU 上限与优先级权重;其三是策略旋钮:是否开启回填、是否允许抢占、预留资源比例。监控指标上重点盯任务等待时间、资源碎片率、GPU 利用率和任务失败率。

调优背后有个“利用率-等待”跷跷板:参数设得越激进,利用率越高,但排队时间越长。可以用排队论估算集群负载强度 ρ(算力需求密度):当 ρ 接近 1 时,队列会无限拉长,必须加配额限制或扩容。

调度中心的旋钮虽多,但方向只有一个:让每一张 GPU(每一辆车)都载满真正在跑的计算(真正出行的人),而不是在等待中空转。

ρ=λjob×kˉ×tˉrunNGPU\displaystyle \rho = \frac{\lambda_{\text{job}} \times \bar{k} \times \bar{t}_{\text{run}}}{N_{\text{GPU}}}
通俗解释:ρ 是“停车场车流强度”——平均每小时来多少个团、每团要几辆车、每团停多久,三者相乘再除以总车位数。ρ 接近 1 说明车场快满了,马上要开始排队。
  1. 到达速率 λ_job单位时间内提交到集群的任务组数量。
    λjob\displaystyle \lambda_{\text{job}}
  2. 平均团规模 k̄每个任务组平均需要占用的 GPU 卡数。
    kˉ\displaystyle \bar{k}
  3. 平均占道时长 t̄_run任务组从启动到结束的平均运行时间。
    tˉrun\displaystyle \bar{t}_{\text{run}}
  4. 总车位数 N_GPU集群 GPU 总数;比值越接近 1 说明集群越饱和。
    NGPU\displaystyle N_{\text{GPU}}

AI 应用场景

千卡大模型预训练的“包场发车”

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

解决的问题:直接提交 512 个独立 Pod 时,默认调度器逐个分配,部分 Pod 先启动后因初始化阻塞等待掉队者,通信超时导致整轮反复重启。Gang Scheduling 通过 PodGroup 一次性预留整组资源,从根本上消除“部分就绪”状态。

具体案例:某 AIGC 公司的 7B 大模型预训练任务,512 张 A100 分 4 个分区部署,Volcano 声明 minMember=512 并开启回填,启动成功率从 68% 提升到 97%,集群利用率从 61% 提升到 78%。

气象 MPI 模拟作业的准点发车

阶段:高性能计算 / 批处理调度

解决的问题:MPI 程序要求 4096 个进程同时完成 MPI_Init,部分进程先跑会导致集合通信阻塞、大面积超时失败,机时被白白浪费。

具体案例:某超算中心用 Slurm 调度气象模拟任务,通过 sbatch --ntasks=4096 保证整组同时分配,并开启 backfill 让短任务插入等待空隙,机时利用率提升约 20%。

多租户 AI 平台的高并发保障

阶段:平台调度 / 多租户隔离

解决的问题:多家团队共享集群时,低优任务占据大量零散资源,高优训练任务长期饥饿。Gang Scheduling 配合队列配额与优先级抢占,保证关键任务不被碎片资源饿死。

具体案例:某云平台为每个租户配置队列配额与优先级权重,启用协作调度后,高优任务 P95 排队时间从 40 分钟降至 8 分钟,租户投诉率明显下降。

Python 演示

python98 行
import randomfrom dataclasses import dataclass NODES = 4          # 景区大巴数量(节点数)SLOTS = 4          # 每辆车的座位数(每节点 GPU 数)GANG_SIZE = 3      # 旅行团人数(组成员数)ARRIVALS = 12      # 提交任务数量  @dataclassclass Job:    jid: int    kind: str          # gang=旅行团, single=散客    size: int    remain: int        # 还需运行多少步    node: int = -1     # 被分配到的车辆    start: int = -1    # 启动时刻  class ClusterSim:    def __init__(self):        self.t = 0        self.slots = [[None] * SLOTS for _ in range(NODES)]        self.queue = []          # 等待发车的旅行团        self.finished = 0        # 已完成的乘客数     def free_seats(self, node):        return [i for i, s in enumerate(self.slots[node]) if s is None]     def try_place(self, job):        # All-or-Nothing:旅行团必须在同一辆车凑齐 GANG_SIZE 个空位        if job.kind == 'gang':            for n in range(NODES):                free_idx = self.free_seats(n)                if len(free_idx) >= job.size:                    for i in free_idx[:job.size]:                        self.slots[n][i] = job                    job.node = n                    job.start = self.t                    return True            return False        # 散客回填:任意一个空位即可        for n in range(NODES):            free_idx = self.free_seats(n)            if free_idx:                i = free_idx[0]                self.slots[n][i] = job                job.node = n                job.start = self.t                return True        return False     def step(self):        self.t += 1        for n in range(NODES):            for i, s in enumerate(self.slots[n]):                if s is not None:                    s.remain -= 1                    if s.remain <= 0:                        self.slots[n][i] = None                        self.finished += 1        # 等待队列中的旅行团,每步都尝试整组发车        waiters = []        for job in self.queue:            if self.try_place(job):                print(f"[t={self.t}] 旅行团 {job.jid} 全员上车,发车运行 {job.remain + 1} 步")            else:                waiters.append(job)        self.queue = waiters     def submit(self, job):        if self.try_place(job):            print(f"[t={self.t}] {job.kind} 任务 {job.jid} 立即获得资源")        elif job.kind == 'gang':            self.queue.append(job)            print(f"[t={self.t}] 旅行团 {job.jid} 资源不足,整组排队等待")        else:            print(f"[t={self.t}] 散客 {job.jid} 无空位,本次放弃")  def main():    random.seed(42)    sim = ClusterSim()    for jid in range(1, ARRIVALS + 1):        if random.random() < 0.4:            job = Job(jid, 'gang', GANG_SIZE, random.randint(3, 6))        else:            job = Job(jid, 'single', 1, random.randint(2, 5))        sim.submit(job)        for _ in range(2):            sim.step()    while sim.queue or any(any(row) for row in sim.slots):        sim.step()    print(f"[t={sim.t}] 模拟结束:完成 {sim.finished} 个任务")  if __name__ == '__main__':    main()
目的:用一个迷你集群模拟 Gang Scheduling——旅行团(整组任务)必须凑齐同节点全部空位才发车,散客(单卡任务)可随时回填空位,直观展示 All-or-Nothing 语义与回填机制。
  1. random.seed(42)固定随机种子,让每次运行结果可复现,方便对照理解。
  2. if random.random() < 0.4:用 40% 概率模拟提交旅行团,其余是散客单卡任务。
  3. if len(free_idx) >= job.size:只有同一辆车(节点)的空位数达到团人数,才允许整组上车,一个座位都不将就。
  4. for i in free_idx[:job.size]:一次占满所需座位,体现 All-or-Nothing 的资源原子性。
  5. self.slots[n][i] = job把任务登记到座位上,标记为运行中。
  6. s.remain -= 1已上车的任务每步消耗 1 单位运行时间。
  7. self.slots[n][i] = None任务运行完毕,释放座位给后续任务使用。
  8. waiters.append(job)凑不齐座位的旅行团进入等待区排队,绝不“先上车一部分”。
  9. while sim.queue or any(any(row) for row in sim.slots):持续推进调度,直到所有等待团发车、所有座位清空。

常见误区

误区我以为 Gang Scheduling 就是让任务在队列里排着,排到队首自然就有资源了,等待本身不会造成什么损失。

正解排队时资源并不是“原地等你”,零散资源会被其他任务占用,形成碎片。如果队首大团一直凑不齐整组资源,队列可能被堵死,后面的任务全被顶住(队首阻塞)。所以必须搭配回填、抢占和弹性调整,否则集群明明有空位,大任务却永远发不了车。

为何常见:从日常排队经验出发,默认“排队等于公平、总会轮到我”,没意识到分布式资源的碎片化会让队首可能永远凑不齐。

误区我分别提交 n 个任务,让它们各自抢占资源,只要都抢到了,效果和 Gang Scheduling 一样。

正解分步调度无法保证“同时”:先启动的任务会立即执行集合通信初始化,阻塞等待后到者;当等待超过通信超时(如 30 分钟),先启动的进程直接报错,整组重启,浪费的算力更多。Gang 调度的价值正是资源分配的原子性——要么同时给,要么都不给。

为何常见:只关注资源总量,忽略了时间上的同时性,不了解分布式程序初始化阶段的同步依赖。

误区把整个集群的训练任务都放在一个 gang 里统一调度,管理最省心,效率最高。

正解gang 越大,凑齐所有资源所需等待的时间越长,碎片越难补,队首阻塞风险越高。实践中通常按“一次迭代必须全员同步”的粒度划分 gang:一个 DDP 训练作业就是一个 gang,而不是把多个作业合并。组规模应与通信同步域一致,而不是越大越好。

为何常见:把“统一管理”误解成“统一成一个组”,忽略了组规模带来的等待时间和碎片成本。

误区抢占就是直接把低优任务杀掉腾地方,集群整体效率一定提升,没有任何副作用。

正解被抢占任务需要保存 checkpoint、释放显存、重新排队,这些开销都要计入总账;频繁抢占还可能引发活锁(两个任务互相驱逐)和连续重启风暴。生产环境通常设置抢占次数上限和不可抢占保护期,把抢占当作最后手段而非第一手段。

为何常见:只看到资源立刻空出来的即时收益,忽略了被驱逐任务的重启成本与稳定性风险。

面试题

Q1. Gang Scheduling 最核心的调度语义是?

单选题 · easy

  • A.所有任务同时获得资源← 正确答案
  • B.任务按优先级逐个启动
  • C.任务失败后单独重试
  • D.资源空闲时立即分配

答案:A。Gang Scheduling 的核心是 All-or-Nothing,即整组任务必须同时获得资源才能启动,类似于旅行团全员到齐才发车。B 是优先级调度,C 是容错重试,D 是即时调度,都不是“整组原子分配”的语义。

Q2. 旅行团比喻中,“全员到齐才发车”对应调度的哪一环节?

单选题 · easy

  • A.任务排队等待阶段
  • B.资源原子分配阶段← 正确答案
  • C.任务运行监控阶段
  • D.结果聚合返回阶段

答案:B。导游在车门前清点人数,等所有团员到齐后同时安排上车,对应的是资源分配阶段的一次性、原子化操作。排队是上车前的等待,监控是发车之后的事,聚合返回则完全不在调度范围内。

Q3. 允许散客任务回填空闲资源的条件是?

单选题 · mid

  • A.散客必须拥有更高优先级
  • B.散客任务必须整组运行
  • C.运行时间不超过队首等待← 正确答案
  • D.仅限集群完全空闲时刻

答案:C。回填的核心约束是“不耽误正主”:散客的预计运行时间不能超过队首旅行团的剩余等待时间,否则会推迟大团发车。优先级不是回填的必要条件,散客本身就是单任务,也不要求整组运行,更不必等集群完全空闲。

Q4. 下列哪种训练架构对 Gang 调度依赖最强?

单选题 · mid

  • A.异步参数服务器训练
  • B.单机单卡模型推理
  • C.离线日志批处理任务
  • D.同步数据并行训练← 正确答案

答案:D。PyTorch DDP 和 MPI 这类同步数据并行架构,每一轮迭代都要 all-reduce 全员同步,任何 worker 缺席都会导致整轮阻塞直至超时,因此最依赖整组同时启动。异步训练和单机任务都能容忍部分成员掉队或根本不需要协同。

Q5. 以下属于 Gang Scheduling 典型代价的是哪些?

多选题 · mid

  • A.部分就绪也需整组等待← 正确答案
  • B.资源碎片导致利用率下降← 正确答案
  • C.组内任务通信带宽提升
  • D.队列前方任务饥饿加剧← 正确答案

答案:ABD。整组等待意味着只要有一个成员没就绪,全部成员都不能启动;等待期间零散资源被占,形成碎片;队首大团一直凑不齐还会导致队首阻塞,让后面的任务饥饿加剧。C 是通信收益,不是代价。

Q6. Volcano 调度器中实现 Gang 调度的核心资源对象是?

单选题 · mid

  • A.Deployment 工作负载
  • B.PodGroup 资源组← 正确答案
  • C.Service 服务暴露
  • D.ConfigMap 配置项

答案:B。Volcano 通过 PodGroup 声明组内最少成员数(minMember),只有就绪成员数达标才启动整个组,这是标准 K8s 调度器不具备的能力。Deployment、Service、ConfigMap 都是普通 K8s 资源,不提供协同调度语义。

Q7. MPI 任务为何必须整组同时启动?

单选题 · easy

  • A.计算任务无需彼此通信
  • B.数据可以事后异步同步
  • C.通信双方需同时就绪← 正确答案
  • D.节点故障不影响全局

答案:C。MPI 程序启动时要执行 MPI_Init,集合通信要求所有进程同时就绪才能建立通信域;部分进程先启动只会阻塞等待队友,直到超时崩溃。就像旅行团的对讲机,喊话必须双方都在频道上才能呼应。

Q8. 生产中缓解资源碎片的手段有哪些?

多选题 · hard

  • A.按节点打包同构任务组← 正确答案
  • B.允许小任务回填空闲资源← 正确答案
  • C.禁用所有低优先级任务
  • D.动态调整任务组规模← 正确答案

答案:ABD。把散客任务集中打包到少数节点、把整节点留给大团是经典的 bin-packing 策略;回填让零散空位被小任务利用而不浪费;弹性调整组规模(如 16 卡降为 8 卡)能适配碎片化的剩余资源。禁用低优任务只会让资源更闲置,不是缓解手段。

Q9. 关于抢占机制,以下描述正确的有哪些?

多选题 · hard

  • A.抢占总能提升集群利用率
  • B.被抢占任务无需重新排队
  • C.高优任务可驱逐低优任务← 正确答案
  • D.抢占会引入重启与回滚开销← 正确答案

答案:CD。抢占允许高优任务驱逐低优任务占用的资源,是保障关键任务的手段;但被驱逐任务需要保存 checkpoint、释放显存并重新排队,重启与回滚开销不可忽视。抢占并不总能提升利用率,频繁抢占甚至可能引发活锁和重启风暴。

Q10. 集群 4 节点各 4 卡,旅行团需同节点 4 卡,当前每节点恰好 3 卡空闲,调度器应如何做?

单选题 · extreme

  • A.等待某节点凑齐 4 卡再发车← 正确答案
  • B.拆散旅行团分到四个节点
  • C.先启动并允许成员稍后加入
  • D.拒绝该任务并移出等待队列

答案:A。每节点只有 3 卡空闲,没有任何节点满足同节点 4 卡的需求,所以调度器应让旅行团继续等待,配合回填让散客使用空位,直到某节点凑齐 4 卡。拆散到四个节点违背集合通信的同节点/同网络要求,先启动再补人会引发初始化超时,直接拒绝则过于粗暴且不公平。