KAI Scheduler 深入拆解(三):调度框架内核——Cycle、Snapshot、Session、Action、Statement
如果说上一篇讲的是控制面工作流,那么这一篇要进入 KAI Scheduler 最核心的部分:调度内核是怎么组织的。
我读 KAI 代码时最大的感受是,它并不是把所有逻辑揉进一个“大调度函数”,而是把调度过程拆成了几层非常稳定的抽象:
- cycle
- cache
- snapshot
- session
- actions
- plugins
- statement / scenario
这些抽象不是为了“显得框架化”,而是为了支撑 KAI 真正需要处理的复杂性:
- gang scheduling
- queue fairness
- reclaim / preempt
- GPU sharing
- DRA
- topology-aware scheduling
- hierarchical podgroups
- time-based fairshare
如果没有这套结构,任何一个高级特性都可能让调度逻辑失控。
先看最短的调度主循环
在 pkg/scheduler/scheduler.go 里,KAI 的调度主干其实很短:
1 | func (s *Scheduler) Run(stopCh <-chan struct{}) { |
真正的重点在 runOnce():
1 | func (s *Scheduler) runOnce() { |
这段代码非常能代表 KAI 的设计哲学:
scheduler 自己不直接写死“怎么调度每个 Pod”,它做的是:打开一个 session,然后把控制权交给有顺序的 action 和可插拔的 plugin。
也就是说,KAI 的内核不是一个算法,而是一个 按周期运行的调度执行框架。
第一层:Cycle —— 调度不是事件回调,而是周期性计算
KAI 文档里一直强调“scheduling cycle”,这不是表述习惯,而是真正的架构选择。
每个 cycle 的流程可以简化成:
flowchart LR
Start([Cycle Start]) --> Cache[Cache Sync]
Cache --> Snapshot[Take Snapshot]
Snapshot --> Session[Open Session]
Session --> Actions[Execute Actions]
Actions --> Close[Close Session]
Close --> End([Cycle End])
为什么 KAI 选择 cycle model
对于只做简单 node fitting 的调度器,事件驱动模型也许足够。但 KAI 需要解决的问题更复杂:
- 一个
PodGroup能不能整体调度成功? - 某个 queue 是不是超出 fair share?
- 为了让高优 workload 运行,当前要不要 reclaim / preempt?
- 某个 topology domain 是否更适合这个 workload?
- 某些资源是否能在一个模拟场景里整体成立?
这些问题都更像“在一个一致性视图上做组合决策”,而不是“处理一个 Pod Add 事件”。
cycle model 的好处正是:
- 所有决策基于同一份快照
- 复杂动作可以按顺序执行
- 调度器可以先模拟,再提交
- 高成本策略可以集中在每轮计算里完成
第二层:Cache —— 调度器真正依赖的是聚合态,而不是零散 API 调用
KAI 的 cache 在架构上是非常重要的一层。文档 docs/developer/scheduler-concepts.md 也把它放在很前面。
它的职责并不是简单“缓存 informer 数据”,而是负责:
- 聚合多个 Kubernetes / KAI 资源对象
- 维护调度需要的派生状态
- 生成 snapshot
- 把调度提交结果写回集群
也就是说,cache 同时承担了:
- 输入聚合层
- 状态维护层
- 提交出口层
在 KAI 里,cache 可以被理解成 scheduler 的“authoritative runtime state”。
为什么这层很关键
因为 scheduler 需要同时看:
- nodes
- pods
- podgroups
- queues
- bindrequests
- storage / DRA / resource claims
- 历史 usage
如果每次 action 执行都直接查 API server,复杂度和性能都会失控。KAI 的做法是先把这些状态汇总到 cache,再在 snapshot 上运行策略。
第三层:Snapshot —— 每一轮调度都从一致性视图出发
在 KAI 的设计里,snapshot 不是一个调试辅助对象,而是调度 cycle 的正式输入。
openSession() 里最关键的一步就是:
1 | snapshot, err := cache.Snapshot() |
也就是说,session 看到的整个世界,都是这一刻从 cache 切出来的静态视图。
snapshot 的意义
snapshot 的核心价值在于三个词:
- 一致性
- 可模拟
- 可复现
一致性
同一轮调度里,所有 action / plugin 面对的是同一个 cluster image。
可模拟
你可以在 session 里虚拟 allocate / evict / reorder,而不立刻影响真实集群。
可复现
如果你想调试某一轮调度,理论上 snapshot 是最接近“输入快照”的东西。这也是 KAI 为什么会有 snapshot-tool 这类工具。
第四层:Session —— 一轮调度真正的执行上下文
如果说 snapshot 是输入,那 session 就是这一轮调度的“运行时容器”。
pkg/scheduler/framework/session.go 里的 Session 结构体非常值得看,因为它几乎把 KAI 的扩展点都明牌写出来了:
GpuOrderFnsNodePreOrderFnsNodeOrderFnsJobOrderFnsSubGroupOrderFnsTaskOrderFnsQueueOrderFnsPrePredicateFnsPredicateFnsBindRequestMutateFnsCanReclaimResourcesFnsReclaimVictimFilterFnsPreemptVictimFilterFnsGetQueueFairShareFns- event handlers
这说明 KAI 的 session 不是“存点变量”的上下文对象,而是 整个调度框架的插件总线。
Session 负责什么
从设计上看,Session 至少承担四件事:
- 保存 snapshot 得到的
ClusterInfo - 承载本轮调度的 callback registry
- 提供绑定、驱逐、节点排序、GPU 排序等调度操作
- 为 statement/scenario 提供统一语义边界
这层设计的妙处
它把“当前一轮调度会发生什么”从长期状态里剥离出来了。
- cache 是长期维护的系统状态
- session 是某一轮的计算上下文
这能让 plugin 在 OnSessionOpen 时做本轮初始化,在 OnSessionClose 时做清理或指标记录,而不用在长期全局状态里互相污染。
第五层:Action —— 流程由 action 决定,不由 plugin 决定
KAI 文档 docs/developer/action-framework.md 给出的 action 顺序是:
AllocateConsolidateReclaimPreemptStaleGangEviction
这五个动作本身就已经表达了一种非常清晰的调度哲学:
先尝试无破坏地调度,再尝试优化碎片,再尝试跨 queue 回收资源,再尝试队列内优先级抢占,最后清理 gang 死锁状态。
换句话说,Action 负责的是“调度流程的阶段控制”。
为什么 action 顺序很重要
KAI 不是随便堆几个插件,而是明确规定了“先做什么,后做什么”。
Allocate
优先处理无需干扰即可落地的工作负载。
Consolidate
在已有布局上做整理,减少碎片。
Reclaim
当 queue 之间资源分配不公平时,做跨 queue 回收。
Preempt
当同一 queue 内高优 workload 需要资源时,再做优先级抢占。
StaleGangEviction
处理 gang 语义下的僵死/失效场景。
这个顺序说明 KAI 很重视“尽量把破坏性动作后置”。
第六层:Plugin —— 策略由 plugin 注入,不由 action 写死
如果说 action 决定“什么时候干什么”,那 plugin 决定“具体按什么规则干”。
在 pkg/scheduler/framework/framework.go 里,OpenSession() 会读取配置里的 tiers 和 plugins,然后依次:
- 取到 plugin builder
- 实例化 plugin
- 调用
plugin.OnSessionOpen(ssn)
在 OnSessionOpen 里,plugin 把自己的 callback 注册进 session。
这意味着什么
KAI 的 plugin 不是“调用时才临时执行的一段代码”,而是会在 session 打开时把行为注入调度上下文。例如:
- queue ordering
- task ordering
- node scoring
- GPU scoring
- predicates
- fairness calculation
- reclaim / preempt validation
- bind request mutation
- allocate / deallocate event handlers
一个非常关键的区分
很多调度框架里,action 和 plugin 的边界不够清楚;而 KAI 这里非常明确:
- Action = 调度流程阶段
- Plugin = 每个阶段使用的策略函数集合
这让系统具备两个非常好的性质:
流程稳定
- cycle 还是那个 cycle,action 还是那些 phase。
策略可演化
- node placement、fairness、topology、min runtime、dynamicresources 都可以通过 plugin 扩展。
第七层:Statement / Scenario —— KAI 最有意思的设计之一
KAI 文档里有两个很重要但很容易被忽略的概念:
ScenarioStatement
我认为这两个概念是 KAI 能处理复杂调度动作的关键。
Scenario:先构造“如果这样调度,会发生什么”
Scenario 本质上是一个假设中的调度状态。
比如某个 reclaim / preempt / consolidate 场景,不是直接去驱逐 Pod,而是先在内存里构造一个可能的结果:
- 如果把这几个 Pod 挪走会怎样?
- 如果这个 PodGroup 迁移到另一个 domain 会怎样?
- 如果先释放某些资源再放入新 workload,会不会整体成立?
这意味着 KAI 不是“想到一个动作就立刻提交”,而是先做 what-if analysis。
Statement:把一组调度操作组织成事务式单元
文档把 Statement 描述成 transaction-like object,我觉得这个词非常准确。
它支持:
Checkpoint()Rollback(checkpoint)Allocate(pod, node)Evict(pod, msg)Pipeline(pod, node)Commit()Discard()
也就是说,调度器可以在本轮 session 中:
- 先尝试若干虚拟操作
- 观察这些操作在 plugin callback、资源状态、fairness 上的影响
- 如果不成立就 rollback
- 如果成立再 commit
为什么这非常适合 KAI
因为 KAI 做的不只是“找个 node fit 一下”。它经常需要处理下面这种组合问题:
- 一个
PodGroup的多个 Pod 必须整体成功 - 某个 queue reclaim 以后,另一个 queue 是否真的能受益
- topology domain 切换后,整体 locality 是否更优
- consolidation 是否会造成新的碎片
- DRA / resource claim 场景是不是整体可行
没有 statement/scenario,很多逻辑要么只能做局部贪心,要么必须把复杂回滚散落在各处。KAI 用一层统一抽象把这个问题收住了。
Action + Plugin + Statement 这三个东西怎么配合
我觉得可以把它们理解成三层:
flowchart TD
A[Action] --> B[Statement / Scenario]
C[Plugin callbacks] --> B
B --> D[Commit to cache / cluster]
A1[Allocate / Consolidate / Reclaim / Preempt] -.控制流程.-> A
C1[Predicate / Order / FairShare / Bind Mutate] -.提供策略.-> C
B1[Checkpoint / Rollback / Commit] -.提供事务化模拟.-> B
- action 决定“当前做哪类动作”
- plugin 决定“按什么规则评估和排序”
- statement/scenario 决定“怎么安全地模拟和提交”
这三个层次组合在一起,才形成 KAI 的调度内核。
为什么这套框架足以承载高级能力
KAI 后续的很多高级特性,其实都不是“另起炉灶”,而是在这套框架上叠加出来的:
- 拓扑感知:本质上是 plugin + node/domain ordering
- fairshare:本质上是 queue ordering / resource accounting / reclaim policy
- time-based fairshare:本质上是 fairness 数据输入变得更丰富
- hierarchical podgroups:本质上是 workload model + subgroup ordering + action evaluation 更复杂
- DRA:本质上是 predicates / bind execution / resource claims 集成
也就是说,KAI 的高阶能力不是靠一堆特判支撑,而是靠一个足够稳的 execution model 支撑。
我对 KAI 调度框架的一个总结
如果只用一句话概括这套内核,我会写成:
KAI 把调度建模成:在一致性 snapshot 上打开一个 session,由 action 驱动流程、由 plugin 注入策略、由 statement/scenario 提供可回滚模拟,然后把成立的结果提交回集群。
这是一个非常适合复杂 workload 调度的模型。
它的好处不是“优雅”,而是很实用:
- 可以承载越来越复杂的 AI workload 语义
- 可以在保证流程稳定的同时持续增加新策略
- 可以把破坏性动作控制在明确的阶段和边界里
- 可以让调试和离线复现更有抓手
下一篇看什么
框架理解了以后,最自然的下一步就是:
- 代码里到底是怎么启动的?
RunApp()到runOnce()之间发生了什么?- action / plugin 注册和配置覆盖怎么配合?
BindRequest是在哪一层创建的?- binder 又是怎么接住这个对象的?
下一篇就从源码入口开始,沿着真正的调用链,走一遍 KAI 的一次调度周期。