第 6 课:容量、配额、公平与 Backfill

参考答案版:包含完整代码实现与问答题深度参考解析。建议先独立完成练习题后再展开核对。

本课目标与完成标准

本课产出:实现不延误保留作业的 backfill 选择,并解释利用率、公平性与碎片的冲突。

通过要求:唯一代码填空题通过给定检查;Q1~Q3 都给出因果链;能指出一个正确性边界和一个性能/运营取舍;总分至少 8/10。

与既有六条主线的边界

上一课解决单个 gang 放置;本课处理多租户队列、配额、优先级、reservation 与空洞利用。

前置:Linux/网络基础、runtime 补充线、train 分布式章节。本课只补 AI Infra 缺口,不重复已经完成的 kernel 或并行算法推导。

核心心智模型

1. 它是什么,解决什么问题

在多团队共享的大规模企业级 AI 算力中心(通常汇聚数百至数千张高端 GPU,单卡硬件与能耗成本极高),平台运营与调度系统面临着三大核心诉求的激烈博弈:

  1. 多租户保底配额与商业公平性(Guaranteed Quota & Tenancy Isolation):
    团队 A 采购了 400 张 GPU,团队 B 采购了 400 张 GPU。平台必须确保任何团队随时都能兑现其购买的“保证配额(Guaranteed Quota)”,杜绝因某一团队恶意突发提交上千任务而将其他团队完全挤占;同时,在某些团队配额闲置时,应允许其他团队弹性突发超售(Elastic Bursting / Oversubscription)借用,极大化集群资产的投资回报率(ROI,如图 6-1 所示)。

  2. 多维异构资源的瓶颈均衡(Multi-Resource Fair Allocation):
    大模型端到端工作流不仅需要 GPU,还重度消耗 CPU 核心(负责数据解压、Tokenization 与分词)、Host 内存(用于显存溢出与 Page Cache)、以及 RDMA 网络带宽。传统单一维度的配额无法解决“CPU 被打满而 GPU 闲置”的资源倾斜,需要引入主导资源公平调度算法(Dominant Resource Fairness, DRF)。

  3. 高优先级大作业等待与算力空洞(The Reservation vs. Utilization Dilemma):
    当一个千亿大模型训练任务(如申请 64 卡或 512 卡)到达队首时,当前集群仅有部分卡空闲(例如 8 张卡空闲,其余 56 张卡需等待 30 分钟后正在运行的前序任务释放)。若为了等待该大任务而将这 8 张卡完全闲置 30 分钟,集群利用率将发生断崖式下跌;若直接把这 8 张卡分配给一个运行 10 小时的普通长任务,大任务的启动时刻将被严重延误!

回填调度(Backfill Scheduling)通过计算确定性的“保留时间窗口(Reservation Window)”,利用时间与空间维度上的缝隙,精准调度短小作业填充算力空洞(如图 6-2 所示),在零推迟大作业启动的前提下最大化集群利用率。

2. 它如何工作

(1) 多租户配额模型与主导资源公平(DRF,如图 6-1 所示)

flowchart TD
    subgraph QuotaPool["集群总资源配额池 (1000 GPUs)"]
        TeamA["部门 A 保证配额 (Guaranteed: 400 GPUs)"]
        TeamB["部门 B 保证配额 (Guaranteed: 400 GPUs)"]
        Shared["共享空闲与突发池 (Oversubscription: 200 GPUs)"]
    end
    subgraph DRF["主导资源公平调度 (Dominant Resource Fairness)"]
        Dominant["根据 CPU、GPU、内存中占比最高的主导份额实现均衡分配"]
    end
    QuotaPool --> DRF

图 6-1:多租户配额保障、弹性突发超售与 DRF 资源公平模型

  • 分层配额池设计(Hierarchical Quota Pools):
    平台将物理卡池划分为“保证配额(Guaranteed)”与“共享突发池(Shared Elastic Pool)”。租户在保证限额内提交的任务享有最高优先级;超出保证限额的任务只能作为“可抢占突发任务(Preemptible Burst Jobs)”在共享池中运行。一旦保证配额的合法持有者提交新作业,突发任务将被优雅回收退让。

  • 主导资源公平算法(Dominant Resource Fairness, DRF):
    设集群总资源向量为 𝑹=⟨RGPU,RCPU,RMem⟩\mathbf{R} = \langle R_{\text{GPU}}, R_{\text{CPU}}, R_{\text{Mem}} \rangle。
    对于租户 ii,其当前已占用的资源向量为 𝑼i=⟨Ui,GPU,Ui,CPU,Ui,Mem⟩\mathbf{U}_i = \langle U_{i,\text{GPU}}, U_{i,\text{CPU}}, U_{i,\text{Mem}} \rangle。
    租户 ii 的主导资源份额(Dominant Share)定义为其占全集群比例最高的那项资源的份额: si=max⁡(Ui,GPURGPU,Ui,CPURCPU,Ui,MemRMem)s_i = \max \left( \frac{U_{i,\text{GPU}}}{R_{\text{GPU}}}, \frac{U_{i,\text{CPU}}}{R_{\text{CPU}}}, \frac{U_{i,\text{Mem}}}{R_{\text{Mem}}} \right) 调度器在每一轮调度中,始终优先为当前主导份额 sis_i 最小的租户分发资源,从而在多维异构环境下严格实现帕累托最优与无嫉妒性(Envy-Freeness)。

(2) 回填调度算法与保留时间窗口填充(Backfill Scheduling,如图 6-2 所示)

flowchart LR
    LargeJob["高优先级大作业等待 64 卡 (预计 30 分钟后到位)"] --> LockTime["调度器计算预留时间窗口 (Reservation Window)"]
    LockTime --> ScanSmall["扫描等待队列中的碎片小作业 (如 4 卡、运行 15 分钟)"]
    ScanSmall --> BackfillCheck{"小作业是否能在预留时间前结束?"}
    BackfillCheck -->|是| RunNow["立即回填调度 (Backfill) 启动小作业,极大化集群利用率"]
    BackfillCheck -->|否| WaitSmall["小作业保持等待,确保绝不推迟大作业启动时刻"]

图 6-2:回填调度 (Backfill Scheduling) 与保留时间窗口填充工作流

如图 6-2 所示,Backfill 调度的核心执行流水线分为三步:

  1. 建立大作业未来预留点(Reservation Window):
    队首高优先级大任务因资源不足无法立即启动。调度器依据当前所有运行中作业声明的截止时间(Walltime),向前模拟时间线,推算出该大任务最早能够集齐全部卡数的未来时间点 TresT_{\text{res}}。调度器将该大作业锁定在该时间点,并计算出当前的空闲卡数 NfreeN_{\text{free}} 与可用时间空洞 Δtgap=Tres−Tnow\Delta t_{\text{gap}} = T_{\text{res}} - T_{\text{now}};

  2. 回填候选任务可行性过滤(Feasibility Filter):
    调度器跳过队首,遍历排队队列中的低优先级或短小任务。候选作业 jj 允许被“回填(Backfill)插队”的充要条件是: Requested GPUs(j)≤Nfree∧Estimated Walltime(j)≤Δtgap\text{Requested GPUs}(j) \le N_{\text{free}} \quad \land \quad \text{Estimated Walltime}(j) \le \Delta t_{\text{gap}} 这一约束保证了该作业必然在 TresT_{\text{res}} 之前运行完毕并释放资源,绝对不推迟队首大作业的预定启动时刻。

  3. 价值最大化排序目标(GPU-Time Maximization):
    在所有满足可行性约束的候选作业中,调度器优先挑选能够最大化填充物理算力面积的作业,即最大化 GPUs×Minutes\text{GPUs} \times \text{Minutes}。平局时通过作业名称字典序实现确定性裁决。

3. 正确性条件与常见误区

  • 误区一:按“申请 GPU 数量最少”作为回填贪心策略
    许多人直觉认为“卡数越小的作业越容易填洞,因此应该最先调度卡数最少的小作业”。然而,极小作业(如 1 卡运行 2 分钟)产生的 GPU-Time 极低(仅 2 GPU·min),频繁调度它们会留下大量无法拼凑出稍大规格的微观碎片,反而降低了整体算力吸收率。回填必须以最大化“GPU-Time 吞吐总面积(GPUs×Duration\text{GPUs} \times \text{Duration})”为优化目标。

  • 误区二:无条件信任用户填写的申请运行时间(Walltime)
    Backfill 的正确性前提是“回填作业绝不推迟大作业的启动时间”。然而在工程实践中,用户填写的时长往往极不准确(例如申报运行 10 分钟,实际由于收敛慢跑了 2 小时)。如果调度器未设置硬性强制超时杀任务(Hard Walltime Enforcement),超时的回填任务将直接霸占资源,导致大作业的预留点被严重延误!系统必须建立基于历史运行数据的运行时预测校准,并在超时时自动触发 Checkpoint 保存或强制终止。

  • 误区三:高估高优先级抢占(Preemption)的零成本性
    平台不能因为支持硬抢占,就肆无忌惮地随意终止低优先级任务。杀掉一个运行了 6 小时的分布式任务,若其未能及时落盘 Checkpoint,意味着白白浪费了数十卡时昂贵算力与电力,甚至造成数据集读写管道损坏。抢占必须将 Checkpoint 转储成本、网络抖动与级联重试开销全面纳入综合收益模型。

4. 性能与工程取舍

  • 保守回填(Conservative Backfill)vs. 激进回填(EASY Backfill):
    • 保守回填:为队列中每一个排队作业都计算并锁定一个严格的未来预留点,极度公平且高度可预测,但会产生过多保守的保留空洞,导致集群平均利用率偏低;
    • EASY 回填:仅为队首的第 1 个大作业维护单一预留点,后续所有小作业只要不推迟这唯一的队首作业即可任意插队抢跑。EASY 回填极大提升了集群吞吐与利用率,是目前高性能超算平台(如 Slurm、Volcano)的主流工程实现。
  • 配额超售(Oversubscription)与排队体验的权衡:
    过高的弹性突发超售比(如 200% 超售)能让集群时刻处于打满状态,但一旦多个部门在月末集中提交突发实验,会引发大面积的抢占排队风暴,导致任务反复失败重跑。工程上通常将弹性超售比例控制在总池的 15%~30% 之间。

具体演示

设当前集群空闲 free_gpus = 8,距离队首大作业的保留时刻还有 gap_minutes = 30 分钟:

  • 候选作业列表为:a(4 卡,20 分钟)、b(8 卡,40 分钟)、c(2 卡,25 分钟)。

分析过程如下:

  1. 可行性筛选:
    • 作业 a:4≤84 \le 8 且 20≤3020 \le 30,可行,贡献算力面积为 4×20=80 GPU·min4 \times 20 = 80\text{ GPU·min};
    • 作业 b:运行时间 40>3040 > 30,若启动将推迟大作业,属于不可行候选,被剔除;
    • 作业 c:2≤82 \le 8 且 25≤3025 \le 30,可行,贡献算力面积为 2×25=50 GPU·min2 \times 25 = 50\text{ GPU·min}。
  2. 最优选择: 作业 a 的算力面积(80)显著高于作业 c(50),算法确定性选择作业 a 立即执行回填启动,高效利用空洞。

请先独立复述“输入 → 状态变化 → 输出/指标”,再开始填空。

实践任务:唯一代码填空题

补齐确定性 backfill:选择可完成候选中 GPU-time 最大者。

只能修改 TODO/______ 位置,不得删除断言或放宽通过条件。

Code
def choose_backfill(candidates, free_gpus, gap_minutes):
    """candidate=(name, gpus, minutes)"""
    feasible = [job for job in candidates
                if job[1] <= free_gpus and job[2] <= gap_minutes]
    if not feasible:
        return None
    # TODO:最大化 gpus*minutes;平局按名称最小。
    return ______

jobs = [("a", 4, 20), ("b", 8, 40), ("c", 2, 25)]
assert choose_backfill(jobs, 8, 30) == ("a", 4, 20)
assert choose_backfill(jobs, 1, 30) is None

检查方法

运行本单元格,所有断言必须通过;再补一个边界输入并解释预期。

Q1

为什么按 GPU 数最少优先不一定提高整体利用率?

你的答案:

Q2

用户低估作业时长,backfill 会造成什么?如何防护?

你的答案:

Q3

高优先级抢占低优先级作业前应计算哪些成本?

你的答案:

评分规则

  • 代码 4 分:正常输入 2 分、边界输入 1 分、解释 1 分;
  • Q1~Q3 各 2 分;
  • 一票否决:混淆测量与推测、忽略租户/请求隔离、用平均值掩盖尾延迟、删除失败路径。

参考答案(仅 answer 分支)

先独立完成。核对后请改变一个规模或故障条件重新推演。

Code
def choose_backfill(candidates, free_gpus, gap_minutes):
    """candidate=(name, gpus, minutes)"""
    feasible = [job for job in candidates
                if job[1] <= free_gpus and job[2] <= gap_minutes]
    if not feasible:
        return None
    return min(feasible, key=lambda job: (-job[1] * job[2], job[0]))

jobs = [("a", 4, 20), ("b", 8, 40), ("c", 2, 25)]
assert choose_backfill(jobs, 8, 30) == ("a", 4, 20)
assert choose_backfill(jobs, 1, 30) is None

Q1 参考答案

在回填与集群容量调度中,盲目采取“按申请 GPU 数量最少优先(Smallest-GPU-First)”的贪心策略往往无法提高甚至会恶化整体有效利用率,核心原因如下:

  1. 算力填充面积(GPU-Time Product)极低:集群利用率的本质是单位时间内完成的有效算力面积总和(GPUs×Duration\text{GPUs} \times \text{Duration})。若优先调度申请卡数极少(如 1 卡)的作业,即便其耗时很短,其贡献的算力吸收面积也非常微小(例如 1 卡运行 5 分钟仅吸收 5 GPU·min 算力);而一个 8 卡运行 25 分钟的作业能吸收 200 GPU·min 算力。过度偏好微小作业会导致算力空洞大面积闲置;

  2. 制造严重的微观碎片(Micro-Fragmentation):极小任务零散散布在各个物理节点的插槽中,将原本规整的物理节点切割破碎,直接破坏了后续原本能够整体回填的较大规格作业(如 4 卡、8 卡短作业)所需的连续拓扑空间;

  3. 队列饥饿与公平性失衡:过度偏好微小作业会导致海量微型作业无限期插队抢跑,造成中等规格作业在队列中长期被压制饿死,违背了多租户平台的 SLA 契约。

Q2 参考答案

若用户或算法工程师在提交作业时低估了作业的运行时间(Underestimated Walltime),会对调度系统造成致命的链式破坏,必须在系统架构层面建立严密防护:

  1. 直接后果:撕毁时间窗口承诺,推迟大作业启动(Reservation Delay):
    Backfill 回填机制的核心契约是“绝对不推迟队首大作业的启动时间点”。若回填作业申报运行 20 分钟,调度器将其填入 30 分钟的空洞中;但该作业实际运行了 60 分钟。当到达 30 分钟的大作业保留时刻时,物理卡仍被回填作业霸占,导致高优先级的核心训练任务无法按时获取全量资源,直接摧毁了调度的确定性并造成严重的生产进度延误;

  2. 系统防御与防护机制:

    • 硬性超时截断机制(Hard Walltime Enforcement):调度系统在作业运行时间达到其申报的 Walltime 时,立即向容器发送优雅终止信号(SIGTERM)并在超时宽限期后强制杀死(SIGKILL),坚决捍卫大作业预留点;
    • 安全时间余量(Safety Headroom Buffer):调度器在计算回填时间窗口时,主动预留 15%~20% 的安全缓冲(例如 30 分钟的空洞只允许申报时长不超过 24 分钟的作业回填),吸收由于数据加载波动引起的微小超时;
    • 用户历史信誉画像与机器学习预测:基于监控数据库记录用户历史任务的申报时长与实际执行时长,建立信誉权重;对长期低报时长的租户自动调高其预估时长,或剥夺其参与 Backfill 插队的资格;
    • 可抢占式检查点机制(Preemptible Checkpointing):要求参与回填的作业必须支持秒级断点续训,当大作业保留时间到达时,若回填作业尚未结束,调度系统可强制通知其快速落盘并挂起释放资源。

Q3 参考答案

当高优先级作业抢占低优先级作业以获取资源时,绝非简单的“零成本释放 GPU”,必须在控制面全面核算以下系统级沉没损失与连锁开销:

  1. 已消耗算力的完全沉没损失(Sunk Compute Cost):被抢占的低优先级作业如果已经连续计算了数小时且未触发 Checkpoint,强行杀死该进程意味着此前消耗的数十甚至上百卡时的电力、硬件折旧与时间完全报废作废;

  2. 优雅停机与 Checkpoint 转储开销(Draining & IO Overhead):若允许被抢占作业在退出前执行紧急 Checkpoint 转储,多个并发终止的 Pod 会瞬间向底层存储网络发起巨大的写 IO 洪峰,挤占高优先级任务启动所需的数据加载带宽;且等待转储完成的超时宽限期(Termination Grace Period)直接构成了高优先级任务的拉起时延;

  3. 级联调度震荡与 API Server 压力(Cluster Churn):大规模抢占会导致被逐出的任务在队列中引发级联重调度,产生大量的 Pod 删除与重建事件,极大加重 Kubernetes API Server、etcd 以及节点 Kubelet 的并发负载;

  4. 综合 ROI 收益决策模型:调度器在执行抢占决策前,必须对比“高优先级任务提前启动带来的业务收益”与“被抢占任务沉没成本 + 重启恢复成本”之间的差值。只有在净收益显著为正时才允许触发抢占,坚决避免为了提前几分钟启动高优先级任务而摧毁一个已完成 99% 的重大长任务。

参考资料

API 与平台能力会演进;部署前应按目标版本重新核对。