一个 Deployment 就能跑 VLLM,为什么还需要 KServe?

KServe 入门:部署第一个 vLLM 推理服务

在 Kubernetes 上启动一个推理服务并不难。如果只有一个模型,vLLM + Deployment 确实够了。但服务多起来以后,模型从哪里加载、使用哪个 Runtime、GPU 怎么分配、服务怎么暴露,每个服务都要重复处理一遍,配置很快就会散落在一堆 YAML 里。KServe 解决的不是怎么启动 vLLM,而是怎么用统一的方式管理这些模型服务。

本文从零安装 KServe Standard 模式和 Envoy Gateway,通过本地 PVC 部署 Qwen2.5-0.5B-Instruct 模型。

KServe 是什么

KServe 官方给出的定位是:

Standardized Distributed Generative and Predictive AI Inference Platform for Scalable, Multi-Framework Deployment on Kubernetes.

KServe 是一个面向 Kubernetes 的可扩展、多框架部署的标准化分布式生成式和预测式 AI 推理平台

即:KServe 是一个 AI 推理平台

KServe AI 推理平台架构概览

KServe 不直接执行模型计算,它负责管理模型服务。我们创建一个 InferenceService,写清楚模型地址、模型格式和资源需求,KServe Controller 就会选择对应的 Runtime,并创建底层的 Deployment、Service、HTTPRoute 等资源。

KServe 最初叫 KFServing,由 Google、IBM、Bloomberg、NVIDIA 和 Seldon 等团队在 2019 年共同发起。2021 年,项目从 Kubeflow 组织迁移到独立的 KServe GitHub 组织,并正式更名。现在 KServe 是独立的 CNCF 孵化项目,同时仍是 Kubeflow 生态中的重要组件,既可以随 Kubeflow 使用,也可以独立安装。

安装

本文使用 Kubernetes 1.32+。假设节点已经安装 NVIDIA 驱动、Container Toolkit 和 Device Plugin,开始前先确认节点能够看到 GPU:

1
2
kubectl get nodes \
  -o custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu

输出中的 GPU 应该大于 0。本文使用标准的 nvidia.com/gpu 资源;如果这里是 <none>,后面的推理 Pod 会一直处于 Pending

服务 API 与部署模式

这里需要分成两层理解。

InferenceService 是通用模型服务 API,它有 Standard 和 Knative 两种部署模式:

  • Standard:使用原生 Kubernetes Deployment、Service 和 Pod 承载模型服务,依赖少、链路清晰,可以通过 HPA 或 KEDA 扩缩容,适合传统模型以及常驻的 GPU、vLLM 推理服务。
  • Knative/Serverless:使用 Knative Serving 承载 InferenceService,支持 Scale to Zero、请求驱动扩缩容和流量切分,适合突发或不可预测的流量,但会增加 Knative Serving 及对应网络层依赖,部署和排障更加复杂。

LLMInferenceService 是另一套独立 API,面向大语言模型的分布式工作负载、Prefill/Decode 分离和智能路由等场景。它不是 InferenceService 的第三种部署模式;如果只是部署单 GPU vLLM,使用 InferenceService Standard 模式通常更简单。

本文后面的安装流程只覆盖 InferenceService。LLMInferenceService 需要额外安装 Addon 和相关依赖,系列最后一篇再单独展开。

可以根据需求选择:

1
2
3
4
5
6
是否需要高级 LLM 分布式推理能力?
├── 是 → LLMInferenceService
└── 否
    └── 是否需要 Scale to Zero、Revision 和请求驱动扩缩容?
        ├── 是 → InferenceService(Knative)
        └── 否 → InferenceService(Standard)

这里我们选择比较简单的 InferenceService(Standard) 模式进行演示。

安装 cert-manager

KServe 依赖 cert-manager,需要提前安装,支持的最低版本是 1.15.0。

1
2
3
4
5
6
7
8
helm repo add cert-manager https://charts.jetstack.io
helm repo update

helm upgrade --install cert-manager cert-manager/cert-manager \
  -n cert-manager --create-namespace \
  --version v1.21.0 \
  --set crds.enabled=true \
  --wait --timeout=10m

查看 Pod:

1
kubectl -n cert-manager get pod
1
2
3
4
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-6c8f88f5b9-4b8zv              1/1     Running   0          25m
cert-manager-cainjector-8577b58487-r6cnt   1/1     Running   0          25m
cert-manager-webhook-7c7694dfd-df4ft       1/1     Running   0          25m

安装 Network Controller

KServe 可以通过 Gateway API 或 Ingress 暴露服务。本文使用 Gateway API,并选择 Envoy Gateway 作为具体实现。

安装 Envoy Gateway

使用 Helm 安装 Envoy Gateway,包括 Gateway API CRD 和 Controller:

1
2
3
4
5
6
helm upgrade --install eg \
  oci://docker.io/envoyproxy/gateway-helm \
  --version v1.8.2 \
  --namespace envoy-gateway-system \
  --create-namespace \
  --wait --timeout=10m

创建 Gateway

先创建 GatewayClass:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cat <<'EOF' > gatewayclass.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
EOF

kubectl apply -f gatewayclass.yaml

然后创建 Gateway。本文的测试集群没有 LoadBalancer,因此使用 EnvoyProxy 将 Envoy Service 配置为 NodePort;如果集群已经有可用的 LoadBalancer,可以删掉 EnvoyProxy 以及 Gateway 中的 parametersRef,直接使用默认配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
cat <<'EOF' > gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kserve
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: kserve-envoy-proxy
  namespace: kserve
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: NodePort
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: kserve-ingress-gateway
  namespace: kserve
spec:
  gatewayClassName: envoy
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
  infrastructure:
    labels:
      serving.kserve.io/gateway: kserve-ingress-gateway
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: kserve-envoy-proxy
EOF

kubectl apply -f gateway.yaml

kubectl wait --for=condition=Programmed \
  gateway/kserve-ingress-gateway \
  -n kserve \
  --timeout=5m

安装 KServe

  1. 安装 KServe CRD:
1
2
3
4
5
helm -n kserve install kserve-crd \
  oci://ghcr.io/kserve/charts/kserve-crd \
  --version v0.18.0 \
  --create-namespace \
  --wait --timeout=10m
  1. 安装 KServe Controller,使用 Standard + Gateway API:
1
2
3
4
5
6
7
8
helm -n kserve upgrade --install kserve \
  oci://ghcr.io/kserve/charts/kserve-resources \
  --version v0.18.0 \
  --set kserve.controller.deploymentMode=Standard \
  --set kserve.controller.gateway.disableIstioVirtualHost=true \
  --set kserve.controller.gateway.ingressGateway.enableGatewayApi=true \
  --set kserve.controller.gateway.ingressGateway.kserveGateway=kserve/kserve-ingress-gateway \
  --wait --timeout=10m
  1. 安装默认 ClusterServingRuntime:
1
2
3
4
5
6
helm -n kserve upgrade --install kserve-runtime-configs \
  oci://ghcr.io/kserve/charts/kserve-runtime-configs \
  --version v0.18.0 \
  --set kserve.servingruntime.enabled=true \
  --set kserve.llmisvcConfigs.enabled=false \
  --wait --timeout=10m

查看 HuggingFaceServer Runtime 使用的基础镜像:

1
2
kubectl get clusterservingruntime kserve-huggingfaceserver \
  -o jsonpath='{.spec.containers[0].image}{"\n"}'

这里配置的是 v0.18.0 基础标签;InferenceService 申请 NVIDIA GPU 后,KServe 会自动在标签后追加 -gpu,最终 Pod 使用的是 v0.18.0-gpu

查看 KServe Controller:

1
kubectl -n kserve get pod
1
2
NAME                                         READY   STATUS    RESTARTS   AGE
kserve-controller-manager-54bb5b957f-m82fd   2/2     Running   0          4m35s

使用

下载模型

KServe 支持从 Hugging Face、PVC、S3 等位置加载模型。由于当前环境不能稳定访问 Hugging Face,本文提前从 ModelScope 下载模型,再通过 PVC 挂载。

1
2
3
4
5
pip install modelscope

modelscope download \
  --model Qwen/Qwen2.5-0.5B-Instruct \
  --local-dir /opt/models/Qwen2.5-0.5B-Instruct

检查模型文件:

1
ls -lh /opt/models/Qwen2.5-0.5B-Instruct

创建 PV 和 PVC

本文只有一个 GPU 节点,因此使用 hostPath 静态 PV。生产环境更适合使用 CephFS、NFS、JuiceFS 等共享存储。

1
kubectl create namespace kserve-test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cat <<'EOF' > qwen-model.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: qwen-model
spec:
  capacity:
    storage: 3Gi
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  hostPath:
    path: /opt/models/Qwen2.5-0.5B-Instruct
    type: Directory
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: qwen-model
  namespace: kserve-test
spec:
  accessModes:
    - ReadOnlyMany
  storageClassName: ""
  volumeName: qwen-model
  resources:
    requests:
      storage: 3Gi
EOF

kubectl apply -f qwen-model.yaml

创建 InferenceService

然后部署 InferenceService:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cat <<'EOF' > qwen-llm.yaml
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: qwen-llm
  namespace: kserve-test
spec:
  predictor:
    model:
      modelFormat:
        name: huggingface
      storageUri: pvc://qwen-model
      args:
        - --model_name=qwen
        - --max_model_len=4096
        - --max-num-seqs=32
        - --gpu-memory-utilization=0.8
      resources:
        requests:
          cpu: "1"
          memory: 4Gi
          nvidia.com/gpu: "1"
        limits:
          cpu: "2"
          memory: 6Gi
          nvidia.com/gpu: "1"
EOF

kubectl apply -f qwen-llm.yaml

核心参数是这部分:

1
2
3
4
5
6
7
8
9
model:
  modelFormat:
    name: huggingface
  storageUri: pvc://qwen-model
  args:
    - --model_name=qwen
    - --max_model_len=4096
    - --max-num-seqs=32
    - --gpu-memory-utilization=0.8
  • model.modelFormat:声明模型格式,会用来匹配 Runtime。当前指定的 huggingface 会匹配到 ClusterServingRuntime/kserve-huggingfaceserver
  • storageUri:模型来源。当前 pvc://qwen-model 说明模型在 qwen-model 这个 PVC 里面。
  • args:传给 HuggingFaceServer 的启动参数。--model_name 设置 API 中的模型名;其余三个参数用于 vLLM,分别限制最大上下文长度、单次调度最多处理的序列数和 GPU 显存使用比例。

查看推理服务状态

1
2
3
4
5
6
7
kubectl wait --for=condition=Ready \
  inferenceservice/qwen-llm \
  -n kserve-test \
  --timeout=30m

kubectl get inferenceservice qwen-llm -n kserve-test
kubectl get pod,deploy,svc,httproute -n kserve-test

服务就绪后,READY 会变成 True

首次启动需要读取模型、初始化 CUDA 并完成 CUDA Graph 预热。普通环境通常只需要几分钟,但磁盘较慢的单节点环境可能需要十几分钟,因此这里将等待上限设置为 30 分钟。

1
2
NAME       URL                                       READY
qwen-llm   http://qwen-llm-kserve-test.example.com   True

Standard 模式下,KServe 会为这个 InferenceService 创建 Deployment、Service 和 HTTPRoute:

1
2
3
4
5
InferenceService/qwen-llm
├── Deployment/qwen-llm-predictor
├── Service/qwen-llm-predictor
├── HTTPRoute/qwen-llm
└── HTTPRoute/qwen-llm-predictor

KServe InferenceService 资源创建与请求链路

查看模型服务日志:

1
2
3
kubectl logs -n kserve-test \
  -l serving.kserve.io/inferenceservice=qwen-llm \
  --tail=200

日志中出现下面的内容,说明 HuggingFaceServer 已经使用 vLLM 加载 /mnt/models

1
2
3
4
Initializing a V1 LLM engine (v0.19.0)
model='/mnt/models'
Using max model len 4096
Uvicorn running on http://0.0.0.0:8080

请求 API

KServe 创建的 HTTPRoute 使用域名匹配:

1
qwen-llm-kserve-test.example.com

如果没有配置 DNS,可以直接访问 Envoy 的 NodePort,同时手动设置 Host Header。

查看 Envoy Service:

1
2
kubectl get service -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=kserve-ingress-gateway

NodePort 是动态分配的,先读取实际访问地址:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ENVOY_SERVICE=$(kubectl get service -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=kserve-ingress-gateway \
  -o jsonpath='{.items[0].metadata.name}')

NODE_PORT=$(kubectl get service -n envoy-gateway-system "$ENVOY_SERVICE" \
  -o jsonpath='{.spec.ports[?(@.port==80)].nodePort}')

NODE_IP=$(kubectl get node \
  -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')

echo "http://${NODE_IP}:${NODE_PORT}"

查看模型列表

1
2
curl -H "Host: qwen-llm-kserve-test.example.com" \
  "http://${NODE_IP}:${NODE_PORT}/openai/v1/models"

返回结果:

1
2
3
4
5
6
7
8
9
{
  "object": "list",
  "data": [
    {
      "id": "qwen",
      "object": "model"
    }
  ]
}

发送 Chat Completions 请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
curl -H "Host: qwen-llm-kserve-test.example.com" \
  -H "Content-Type: application/json" \
  "http://${NODE_IP}:${NODE_PORT}/openai/v1/chat/completions" \
  -d '{
    "model": "qwen",
    "messages": [
      {
        "role": "user",
        "content": "请用一句话介绍 Kubernetes。"
      }
    ],
    "max_tokens": 64,
    "temperature": 0.2
  }'

返回 chat.completion,说明下面这条链路已经打通:

1
2
NodePort -> Envoy Gateway -> HTTPRoute -> Service
         -> KServe HuggingFaceServer -> vLLM -> GPU

Qwen2.5-0.5B-Instruct 主要用于验证部署链路,模型规模很小,输出质量不能代表生产模型。

总结

到这里,我们已经用 KServe 跑通了一条完整的 LLM 推理链路,从本地 PVC 加载模型,再通过 Envoy Gateway 调用 OpenAI 兼容接口。

KServe 的价值不只是启动一个模型进程,而是把模型地址、Runtime、GPU 资源和访问入口收敛到 InferenceService 中,让推理服务也能沿用 Kubernetes 的声明式方式统一管理。

这也是理解 KServe 的起点。下一篇继续往下拆,看看一份 InferenceService YAML 提交之后,KServe 在集群里到底创建了什么。

0%