想象一下,你所在的科技园区新开了一家大型食堂。园区里有十几家公司,几千名员工,食堂却只有 8 个打饭窗口。每到饭点,各个窗口前都排起长队——有人急着吃完饭回去开会,有人只是想来蹭碗汤,还有几个大部门恨不得把整排窗口都包下来。这时候,食堂经理就得定规矩:谁先打饭?每个部门最多占几个窗口?排队太久的人能不能插队? GPU 集群的调度,和食堂打饭几乎是同一套逻辑。在一家公司的 GPU 集群里,可能有十几个团队共享几百块显卡:算法工程师要跑训练、数据团队要做批处理、线上推理服务一刻不能停。计算资源有限,需求却永远旺盛,所以需要一套“排队规则”来分配资源。这就是队列(Queue)与优先级(Priority)调度干的事——队列是排队的队伍,优先级是加急餐券,权重是各窗口蒸锅的大小,配额则是每桌限点的菜数。 这篇文章就带你以食堂经理的视角,把 GPU 集群中的队列、优先级、权重、配额、多租户治理、效果评估和配置实践一次讲明白。读完你就能理解:如何让重要任务“插队”的同时不饿死小团队,如何让 100 块 GPU 养活 500 个跑任务的工程师。

岗位关联

掌握队列与优先级调度,你就拥有了 GPU 集群资源分配的“指挥棒”——既能保证核心业务优先,又能让集群利用率拉满,这是每位 AI 平台工程师的必修课。

目录
  1. 可视化演示
  2. 队列基础概念
  3. 优先级机制
  4. 加权公平调度
  5. 资源配额管理
  6. 多租户治理
  7. 策略效果评估
  8. 配置最佳实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示GPU集群多租户权重队列设计、优先级计算与抢占式调度机制及Slurm与K8s调度流程对比,通过交互切换三种模式理解集群资源调度策略。

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

权重队列与多租户 Q1(w=0.5): 高权重队列分配50%GPU资源 Q2(w=0.3)+Q3(w=0.2)按权重公平分享 allocation_i = total_gpu × weight_i 多租户通过权重队列实现资源分区隔离

队列基础概念

食堂开门迎客,第一件事就是立起几个窗口:套餐窗口、面食窗口、清真窗口。每个窗口前拉一条隔离带,员工按顺序排队,后来的人自动站到队尾。GPU 集群里的队列(Queue)也是同样的东西——它是一个作业等待区的名称。当一位工程师提交一个训练任务,调度器不会立刻分配 GPU,而是把任务放进某个队列里排队。队列本质上就是一个有序的作业集合,调度器从队首依次取出作业,分配给空闲的 GPU。

为什么要分多条队列而不是一个大队?因为不同的业务有不同的诉求:在线推理要秒级响应,离线训练等半小时无所谓,数据预处理则只在深夜跑。如果所有任务混在一个大队里,一个跑 3 天的训练任务会堵住所有想插进来的小任务。所以集群管理员会按照业务类型、团队归属、GPU 型号划分多个队列,每个队列拥有独立的排队空间和调度参数。

队列和作业的绑定关系,决定了任务去哪里排队。在 Slurm 中,提交作业时用 -p partition 指定分区名;在 Kubernetes 中,则通过 namespace 或调度器配置把 Pod 归入某个队列。可以理解为食堂每个窗口上方挂着的LED屏:你的任务属于哪个团队、需要哪种卡型,就自动去对应窗口排队。默认情况下,队列内部按FIFO(先进先出)的顺序服务,先提交的作业先拿卡。

有了队列,调度就有了“秩序”。没有队列的集群就像没有隔离带的食堂,所有人挤在窗口前,谁也吃不上饭。队列的存在,让每个任务都知道自己在队伍中的位置,也让管理员能针对不同窗口制定不同的服务策略。

Qqueue={T1,T2,…,Tn}Q_{queue} = \{ T_1, T_2, \ldots, T_n \}
队列就是一个有序任务集合,调度器从队首开始依次分配资源。前面的人没打完饭,后面的就得等着。
  1. 队列符号定义用集合符号表示一个队列,里面装着 n 个等待调度的任务,下标越大表示越晚到达。
    Qqueue={T1,T2,…,Tn}Q_{queue} = \{ T_1, T_2, \ldots, T_n \}
  2. 队首任务$T_1$ 是队首任务,也就是最早提交、下一个将被调度的任务。
    T1 先出队 T_1 \text{ 先出队 }
  3. 队尾任务$T_n$ 是队尾任务,新提交的作业会追加到它后面。
    Tn 最后出队 T_n \text{ 最后出队 }

优先级机制

排队的队伍有了,但问题来了:食堂经理时不时会收到“加急订单”——某位核心客户的模型要上线,必须在 10 分钟内跑完推理验证;或者某个训练任务已经等了一个小时,再等下去项目就延期了。这时候,光靠“先来后到”就不够了。于是食堂推出了加急餐券:持有急事券的员工可以走到队伍最前面,甚至可以把正在窗口打饭的人劝回去,让对方稍后再来。

在 GPU 集群里,这个“加急餐券”就是优先级(Priority)。每个任务提交时都会携带一个优先级数值,调度器在决定“下一个谁上车”时,首先比较优先级。优先级通常由几个因素共同决定:用户提交时显式指定(比如用 --priority=high)、任务的等待时长(等得越久,优先级自动增长,防止饿死)、以及业务紧急度策略(管理员为某些关键业务设置的基础优先级)。

优先级比较的规则要牢记:数值越小,优先级越高。这就好比餐券上印的等级,1 级是最高级,3 级排最后。当两个任务的优先级相同时,再按 FIFO 的先后顺序排队。更复杂的情况是抢占(Preemption)——当集群没有空闲 GPU,而一个高优任务急需执行时,调度器会把正在运行的低优任务“请出”窗口,让高优任务立即上车。被抢占的任务不会丢失,而是回到队列前端等待,下次有机会继续跑。

优先级机制就像食堂的加急通道,它让“重要且紧急”的任务能插队,但绝不是无限特权——如果加急的任务太多,调度器依然要兼顾其他队伍。优先级解决了“谁先吃”的问题,但还没回答“每家能吃多少”的问题,后者要交给权重机制。

Ptotal=Pbase+ΔPwaitP_{total} = \displaystyle P_{base} + \Delta P_{wait}
任务的最终优先级由基础优先级加上等待时间增益得到。等得越久,优先级自动“涨价”,避免低优任务永远排不上。
  1. 基础优先级管理员或用户为任务设定的初始优先级,数值越小越优先。
    Pbase∈{0,1,2,…}P_{base} \in \{0, 1, 2, \ldots\}
  2. 等待增益任务在队列中等待的时间越长,这个增量越大,让老任务逐渐“插队”。
    ΔPwait∝等待时间\Delta P_{wait} \propto \text{等待时间}
  3. 总优先级两者相加得到调度器用来排序的最终分数。
    Ptotal=Pbase+ΔPwaitP_{total} = P_{base} + \Delta P_{wait}

加权公平调度

想象食堂有三个大蒸锅,经理定了个规矩:套餐窗口有 3 个蒸锅,面食窗口 2 个,清真窗口 1 个。每个窗口每批蒸出来的馒头数固定,窗口前排队的人越多,每个人分到的就越少。但蒸锅不是“专属”的——如果面食窗口没人排队,套餐窗口可以借用它的蒸锅多蒸几笼。这就是加权公平调度(Weighted Fair Scheduling)的核心思想。

在 GPU 集群中,权重(Weight)决定了每个队列在“同时有需求”时能分到的资源比例。假设高优队列权重 3、普通队列权重 2、低优队列权重 1,总权重是 6。那么当所有队列都有任务排队时,高优队列获得集群资源的 3/6(50%),普通队列获得 2/6(约 33%),低优队列获得 1/6(约 17%)。这与“按人头分配”不同,权重高的队列在竞争中的话语权更大。

加权公平调度还有个重要特性:空闲资源的弹性复用。如果高优队列暂时没有任务,它的份额不会空着浪费,而是自动让给其他有任务的队列使用。这保证了集群利用率始终很高。同时,调度器还有一个防饿机制(Anti-starvation)——即使低权重的队列,也保证它能得到“最少一份”资源,不至于因为权重太低而永远等不到调度。

回到食堂,蒸锅的比喻完美诠释了权重:它保证“大部门”在高峰时段能吃到更多,但也留了小灶给“小部门”,不至于让人饿肚子。不过,权重管的是“相对比例”,如果某个团队野心太大,提交 10 倍于份额的任务,整个食堂都会被它挤爆。这时候就需要配额来设一道“绝对上限”。

sharei=wi∑j=1nwj×Rtotalshare_i = \displaystyle\frac{w_i}{\sum_{j=1}^{n} w_j} \times R_{total}
队列 i 获得的资源份额 = 队列 i 的权重 ÷ 所有队列权重之和 × 集群总资源。权重越高,份额越大,但总有一个下限兜底。
  1. 队列编号与权重假设集群有 n 个队列,第 i 个队列的权重记为 $w_i$,权重由管理员根据业务重要性设定。
    i∈{1,2,…,n},wi>0i \in \{1, 2, \ldots, n\}, \quad w_i > 0
  2. 权重总和分母是所有队列权重之和,它把权重归一化成“占比”。
    ∑j=1nwj=w1+w2+⋯+wn\sum_{j=1}^{n} w_j = w_1 + w_2 + \cdots + w_n
  3. 权重比例第 i 个队列的权重占全体权重的比例,这个比例在 0 到 1 之间。
    wi∑j=1nwj\frac{w_i}{\sum_{j=1}^{n} w_j}
  4. 乘上总资源用这个比例乘以集群可调度的总资源量 $R_{total}$,就得到该队列理论上可获得的资源份额。
    sharei=wi∑jwj×Rtotalshare_i = \frac{w_i}{\sum_{j} w_j} \times R_{total}

资源配额管理

食堂可以给每个窗口规定“每桌每次最多打几份菜”吗?自助餐是可以的——经理在取餐台立了块牌子:每家公司每天最多消耗 200 份套餐,其中早餐最多 50 份。这就是配额(Quota)。它跟权重不同,权重管的是“份额比例”,配额管的是“绝对数量上限”。在 GPU 集群里,配额通常这样定义:每个团队最多同时使用多少块 GPU、多少 GB 显存、多少 CPU 核数。

配额有两个关键数值:最小保证(min)和最大上限(max)。最小保证意味着,只要集群资源足够,调度器优先满足这个底线——就像食堂承诺每家公司至少能订到 20 份午饭;最大上限则意味着,即使集群有空闲资源,你的团队也不能超过这个数——防止某个团队囤积资源搞“独占”。实际使用量在最小保证和最大上限之间动态浮动。

配额的设置有个反直觉的技巧:所有配额的总和可以大于物理资源总量。比如集群只有 100 块 GPU,但三个团队各配了 60 块的配额上限,加起来 180。这在工程上叫“超卖”(overcommit)。为什么不直接设成 100?因为大部分团队不会同时把配额打满,利用率的低谷期可以共享。当任务请求超过配额上限时,调度器可以选择让任务排队等待其他任务释放资源,或者直接拒绝并提示用户。

配额就像自助餐的“限取量”牌子,它让每个团队都清楚自己的边界。权重负责“公平地分蛋糕”,配额负责“防止一个人把蛋糕全拿走”。两者配合,再加上优先级决定“先切哪块”,多租户的食堂才算有了完整的规矩。

Qmin≤Rused≤QmaxQ_{min} \le R_{used} \le Q_{max}
每个团队的实际资源使用量被限制在最小保证和最大上限之间。低于下限会被优先补足,高于上限则会被排队或拒绝。
  1. 最小保证配额调度器承诺为该团队保留的资源量,只要集群有资源就优先满足。
    Rused≥QminR_{used} \ge Q_{min}
  2. 实际使用量当前该团队正在占用的 GPU 数量,它随着任务提交和结束而动态变化。
    Rused=∑运行中任务GPU数R_{used} = \sum_{\text{运行中任务}} \text{GPU数}
  3. 最大上限配额该团队在任何时刻都不得超过的资源量,超过后新任务只能排队。
    Rused≤QmaxR_{used} \le Q_{max}

多租户治理

园区食堂不只是服务一家公司,而是十几家公司共用的。每家公司的员工饭卡不同,但都在同一个食堂就餐。这时候管理就复杂了:A 公司包的机房(啊不,包场)不能让人乱进,但 B 公司临时人多,能不能借 A 公司的空桌子?这就是多租户治理要解决的问题——多个团队(租户)共享同一个 GPU 集群,既要相互隔离,又要灵活共享。

租户隔离是第一道防线。每个租户拥有自己独立的命名空间(namespace),里面运行的作业、配置的配额、创建的密钥互不可见。在 Kubernetes 中,namespace 天然成为资源配额的作用域;在 Slurm 中,则通过 partition 和账号(account)来隔离。隔离的意义在于:A 团队的错误配置不会影响 B 团队,一个租户的异常流量也不能挤占别人的资源。

隔离之后是共享策略。完全隔离的集群利用率极低——A 团队周末不上班,GPU 全部闲置,B 团队却排队到深夜。所以调度器需要支持“弹性共享”:各租户的预留配额加上一个共享资源池,总资源等于二者之和。当某租户的预留配额用不完时,剩余部分自动流入共享池,供其他租户临时借用;一旦该租户有新任务提交,借出去的资源会被逐步收回。

多租户治理的精髓,是“隔离但不孤立”。隔离保证了安全与稳定,共享保证了效率与弹性。我们的食堂既能容纳十几家公司,又不让某家公司把食堂变成自家餐厅——这正是 GPU 集群管理员最想达到的境界。但规则定了,怎么知道定得好不好?要看实际效果。

Rcluster=∑t=1MQt+RsharedR_{cluster} = \displaystyle\sum_{t=1}^{M} Q_t + R_{shared}
集群总资源 = 各租户预留配额之和 + 共享资源池。闲时资源自动汇入共享池供所有人使用,忙时各归各。
  1. 租户数量集群中一共有 M 个租户(团队、部门、项目组),每个租户都有自己的预留配额。
    t∈{1,2,…,M}t \in \{1, 2, \ldots, M\}
  2. 各租户配额求和把所有租户的预留配额加起来,这是“保底”的分配部分。
    ∑t=1MQt=Q1+Q2+⋯+QM\sum_{t=1}^{M} Q_t = Q_1 + Q_2 + \cdots + Q_M
  3. 共享资源池管理员特意保留的公共资源,供各租户在高峰期临时借用。
    Rshared≥0R_{shared} \ge 0
  4. 总资源分解集群所有物理资源恰好分成“租户预留”和“共享池”两部分。
    Rcluster=∑t=1MQt+RsharedR_{cluster} = \sum_{t=1}^{M} Q_t + R_{shared}

策略效果评估

食堂经理折腾了这么多规矩,总得有个数据来衡量:员工平均排队多久?窗口的翻台率(利用效率)怎么样?如果排队时间太长,说明资源不够或调度不合理;如果窗口经常空着,说明规则太死板、资源浪费。GPU 集群的调度策略同样需要量化评估,主要看两个核心指标:排队时间和集群利用率。

排队时间(Waiting Time)指任务从提交到真正开始执行的时间间隔。它是调度公平性最直观的体现——如果低优任务的平均排队时间是高优任务的 10 倍,说明优先级权重设置得太激进;如果所有任务都在排长队,说明集群容量不足以支撑当前负载。管理员通常会统计每个队列的 p50/p95 排队时间,用来发现“堵点”。

集群利用率(Utilization)则反映资源的整体忙碌程度,包括 GPU 平均利用率、节点分配率、空闲资源比例等。这里有个微妙的权衡:排队论告诉我们,当系统利用率接近 100% 时,平均等待时间会急剧上升。好比食堂上座率 95% 时,队伍会排到门口;上座率 80% 时,几乎随到随吃。因此,成熟的集群通常会保留 10%~20% 的冗余,用适度空闲换取低延迟响应。

看到这个公式,食堂经理就能理解:为什么不能把窗口排得满满当当——一旦利用率逼近 100%,队伍会呈指数级恶化。好的调度策略,是在“想尽办法提高利用率”和“保证排队时间可接受”之间找平衡。现在理论和指标都有了,最后一节看看落地的配置。

Wq=ρμ−λ,ρ=λμW_q = \displaystyle\frac{\rho}{\mu - \lambda}, \quad \rho = \frac{\lambda}{\mu}
排队等待时间与系统繁忙程度密切相关。利用率 ρ 越接近 1,等待时间增长得越快,这也是“不能把集群塞满”的数学原因。
  1. 到达率单位时间内新提交任务的个数,用 λ 表示,可以理解为食堂每分钟进店的人数。
    λ=新任务数单位时间\lambda = \frac{\text{新任务数}}{\text{单位时间}}
  2. 服务率单位时间内调度器能处理完的任务数,用 μ 表示,即窗口每分钟能打多少份饭。
    μ=完成任务数单位时间\mu = \frac{\text{完成任务数}}{\text{单位时间}}
  3. 系统利用率到达率与服务率的比值 ρ,表示系统有多忙。
    ρ=λμ\rho = \frac{\lambda}{\mu}
  4. 平均等待时间任务在队列中等待的时间,与利用率成正比,与剩余服务能力成反比。
    Wq=ρμ−λW_q = \frac{\rho}{\mu - \lambda}

配置最佳实践

食堂经理把所有的规矩写成一本《食堂管理手册》:窗口怎么设、餐券怎么发、蒸锅怎么分、每桌限额多少。GPU 集群的调度配置也是一本“手册”,只是它写在不同系统的配置文件里。最主流的两个调度系统——Slurm 和 Kubernetes——各有各的写法,但思路相通。

Slurm 是传统 HPC 领域的事实标准。管理员通过 slurm.conf 定义多个 Partition(分区),每个分区相当于一个队列,可以设置 PriorityTier(优先级层级)、MaxTime(最长运行时间)、MaxNodes(最大节点数)等参数。作业提交时用 -p 指定分区,用 --priority 或 --qos 设置优先级。Slurm 还支持多因素优先级插件,把年龄(等待时间)、QOS、公平共享因子加权计算成最终优先级。

Kubernetes 则是云原生时代的选择。它通过 PriorityClass 对象定义优先级:创建一个名为 high-priority 的 PriorityClass,设置 value: 1000000,再把 Pod 关联到它,调度器就会优先调度该 Pod。kube-scheduler 支持抢占(Preemption),高优先级 Pod 无法调度时会驱逐低优先级 Pod。ResourceQuota 和 LimitRange 则负责配额管理。

实战中,组合配置才能发挥最大效果:用队列组织任务流,用权重控制比例,用优先级处理紧急任务,用配额设置上限。例如,一个 100 卡的集群可以这样规划:在线推理队列权重 5、配额 50 卡、优先级最高;模型训练队列权重 3、配额 35 卡;离线评测队列权重 1、配额 15 卡、优先级最低。最后,别忘了持续监控——每周看一次队列堆积量、平均等待时间、GPU 利用率,按实际情况调整权重和配额。

手册写好了,食堂才能高效运转。Slurm 和 K8s 只是不同的“手册格式”,背后都是同一套管理哲学:队列定秩序、优先级定先后、权重定比例、配额定边界、监控定调优。掌握了这套组合拳,你就能从容地经营一个服务数千人的“GPU 食堂”。

Pfinal=Pclass+α×AageP_{final} = P_{class} + \alpha \times A_{age}
实际的调度优先级可以理解为“业务等级 + 等待时长补偿”。业务等级由 PriorityClass 或 Partition 的优先级字段决定,等待补偿则随着排队时间增长,防止老任务被无限搁置。
  1. 业务等级管理员为任务类别设定的基础优先级,如关键业务 100、普通任务 10。
    Pclass∈{0,10,100,…}P_{class} \in \{0, 10, 100, \ldots\}
  2. 等待时长任务入队后已经等待的时间,单位可以是分钟或小时。
    Aage=当前时间−提交时间A_{age} = \text{当前时间} - \text{提交时间}
  3. 补偿系数一个可调的超参数 α,决定等待时间对最终优先级的影响力度。
    α>0\alpha > 0
  4. 最终优先级业务等级加上等待补偿,作为调度器排序的最终依据。
    Pfinal=Pclass+α×AageP_{final} = P_{class} + \alpha \times A_{age}

AI 应用场景

大模型训练平台的团队级队列治理

阶段:算力资源调度与治理(MLOps)

解决的问题:多个算法团队共享 GPU 集群时,核心训练任务常被离线批处理任务堵住。传统做法是人工协调:谁急着用卡就找人改配置,效率低且容易出错。通过队列权重与优先级组合,能让紧急实验自动“插队”,低优任务只在空闲时运行。

具体案例:某公司 100 卡集群中,A 团队做在线推理(时延敏感),B 团队跑夜间训练(可等待)。管理员建了三个队列:高优在线(权重 5、配额 50 卡)、普通训练(权重 3、配额 35 卡)、离线批处理(权重 1、配额 15 卡),并给在线推理任务设置最高 PriorityClass。结果在线推理时延稳定在 50ms 内,训练任务也能在 2 小时内等到卡。

高校计算中心的 Slurm 多租户管控

阶段:HPC 集群运维与科研计算支持

解决的问题:全校各课题组共用一台 Slurm 集群时,热门的深度学习组总把 GPU 占满,冷门课题组的作业永远排不上。手动删任务、私下协调屡见不鲜。通过分区(Partition)加优先级层级(PriorityTier)加 QOS 配额,把资源分配规则制度化。

具体案例:某高校计算中心为“国家重点项目”分区设 PriorityTier=1000,为普通课程作业设 PriorityTier=100;同时配置 QOS 限制单任务最长运行 7 天、每账号最多同时运行 20 个任务。短小的作业可以“插队”运行,长任务通过抢占被暂停后恢复,学生的课程实验不再需要熬夜抢卡。

K8s 集群的 GPU 共享与抢占

阶段:云原生平台调度与资源治理

解决的问题:K8s 集群中多个业务方共享 GPU 节点,低优先级的批处理模型推理与高优先级的在线 API 服务互相争抢资源,导致核心服务响应变慢。传统做法是给每个业务单独建集群,成本翻倍。

具体案例:某 AI 平台在 K8s 上定义了三个 PriorityClass:critical(value=1000000)用于在线 API,normal(value=1000)用于模型训练,batch(value=100)用于数据预处理。当 GPU 不足时,batch Pod 会被 critical Pod 抢占驱逐;配合 ResourceQuota 限制每个命名空间最多 50 卡,杜绝了单业务“囤卡”现象。

Python 演示

python108 行
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""加权优先级调度模拟器: 演示GPU集群中队列权重与优先级抢占的基本逻辑"""import random  class Task:    """一个GPU作业任务, priority越小优先级越高"""    def __init__(self, tid, priority):        self.tid = tid        self.priority = priority      # 0 高, 1 中, 2 低        self.remaining = random.randint(3, 8)  # 剩余执行秒数     def __repr__(self):        return "Task({}, P{})".format(self.tid, self.priority)  class WeightedScheduler:    """按权重轮询 + 高优抢占 的简易调度器"""    def __init__(self, weights, gpu_num=4):        self.weights = weights               # [3, 2, 1] 代表高:中:低        self.queues = [[], [], []]        self.gpus = [None] * gpu_num        self.clock = 0        self.cursor = 0                      # 权重轮询游标        self.total_weight = sum(weights)        self.finished = []        self.task_count = 0     def submit(self, priority):        self.queues[priority].append(Task(self.task_count, priority))        self.task_count += 1     def _pick_queue(self):        """按权重比例轮询, 返回当前可取的队列编号, 无任务则返回None"""        for _ in range(self.total_weight):            pos = self.cursor % self.total_weight            acc = 0            for i, w in enumerate(self.weights):                acc += w                if pos < acc:                    self.cursor += 1                    if self.queues[i]:                        return i                    break            self.cursor += 1        return None     def _preempt(self):        """高优任务在GPU全满时抢占低优任务, 被抢占者回到队首"""        for qi in range(len(self.queues)):            if not self.queues[qi]:                continue            victim = -1            victim_prio = -1            for i, gpu in enumerate(self.gpus):                if gpu is not None and gpu.priority > qi and gpu.priority > victim_prio:                    victim_prio = gpu.priority                    victim = i            if victim >= 0:                old = self.gpus[victim]                old.remaining += 1                self.queues[old.priority].insert(0, old)                self.gpus[victim] = self.queues[qi].pop(0)                return True        return False     def step(self):        self.clock += 1        r = random.random()        if r < 0.35:            self.submit(2)        elif r < 0.70:            self.submit(1)        else:            self.submit(0)         for i in range(len(self.gpus)):            if self.gpus[i] is not None:                self.gpus[i].remaining -= 1                if self.gpus[i].remaining <= 0:                    self.finished.append(self.gpus[i])                    self.gpus[i] = None            if self.gpus[i] is None:                qi = self._pick_queue()                if qi is not None:                    self.gpus[i] = self.queues[qi].pop(0)         if all(g is not None for g in self.gpus):            self._preempt()     def run(self, steps=200):        for _ in range(steps):            self.step()        return len(self.finished)  if __name__ == "__main__":    sch = WeightedScheduler(weights=[3, 2, 1], gpu_num=4)    done = sch.run(200)    remain = sum(len(q) for q in sch.queues)    per_prio = [0, 0, 0]    for t in sch.finished:        per_prio[t.priority] += 1    print("==== 加权优先级调度模拟结果 ====")    print("提交任务总数:", sch.task_count)    print("完成数量:", done, " 仍在排队:", remain)    print("高/中/低优先级完成数:", per_prio)
目的:用 200 个模拟时间步演示 GPU 集群的加权轮询和优先级抢占:高优先级任务提交概率低但能抢占低优任务,跑完后统计各队列完成情况。
  1. class WeightedScheduler:定义一个调度器类,内部包含队列、GPU 槽位和轮询游标。
  2. self.weights = weights保存队列权重配置,如高:中:低 = 3:2:1,权重大的队列调度机会多。
  3. self.queues[priority].append(...)新任务提交时进入对应优先级的队列,相当于食堂员工站到对应窗口的队伍末尾。
  4. pos = self.cursor % self.total_weight用游标取模实现权重轮询,游标每走一步选一个队列。
  5. if pos < acc: ... return i判断游标落在哪个权重区间,落在哪个区间就选哪个队列出队。
  6. if gpu.remaining <= 0: self.finished.append(gpu)任务执行完毕,释放这个 GPU 槽位并记入完成列表。
  7. old.remaining += 1被抢占的任务重新入队时增加 1 秒惩罚开销,模拟“被赶下 GPU 后的冷启动损耗”。
  8. if all(g is not None for g in self.gpus): self._preempt()只有全部 GPU 都忙时才触发抢占,避免不必要的任务迁移。

常见误区

误区把任务的优先级设到最高,提交后就能马上拿到 GPU 开始跑,不用排队。

正解优先级只决定“排队顺序”,不改变“资源是否可用”这一前提。如果集群的 GPU 全部被其他高优任务占满,且没有可抢占的低优任务,最高优先级的任务也只能等着。就像食堂的 VIP 券只能让你排到最前面,但如果打饭窗口全都忙不过来,VIP 也得等出餐。

为何常见:很多运维文档强调“高优先级任务优先调度”,让初学者误以为优先级高等于“瞬间执行”,忽略了资源容量和排队前提。

误区给 A 队列设了权重 5,那 A 队列就永远独占 5 份资源,其他队列碰都不能碰,即使 A 队列根本没用满。

正解权重描述的是“竞争状态下的分配比例”,不是“预留的专属资源”。当高权重队列空闲时,它的份额会自动让给其他有需求的队列使用,以提高集群利用率。类比食堂的蒸锅:套餐窗口有 3 口锅,但面食窗口排长队时,套餐窗口空着的锅可以借来蒸馒头。

为何常见:把“权重”和“配额”混为一谈。权重强调弹性共享、按比例分配;配额强调绝对上限。两者是不同维度的机制。

误区给每个团队都设置接近集群总量的配额上限,这样自己想用多少都有保障。

正解配额上限设得过大等于没有配额。如果所有租户都设成集群总量,出现资源竞争时调度器无法保证任何一家的资源底线,排队时间不可预期。合理的做法是:配额上限覆盖团队 90% 场景的峰值需求,再留一个共享池应对突发流量。

为何常见:看到“配额”二字就联想到“帮我预留的资源”,忽略了配额只是“最多能用多少”的限制,并不具备预留语义。

误区集群搭建时把队列、权重、配额一次性配好,之后就再也不用管了,任务排队慢就是集群不行。

正解业务负载是动态变化的:新团队加入、旧任务变多、模型规模翻倍、节假日用量骤降。管理员需要定期观察各队列的堆积量、平均等待时间、GPU 利用率,动态调整权重和配额。食堂经理也不可能只开学第一天定规矩就再也不看窗口排队情况。

为何常见:配置文件写好后静态存储,看起来像“基础设施”,让人误以为调度策略是一次性工程,忽略了资源治理是持续运营的过程。

面试题

Q1. GPU集群中引入“队列”的核心目的是什么?

单选题 · easy

  • A.让所有作业随机获得资源
  • B.有序管理等待中的作业← 正确答案
  • C.提高每个任务的运行速度
  • D.减少服务器上的GPU数量

答案:B。队列把等待资源的作业排成有序序列,按规则依次调度,就像食堂窗口前的队伍。随机获取会让调度混乱,队列本身不加速任务运行,也不改变 GPU 数量。

Q2. 队列默认的调度顺序通常是哪种?

单选题 · easy

  • A.后进先出按栈的顺序
  • B.随机选取任一作业运行
  • C.先来后到依次执行← 正确答案
  • D.按作业大小倒序排列

答案:C。默认先进先出(FIFO),先提交的作业先获得资源。这保证排队秩序可预测,用户能估算等待时间。后进先出、随机、倒序都不是默认规则。

Q3. 关于任务的优先级数值,下列哪项说法正确?

单选题 · mid

  • A.数值越大优先级越高
  • B.数值越小优先级越高← 正确答案
  • C.数值大小与优先级无关
  • D.所有任务的优先级相同

答案:B。在 Slurm 和 K8s 等主流调度器中,优先级数值越小代表级别越高,越先被调度。好比餐券等级 1 级永远排在 3 级前面。

Q4. 权重为3:2:1的三个队列,高优队列理论应得多少资源?

单选题 · mid

  • A.三分之一左右的资源
  • B.二分之一左右的资源← 正确答案
  • C.六分之一左右的资源
  • D.百分百的集群资源

答案:B。总权重是 3+2+1=6,高优队列占 3/6=1/2,即一半资源。权重决定竞争时的分配比例,而不是固定数量。

Q5. 资源配额 Q_min 的主要作用是什么?

单选题 · mid

  • A.限制集群最大资源总量
  • B.保证租户最低资源底线← 正确答案
  • C.提高任务单个执行速度
  • D.禁止租户使用共享资源

答案:B。Q_min 是最小保证配额,调度器承诺优先为该租户补足这部分资源,防止某团队永远抢不到资源。它与集群总量无关,也不影响单任务速度。

Q6. 加权公平调度中“防饿死”机制解决的是什么问题?

单选题 · hard

  • A.高优任务抢占低优任务
  • B.低权重队列长期拿不到资源← 正确答案
  • C.任务执行时间无限延长
  • D.队列中任务数量无限增长

答案:B。防饿死保证即使权重极低的队列也能获得最少一份资源,不让小团队永远排不上。类比食堂保证每家公司至少能订到一份午饭。

Q7. 排队论中当系统利用率趋向于1时,平均等待时间如何变化?

单选题 · extreme

  • A.线性缓慢地增加
  • B.逐渐降低趋于零
  • C.急剧快速地增加← 正确答案
  • D.保持恒定不再变化

答案:C。由 W_q = ρ/(μ-λ) 可知,当利用率 ρ 趋近 1 时,μ-λ 趋近 0,等待时间趋向无穷大。因此几乎满载的集群会出现排队时间急剧恶化。

Q8. 哪些方式可以影响一个任务的调度优先级?

多选题 · mid

  • A.用户提交时显式指定← 正确答案
  • B.等待时间自动增长← 正确答案
  • C.修改服务器网卡型号
  • D.业务紧急度高级策略← 正确答案

答案:ABD。用户可显式设优先级;多因素优先级插件会让等待时间参与计算;管理员也能为关键业务设置更高基础优先级。网卡型号与 CPU 调度优先级无关。

Q9. 关于高优先级任务的抢占机制,正确的有哪些?

多选题 · hard

  • A.GPU全忙时高优可抢占低优← 正确答案
  • B.被抢占的任务会回到队列中← 正确答案
  • C.抢占过程完全没有任何开销
  • D.抢占通常伴随着迁移开销← 正确答案

答案:ABD。抢占发生在资源全忙且高优任务到来时,被抢占任务回到队列重新排队。抢占涉及保存/恢复状态、重新调度等开销,并非零成本。

Q10. Slurm 与 Kubernetes 中配置优先级常用的手段是哪些?

多选题 · mid

  • A.Slurm 中设置分区优先级权重← 正确答案
  • B.K8s 中创建 PriorityClass 对象← 正确答案
  • C.直接修改内核的任务调度器
  • D.等待时间自然增长来调节← 正确答案

答案:ABD。Slurm 用 PriorityTier/多因素优先级,K8s 用 PriorityClass 的 value 值,等待时长作为年龄因子参与计算。修改内核调度器属于系统底层操作,不是应用层常规配置手段。