从 Spec 到资源:Predictor/Transformer/Explainer 如何变成 Deployment

KServe 源码拆解系列 · 第 4 篇

第 3 篇走完了调和循环主流程:事件进来,控制器按 spec 组装 reconciler 链,逐个组件调和。但"调和一个组件"这一步里发生了什么?你的 YAML 只有几行 predictor:transformer:,集群里却长出了 Deployment、Service、探针、资源配额。意图声明(Spec)和集群现实(资源)之间的这段距离,就是本篇要拆的:组件构建器。结尾留下线头------模型怎么进 Pod------交给第 5 篇。


翻译分两段:webhook 改 Spec,构建器改现实

Spec → Deployment 的翻译分两个阶段:admission webhook 阶段改 Spec (第 2 篇讲过),Reconcile 阶段建现实 (本篇主场)。阶段一的伏笔:Defaulter 在对象落库前把老式框架 predictor 改写成 Model + ServingRuntimepkg/apis/serving/v1beta1/inference_service_defaults.go):

go 复制代码
isvc.Spec.Predictor.Model = &ModelSpec{...}
isvc.Spec.Predictor.SKLearn = nil   // ← 老字段被清掉

你写的 predictor.sklearn 进集群时已被换成 predictor.model(modelFormat=sklearn),所以构建器主线里 Model 路线是第一优先级(ServingRuntime 细节第 8 篇)。

阶段二的代码在 pkg/controller/v1beta1/inferenceservice/components/predictor.gotransformer.goexplainer.go 三个构建器,实现同一接口 Component{ Reconcile(ctx, isvc) }component.go),三者结构同构,读懂 predictor 就懂全部。


predictor.go:一个 PodSpec 怎么拼出来

Predictor.Reconcile 的第一步是 buildPredictorResources,核心是二选一(写了 storageUri 则只记注解 internal.serving.kserve.io/storage-initializer-sourceuri,不动 PodSpec):

go 复制代码
if isvc.Spec.Predictor.Model != nil {
	sRuntime, runtimeAnnotations, err = p.reconcileModel(ctx, isvc, multiNodeEnabled) // ← 路线一:选 runtime
	podSpec, err = p.buildPodSpec(isvc, sRuntime, runtimeAnnotations)                  // ← 拼容器
} else {   // 路线二:框架/custom 兜底,直接用你写的容器
	podSpec = corev1.PodSpec(isvc.Spec.Predictor.PodSpec)
	podSpec.Containers = []corev1.Container{*predictor.GetContainer(...)}
}

镜像从哪里来

reconcileModel 按名字找 ServingRuntime(GetServingRuntime),没写名字就按 modelFormat 自动匹配(GetSupportingRuntimes,按 priority 排序)。选中的 runtime 长这样(config/runtimes/kserve-sklearnserver.yaml 节选):

yaml 复制代码
- name: kserve-container
  image: kserve-sklearnserver:replace     # ← 镜像在这(安装时 kustomize 替换 tag)
  args: [--model_name={{.Name}}, --model_dir=/mnt/models, --http_port=8080]

所以答案:runtime 的容器是模板,你的 Spec 是补丁buildPodSpec 完成拼装:

go 复制代码
_, predContainer, mergedPodSpec, err = isvcutils.MergeServingRuntimeAndInferenceServiceSpecs(
	sRuntime.Containers, isvc.Spec.Predictor.Model.Container, isvc,
	constants.InferenceServiceContainerName,   // ← 目标容器固定叫 "kserve-container"
	sRuntime.ServingRuntimePodSpec, isvc.Spec.Predictor.PodSpec)
if err = isvcutils.ReplacePlaceholders(predContainer, isvc.ObjectMeta); err != nil {...}  // ← {{.Name}} → sklearn-iris

合并规则(utils/utils.go):runtime 容器打底,你 Spec 覆盖同名字段,Args 是拼接不是替换ReplacePlaceholderstext/template 把整个容器 JSON 渲染一遍。之后 UpdateImageTag:指定 runtimeVersion 就改 tag;开了 GPU 且没指定版本时,给部分框架镜像补 -gpu 后缀。

存储挂载:只写注解,挂载交给别人

挂载在 Reconcile 按 deploymentMode 分岔:Raw(Standard)模式下 raw.NewRawKubeReconciler 构建期调 pod.CommonStorageInitialization,直接把 storage-initializer init 容器塞进 PodSpec;Knative 模式只创建 knative Service(源码注释原话:"Knative 不支持 init 容器"),注解留给 Pod webhook 在 Deployment 生成时注入------第 5 篇的主线,先埋个钩子。


三个组件如何串起来:拓扑与命名规则

三个构建器产出的资源,靠命名规则启动参数 咬合。以 sklearn-iris 为例:

arduino 复制代码
               ┌──────────▼──────────┐
               │   Ingress 网关       │  VirtualService / HTTPRoute(第 6 篇)
               └──────────┬──────────┘
               ┌──────────▼──────────┐
               │  transformer(可选)  │  sklearn-iris-transformer
               └──────────┬──────────┘
                          │ --predictor_host sklearn-iris-predictor.default
               ┌──────────▼──────────┐
               │  predictor           │  sklearn-iris-predictor
               └──────────────────────┘
                          ▲
               ┌──────────┴──────────┐
               │  explainer(可选)    │  sklearn-iris-explainer(旁路,不挡主链路)
               └──────────────────────┘

命名规则集中在 pkg/constants/constants.go

go 复制代码
func PredictorServiceName(name string, predictorName ...string) string {
	if len(predictorName) > 0 && predictorName[0] != "" {
		return name + "-" + predictorName[0] + "-" + string(Predictor)   // isvc-名-predictor
	}
	return name + "-" + string(Predictor)                                 // isvc-predictor
}

TransformerServiceName = isvc-transformerExplainerServiceName = isvc-explainerDeployment 和 Service 同名 。Service 由 reconcilers/service/service_reconciler.gocreateDefaultSvc 生成:端口 80 转发到容器第一个端口,selector 是 app: isvc.<组件名>,gRPC 端口还会自动打上 kubernetes.io/h2c AppProtocol。transformer 怎么找到 predictor?不靠服务发现,而是构建期把地址写进启动参数(pkg/apis/serving/v1beta1/transformer_custom.go):

go 复制代码
argumentPredictorHost := fmt.Sprintf("%s.%s", predictorHost[0], metadata.Namespace)
container.Args = append(container.Args,
	constants.ArgumentModelName, metadata.Name,             // --model_name sklearn-iris
	constants.ArgumentPredictorHost, argumentPredictorHost) // --predictor_host <svc>.<ns>

transformer.go 把 PredictorServiceName(...) 算出来传给 GetContainer,explainer.go 同理。三组件间是构建期写死的点对点调用

一条捷径:collocation(同 Pod 部署) 。predictor 的 buildPodSpec 检查 ServingRuntime 和 Spec 里有没有 transformer-container,有就合并进同一个 PodSpec,省一次网络往返。


资源配置:GPU/内存从 Spec 到 Pod 的路径

你写在组件实现里的 resources:,路径很直接:defaulter 补默认 → MergeRuntimeContainers 用你的值覆盖 runtime → 原样进 Deployment.Spec.Template.Spec单节点场景控制器不加工 GPU/内存,真正动资源的是三处:

1. defaulter 补默认值。 setResourceRequirementDefaultsinference_service_defaults.go)从 inferenceservice-config ConfigMap 读 cpuRequest/memoryRequest/cpuLimit/memoryLimit,只填你没写的键------用户显式写的永远优先。

2. 多节点重算 GPU。 写了 workerSpec 时,multiNodeProcessPipelineParallelSize/TensorParallelSize 算出节点数并分配 GPU,deployment_reconciler.goaddGPUResourceToDeployment 再写回 limits+requests。默认 GPU 类型 nvidia.com/gpu,可用注解 serving.kserve.io/custom-gpu-resource-types 扩展。

3. 扩缩容字段分流。 minReplicas/maxReplicas 不直接进 Deployment------它们交给 autoscaler reconciler(第 7 篇)。只有 autoscaler class 为 none 时,minReplicas 才直接写成 deployment.Spec.Replicas


探针与优雅停机:Deployment 的最后一道加工

PodSpec 拼好后,createRawDefaultDeploymentreconcilers/deployment/deployment_reconciler.go)调 setDefaultPodSpec 收尾,最关键是默认 readiness 探针------只为 kserve-containertransformer-container 生成,仅在没定义时补:

go 复制代码
if container.ReadinessProbe == nil {   // ← 你/runtime 定义了就不动
	container.ReadinessProbe = &corev1.Probe{
		ProbeHandler: corev1.ProbeHandler{
			TCPSocket: &corev1.TCPSocketAction{Port: intstr.IntOrString{IntVal: 8080}},
		},
		TimeoutSeconds: 1, PeriodSeconds: 10, FailureThreshold: 3,
	}
}

默认探针只是 TCP 连 8080,怎么知道模型加载完没?答案在数据面:kserve/python 的模型服务器先加载模型、后监听端口 ------TCP 能连上,间接等于模型就绪。大模型加载要几分钟,runtime 就自带更精确的探针,如 config/runtimes/kserve-vllmserver.yaml 里 httpGet /v1/models(注释原话:"only returns 200 once the model is registered")。collocation 下同 Pod 两个服务都有探针,sidecar 一律不管。

优雅停机也在同一个函数:TerminationGracePeriodSeconds 未设置时补上 corev1.DefaultTerminationGracePeriodSeconds(30 秒)------SIGTERM 后有半分钟跑完 in-flight 请求。同函数还补:镜像拉取策略 IfNotPresent、滚动更新 MaxUnavailable 25% / MaxSurge 25%。多节点 worker 则强制 0% / 100%------先起新 Pod、就绪再杀旧 Pod。

最后一问:PodSpec 变了 Deployment 怎么更新?checkDeploymentExist 先 dry-run update 补全服务端默认值,再 kmp.SafeDiff 比对,有差异走 strategic merge patch。细节:挂了 HPA 的 Deployment,比对和 patch 都剔除 Replicas 字段------这个经典打架场景,KServe 在构建器里就防住了。


一句话总结

Spec 到 Deployment 的翻译分两段:webhook Defaulter 先把老框架 predictor 改写成 Model + ServingRuntime;构建器再以 runtime 容器为模板、你的 Spec 为补丁,合并出 PodSpec,交给 deployment/service/raw 三个 reconciler 落成同名 Deployment + Service。命名规则和构建期写入的 --predictor_host 把三个组件串成链路;资源直通 Pod,探针只补默认不覆盖,HPA 与 Deployment 的 replicas 冲突在比对阶段被主动隔离。

下一篇预告:第 5 篇 模型加载:storage-initializer 与节点级缓存。本篇埋下的注解 internal.serving.kserve.io/storage-initializer-sourceuri 终于派上用场------它如何变成 Pod 里的 init 容器?模型为什么每起一个 Pod 就要重新下载?节点级缓存怎么把第二次启动提速?

相关推荐
面试鸭1 小时前
Codex 跑进法务部,周活居然暴涨了 108 倍???
后端·面试·求职
tachibana22 小时前
RAGAS 指标解读
数据库·人工智能·算法·机器学习·架构·大模型·llm
a187927218312 小时前
从一条直线到大模型输出一个token(五):Transformer Block 全景与层归一化
ai·大模型·llm·transformer·layernorm·deepseek·层归一化
谢白羽2 小时前
vLLM-Omni 部署 IndexTTS 2.5
llm·agent·tts·vllm·大模型部署
Komorebi_99993 小时前
RAG23-RAG28
llm·rag
武子康3 小时前
单卡 A6000 跑 27B FP8:我把“能跑”拆成了五组证据
人工智能·llm·agent
网络工程小王3 小时前
【LLM开发实验】 QLoRA实现原理及实战
人工智能·深度学习·机器学习·llm·qlora
程序员爱钓鱼4 小时前
Rust const泛型详解:让常量也成为泛型参数
后端·面试·rust
苹果二4 小时前
【案例说明】融合知识图谱、大语言模型和 AI Agent 完成能源领域知识工程工作
llm·知识图谱·aiagent·能源领域