想象一家大型综合医院:手术室是全院最宝贵的资源,每一间都排满了择期手术——关节置换、胆囊切除、白内障……突然,120 救护车拉来一位急性心肌梗死病人,心电监护仪在尖叫。主刀医生必须做出选择:暂停一台正在进行的常规手术,把手术室让给抢救。但被暂停的手术台上还开着腹腔,绝不能丢下不管——麻醉师记录当前用药剂量,主刀护士清点器械数量,巡回护士在病历上写下“切至第 3 层组织、止血完善”。等抢救结束,医生回到这台手术,照着记录无缝续做。 GPU 集群的抢占式调度,演的就是这出医院大戏。GPU 是手术室,训练任务是排队等手术的病人;有紧急微调、事故驱动的模型回滚(急诊),也有日常实验、数据批处理(择期)。调度器要回答五道题:什么时候该抢?抢谁最划算?被抢任务怎么记住进度?普通任务会不会永远轮不上?以及——在 Slurm 和 K8s 集群里到底怎么配?这篇文章用贯穿始终的“医院急诊”比喻,把这五道题逐个讲透。

岗位关联

抢占式调度是共享 GPU 集群公认的“硬骨头”。掌握触发条件、检查点、SLA 保护与两大集群的配置实践,你就能在千卡集群里既保住紧急任务的时效,又不让常规任务饿死——这是 AI 平台工程师升职加薪的必修课。

目录
  1. 可视化演示
  2. 抢占触发条件
  3. 抢占策略设计
  4. 检查点机制
  5. 保护与SLA
  6. Slurm配置实践
  7. K8s配置实践
  8. 平衡与调优
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

交互展示抢占触发条件、三种抢占策略对比和检查点恢复机制,理解多租户环境下的资源调度与SLA保障

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

抢占触发 高优先级任务提交 调度器检测资源不足 低优先级任务被抢占释放资源

抢占触发条件

急诊分诊台上,护士手握电话:120 报告三分钟后送来一位心梗病人。分诊护士要回答一个关键问题——现在就要清空手术室,还是先等等?答案取决于两条硬指标:新病人是否比正在手术的病人危急得多?手术室门口的常规病人是否已经等了太久、再拖要出医疗事故?

GPU 集群的调度器也在做同样的分诊。它以事件驱动的方式监听任务状态变化,抢占触发条件归结为三类:高优任务插队、排队超时提升和人工优先级调整。高优任务插队是最常见的触发点——当一个新的高优任务提交,且当前所有 GPU 都被占用时,调度器会比较新任务与运行中任务的优先级。注意,不是“新任务优先级更高就抢”,而是必须高出一个阈值 Δ。这个阈值是防抖设计:集群里任务进进出出,优先级波动是常态,设太小会导致调度器频繁打断任务,检查点开销甚至超过抢占收益。

排队超时提升是防止任务“饿死”的第二道保险。一个低优任务如果在队列里等待超过 SLA 承诺的上限(比如 2 小时),系统会动态上调它的优先级,直到它能抢占一个 GPU。这相当于分诊台承诺:“普通病人等待不超过 4 小时,超时自动升级为加急。”人工调整则是管理员用命令把某个任务标记为紧急,对应救护车直接打电话指定手术室。

分诊台最后给出结论:抢不抢,不是看心情,而是看两条量化指标的“或”运算。急诊通道的大门,始终由规则把关,而不是调度员拍脑袋——这正是抢占式调度在工业集群里能被信任的前提。

trigger=(prionew>priorunning+Δ)  ∨  (Twait>TSLA)\displaystyle \text{trigger} = \left( \text{prio}_{\text{new}} > \text{prio}_{\text{running}} + \Delta \right) \;\lor\; \left( T_{\text{wait}} > T_{\text{SLA}} \right)
这个公式是抢占触发的一页病历:只要“新病人比床上的病人危急得多”或“普通病人等得太久”任一为真,系统就决定抢占。
  1. 新任务优先级$\text{prio}_{\text{new}}$ 表示新提交任务的基础优先级,数值越大越紧急,由用户提交时指定。
    prionew\text{prio}_{\text{new}}
  2. 运行中任务优先级与阈值$\text{prio}_{\text{running}}$ 是当前占用 GPU 的任务优先级,$\Delta$ 是防抖阈值,避免轻微优先级差异就触发抢占。
    priorunning+Δ\text{prio}_{\text{running}} + \Delta
  3. 逻辑或门$\lor$ 符号表示“两者任一成立即可”,任何一边满足,抢占触发条件就为真。
    (...)∨(...)\left( ... \right) \lor \left( ... \right)
  4. 排队超时判定$T_{\text{wait}} > T_{\text{SLA}}$ 比较任务实际排队时间与 SLA 承诺上限,超时则触发“排队升级”机制。
    Twait>TSLAT_{\text{wait}} > T_{\text{SLA}}

抢占策略设计

急诊科主任面临一个更棘手的抉择:手术台上有两台常规手术,一台刚开腹 10 分钟,另一台已经缝合到最后一层。现在必须腾出一间手术室抢救心梗病人——停哪台?答案显而易见:停缝合到一半的那台太可惜,不如停刚开腹的。

这就是抢占策略设计的核心:决定“抢谁”以及“怎么抢”。业界有两大模式:全抢占(Preemption)和半抢占(Graceful Preemption)。全抢占简单粗暴——调度器立刻杀掉或挂起低优任务,把资源让给高优任务,适合任务有完善检查点、抢占成本低的场景。半抢占则像医生缝合完最后一针再下台——给被抢占任务一个宽限期(Grace Period),允许它把当前迭代跑完、保存检查点后主动退出,适合无法强制中断的场景。Slurm 里的 CANCEL/REQUEUE 对应全抢占,CHECKPOINT 模式则更接近半抢占。

选定模式后,还要选受害者。调度器不会随机挑一个任务下手,而是计算每个候选任务的抢占代价:运行进度越多,被夺走时浪费的计算量越大;优先级越高,动它引发的“民愤”越大。所以最优受害者往往是“进度少、优先级低”的任务——就像急诊医生选择开腹不久的病人,而不是缝合到一半的病人。

急诊科的智慧在于:抢救不是把手术室掀个底朝天,而是精确选择代价最小的停台方案。集群管理员配置抢占策略时,本质上就是在给调度器写一本“停台手册”——明确哪些台子可以停、停之前给多少时间穿针引线。

cost(v)=α×progress(v)+ckpt_cost(v)prio(v)\displaystyle \text{cost}(v) = \frac{\alpha \times \text{progress}(v) + \text{ckpt\_cost}(v)}{\text{prio}(v)}
公式表达的是“动哪个病人最不心疼”:进度越多、检查点开销越大、优先级越高的任务,被抢占的代价越高,调度器优先避让。
  1. 运行进度占比$\text{progress}(v)$ 是候选任务 v 已完成的工作量(如已训练步数),进度越高,剥夺它浪费的计算越多。
    progress(v)\text{progress}(v)
  2. 检查点保存开销$\text{ckpt\_cost}(v)$ 是把任务 v 当前状态固化到磁盘所需的时间与存储成本,是抢占时必须额外支付的“账单”。
    ckpt_cost(v)\text{ckpt\_cost}(v)
  3. 优先级权重$\text{prio}(v)$ 放在分母上,说明任务的优先级越高,清算代价时折扣越大——高优任务几乎不会被当作受害者。
    prio(v)\text{prio}(v)
  4. 分子系数 α$\alpha$ 是进度敏感系数,用于调节“已做工作”与“检查点成本”之间的权重比例。
    α×progress(v)\alpha \times \text{progress}(v)

检查点机制

被暂停的手术为什么能无缝续接?因为主刀医生留下了详细的手术记录:切到第几层、出血量多少、缝了几针、用了哪种麻醉。没有这份记录,重新打开腹腔等于从零开始,病人白挨一刀。

GPU 任务的“手术记录”就是检查点(Checkpoint)。它是一份完整的训练状态快照,通常包含四类内容:模型权重、优化器状态(如 Adam 的一阶二阶动量)、当前 epoch 和步数、随机数生成器种子及学习率调度器状态。缺了任何一项,恢复后的训练就“失忆”——要么精度异常,要么数据顺序错乱。这就是为什么框架广泛采用 torch.save 与 torch.load 配合 model.state_dict() 与 optimizer.state_dict() 落盘的原因。

检查点的保存频率是个权衡艺术。存太频繁,IO 开销拖慢训练——假设每 10 分钟存一次、每次花 30 秒,训练有效时间就少了 5%;存太稀疏,一旦被抢占,丢失的进度太多。工业界常用的做法是周期快照 + 异步落盘:每 N 个 step 触发一次内存快照,再用后台线程写入分布式文件系统。恢复流程则像一个标准抢救预案:加载权重 → 装载优化器状态 → 恢复随机种子 → 续跑。断点续跑的恢复时间完全取决于检查点加载的 IO 速度。

手术记录越详细,抢救后恢复越快;但记录本身也占用医生时间。GPU 集群的检查点设计就是在这对矛盾中找平衡——让“病历”足够细,又不至于让医生光顾着写病历忘了做手术。

Trecover=Tload+Tresume,Toverhead=TckptTinterval\displaystyle T_{\text{recover}} = T_{\text{load}} + T_{\text{resume}}, \qquad T_{\text{overhead}} = \frac{T_{\text{ckpt}}}{T_{\text{interval}}}
左边是“从病历到重新开刀”的时间:加载权重加上恢复优化器;右边是“写记录的代价占比”:存一次检查点的时间除以保存间隔,占比越小开销越低。
  1. 加载时间$T_{\text{load}}$ 是从共享存储读回检查点文件的时间,取决于文件大小与存储带宽。
    TloadT_{\text{load}}
  2. 恢复时间$T_{\text{resume}}$ 是重建优化器状态、重置随机种子到正确步数的时间,通常远小于加载时间。
    TresumeT_{\text{resume}}
  3. 单次保存时长$T_{\text{ckpt}}$ 是生成一次快照并落盘所花的时间,是检查点的“单次成本”。
    TckptT_{\text{ckpt}}
  4. 保存间隔$T_{\text{interval}}$ 是两次检查点之间的训练时长,间隔越短,潜在进度损失越小但开销越高。
    TintervalT_{\text{interval}}

保护与SLA

急诊室的秩序不能失控:如果救护车一辆接一辆地送来病人,普通门诊的病人是不是永远排不上手术?医院对此有明文规定——择期手术病人等待超过一定时限,必须安排手术,哪怕要临时加台。这条规定,就是医院的"SLA"。

GPU 集群里,SLA 保护是抢占式调度的刹车片。没有它,低优先级任务可能被反复抢占,“饿死”在队列里——永远无法完成,GPU 浪费在反复保存与恢复检查点上。三种常见保护机制构成了护栏:最小运行时间保障,任务一旦开始运行,在至少 X 分钟内禁止被抢占,相当于手术后病人入住恢复室,不会被立刻赶走;排队超时升级,低优任务排队超过 SLA 上限后自动提权,保证它能抢到资源;抢占次数上限,单个任务在一天内最多被抢占 K 次,超过后调度器拒绝再抢它——相当于合情合理的“免死金牌”。

此外还有配额与权限控制:不是任何用户都能提交高优任务。集群通过优先级配额(如 Slurm QOS 的 GrpTRES、K8s 的 ResourceQuota)限制高优任务的总量,防止有人滥用急诊通道。SLA 的量化指标通常是任务排队时间 P95 分位数:系统承诺 95% 的任务等待时间不超过规定值,剩余的 5% 允许超限。

急诊室不会让普通病人等到天荒地老。SLA 保护的实质,是把“不饿死任何人”从道德口号变成可量化的数学约束——这是共享 GPU 集群在多团队协作中维持信任的基石。

starvation_prob=P(Twait>TSLA)≤0.05\displaystyle \text{starvation\_prob} = P\left( T_{\text{wait}} > T_{\text{SLA}} \right) \leq 0.05
这条公式是“SLA 的体检指标”:任务排队时间超过承诺上限的概率必须控制在 5% 以内,否则就要给低优任务加保护。
  1. 等待时间随机变量$T_{\text{wait}}$ 是一个任务从提交到真正运行的总等待时长,受队列长度、抢占频率影响。
    TwaitT_{\text{wait}}
  2. 概率符号$P( \cdot )$ 表示括号内事件发生的概率,对抗的是“任务排队过长”这一随机事件。
    P(...)P\left( ... \right)
  3. 超限概率上限$\leq 0.05$ 是硬性指标,意思是 95% 的任务等待时间必须小于 $T_{\text{SLA}}$。
    ≤0.05\leq 0.05

Slurm配置实践

医院的信息系统里,每个科室都有应急响应等级:急诊科最高,门诊部最低。Slurm 就像这套信息系统——管理员在配置文件中定义谁有资格抢占、怎么抢。

Slurm 抢占调度的第一站是 slurm.conf 的全局配置。PreemptType 指定抢占插件,常用 preempt/qos(按 QoS 优先级抢占);PreemptMode 决定抢占动作:OFF 完全关闭,CANCEL 直接取消低优任务,REQUEUE 取消后让任务重新排队(无检查点则从头跑),CHECKPOINT 先保存检查点再抢占——生产环境推荐 REQUEUE 或 CHECKPOINT。

第二站是 QOS 关联:每个任务队列(Partition)绑定指定的 QoS,QOS 里配置 Priority 数值或 GrpTRES 资源上限。高优 QOS 的任务可以抢占低优 QOS 的任务,优先级数值差满足 PreemptExemptTime 的豁免规则——任务运行超过豁免时间后不再接受抢占。最后,禁止抢占的配置用 PreemptExemptTime 或单独设置任务的 --no-requeue 标志实现,相当于给重要任务贴“免打扰”标签。

管理员在 Slurm 里的工作,就像给医院各科室设定响应等级:急诊科随时可以叫停门诊,但门诊病人等久了也会自动升级。配置的关键是先想清楚业务优先级分层,再落成 QOS 数字——而不是等出了事故才手忙脚乱。

Priorityslurm(j)=QOSprio(j)+wa⋅Age(j)+ws⋅Size(j)\displaystyle \text{Priority}_{\text{slurm}}(j) = \text{QOS}_{\text{prio}}(j) + w_a \cdot \text{Age}(j) + w_s \cdot \text{Size}(j)
Slurm 眼里,任务优先级不是单一数字,而是 QOS 基数、排队时长和资源大小的加权和——就像医院综合病情、等待时长和手术难度给出分诊评分。
  1. QOS 基础分$\text{QOS}_{\text{prio}}(j)$ 是任务所属 QoS 层级的初始分数,数值越高,在抢占博弈中越占优。
    QOSprio(j)\text{QOS}_{\text{prio}}(j)
  2. 排队老化加分$w_a \cdot \text{Age}(j)$ 是队列等待时间带来的加分,等待越久分数越高,防止长期饥饿。
    wa⋅Age(j)w_a \cdot \text{Age}(j)
  3. 资源大小加分$w_s \cdot \text{Size}(j)$ 是任务申请资源规模带来的加分,大任务对集群调度影响更大,因此也更容易被提前调度。
    ws⋅Size(j)w_s \cdot \text{Size}(j)

K8s配置实践

如果说 Slurm 是老牌三甲医院的信息系统,K8s 就是互联网医院的新架构——一切都是声明式配置,调度器像 AI 分诊机器人一样自动化运作。

K8s 的抢占机制核心是 PriorityClass 资源对象。管理员先定义一个调度优先级类(如 high-priority 值 1000),Pod 通过 priorityClassName 引用它。调度器为每个待调度 Pod 计算优先级数值;当高优 Pod 无法找到满足资源需求的节点时,就会进入抢占流程:找出所有可能腾出资源的低优 Pod,驱逐它们,让高优 Pod 落座。preemptionPolicy 参数可以设置 Never,表示该 PriorityClass 的 Pod 永不抢占别人,只做排队等待。

K8s 的驱逐比 Slurm 更温和——遵循 PDB(PodDisruptionBudget) 保护,确保每次驱逐不会让某个服务的副本数低于红线;真正的驱逐动作是向 kubelet 发送优雅终止信号,给 Pod 留出退出时间,运行中的进程收到 SIGTERM 后可以尝试保存状态。这就是 K8s 的“半抢占”底色。

K8s 的抢占哲学和急诊室一样:不动最危重的病人,专挑已完成治疗、即将出院的床位周转。POD 被驱逐不是灾难——弹性本来就是云原生的底色,而抢占只是让资源流动得更聪明。

score(node)=∑v∈victims1prio(v)\displaystyle \text{score}(\text{node}) = \sum_{v \in \text{victims}} \frac{1}{\text{prio}(v)}
这个公式是 K8s 调度器挑选“牺牲品”的评分逻辑:低优 Pod 越多、优先级越低,节点越适合被抢占——相当于挑一个“病床周转率最高”的病区下手。
  1. 受害者集合$\text{victims}$ 是候选节点上可以被驱逐的 Pod 集合,排除受 PDB 保护的 Pod。
    v∈victimsv \in \text{victims}
  2. 优先级倒数$\frac{1}{\text{prio}(v)}$ 让低优 Pod 贡献更高的分数,驱逐它们对集群的“冲击”最小。
    1prio(v)\frac{1}{\text{prio}(v)}
  3. 求和累加$\sum$ 把所有受害者的得分加总,总分数越高的节点越优先被抢占腾退。
    ∑v∈victims\sum_{v \in \text{victims}}

平衡与调优

医院院长站在月中复盘会上:急诊抢救成功率是高了,可普通门诊的手术量下滑了 20%,医生们抱怨手术台被反复打断。这就是抢占式调度的永恒课题——平衡。

调优的量化目标是一个多目标优化问题:最大化 GPU 利用率(U),最小化任务平均等待时间(T_wait),同时控制抢占频率(f_preempt)。三者互相牵制:抢占太激进,高优任务爽了,但检查点开销吃掉利用率;抢占太保守,低优任务不饿死了,但紧急任务被堵在队列里。工程实践通常用加权评分法,给三个目标配权重系数 α、β、γ,再滚动观察指标曲线。

具体调优手段包括:调整抢占触发阈值 Δ(防抖),修改检查点间隔(IO 开销与进度损失的折中),设置宽限期长短(半抢占的耐心程度),以及微调 SLA 上限。推荐做法是在测试集群上回放历史任务日志,比较不同参数组合下的目标函数值——就像医院用过去一年的急诊数据做仿真,而不是直接拿病人做实验。

没有放之四海皆准的参数,只有持续观测、小心调优的耐心。医院院长最终发现:把急诊通道拓宽一点,同时给门诊安排“最长等待不超过 3 小时”的承诺,整体满意度反而最高——抢占式调度的艺术,正是在冲突的目标之间找到那道最优的窄门。

Objective=α⋅U−β⋅Tˉwait−γ⋅fpreempt\displaystyle \text{Objective} = \alpha \cdot U - \beta \cdot \bar{T}_{\text{wait}} - \gamma \cdot f_{\text{preempt}}
这是集群调优的“院长评分表”:利用率是加分项,等待时间和抢占频率是减分项,三个权重系数表达管理者的偏好。
  1. 利用率$U$ 是所有 GPU 实际计算时间占比,越高代表硬件越不闲着,是集群收益的基础。
    UU
  2. 平均等待时间$\bar{T}_{\text{wait}}$ 是任务从提交到运行的平均排队时长,越低用户体验越好。
    Tˉwait\bar{T}_{\text{wait}}
  3. 抢占频率$f_{\text{preempt}}$ 是单位时间内发生过抢占的次数,过高说明配置太激进。
    fpreemptf_{\text{preempt}}
  4. 权重系数$\alpha, \beta, \gamma$ 是管理员对三个维度的偏好权重,优先保交付就调大 $\beta$。
    α⋅U−β⋅Tˉwait−γ⋅fpreempt\alpha \cdot U - \beta \cdot \bar{T}_{\text{wait}} - \gamma \cdot f_{\text{preempt}}

AI 应用场景

场景一:共享 GPU 集群的高优任务保障

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

解决的问题:在多人共享的 GPU 集群中,算法团队日常跑实验占满了所有卡;突然出现线上事故,需要紧急重新训练一个模型做模型回滚,传统方式只能干等排队,错过黄金修复时间。抢占式调度让紧急训练任务直接插队,几分钟内开始计算。

具体案例:某大厂推荐系统广告模型出现线上效果回退,算法负责人提交一个优先级为 100 的紧急微调任务(训练时长 20 分钟),调度器立刻抢占 8 张正在跑离线数据清洗的低优 GPU,保存检查点后让出资源。紧急任务 18 分钟跑完,模型快速上线止血,低优任务随后无缝恢复。

场景二:大模型紧急微调的“插队抢救”

阶段:大模型微调与部署

解决的问题:大模型微调动辄上百 GPU·小时,如果被低优任务占着卡,紧急的合规修改或安全对齐任务无法及时执行。抢占式调度配合检查点,能在大规模任务之间实现“安全换挡”。

具体案例:某机构正在用 64 张 A100 跑一周的 Llama 3 继续预训练,突然监管要求模型输出内容在 24 小时内完成安全对齐。管理员提交高优微调任务,Slurm 触发 CHECKPOINT 抢占,继续预训练任务保存检查点后让出 32 张卡;微调完成后继续预训练从断点恢复,全程人工干预不超过 10 分钟。

场景三:跨团队共享集群的 SLA 管理

阶段:资源治理与成本核算

解决的问题:公司内部多个业务线共享 GPU 集群,业务线之间优先级不清晰,经常发生“高优任务堵死低优任务”的口水战。用优先级分层配合 SLA 超时升级,让每个团队都有明确的交付预期。

具体案例:某公司把集群分为 P0/P1/P2 三级,P0 是线上紧急任务,P1 是日常训练,P2 是离线评测。P2 任务排队超过 4 小时自动升为 P1 进入抢占队列;每个团队每月提交 P0 任务的配额为 200 卡·小时。三个月运行后,P2 任务的平均等待时间从 9 小时降到 3.5 小时,高优任务的资源保证率提升到 99%。

Python 演示

python81 行
import heapq  class Job:    """模拟一个 GPU 训练任务"""     def __init__(self, name, priority, submit_time, duration):        self.name = name        self.priority = priority        # 数值越大越紧急        self.submit_time = submit_time        self.duration = duration        self.progress = 0.0             # 已完成 GPU 时长        self.checkpointed = 0.0         # 已保存的检查点进度     def remaining(self):        return self.duration - self.progress     def __repr__(self):        return (f"{self.name}(prio={self.priority}, "                f"progress={self.progress:.1f}/{self.duration})")  def preemptive_schedule(jobs, gpu_count=1, dt=0.5):    """逐帧推进抢占式调度, 返回事件时间线"""    queue = []          # 元组(-priority, submit_time, job)    gpus = [None] * gpu_count    timeline = []    time = 0.0    idx = 0    jobs = sorted(jobs, key=lambda j: j.submit_time)     while idx < len(jobs) or queue or any(gpus):        # 提交到点的新任务        while idx < len(jobs) and jobs[idx].submit_time <= time:            job = jobs[idx]            heapq.heappush(queue, (-job.priority, job.submit_time, job))            idx += 1         # 抢占判定: 队头任务比运行中任务更紧急则抢占        if queue:            top_prio = -queue[0][0]            for gi in range(gpu_count):                g = gpus[gi]                if g is not None and top_prio > g.priority:                    g.checkpointed = g.progress                    gpus[gi] = None                    heapq.heappush(queue, (-g.priority, g.submit_time, g))                    _, _, attacker = heapq.heappop(queue)                    gpus[gi] = attacker                    timeline.append(                        (time, f"抢占: {attacker.name}拦截了{g.name}"))                    break         # 运行与恢复: 空槽位弹队列, 正在运行则推进        for gi in range(gpu_count):            if gpus[gi] is None:                if queue:                    _, _, nxt = heapq.heappop(queue)                    nxt.progress = nxt.checkpointed                    gpus[gi] = nxt                    timeline.append((time, f"启动: {nxt.name}"))            else:                g = gpus[gi]                g.progress += dt                if g.progress >= g.duration:                    timeline.append((time, f"完成: {g.name}"))                    gpus[gi] = None         time += dt     return timeline  if __name__ == "__main__":    sample_jobs = [        Job("常规训练A", priority=5, submit_time=0.0, duration=10),        Job("紧急微调B", priority=80, submit_time=1.0, duration=4),        Job("数据分析C", priority=20, submit_time=2.5, duration=6),    ]    for t, event in preemptive_schedule(sample_jobs):        print(f"t={t:5.1f} | {event}")
目的:模拟三个任务在单 GPU 集群上的执行过程——B 任务到达后抢占 A 的 GPU,A 保存检查点;B 完成后 A 从断点恢复。运行后输出完整事件时间线。
  1. heapq.heappush(queue, (-job.priority, job.submit_time, job))用负优先级技巧把最小堆变成“最高优先级先出队”,是抢占模拟的发动机。
  2. if g is not None and top_prio > g.priority:抢占判定的核心行:只有新任务优先级严格高于运行中任务才触发,对应正文中的 Δ 阈值思想。
  3. g.checkpointed = g.progress保存检查点:记录当前已完成进度,后续恢复时从这里续跑,而不是从零开始。
  4. nxt.progress = nxt.checkpointed从检查点恢复:任务再次启动时,把进度设置成上次保存的值,模拟“断点续跑”。
  5. timeline.append((time, f"抢占: {attacker.name}拦截了{g.name}"))把抢占事件写入时间线,便于分析调度器的行为是否符合预期。

常见误区

误区我理解的抢占式调度就是高优任务一来,系统就把低优任务 kill 掉,代码想删就删,简单粗暴。

正解直接 kill 是 Slurm CANCEL 模式的下下策——被 kill 的任务训练进度全部丢失,下次从头再来,浪费的算力由集群买单。真正可靠的抢占式调度一定配合检查点机制:先保存断点,再让出资源,等资源空闲了从断点恢复。就像急诊抢救不会把普通手术台掀翻,而是把开腹的病人妥善缝合好再腾地方。

为何常见:很多入门者看到“抢占”这个词望文生义,以为抢占=终止,忽略了“保存现场”这个关键步骤。实际上,保存检查点才是抢占能落地的前提。

误区反正高优任务能抢占,那我把自己所有任务都标成最高优先级,不就能最快跑完吗?

正解这相当于医院里所有人都挂急诊号——结果就是没有急诊,全部变成排队。集群的优先级需要通过 QoS 配额和权限控制来约束:高优任务数量有限,且抢占会带来检查点开销。人人都高优,等于没有高优;而且调度器会限制单个用户的高优配额,滥用者会被风控。

为何常见:初学者只看到抢占的“好处”,没有意识到抢占是零和博弈——你抢来的资源,正是别人被迫让出的,频繁抢占反而降低集群整体吞吐。

误区为了不让进度丢失,我应该每训练一步就保存一次检查点,这样任何时刻被抢都不心疼。

正解检查点保存本身有巨额 IO 成本。假设一次保存花 60 秒,如果每 5 分钟存一次,训练效率直接下降 20%。更合理的做法是周期快照(如每 30 分钟)配合异步落盘,把开销控制在 2% 以内。过度频繁的快照,等于主刀医生每缝一针就叫护士拍一张照片——手术没做完,时间全耗在拍照上了。

为何常见:人们天然对“丢失”感到恐惧,却忽略了“保存”本身也要付出代价。在分布式训练中,全量检查点动辄几十 GB,IO 瓶颈往往比算力瓶颈更先到来。

误区SLA 就是保证高优任务尽快运行,低优任务无所谓,等多久都行。

正解恰恰相反,SLA 保护的重点恰恰是低优任务——防止它们被无限期饿死。通过排队超时升级、最小运行时间保障、抢占次数上限,系统承诺每个任务等待时间有上限。订阅了一个“最迟 4 小时接诊”的承诺,急诊病人再多,门诊病人在第 4 小时也会被自动排上台。

为何常见:SLA 在商业语境里常被理解为“优先客户的权益”,但在调度系统里,它更多是“最差情况下的公平底线”。忽略了这一点,集群就会出现“饿死”任务,长期占用队列却不产出结果。

面试题

Q1. 抢占式调度中,检查点(Checkpoint)的核心作用是什么?

单选题 · easy

  • A.定期保存任务进度以便恢复← 正确答案
  • B.提高 GPU 的计算运行频率
  • C.压缩模型文件的存储体积
  • D.自动修复训练代码中的 Bug

答案:A。检查点的作用是把训练状态(权重、优化器、步数)持久化,让任务被抢占后能从断点继续。它不提升频率、不压缩体积,更不能修 Bug——就像手术记录的价值是让手术能续接,而不是加快心跳。

Q2. 下列哪种情况最应该触发抢占式调度?

单选题 · easy

  • A.新任务优先级显著高于运行任务← 正确答案
  • B.集群网络发生了短暂抖动故障
  • C.GPU 散热风扇转速明显降低
  • D.管理员想测试一下抢占功能

答案:A。抢占的核心触发条件是“优先级差值超过阈值”。网络故障、风扇问题属于硬件运维事件,管理员测试也需要有真实的高优任务配合。急诊通道只为病情危急的病人开放,不能因为好奇就拉响警报。

Q3. 下列哪项策略能有效防止低优任务被“饿死”?

单选题 · easy

  • A.排队超时后自动提升优先级← 正确答案
  • B.将所有任务优先级设为相同
  • C.禁止任何形式的任务抢占
  • D.提高所有任务的检查点频率

答案:A。排队超时升级是标准的反饿死机制——低优任务等太久就自动提权。全部同优先级等于没有优先级,完全禁止抢占会让紧急任务无法插队,单纯提高检查点频率与饿死无关。

Q4. 在 Slurm 中,要关闭抢占功能应如何设置 PreemptMode?

单选题 · mid

  • A.设置为 OFF 直接关闭抢占← 正确答案
  • B.设置为 CANCEL 排队等待
  • C.设置为 SUSPEND 提升频率
  • D.设置为 GANG 人工审批

答案:A。Slurm 中 PreemptMode=OFF 表示彻底关闭抢占。CANCEL 是抢占后直接取消低优任务,SUSPEND 是挂起,GANG 是轮转调度,三者都不是关闭抢占。就像医院关停急诊通道的开关只有 OFF 一个档位。

Q5. K8s 中定义 Pod 调度优先级的机制是哪个对象?

单选题 · mid

  • A.PriorityClass 资源对象← 正确答案
  • B.NodeSelector 节点配置
  • C.ServiceAccount 服务账户
  • D.ConfigMap 配置字典

答案:A。K8s 通过 PriorityClass 定义优先级数值,Pod 用 priorityClassName 引用。NodeSelector 管节点选择,ServiceAccount 管权限,ConfigMap 管配置,它们都和抢占调度无关。

Q6. 检查点保存频率过高会带来什么直接后果?

单选题 · mid

  • A.存储 IO 开销急剧增加← 正确答案
  • B.模型训练精度明显下降
  • C.GPU 算力成倍地提升
  • D.任务优先级被自动重置

答案:A。检查点本质是写磁盘,频率越高,IO 占用的时间越多,有效训练时间减少。它不影响模型精度,不会提升算力,更不会影响优先级——就像拍照太频繁会耽误手术时间,但照片本身不会改变病人的病情。

Q7. 抢占式调度的触发条件可能包括以下哪些?

多选题 · mid

  • A.高优任务提交且无空闲 GPU← 正确答案
  • B.低优任务排队时间超过 SLA← 正确答案
  • C.管理员手动调高任务优先级← 正确答案
  • D.GPU 硬件发生掉卡等故障

答案:ABC。三类典型触发:高优任务插队、排队超时升级、人工干预调优先级。GPU 硬件故障触发的是故障转移机制,不是抢占——硬件坏了没有“暂停保存”的余地,相当于手术室着火了只能转移病人,不是让台。

Q8. SLA 对低优任务最重要的保护意义是什么?

单选题 · hard

  • A.保证等待时间有明确上限← 正确答案
  • B.保证永远不被高优任务抢占
  • C.保证获得最高优先级配额
  • D.保证检查点文件永不丢失

答案:A。SLA 的核心是给低优任务一个“最多等多久”的承诺,超时自动提权。它不能保证永不被抢,也绝不意味着最高优先级,检查点文件属于存储可靠性问题,与 SLA 无直接关系——等太久会升级加急,但急诊来了该让还是让。

Q9. 抢占式调度带来的开销包括哪些方面?

多选题 · hard

  • A.检查点保存的存储和时间成本← 正确答案
  • B.恢复时加载检查点的 IO 耗时← 正确答案
  • C.被抢占任务回退进度的损失← 正确答案
  • D.任务源代码的编写和维护成本

答案:ABC。抢占开销集中在三处:保存检查点花的时间与存储、恢复时加载检查点的时间、两次检查点之间的进度丢失。源代码编写成本是研发开销,不是调度开销——就像急诊让台的成本是手术中断的损失,而不是医生学手术的学费。

Q10. 某任务被抢占后恢复,但其训练进度回退了一小段,可能的原因有哪些?

多选题 · extreme

  • A.检查点只按周期保存而非实时← 正确答案
  • B.最后几秒的训练进度未写入检查点← 正确答案
  • C.检查点文件从存储中被意外删除← 正确答案
  • D.GPU 时钟频率在运行中发生了波动

答案:ABC。回退的根源是“检查点不是实时镜像”:周期保存使得最后一段进度丢失,这与选项 A、B 一致;文件被删除则导致恢复到更早的备份、进退更多。GPU 频率波动只影响训练速度,不影响进度存档的完整性——病历上没记录的内容,做了手术也算没做。