AI Infra实战07(上):在K8s部署KServe,把大模型跑成推理服务
本篇配套的全部脚本、YAML 和 Terraform 配置已开源在 GitHub:https://github.com/Jich1123/gpu-k8s-lab 。clone 下来可按顺序复现全部实验(仓库不含任何真实 IP、密钥或账号信息)。
本篇目标
第03到05篇,我们用 vLLM 把大模型跑起来,也用 Helm 把它部署进了 K8s。但那还是把模型当成一个普通服务在管。到了 KServe,思路变了:你不再自己拼 Deployment、Service、Ingress,而是只声明一个 InferenceService,平台帮你把这一整套配齐。
这篇是 KServe 实战的上篇,目标是从零把控制面搭起来,最终用一个 InferenceService 把 Qwen 跑成 OpenAI 兼容接口。
学完本篇你会掌握:
- 为什么宿主机能看到 GPU,K8s 却调度不了,以及 GPU Operator 到底装了什么
- k3s、cert-manager、Istio、KServe 各自的角色和安装顺序
- KServe 0.20 一个容易卡住的真实坑:默认不带 ServingRuntime
- 提交一个 InferenceService 后,KServe 自动生成了哪些资源
- 用 OpenAI 兼容接口调用真实模型
证据等级:B级实验复现。本篇命令、输出和结果来自一次真实执行(AWS g4dn.2xlarge / T4 16GB,us-east-1)。
实操环境
| 配置 | 值 |
|---|---|
| 平台 | AWS EC2 g4dn.2xlarge |
| GPU | Tesla T4(16GB显存) |
| 系统 | Ubuntu 22.04.5 LTS |
| 驱动 | 595.84 / CUDA 13.2(宿主机预装) |
| k3s | v1.34.10+k3s1 |
| GPU Operator | v26.3.3 |
| cert-manager | v1.21.1 |
| Istio | 1.28.10 |
| KServe | v0.20.0 |
| 模型 | Qwen/Qwen2.5-0.5B-Instruct |
为什么用 g4dn.2xlarge 而不是前几篇的 g4dn.xlarge:KServe 要叠加 cert-manager、Istio、KServe 控制面,还有下篇的 Knative,组件多,16GB 内存偏紧。8 vCPU / 32GB 的 g4dn.2xlarge 更稳。这也需要 AWS 的 G/VT 实例配额达到 8 vCPU。
为什么要装 GPU Operator:宿主机能看到 GPU,不等于 K8s 能调度 GPU
这是很多人第一次在 K8s 上跑 GPU 时最容易困惑的地方,先讲清楚。
宿主机上nvidia-smi正常,只说明一件事:这台机器的驱动装好了,操作系统能用 GPU。但 K8s 的调度器并不直接看硬件,它只认「资源」。默认情况下,节点的可分配资源里只有 cpu、memory 这些,并没有 GPU。
也就是说:
宿主机 nvidia-smi 正常 → 机器能用 GPU K8s 节点 Allocatable 无 gpu → 调度器不知道有 GPU 结果:GPU 的 Pod 会一直 Pending要让 K8s 能把 GPU 当成资源调度出去,需要 NVIDIA GPU Operator。它不是单一组件,而是一套:
| 组件 | 作用 |
|---|---|
| GPU 驱动 | 宿主机的 NVIDIA 驱动(我们已预装 595.84,不用它再装) |
| Container Toolkit | 让容器运行时能访问 GPU |
| Device Plugin | 把 GPU 注册成 K8s 资源 nvidia.com/gpu(关键) |
| GPU Feature Discovery | 给节点打 GPU 相关标签 |
| DCGM Exporter | GPU 监控指标(第02篇用过) |
其中最关键的是 Device Plugin。没有它,kubectl describe node里就没有nvidia.com/gpu,KServe 的模型 Pod 拿不到 GPU。
所以本篇的安装策略是:驱动不用再装(宿主机已有),但 GPU Operator 要装,用它的 Device Plugin 把 GPU 接入 K8s。这就是下面 GPU Operator 用 preinstalled 驱动模式的原因。
组件栈与安装顺序
整套控制面从下到上是这样堆起来的,每一层都依赖下一层:
| 层 | 组件 | 职责 |
|---|---|---|
| 1 | k3s | 轻量 Kubernetes,集群底座 |
| 2 | GPU Operator | 让 K8s 能调度 GPU |
| 3 | cert-manager | 给 KServe 的 webhook 签发证书 |
| 4 | Istio | KServe 的网关,负责对外路由 |
| 5 | KServe | 模型服务平台,本篇主角 |
下面按这个顺序装。每步只贴关键片段,完整脚本在 GitHub 仓库的07-kserve/scripts/下。
第一步:装 k3s
k3s 安装有两个决策要说清楚:
curl-sfLhttps://get.k3s.io|\INSTALL_K3S_VERSION="v1.34.10+k3s1"\INSTALL_K3S_EXEC="--disable=traefik --write-kubeconfig-mode=644"\sh-- 版本固定
v1.34:KServe 0.20 的兼容矩阵覆盖 Kubernetes 1.32 到 1.34,所以不用更新的 1.36,避免踩兼容性问题。 --disable=traefik:k3s 默认自带 Traefik 做 Ingress,但我们要用 Istio 做 KServe 的网关,两者会冲突,所以先关掉。--write-kubeconfig-mode=644:让普通用户免 sudo 就能用 kubectl。
装完确认节点 Ready,同时看一眼节点资源:
kubectl getnode-ojsonpath='{.items[0].status.allocatable}'# cpu:8 memory:32484040Ki pods:110# 注意:这里没有 nvidia.com/gpu这一步就印证了前面说的:k3s 装好、驱动也正常,但 K8s 里还看不到 GPU。这是预期结果,正好说明下一步为什么必须装 GPU Operator。
第二步:装 GPU Operator(preinstalled 驱动模式)
这一步最容易踩坑的是 k3s 的 containerd 路径。先看关键片段:
helm upgrade--installgpu-operator nvidia/gpu-operator\--namespacegpu-operator--versionv26.3.3\--setdriver.enabled=false\--settoolkit.enabled=true\--set-string toolkit.env[0].name=CONTAINERD_CONFIG\--set-string toolkit.env[0].value=/var/lib/rancher/k3s/agent/etc/containerd/config.toml\--set-string toolkit.env[1].name=CONTAINERD_SOCKET\--set-string toolkit.env[1].value=/run/k3s/containerd/containerd.sock\--set-string toolkit.env[2].name=RUNTIME_CONFIG_SOURCE\--set-string toolkit.env[2].value=file=/var/lib/rancher/k3s/agent/etc/containerd/config.toml两个关键点:
driver.enabled=false:复用宿主机的 595.84 驱动。如果不关,Operator 会尝试装自己的驱动容器,和宿主机驱动冲突,通常会反复重启或失败。toolkit.env那三行:k3s 的 containerd 配置和 socket 路径和标准 Kubernetes 不一样(在/var/lib/rancher/k3s/下),必须显式告诉 Operator,否则 Container Toolkit 无法正确配置 GPU 运行时。这是 k3s 上装 GPU Operator 最常见的坑。
装完等各组件就绪,然后做两个验证。
验证一:节点是否公布了 GPU 资源
kubectl getnode-ojsonpath='{.items[0].status.allocatable.nvidia\.com/gpu}'# 输出:1对比第一步(k3s 装完时没有这个资源),现在出现了nvidia.com/gpu: 1。这就是 GPU Operator 的价值:把 GPU 接进了 K8s 的调度体系。
验证二:容器里能不能真正用 GPU
跑一个官方 CUDA vectorAdd 测试 Pod:
kubectl run cuda-vectoradd--restart=Never\--image=nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04\--limits=nvidia.com/gpu=1kubectl logs cuda-vectoradd输出:
[Vector addition of 50000 elements] CUDA kernel launch with 196 blocks of 256 threads Test PASSED Done到这里,「宿主机能看到 GPU → K8s 能调度 GPU → 容器内能用 GPU」三层就全部打通了。
第三步:装 cert-manager、Istio、KServe
这三个按依赖顺序装。cert-manager 先装,因为 KServe 的 webhook 需要它签发的证书;Istio 提供网关;最后装 KServe。
cert-manager
helm upgrade--installcert-manager oci://quay.io/jetstack/charts/cert-manager\--versionv1.21.1--namespacecert-manager --create-namespace\--setcrds.enabled=true--wait它的角色单一:给 KServe 控制器的准入 webhook 提供 TLS 证书。少了它,KServe 装上也起不来。
Istio
istioctlinstall--setprofile=default-yIstio 是 KServe 对外暴露推理服务的网关。它的 Ingress 网关(istio-ingressgateway)是外部请求进入模型的入口。
KServe(Standard 模式)
helm upgrade--installkserve oci://ghcr.io/kserve/charts/kserve-resources\--versionv0.20.0--namespacekserve\--setkserve.controller.deploymentMode=Standard\--setkserve.controller.gateway.ingressGateway.className=istio--waitdeploymentMode=Standard:这是给 GPU 生成式推理用的模式,固定副本、常驻 GPU。KServe 官方建议生成式大模型用 Standard,因为请求时间长、GPU 贵,需要可预测的资源控制。另一种 Knative(Serverless)模式,是下篇讲 Scale to Zero 和灰度时用的。
装完把 Istio 网关暴露成一个固定 NodePort(本实验用 30080),外部才能通过它访问:
kubectl patch svc istio-ingressgateway-nistio-system--type=json\-p='[{"op":"replace","path":"/spec/ports/1/nodePort","value":30080}]'提示:如果用脚本自动 patch,注意确认端口是否真的落到了 30080。实测中用某些方式 patch 会被随机分配成别的端口,需要按端口索引精确指定。
一个容易卡住的坑:KServe 0.20 默认不带 ServingRuntime
控制面装完,正准备部署模型时,会遇到一个不查日志根本想不到的问题。
Qwen 的 InferenceService 里写的是modelFormat.name: huggingface,这需要集群里存在一个支持 huggingface 的 ClusterServingRuntime 来匹配。但装完 KServe 后查一下:
kubectl get clusterservingruntime# 空的一个都没有。KServe 0.20 把内置 ServingRuntime 与核心 chart 解耦了,默认不再随控制面一起装。这和很多旧教程不一样,照着做会卡在「no runtime found」。
解法是单独装官方的 huggingface runtime 清单。但还有一个细节:官方清单里镜像是占位符huggingfaceserver:replace,用 Helm 装 runtime 时才会自动填充,单独用 YAML 必须手动替换成真实镜像。而且 GPU 推理要用带 CUDA 的-gpu版本:
containers:-name:kserve-container# 必须用 -gpu 版本,CPU 版缺少 CUDA 跑不了 GPU 推理image:kserve/huggingfaceserver:v0.20.0-gpukubectl apply-fstandard/huggingface-runtime.yaml kubectl get clusterservingruntime# kserve-huggingfaceserver huggingface ...记住这个结论:KServe 0.20 上,ServingRuntime 要单独装,GPU 模型要用 -gpu 镜像。这是照旧教程操作时最容易卡的一步。
部署 Qwen:只写一个 InferenceService
前面都是铺垫,真正部署模型只需要一个 InferenceService。核心片段:
apiVersion:serving.kserve.io/v1beta1kind:InferenceServicemetadata:name:qwen-llmspec:predictor:minReplicas:1maxReplicas:1model:modelFormat:name:huggingfacestorageUri:hf://Qwen/Qwen2.5-0.5B-Instructargs:---model_name=qwen---backend=vllm---max_model_len=4096---gpu_memory_utilization=0.90resources:limits:nvidia.com/gpu:"1"几个要点:
modelFormat: huggingface+backend=vllm:用 huggingface runtime,底层跑 vLLM。前几篇的 vLLM 调优经验在这里依然生效。storageUri: hf://...:直接从 HuggingFace 拉模型。minReplicas=maxReplicas=1:单张 T4 固定一个副本。
部署后 Pod 会分两个阶段:先 init 容器 storage-initializer 下载模型,再主容器用 vLLM 加载。整个过程在本次实验约 4 分 40 秒就绪。
就绪后看一眼 KServe 到底帮我们创建了什么:
kubectl get inferenceservice,deployment,replicaset,pod,svc,ingress-nkserve-testinferenceservice/qwen-llm URL: http://qwen-llm-kserve-test.example.com READY True deployment/qwen-llm-predictor 1/1 replicaset/qwen-llm-predictor-xxxx 1 pod/qwen-llm-predictor-xxxx 1/1 Running service/qwen-llm-predictor ClusterIP 80/TCP ingress/qwen-llm istio qwen-llm-kserve-test.example.com,...这就是 KServe 作为「平台」的意义:你只写了一个 InferenceService,Deployment、ReplicaSet、Pod、Service、Ingress 全套都是它自动生成的。对照第06篇的定位,vLLM 和 Triton 是「引擎」,KServe 是「平台」,这里就是最直观的体现。
终端一屏展示kubectl get inferenceservice,deployment,replicaset,pod,svc,ingress -n kserve-test的实际输出,证明只提交一个 InferenceService 后 KServe 自动创建了整套资源。
此时看 GPU 占用:
nvidia-smi --query-gpu=memory.used,memory.total--format=csv# 14043 MiB, 15360 MiBgpu_memory_utilization=0.90生效,vLLM 为 0.5B 模型加 KV Cache 预留了约 14GB。
nvidia-smi完整输出,可见 vLLM 进程与约 14GB 显存占用,证明模型确实加载到 T4 上运行。
调用推理接口
KServe 默认按 Host 路由(下篇会深挖这个机制)。所以调用时要连 NodePort,但带上模型的 Host 头:
curl-H"Host: qwen-llm-kserve-test.example.com"\http://<公网IP>:30080/openai/v1/chat/completions\-H"Content-Type: application/json"\-d'{"model":"qwen","messages":[{"role":"user","content":"用一句话解释什么是Kubernetes"}],"max_tokens":80}'真实返回(节选):
{"model":"qwen","choices":[{"message":{"role":"assistant","content":"Kubernetes(简称K8s)是一种开源的容器编排平台,用于管理和调度应用程序在多个节点上运行..."},"finish_reason":"stop"}],"usage":{"prompt_tokens":35,"completion_tokens":71,"total_tokens":106}}OpenAI 兼容格式,后端是 vLLM。到这里,GPU 主线就跑通了:KServe 控制面就绪、InferenceService Ready、Predictor 拿到 T4、OpenAI 接口返回真实回答。
踩坑提示:KServe 默认 Host 路由,直接用 IP 加路径访问会返回 404。必须带上
-H "Host: <模型域名>",否则网关找不到对应路由。为什么是 Host 而不是 path、这个 Host 路由到底配在哪,下篇专门讲。
实践边界
如实说明本篇的验证范围:
- GPU InferenceService 的部署与推理:已在真实 T4 上验证。
- 单张 T4 固定一个副本运行,未验证多 GPU 副本的水平扩容。配额为 8 vCPU(一张 T4),扩到 2 个 GPU 副本需要第二张卡或 MIG。这一点不夸大。
小结
- 宿主机能看到 GPU,不等于 K8s 能调度 GPU:前者靠驱动,后者靠 GPU Operator 的 Device Plugin。
- 组件依赖顺序:k3s、GPU Operator、cert-manager、Istio、KServe,每层依赖下一层。
- k3s 上装 GPU Operator 的坑:驱动预装用
driver.enabled=false,并通过toolkit.env指定 k3s 的 containerd 路径。 - KServe 0.20 的坑:默认不带 ServingRuntime,需单独装,GPU 模型用
-gpu镜像。 - KServe 是平台:只写一个 InferenceService,自动生成 Deployment、Service、Ingress 一整套。
下一篇预告
AI Infra实战07(下):KServe 路由机制与 Serverless 灰度
下篇讲两件上篇留下的事:
- KServe 的 Host 路由到底配在哪,和手写 Istio 的 Gateway + VirtualService 按 path 分流有什么区别(会用真实的 Envoy 配置和 Ingress 对照)
- 用 Knative 模式验证 Scale to Zero、从 0 冷启动、10% Canary 灰度、回滚和推广
参考链接
- KServe 官方文档
- NVIDIA GPU Operator 文档
- Kubernetes Schedule GPUs
- k3s 官方文档
- Istio 官方文档
- 本篇配套代码仓库