Kthena 源码解析:云原生 LLM 推理平台的架构与实现

项目地址:https://github.com/volcano-sh/kthena | 官网:https://kthena.volcano.sh

本文基于 kthena 仓库 main 分支源码进行系统分析,覆盖整体架构、控制面 CRD 与控制器、数据面路由与调度器、PD 分离、弹性扩缩容与运行时 Agent 等核心模块。

一、Kthena 是什么

Kthena 是 Volcano 社区推出的 Kubernetes 原生 LLM 推理平台,目标是把大模型推理这件事在 K8s 上变得简单、可扩展、低成本。它不替代 vLLM / SGLang 这类推理引擎,而是作为推理引擎之上的「流量枢纽 + 调度中心」,用声明式 CRD 管理模型全生命周期,用智能路由分发推理流量。

定位上有三个关键词值得记住:

  • Lightweight(轻量):整个平台就是两个自包含的 Go 二进制,依赖面小,安装便宜、升级简单。
  • Modular(可组合):控制面(workload)和数据面(networking)是两个独立的 Helm 子 chart,各自有独立的 CRD group 和发布周期,可以单独部署
  • Enterprise-Grade(企业级):支持多推理引擎(vLLM、SGLang、Triton)、Prefill-Decode 分离、成本驱动扩缩容、异构算力。

为什么需要它?生产级 LLM Serving 有几个老大难:KV Cache 动态占用显存导致传统 Round-Robin 负载均衡利用率低;Prefill(计算密集)和 Decode(访存密集)混部无法针对性优化;多租户多模型公平调度复杂;现有方案要么脱离 K8s 生态要么过于笨重。Kthena 正是冲着这些痛点来的。

二、双平面架构:控制面与数据面彻底解耦

Kthena 最核心的设计哲学是双平面(Two-Plane)架构,把「你声明什么」和「请求怎么流」彻底分开。

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────── 控制面 (Control Plane) ───────────────────────────┐
│ kthena-controller-manager(workload 子 chart / workload.serving.volcano.sh) │
│ 职责:把 CRD 协调成运行时资源(部署、扩缩、升级推理副本) │
│ 控制器:ModelBooster / ModelServing / Autoscaler │
└──────────────────────────────────┬───────────────────────────────────────────┘
│ 只与 K8s API 交互
│ 两个平面之间无任何运行时调用、无进程内状态共享
┌──────────────────────────────────┴───────────────────────────────────────────┐
│ 数据面 (Data Plane) │
│ kthena-router(networking 子 chart / networking.serving.volcano.sh) │
│ 职责:每个推理请求经 6 阶段管线,模型感知地调度到最优 Pod │
│ 管线:鉴权 → 限流 → 公平调度 → 调度(filter/score/select) → 负载均衡 → 代理 │
└──────────────────────────────────────────────────────────────────────────────┘

这一点在源码里体现得非常干净。两个二进制入口分别是 cmd/kthena-controller-manager/main.gocmd/kthena-router/main.go,它们各自启动自己的 webhook、controller、server,互不感知。部署形态因此有三种合法组合:

你想要…… 安装 说明
只管模型工作负载 workload 子 chart ModelServing/AutoscalingPolicy,流量走你自己的网关或 Service
只做推理路由 networking 子 chart ModelServer 通过 workloadSelector 标签发现任意后端 Pod
全平台 两个子 chart 才能用一站式 ModelBooster API,它会级联创建两组 CRD
1
2
3
4
5
6
7
8
9
# 只装控制面
helm install kthena oci://ghcr.io/volcano-sh/charts/kthena \
--namespace kthena-system --create-namespace \
--set networking.enabled=false

# 只装数据面
helm install kthena oci://ghcr.io/volcano-sh/charts/kthena \
--namespace kthena-system --create-namespace \
--set workload.enabled=false

「每个组件只跟 K8s API 说话,从不互相调用」——这是 Kthena 模块化的根基,也是它能被分块采用的前提。

三、控制面:分层 CRD 与三个控制器

控制面用一套分层 CRD 模型表达意图:一个高层资源级联展开成一组细粒度原语,你可以选择在任意抽象层操作。

1
2
3
4
5
6
ModelBooster  (一站式部署 API)
├── ModelRoute – 路由规则、灰度权重、限流
├── ModelServer – 服务暴露、流量策略、后端发现
├── ModelServing – 副本拓扑:ServingGroup × Role(Prefill/Decode)
├── AutoScalingPolicy – 指标触发、扩缩行为、panic 模式
└── AutoScalingPolicyBinding – 把策略绑到工作负载,做成本感知优化

CRD 分属两个 group:workload.serving.volcano.shModelServing/ModelBooster/AutoscalingPolicynetworking.serving.volcano.shModelRoute/ModelServer/ExternalModelProvider。源码定义在 pkg/apis/workload/v1alpha1/pkg/apis/networking/v1alpha1/

3.1 ModelServing:三级工作负载模型

ModelServing 是工作负载的核心抽象,采用 ModelServing → ServingGroup → Role 三级结构。看 model_serving_types.go 的 Spec:

1
2
3
4
5
6
7
8
type ModelServingSpec struct {
Replicas *int32 // ServingGroup 数量,即实例数,默认 1
SchedulerName string // 默认 volcano
Plugins []PluginSpec // 可选插件链定制 Pod
Template ServingGroup // ServingGroup 模板
RolloutStrategy *RolloutStrategy // 滚动更新策略
RecoveryPolicy RecoveryPolicy // 故障恢复策略,默认 RoleRecreate
}

几个值得注意的设计:

  • RecoveryPolicy 有三档:ServingGroupRecreate(组内任一 Pod 重建则整组重建)、RoleRecreate(按角色整组重建,默认)、None(跟随默认 Pod/Deployment 行为)。这是为分布式推理服务的——张量并行下各 worker 必须同时启动,局部重建会破坏一致性。
  • RolloutStrategy 支持 ServingGroupRollingUpdate(按组滚动)和 RoleRollingUpdate(跨组按角色同时滚动,建议单组场景用)。
  • PluginsPluginSpec(name + type + opaque JSON config + scope)声明插件实例,scope 可以按 roles 和 target(Entry/Worker/All) 收窄。这给了工作负载很强的可定制性,而不必改控制器代码。

每个 replica(ServingGroup)内部按角色组织,角色对应 LLM 推理两种本质不同的负载:

角色 负载特征 扩缩策略
Prefill 计算密集的 prompt 初始化与上下文编码 按吞吐扩——多 prompt 批处理
Decode 访存密集、延迟敏感的逐 token 生成 按延迟扩——最小化每 Pod 队列深度

角色副本数独立设置(如 2 prefill : 4 decode),让硬件匹配负载特征,避免任一阶段过供。

3.2 ModelBooster:开箱即用的模型上架

ModelBooster 是高层 opinionated API,一个资源把模型 spec 写清楚,它就级联创建/更新/删除所有下游资源。看 model_booster_types.go,核心是 ModelBackend

1
2
3
4
5
6
7
8
9
type ModelBackend struct {
Name string // 后端名,改了会删旧 ModelServing 建新的
Type ModelBackendType // vLLM / vLLMDisaggregated / SGLang / MindIE...
ModelURI string // hf:// ms:// s3:// obs:// pvc:// 多源
CacheURI string // pvc:// 或 hostpath://,模型缓存挂载点
Replicas int32
Workers []ModelWorker // server/prefill/decode/controller/coordinator
...
}

ModelURI 支持 HuggingFace、ModelScope、S3、OBS、PVC 多种模型源;WorkersType 可以是 server/prefill/decode/controller/coordinator,覆盖原生部署、PD 分离、大规模 EP 等多种形态。一个 ModelBooster YAML 声明清楚 backend + workers + 资源 + engine config,剩下的路由/服务/扩缩全由控制器自动生成。

3.3 三个控制器

控制器 职责
Model Booster Controller reconcile ModelBooster → 下游原语,传播更新,编排级联生命周期
Model Serving Controller 管理 ServingGroup 与角色副本,拓扑/ gang 感知放置、故障恢复、滚动升级、entry/worker 模板
Autoscaler Controller 按运行时指标对照 AutoScalingPolicy 计算期望副本数,经 Binding 在异构硬件间调整实例组合

Model Serving Controller 是最重的一个,看 model_serving_controller.go 的方法列表就能感受到它的复杂度:syncServingGroupReplicassyncRoleReplicasscaleUpRoles/scaleDownRolesmanageRollingUpdatehandleReadyPod/handleErrorPod/handlePodAfterGraceTime……它把 ServingGroup 的扩缩、角色的滚动、Pod 的就绪/故障/优雅删除都管了起来。它还通过 podgroupmanager 与 Volcano scheduler 集成,实现拓扑感知与 gang 调度——gang 调度保证分布式推理组(如 xPyD)原子调度,避免部分部署造成的资源浪费。

四、数据面:请求级智能路由

数据面是 Kthena 的运行时路径。每个推理请求都流经 Kthena Router,在分发到最优推理 Pod 之前,依次过安全、公平、模型感知调度六道关。

4.1 六阶段请求管线

阶段 做什么
1. 鉴权认证 校验身份与权限(JWT,见 filters/auth
2. 限流 按 model/tenant 的吞吐限制,基于 token 而非仅请求数
3. 公平调度 按 model 的公平排队,防止一个模型的流量尖峰饿死共享集群的其他模型
4. 调度 核心智能层。可插拔调度器跑 filter → score → select,选出候选 Pod 集
5. 负载均衡 最终路由到候选后端实例,支持重试
6. 代理 分发请求到选中的推理 Pod,流式回传响应

Router 的入口在 pkg/kthena-router/router/router.go,基于 gin 构建。Router 结构体聚合了 scheduler、authenticator、store、loadRateLimiter、accessLogger、metrics、tokenizer 以及 KV connector factory:

1
2
3
4
5
6
7
8
type Router struct {
scheduler scheduler.Scheduler
authenticator *auth.JWTAuthenticator
store datastore.Store
loadRateLimiter *ratelimit.TokenRateLimiter
...
connectorFactory *connectors.Factory
}

两个互斥的调度策略由环境变量开关:ENABLE_FAIRNESS_SCHEDULING(按用户历史 token 用量排序的公平队列)和 ENABLE_SESSIONBoost(会话感知提升 prefix cache 复用),同时开启会直接 klog.Fatalf。限流是 token 级的——TokenRateLimiter 不仅按请求限,还按输入/输出 token 限,这比传统网关更贴合 LLM 的成本模型。

4.2 可插拔调度器:filter/score 框架

调度器是数据面的心脏。它镜像了 K8s 调度器的 filter/score 模式,但作用在请求级,运行在微秒级而非毫秒级。框架接口在 scheduler/framework/interface.go

1
2
3
4
5
6
7
8
9
10
11
12
13
type FilterPlugin interface {
Name() string
Filter(ctx *Context, pods []*datastore.PodInfo) []*datastore.PodInfo
}
type ScorePlugin interface {
Name() string
// 每个插件给 pod 打 [0,100] 分
Score(ctx *Context, pods []*datastore.PodInfo) map[*datastore.PodInfo]int
}
type PostScheduleHook interface { // 调度完成后执行(如更新缓存)
Name() string
PostSchedule(ctx *Context, index int)
}

调度上下文 Context 携带 Model、Prompt、SessionID、Hashes,以及 PD 分组信息(DecodePods/PrefillPods 或 BestPods)。调度主循环在 scheduler_impl.goSchedule()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *SchedulerImpl) Schedule(ctx *framework.Context, pods []*datastore.PodInfo) error {
if s.syncOnFlight { s.store.SyncOnFlightCounts() } // least-request 启用时先从 Redis 同步在途计数

if ctx.PDGroup != nil {
// PD 分离:先 score decode pod,再为每个 decode 配同组 prefill,保证 KV 局部性
decodePods, _ := s.store.GetDecodePods(ctx.ModelServerName)
decodePods, _ = s.RunFilterPlugins(decodePods, ctx)
scores := s.RunScorePlugins(decodePods, ctx)
topNDecodePods := TopNPodInfos(scores, topN) // topN=5
// 为每个 top decode pod 找同 PD group 的 prefill,filter+score 后配对
...
return nil
}
// PD 聚合:filter → score → 取 topN
pods, _ = s.RunFilterPlugins(pods, ctx)
scores := s.RunScorePlugins(pods, ctx)
ctx.BestPods = TopNPodInfos(scores, topN)
return nil
}

注意几个细节:topN=5(取前五而不是最优一个,给负载均衡留余量);least-request 启用时会先从 Redis 同步跨 router 的在途计数(syncOnFlight),让多副本 router 共享全局视图。

Filter 插件(淘汰不合格 Pod)

插件 逻辑
Least Requests 丢弃超过可配 active-request 阈值的 Pod
LoRA Affinity 排除没加载所需 LoRA adapter 的 Pod

Score 插件(给剩余 Pod 加权打分)

插件 优化目标
Least Requests 偏好在途请求最少的 Pod,最小化排队延迟
Least Latency 最小化 TTFT(首 token 时间)和 TPOT(每 token 时间)
KV Cache Aware 偏向 KV cache 命中潜力最大的 Pod
Prefix Cache 把请求 prompt 前缀与各 Pod 缓存前缀匹配,最大化缓存命中
GPU Cache 考虑 GPU 显存利用率,避免请求被抢占

4.3 深入两个核心 Score 插件

这两个插件是 Kthena 路由智能的精华,值得单独看。

KV Cache Awarekvcache_aware.go):用 token 级 block 匹配 + Redis 分布式协调实现 KV cache 命中感知。流程是:

  1. 把 prompt 经 tokenizer(vLLM/SGLang 引擎的 tokenizer 端点)切成 token 序列;
  2. blockSizeToHash(默认 16 token)分块,每块用 SHA-256 算 63 位正整数 hash;
  3. 用 Redis pipeline 批量查询 matrix:kv:block:{model}@{hash} 这个 hash 结构,看哪些 Pod 缓存了该 block(field 是 pod.namespace,value 是写入时间戳);
  4. 只缓存了第一个 block 的 Pod 才有资格得分,然后做前缀逐块交集匹配——podScores[pod] == i 表示该 pod 在第 i 块仍匹配,巧妙地用分数本身充当活跃集合,无需额外交集 map;
  5. 得分 = 匹配块数 / 总块数 * 100

它还有一套 GC:后台 gcStaleFields 定时 SCAN + 删除超过 24h 的 stale field;freshOwners 会丢弃「写入时间早于 Pod 容器启动时间」的归属(Pod 重启后旧 cache 已失效),避免路由到一个已经不存在的 cache。这套设计让 KV cache 感知在多 router 副本、Pod 频繁重建的生产环境下仍然准确。

Prefix Cacheprefix_cache.go):与 KV Cache Aware 思路类似但更轻量,用滚动 hash 链做前缀匹配。每个 block 的 hash 由「上一块 hash + 当前块内容」组合生成,形成依赖链——这样匹配时从后往前,一旦某个 hash 命中就保证之前所有块都命中,无需逐块检查。用 LRU cache 记录 hash,淘汰时同步从三级 map(model → hash → pod)删除。它还实现了 PostScheduleHook,调度成功后把本次 prompt 的 hash 写回缓存。

两者的区别:KV Cache Aware 走 Redis 分布式、token 级、跨 router 共享;Prefix Cache 走本地 LRU、字符级(默认 64 字节 block)、单机。可以按场景混搭。

4.4 PD 分离路由:prefill 与 decode 配对

当请求目标是分离式 PD group 时,调度器先 score decode pod,再为每个候选 decode 在同一 PD group内配一个兼容的 prefill,保证 KV cache 局部性、最小化数据传输。这是 Schedule()ctx.PDGroup != nil 分支的逻辑。

实际代理在 proxyToPDDisaggregated(router.go:1394):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for i := 0; i < maxRetry; i++ {
prefillPod := ctx.PrefillPods[i].GetPod()
decodePod := ctx.DecodePods[i].GetPod()
// 构建在途计数 hooks,让 connector 在 prefill/decode 各阶段起止时增减计数
hooks := &connectors.OnFlightHooks{
IncrPrefill: func() { r.store.IncrPodOnFlightRequests(prefillPodName) },
DecrPrefill: func() { r.store.DecrPodOnFlightRequests(prefillPodName) },
IncrDecode: func() { r.store.IncrPodOnFlightRequests(decodePodName) },
DecrDecode: func() { r.store.DecrPodOnFlightRequests(decodePodName) },
}
outputTokens, err := kvConnector.Proxy(c, modelRequest, prefillAddr, decodeAddr, hooks)
if err != nil { continue } // 失败则换下一对
r.scheduler.RunPostHooks(ctx, i) // 成功则更新缓存
return nil
}

它会尝试多对 prefill/decode(最多 min(len(decode), len(prefill)) 对),某对失败就换下一对,全部失败才返回 500。KV cache 的实际传输由 KV Connector 抽象承担,Kthena 支持三种后端(见 connectors/):

Connector 取舍
LMCache 内存级,最低延迟,同节点或 RDMA;走共享 Redis backend
MoonCake 分布式,跨节点,容错
NIXL 轻量,基于 NCCL,GPU-direct;in-band kv_transfer_params 握手,无需共享后端

选择对应用透明——Runtime Agent 和 KV Connector 自动处理传输。

4.5 外部模型 Provider

Router 不只能路由到 Kthena 自己管理的 Pod。通过 ExternalModelProvider CRD 和 providers/(含 OpenAI、Anthropic adapter),它可以把请求路由到外部托管模型,ModelServerworkloadSelector 标签发现任意后端——Deployment、StatefulSet、别的 operator 管的 Pod、甚至外部 OpenAI 都行。这正是「数据面可单独部署」的底气。

五、弹性扩缩容:从同构到异构成本优化

Autoscaler 在 pkg/autoscaler/ 实现,分同构(Homogeneous)和异构(Heterogeneous)两种模式。

同构模式scaler.go):经典的单目标扩缩。MetricCollector 从 Pod metrics endpoint 采集 CPU/GPU/内存/自定义指标,scaleOneTarget 算法按 metric target、tolerance、min/max 副本、未就绪实例数计算推荐副本数,再经 behavior(stabilization window 防 flapping、panic window 应对流量尖峰)校正。支持 stable 和 panic 两种模式。

异构模式 / Optimizeroptimizer.go):这是 Kthena 的亮点——跨异构硬件的成本感知扩缩。比如同时有 H100 和 A100 两种实例,怎么分配副本最省钱又满足 SLO?看核心结构:

1
2
3
4
5
6
type ReplicaBlock struct {
targetKey string // 哪个后端
index int32
replicas int32
cost int64
}

NewOptimizerMeta 把每个后端的 [MinReplicas, MaxReplicas] 区间按 CostExpansionRatePercent 切成多个 ReplicaBlock(成本扩展率控制打包粒度),然后按 cost 升序排序,形成扩容顺序。RestoreReplicasOfEachBackend 按这个顺序贪心填充:先满足所有后端的 minReplicas,再把剩余配额按成本从低到高分配。这就是「成本-能力贪心放置」——优先扩便宜的实例,贵的留到最后。

Optimizer.Optimize 会聚合各后端 ready/unready 实例的指标、外部样本,按聚合后的指标算总需求,再映射回各后端的副本数。两种模式都支持 stable/panic 双窗口。

六、推理 Pod 架构与运行时 Agent

每个 replica 部署可能包含这些组件:

组件 职责
Entry Pod 角色请求的入口端点
Worker Pod(s) 执行模型推理计算(大模型跨 worker 张量并行)
Downloader 通用模型/制品拉取器——支持 HF、ModelScope、S3/OBS、PVC,并发下载 + 文件锁
Runtime Agent sidecar,代理并标准化引擎指标、暴露 LoRA 生命周期 API、处理 PD 的 KV cache 事件、提供模型下载端点
LLM Engine 真正的推理后端——vLLM / SGLang 等

Downloader 和 Runtime Agent 是 Python 实现,在 python/kthena/ 下,分成 downloader/runtime/ 两个子包:

  • downloader/huggingface.pymodelscope_downloader.pys3.pypvc.py + lock.py(文件锁防并发冲突)+ base.py 抽象基类。多源统一接口,PVC 共享缓存。
  • runtime/app.py 是 sidecar 入口,metric.py/collect.py 标准化 vLLM/SGLang 指标,kv_cache_manager.py 处理 KV cache 事件,label.py 打 Pod 标签,zmq_subscriber.py/sglang_zmq_subscriber.py 订阅引擎事件,redis_client.py 与 Redis 交互(KV cache 归属写入就靠它)。standard.py 把不同引擎的指标/事件归一化成统一格式。

这套 runtime agent 让 Kthena 能用统一抽象管理多种引擎,而不必为每个引擎写一套控制逻辑。

七、工程化与可观测性

Kthena 的工程化水准是生产级的:

  • 代码生成make generate 一键再生 CRD、deepcopy、client-go、文档;make gen-check 在 CI 里卡「生成产物是否 dirty」。CRD 字段改动必须连带提交生成文件。
  • 多层测试:单元测试(table-driven,并发敏感的加 -race)、Kind-based E2E(分 controller-manager / router / gateway-api / gateway-inference-extension 四类矩阵,见 test/e2e/)、Python 测试(ruff lint)。
  • 可观测性:Router 内建 Prometheus metrics(metrics-port 9090),调度器每个插件每阶段的执行耗时都被 MetricsRecorder 记录(RecordSchedulerPluginDurationRecordKVCacheTokenizeDurationRecordKVCacheMatchRatio 等),还有 access log(text/json 可配)。debug server(debug-port 15000,仅 localhost)可 dump 内部缓存。
  • CLIcli/kthena/ 提供独立 kthena 命令和 kubectl 插件,支持 create/get/describe,内置主流模型部署模板(templates.go),降低上手门槛。
  • webhook:两个二进制各自跑 validating/mutating webhook,证书自动生成存 Secret(pkg/webhook/cert)。CRD/API 变更要保持向后兼容。

八、性能数据与场景价值

根据官方博客的基准测试,在长系统 prompt(如 4096 token)场景下,KV Cache-aware + Least Request 相比随机基线:

  • 吞吐提升约 2.73×
  • TTFT 降低约 73.5%
  • 端到端延迟降低 60%+
插件配置 吞吐(req/s) TTFT(s) 端到端延迟(s)
Least Request + KVCacheAware 32.22 9.22 0.57
Least Request + Prefix Cache 23.87 12.47 0.83
Random 11.81 25.23 2.15

短 prompt 场景差距会随 prompt 长度收敛,但在多轮对话、模板化生成、高相似前缀的业务场景里,KV Cache-aware 路由收益显著。这正是「混搭、按场景选插件」的价值——调度器是可插拔的,按业务特征组合 filter/score 插件即可。

九、总结:Kthena 的设计取舍

通读源码后,Kthena 的几个关键设计取舍清晰可见:

  1. 双平面彻底解耦 是最大的架构赌注。它牺牲了一点「一站式」的便利(全平台才能用 ModelBooster),换来了极强的可分块采用性——你今天只需要一个模型感知网关,明天再加控制面,平滑演进。两个平面只跟 K8s API 说话,没有运行时耦合。

  2. 请求级 filter/score 调度框架 借鉴 K8s 调度器但作用域不同。微秒级的请求调度 + 可插拔插件,让 KV cache 感知、prefix cache、LoRA 亲和等 LLM 特有智能能以插件形式组合,而不是硬编码进路由。

  3. PD 分离一等公民。从 CRD(ModelBackendType.vLLMDisaggregated、worker 的 prefill/decode 角色)到调度器(PDGroup 配对)到代理(多对重试 + KV connector)到扩缩(disaggregated ratio),全链路把 PD 分离当一等场景支持,而不是事后补丁。

  4. 异构成本感知扩缩 把「省钱」做成调度问题。ReplicaBlock 按 cost 排序的贪心填充,让 H100+A100 混部时优先扩便宜实例,贵的留到最后——这是云原生推理平台少见的成本视角。

  5. 运行时 Agent 统一多引擎抽象。用 Python sidecar 把 vLLM/SGLang 的指标、事件、LoRA 生命周期、KV cache 事件归一化,控制面就能用统一逻辑管理多种引擎,避免每引擎一套控制代码。

Kthena 仍在活跃迭代(router 明确标注为 reference implementation,因为 Gateway Inference Extension 原生不支持 PD 分离),但它的架构骨架——双平面解耦、请求级可插拔调度、PD 分离全链路、成本驱动扩缩——已经为云原生 LLM 推理提供了一个相当完整且工程化成熟的范本。对于想做大规模 LLM 推理平台的人来说,这份源码值得逐模块精读。

参考阅读