vllm serve 进入模型服务时,最先看到的是推理引擎本身:PagedAttention、Continuous Batching、KV Cache、TTFT、TPOT,以及 GPU 上如何更高效地完成 Prefill 和 Decode。

但视角一旦从“启动一个推理进程”切换到“在 Kubernetes 中长期运行一批模型服务”,问题会立刻变多:

  • 模型服务如何声明和部署
  • 不同推理框架如何统一接入
  • 模型文件如何下载到 Pod
  • 服务如何暴露、扩缩容和灰度
  • 多个模型之间如何编排
  • 控制器如何感知服务状态并完成故障恢复

vllm serve 解决的是推理引擎和服务进程的问题。KServe 解决的则是模型服务在 Kubernetes 上的平台化管理问题。

这篇文章记录一次从本地安装 KServe 到理解核心抽象的过程,重点放在 Control Plane、Data Plane、InferenceService、ServingRuntime 和 InferenceGraph。LLM Runtime、vLLM 集成和 GPU 环境会放到后续文章继续展开。

KServe 是什么

KServe 是一个运行在 Kubernetes 上的模型推理平台,同时覆盖传统预测模型和生成式 AI 模型。

它不是另一个 vLLM,也不会亲自完成矩阵计算和 Token 生成。更准确地说,KServe 位于推理引擎的上层,负责模型服务的声明、编排和生命周期管理。

一个简化的调用链如下:

1
2
3
4
5
6
7
8
9
客户端请求

Kubernetes Service / Gateway

KServe 管理的模型服务

sklearn server / vLLM / Triton

CPU / GPU

其中:

  • KServe 决定模型服务应该如何运行
  • vLLM、Triton 等 Runtime 真正执行推理
  • Kubernetes 负责调度和维持底层工作负载

可以把几个概念先粗略类比为:

1
2
3
4
KServe              ≈ 面向模型服务的 Operator
InferenceService ≈ 模型服务的声明
ServingRuntime ≈ 模型服务器的运行模板
vLLM / Triton ≈ 真正执行推理的进程

KServe 当前同时覆盖两类场景。

第一类是 Predictive AI,例如 scikit-learn、XGBoost、TensorFlow、PyTorch、ONNX。

第二类是 Generative AI,例如通过 Hugging Face Runtime、vLLM 和 llm-d 部署 Qwen、Llama 等大语言模型。

为什么不直接写 Deployment

如果只是部署一个固定的 vLLM 服务,直接写 Kubernetes 原生资源当然可以:

  • Deployment
  • Service
  • 模型下载逻辑
  • 启动参数
  • 健康检查
  • GPU 资源
  • 扩缩容策略
  • 版本发布和路由
  • 监控指标

当模型数量很少时,这样做足够直接。

但一旦平台上存在多个团队、多个模型格式和多个推理框架,每个模型都手写一套 Deployment,很快会产生大量重复且不一致的配置。模型使用者需要理解太多运行时细节,平台团队也很难统一治理镜像、资源、路由和发布策略。

KServe 的思路是把“模型是什么”和“模型服务器怎么运行”分开。

用户只声明模型:

1
2
3
4
5
6
7
8
9
10
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: sklearn-iris
spec:
predictor:
model:
modelFormat:
name: sklearn
storageUri: gs://kfserving-examples/models/sklearn/1.0/model

至于使用什么镜像、启动什么命令、监听哪个端口,则由 ServingRuntime 描述。

这样一来,平台团队可以统一维护 Runtime,模型使用者只需要关心模型本身。

安装方式怎么选

KServe 的安装文档里有几种模式:

  • Standard Kubernetes Installation
  • Knative Installation
  • ModelMesh Installation
  • Quick Installation
  • Kubeflow Installation

第一次接触时容易把它们理解成不同安装工具。实际上,它们更像是在选择底层运行模式。

Standard 模式

Standard 模式直接使用原生 Kubernetes 资源:

1
2
3
4
5
6
7
InferenceService

Deployment

ReplicaSet

Pod

它依赖少、排查直观,适合常驻模型服务,也适合作为第一次学习 KServe 的入口。

它默认不提供 Knative 带来的请求驱动 Scale-to-Zero 和基于 Revision 的流量切分能力。

Knative 模式

Knative 模式会在 KServe 与 Pod 之间加入 Knative Serving:

1
2
3
4
5
6
7
InferenceService

Knative Service

Revision

Deployment / Pod

它可以提供请求驱动扩缩容、Scale-to-Zero、Revision 管理、流量切分和灰度发布。代价是控制链路和网络链路都会更复杂。

ModelMesh

ModelMesh 面向大量、小型、频繁变化的模型。

它并不是简单地给每个模型创建一个独立 Pod,而是让多个模型共享 Serving Runtime,并按需加载和卸载模型。这种设计适合大量传统机器学习模型,不一定适合每个模型都需要占用大量 GPU 显存的大语言模型。

本次安装选择

为了先理解 KServe 自身,本次采用:

  • kind
  • KServe Standard Mode

暂时不引入 Knative、Istio 和 ModelMesh。

本地环境如下:

  • MacBook Apple Silicon
  • Docker Desktop
  • kind
  • Kubernetes v1.35
  • KServe v0.18
  • Standard Deployment Mode

KServe 的快速安装脚本会安装:

  • cert-manager
  • KServe CRD
  • KServe Controller
  • Admission Webhook
  • 默认 ClusterServingRuntime

安装命令如下:

1
2
3
curl -fsSL \
"https://github.com/kserve/kserve/releases/download/v0.18.0/kserve-standard-mode-full-install-with-manifests.sh" \
| bash

安装完成后,可以通过下面的命令验证控制面:

1
2
3
4
kubectl get pods -n cert-manager
kubectl get pods -n kserve
kubectl get crd | grep serving.kserve.io
kubectl get clusterservingruntime

cert-manager 为什么是依赖

KServe 会安装 Admission Webhook,用于处理 CR 的默认值填充、校验和转换。

调用路径大致是:

1
2
3
4
5
kubectl apply

kube-apiserver
↓ HTTPS
KServe Admission Webhook

API Server 调用 Webhook 时要求 HTTPS,因此 KServe 需要一套 Webhook 证书。

cert-manager 负责:

  • 创建证书
  • 把证书保存到 Secret
  • 给 Webhook 配置注入 CA
  • 在证书到期前续期

所以 cert-manager 属于 KServe 控制面的证书依赖,并不参与模型推理。

本地安装中的两个现实问题

本地安装过程里有两个问题值得单独记录:代理链路和镜像架构。它们不是 KServe 的核心概念,但非常容易影响第一次体验。

kind 节点里的代理地址

当网络需要通过本机代理访问外部镜像仓库时,kind 节点不能直接使用宿主机视角下的 127.0.0.1:<proxy-port>

kind 节点本身是 Docker 容器。在这个容器中,127.0.0.1 指向的是节点容器自己,而不是 Mac 宿主机。因此 containerd 拉镜像时可能出现:

1
2
3
proxyconnect tcp:
dial tcp 127.0.0.1:<proxy-port>:
connect: connection refused

正确的网络关系应该是:

1
2
3
4
5
6
7
kind node

host.docker.internal:<proxy-port>

Mac 上的本机代理

外部镜像仓库

创建 kind 集群时可以传入:

1
2
3
4
HTTP_PROXY=http://host.docker.internal:<proxy-port> \
HTTPS_PROXY=http://host.docker.internal:<proxy-port> \
NO_PROXY=localhost,127.0.0.1,::1,.svc,.cluster.local,<service-cidr>,<pod-cidr> \
kind create cluster --name kserve-demo

同时需要保证本机代理允许来自 Docker 网络的连接。

这也能顺手区分几类代理配置:

  • Shell 代理
  • Docker Desktop 代理
  • Docker CLI 拉镜像代理
  • kind 节点 containerd 代理
  • Pod 内部代理

它们不是同一条链路。宿主机 curl 成功,并不能证明 Kubernetes 节点一定能拉镜像。

ARM Mac 与 AMD64 镜像

按照官方示例部署 Hugging Face LLM 时,InferenceService 可以完成 Runtime 匹配,并创建模型 Deployment,但 Pod 拉取下面的镜像会失败:

1
kserve/huggingfaceserver:v0.18.0

检查镜像平台:

1
2
docker buildx imagetools inspect \
kserve/huggingfaceserver:v0.18.0

结果只有:

1
linux/amd64

而本地 kind 节点是:

1
linux/arm64

因此 containerd 会报错:

1
no match for platform in manifest

在 Apple Silicon 上模拟完整的 AMD64 kind 节点并不划算。跨架构模拟不只是运行一个业务容器,还要运行 containerd、kubelet、etcd、kube-apiserver、scheduler、controller-manager 等控制面和节点组件,兼容成本明显更高。

当前还处于实验阶段,目标是先用最简单的方式理解 KServe 的控制面和资源抽象,不需要为了跑通一个 LLM Runtime 引入过多兼容成本。更简单的方式是:

1
2
3
4
5
Mac ARM kind
→ 学习 KServe 控制面、CRD 和工作负载编排

远程 AMD64 Linux + NVIDIA GPU
→ 运行 KServe + vLLM + 真正的 LLM 服务

Control Plane 与 Data Plane

理解 KServe 时,最重要的概念之一是 Control Plane 与 Data Plane。

一句话概括:

1
2
Control Plane 决定服务应该如何运行
Data Plane 真正处理推理请求

KServe Control Plane

KServe 的控制面主要包括:

  • InferenceService CR
  • KServe Controller
  • Admission Webhook
  • ServingRuntime 配置
  • Status 回写

当创建一个 InferenceService 后,KServe Controller 会进行 Reconcile:

1
2
3
4
5
6
7
8
9
读取 InferenceService

匹配 ServingRuntime

生成 Deployment 和 Service

观察工作负载状态

更新 InferenceService Status

KServe Controller 不会接收用户的推理流量,也不会生成 Token。

KServe Data Plane

Data Plane 是实际处理请求的部分:

1
2
3
4
5
6
7
8
9
客户端

Service / Router

模型 Pod

sklearn server / vLLM / Triton

预测结果

它更关注请求延迟、吞吐、Batch、CPU/GPU 使用率、KV Cache、TTFT 和 TPOT。

这两个平面可以独立扩展。Controller 不需要随着每秒推理请求数线性扩容,而模型服务 Pod 可以按照负载增加副本。

InferenceService:声明一个模型服务

InferenceService 是用户最常接触的 KServe CR。

它描述的是:

1
希望集群中存在一个怎样的模型服务

例如:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: sklearn-iris
namespace: kserve-test
spec:
predictor:
model:
modelFormat:
name: sklearn
storageUri: gs://kfserving-examples/models/sklearn/1.0/model

这里声明了:

  • 服务名称:sklearn-iris
  • 模型格式:sklearn
  • 模型位置:gs://...

但没有直接写容器镜像。

应用后,KServe 会生成类似资源:

1
2
3
4
5
6
7
8
9
10
11
InferenceService

Deployment

ReplicaSet

Pod

InferenceService

Service

当模型 Pod 就绪后,InferenceService 的状态中会出现:

1
2
InferenceServiceReady
Ready=True

这说明控制器已经把声明状态落实成了真实工作负载。

ServingRuntime:模型如何运行

ServingRuntime 是 KServe 非常关键的一层抽象。

InferenceService 只说明模型是什么,ServingRuntime 则说明:

1
这种模型应该使用什么镜像和参数运行

以 sklearn 为例,ClusterServingRuntime 大致会声明:

1
2
3
4
5
6
spec:
supportedModelFormats:
- name: sklearn
containers:
- name: kserve-container
image: kserve/sklearnserver

当 InferenceService 中出现:

1
2
modelFormat:
name: sklearn

KServe 就会查找支持 sklearn 格式的 Runtime,例如 kserve-sklearnserver,再根据 Runtime 中的容器模板生成实际的 Deployment 和 Pod。

ServingRuntime 分为两种作用域:

  • ServingRuntime:只在当前 Namespace 生效
  • ClusterServingRuntime:对整个集群生效

平台团队可以通过 ClusterServingRuntime 为所有租户提供统一、受控的推理镜像。业务团队也可以在自己的 Namespace 中定义专用 Runtime。

未来部署 LLM 时,也可以沿用同样的思路:InferenceService 声明模型格式,ServingRuntime 负责把它映射到 Hugging Face Runtime、vLLM 等具体模型服务器。

模型如何进入 Pod

KServe 并不要求模型一定被打包进容器镜像。

InferenceService 通过 storageUri 指定模型地址,KServe 会给 Pod 注入 Storage Initializer。

启动时,Storage Initializer 会先从对象存储或模型仓库下载文件,写入共享目录 /mnt/models,随后模型服务器容器再从该目录加载模型。

Pod 中通常会出现一个共享的 emptyDir

1
kserve-provision-location

Storage Initializer 写入该目录,主容器以只读方式挂载它。

这种设计把“模型文件获取”和“模型服务器运行”拆成了两个职责。

InferenceGraph:多个模型之间如何路由

一个 InferenceService 通常代表一个模型服务。

当请求需要经过多个模型,或者需要在多个版本之间分流时,可以使用 InferenceGraph。

InferenceGraph 由 Node 和 Step 组成:

1
2
3
4
5
6
7
InferenceGraph

Node

Steps

InferenceService 或另一个 Node

KServe 支持四类路由节点。

Sequence

按照顺序执行多个步骤:

1
2
3
4
5
6
7
请求

预处理服务

模型服务

后处理服务

前一个 Step 的响应可以作为后一个 Step 的输入。

Switch

根据条件选择第一个匹配的分支:

1
2
3
4
5
6
请求

判断任务类型
├── text → 文本模型
├── image → 图像模型
└── audio → 音频模型

它类似程序中的 if / else if

Ensemble

将同一个请求发送给多个模型,再聚合响应:

1
2
3
             ┌→ model-a
请求 → Router├→ model-b
└→ model-c

KServe 可以组合各步骤的响应,但“多数投票”“平均值”等业务融合逻辑通常仍需要专门的后处理服务。

Splitter

按照权重把不同请求分配给不同目标:

1
2
3
4
5
请求

Splitter
├── 90% → model-v1
└── 10% → model-v2

它适合灰度发布、A/B 测试和新模型小流量验证。

Splitter 与 Ensemble 的区别是:

  • Splitter:一个请求只进入一个目标
  • Ensemble:一个请求同时进入多个目标

一个最简单的 InferenceGraph 如下:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: serving.kserve.io/v1alpha1
kind: InferenceGraph
metadata:
name: iris-graph
namespace: kserve-test
spec:
nodes:
root:
routerType: Sequence
steps:
- name: predict
serviceName: sklearn-iris

InferenceGraph 创建后,KServe 会额外部署 Graph Router。InferenceGraph 本身是控制面中的声明,Graph Router 才是真正处理请求路由的 Data Plane 组件。

KServe 与 vLLM 的关系

KServe 与 vLLM 不是替代关系。KServe 负责模型服务生命周期、工作负载、路由、存储和扩缩容;vLLM 负责加载大语言模型、管理 KV Cache、执行 Continuous Batching,并完成 Prefill、Decode 和 Token 生成。

换句话说,KServe 更像模型服务的平台控制层,vLLM 是其中一种具体的 LLM 推理运行时。上篇先到这里,后续再继续看 KServe 如何和 vLLM、GPU 调度以及 LLM Runtime 结合。

参考资料

  • KServe 官方文档
  • KServe GitHub
  • KServe Standard Kubernetes Installation
  • Deploy Your First LLM with InferenceService
  • KServe InferenceGraph
  • vLLM 与 KServe 集成