从零搭建一个 AI Infra 实验室⑰:把 vLLM 正式跑进 Kubernetes 上一篇我们研究了一张 GPU 怎样同时提供给多个 Kubernetes Pod 使用。从默认的 Exclusive GPU到 Time-Slicing、MPS、MIG再到 Kubernetes 新的 DRA 资源模型我们已经逐步弄清楚GPU 不仅需要被程序使用还需要被集群发现、分配和管理。至此这台 RTX 3060 实验机已经拥有Kubernetes containerdNVIDIA Container ToolkitNVIDIA Device PluginGPU OperatorGPU Feature DiscoveryDCGM ExporterGPU Sharing但回头看看前几篇的实验会发现一个问题。我们运行的大多还是nvidia-smi CUDA Vector Addition PyTorch Matrix Multiplication GPU Benchmark它们证明了 GPU 可以在 Kubernetes 中工作却还称不上真正的 AI 服务。实际上在本系列前面的 vLLM 实验中我们已经研究过PagedAttention KV Cache Continuous Batching TTFT TPOT Throughput还使用 Qwen2.5 7B AWQ 量级的模型进行过推理性能测试。既然 vLLM 已经可以在单机上运行Kubernetes 也已经能够调度 GPU那么接下来很自然能不能把 vLLM 从一条启动命令变成 Kubernetes 正式管理的推理服务这就是本篇的目标。需要说明的是这次不重新介绍 PagedAttention、KV Cache 的调度原理也不重复安装 GPU Operator。我们重点研究真正部署一个推理服务时基础设施还需要解决什么问题。一、从运行一个模型到运行一个服务在单机时代启动 vLLM 通常很简单。例如vllm serve Qwen/Qwen2.5-7B-Instruct-AWQ \ --host 0.0.0.0 \ --port 8000模型加载完成后Client │ ▼ HTTP :8000 │ ▼ vLLM │ ▼ GPU │ ▼ LLM Inference这种方式非常适合学习模型推理测试模型兼容性运行 Benchmark研究 vLLM 参数但是如果我们希望这个服务长期运行情况就不一样了。假设进程意外退出。谁负责重新启动如果服务器重新启动谁负责恢复推理服务如果模型有几个 GB每次启动都重新下载吗如果 vLLM 还在加载模型客户端能不能知道服务尚未就绪如果以后换成两台 GPU Node客户端要怎样找到推理服务这些问题其实已经不完全属于模型推理。它们属于Application Lifecycle Service Discovery Persistent Storage Resource Management Health Checking也就是我们熟悉的云原生基础设施问题。这时 Kubernetes 才真正开始体现价值。我们希望最终实现Client │ ▼ Kubernetes Service │ ▼ vLLM Deployment │ ▼ vLLM Pod │ ├── GPU Resource │ ├── Model Cache / PVC │ └── vLLM Engine │ ▼ RTX 3060注意Kubernetes 并不会替代 vLLM。vLLM 仍然负责推理引擎内部的模型执行、请求调度和 KV Cache 管理。Kubernetes 负责的是另外一层怎样让这个推理进程作为一个服务被部署、发现、监控和恢复。二、先理解这次涉及的几个核心概念这次实验会出现几个 Kubernetes 对象Deployment Service PersistentVolume PersistentVolumeClaim Job以及两类缓存Hugging Face Cache vLLM Runtime / KV Cache这些东西分别解决不同的问题。1. Deployment让 vLLM 持续存在如果只是创建一个普通 Podkind: Pod我们相当于告诉 Kubernetes运行这个 Pod而 Deployment 的思路是我希望始终有指定数量的 Pod 满足期望状态例如kind: Deployment spec: replicas: 1表示Desired Replicas: 1Deployment 通过 ReplicaSet 管理 Pod。当 Pod 意外消失时控制器会尝试重新创建一个。如果我们修改镜像版本或者启动参数Deployment 还负责执行更新。所以Deployment │ ▼ ReplicaSet │ ▼ vLLM Pod相对于直接运行一个 PodDeployment 增加了持续维护 Desired State 的能力。这与前面研究 GPU Operator 时提到的 Reconciliation 思想是一致的。区别是GPU Operator ↓ 管理 GPU Software Stack Deployment Controller ↓ 管理 Application Pods因此vLLM 首先需要变成 Deployment 管理的 Workload。2. Service让客户端找到 vLLMPod 有自己的 IP。但 Pod 并不是一个稳定的网络实体。例如vLLM Pod A IP: 10.244.0.21如果 Pod 重建新 Pod 可能变成vLLM Pod B IP: 10.244.0.35如果客户端直接连接 Pod IP每次 Pod 重建都可能需要修改地址。Kubernetes Service 就是为了解决这个问题。我们创建kind: Service并通过 Label Selector 找到相应的 Pod。于是客户端访问的变成vllm-service:8000而不是某个具体 Pod IP。Client │ ▼ vllm-service:8000 │ ▼ Service │ ▼ Ready vLLM Pod │ ▼ vLLM APIService 提供的是相对稳定的服务入口以及到后端 Endpoint 的流量转发。即使 Pod 被重新创建只要新的 Pod 正常 ReadyService 仍然可以把流量发送过去。不过Service 本身不会让模型加载得更快也不会保证应用在任何时刻都可用。尤其只有一个 vLLM Replica 时Pod 重启期间依然可能出现服务中断。3. GPU ResourceKubernetes 负责分配 GPU这个概念在⑭和⑯已经研究过。本篇直接使用resources: limits: nvidia.com/gpu: 1这里有一个容易忽略的细节。如果上一回实验中的 Time-Slicing 配置仍然存在Kubernetes 可能看到nvidia.com/gpu: 4但这不意味着 RTX 3060 真的拥有了四份独立显存。因此这次部署 vLLM我们采用1 个 vLLM Replica 1 个 GPU Resource同时确保没有其他高显存 GPU Workload 竞争这张卡。尤其对于大模型推理Time-Slicing 并不提供显存隔离。这一点上一篇已经详细研究本篇不再展开。4. PVC模型文件不能跟着 Pod 一起消失这是本篇第一个真正需要深化的知识点。假设我们让 vLLM 在启动时直接下载模型Pod Start ↓ Download Model ↓ Load Model ↓ Start API第一次可能没有问题。但 Pod 删除以后如果模型只存在容器的可写层中这些数据通常也会随旧容器消失。新 Pod 再启动New Pod ↓ Download Model Again ↓ Load Model显然这并不是理想的服务部署方式。所以我们需要把Container Lifecycle和Model Storage Lifecycle分开。Kubernetes 的 PersistentVolumePV与 PersistentVolumeClaimPVC正是用来解决持久化存储问题的。可以简单理解成PersistentVolume ↓ 提供存储资源 PersistentVolumeClaim ↓ 申请存储资源 Pod ↓ Mount PVC ↓ 访问模型文件当 Pod 重建时新的 Pod 可以重新挂载同一个 PVC。只要底层存储和数据还存在模型文件就不需要跟着 Pod 重新创建。这也是我们从临时实验逐步转向正式服务时必须处理的问题。三、HF Cache 和 KV Cache 完全不是一回事前面研究 vLLM 时我们已经多次遇到 KV Cache。现在又出现HF Cache这两个名字都有 Cache但处于完全不同的层次。1. Hugging Face Cache当我们使用Qwen/Qwen2.5-7B-Instruct-AWQ这样的模型名称时Hugging Face Hub 提供模型仓库中的文件例如config.json tokenizer.json tokenizer_config.json model-00001-of-00002.safetensors model-00002-of-00002.safetensorsHugging Face 的文件缓存通常采用类似下面的结构HF Cache │ └── hub/ │ └── models--Qwen--Qwen2.5-7B-Instruct-AWQ/ │ ├── refs/ ├── blobs/ └── snapshots/其中blobs保存实际缓存的文件内容。refs记录分支、标签等引用与提交版本之间的对应关系。snapshots表示特定版本的仓库文件视图。这让同一个模型的不同 Revision 可以复用部分底层文件而不需要反复下载完全相同的数据。我们可以通过HF_HOME/models/hf指定 Hugging Face 的缓存根目录。这样模型文件就不再依赖容器默认的~/.cache/huggingface而是可以统一保存在 PVC 中。2. vLLM 的 KV CacheKV Cache 则完全不同。它保存的是模型推理过程中 Attention 所需要的 Key 和 Value。例如Request ↓ Prefill ↓ KV Cache ↓ Decode这些数据主要位于推理过程中使用的 GPU 显存中。KV Cache 的大小会影响推理服务能够容纳的上下文与并发请求。因此HF Cache ↓ 模型文件缓存 ↓ 主要占用磁盘空间 KV Cache ↓ 推理状态缓存 ↓ 主要占用 GPU 显存把 HF Cache 放入 PVC并不意味着 KV Cache 也会被持久化。当 vLLM 进程重新启动时通常仍然需要读取模型权重 ↓ 加载到 GPU ↓ 初始化推理引擎 ↓ 重新建立 KV Cache也就是说PVC 解决的是模型文件重复下载的问题而不是让 GPU 中已经加载好的模型永久存在。除此之外vLLM 还可能使用磁盘上的编译缓存。它可以帮助复用某些编译产物但也不能等同于 KV Cache更不能保证启动完全不需要初始化。这三个层次一定要分清楚。四、本次实验的完整架构现在把前面的概念画到一张图里。Client │ │ HTTP Request ▼ Kubernetes Service vllm-service │ ▼ Deployment replicas: 1 │ ▼ vLLM Pod ┌────────────┼────────────┐ │ │ │ ▼ ▼ ▼ GPU Resource PVC Mount vLLM API │ │ │ ▼ ▼ ▼ nvidia.com/gpu HF Cache vLLM Engine │ │ │ ▼ │ ┌────┴─────┐ Device Plugin │ │ │ │ │ Model Load KV Cache ▼ │ │ │ RTX 3060 ◄───────┼───────┘ │ │ │ Persistent Storage │ │ │ Model Weights │ ▼ GPU Memory这张图中的三个核心方向分别是第一条Kubernetes Control PlaneDeployment ↓ Pod Lifecycle第二条存储PVC ↓ HF Cache ↓ Model Weights第三条推理vLLM ↓ GPU ↓ InferenceService 则负责把客户端请求连接到正在运行并且已经 Ready 的 vLLM Pod。前面几篇我们分别研究了其中的某些部分。这一次终于把它们组合成一个完整的 AI Inference Service。五、实验环境与目标继续使用本系列的 AI Infra 实验机项目配置OSUbuntu 24.04CPUIntel Core i5-10400FGPUNVIDIA GeForce RTX 3060VRAM12GBKubernetes1.37.xRuntimecontainerdGPU 管理NVIDIA GPU OperatorvLLM官方 OpenAI API 镜像模型Qwen2.5-7B-Instruct-AWQ这次选择同样是 Qwen2.5 7B AWQ 量级的 Instruct 模型是为了和前面的 vLLM 实验保持延续性同时方便直接调用 Chat Completions API。模型仓库https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-AWQ其仓库文件总体约 5.58GB。需要注意磁盘中的权重大小不等于实际 GPU 显存占用。显存还需要容纳运行时缓冲区、KV Cache 以及其他开销。本篇的目标不是把 RTX 3060 性能调到极致而是完成Model Download ↓ Persistent Cache ↓ vLLM Deployment ↓ GPU Scheduling ↓ Service ↓ OpenAI-compatible API最后观察两个重要现象模型缓存以后Pod 重启是否避免重复下载Pod 从创建到 Ready到底需要多长时间下面开始实验。六、实验一确认 Kubernetes GPU 环境先检查 Nodekubectl get nodes -o wide确认 Node 处于Ready查看 GPUnvidia-smi检查 Kubernetes 的 GPU Resourcekubectl get nodes \ -o custom-columnsNAME:.metadata.name,GPU:.status.allocatable.nvidia\\.com/gpu如果已经恢复默认独占模式应该显示GPU: 1如果上一回 Time-Slicing 实验仍然有效则可能是GPU: 4这次无论是哪种情况都只准备运行一个 vLLM 实例并先停止前面遗留的 GPU 测试 Pod。接着检查 Operatorkubectl get clusterpolicy确认 GPU Software Stack 正常。这些都是前面已经配置过的基础设施本篇不再重复安装过程。再检查一下磁盘空间df -h原因很简单。vLLM 镜像、模型文件、Hugging Face Cache 都可能占用大量磁盘。这次不只是运行一个很小的 CUDA Sample需要提前为镜像和模型准备足够空间。七、实验二给模型准备持久化存储我们先创建独立的 Namespacekubectl create namespace ai-infra接下来创建 PV 和 PVC。考虑到这次只有一台 Kubernetes Node为了避免额外引入 NFS、Ceph 或 CSI Driver我们暂时使用单节点实验中最容易理解的静态 PV。首先在宿主机上创建目录sudo mkdir -p \ /srv/ai-infra/vllm-model-cache创建vllm-storage.yamlapiVersion: v1 kind: PersistentVolume metadata: name: vllm-model-pv spec: capacity: storage: 20Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: hostPath: path: /srv/ai-infra/vllm-model-cache type: Directory --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vllm-model-pvc namespace: ai-infra spec: accessModes: - ReadWriteOnce storageClassName: volumeName: vllm-model-pv resources: requests: storage: 20Gi应用kubectl apply -f vllm-storage.yaml检查kubectl get pv kubectl get pvc -n ai-infra期待PVBound PVCBound这里几个地方值得注意。首先storageClassName: 表示我们不希望 PVC 自动使用集群可能存在的默认 StorageClass。而是明确绑定预先准备的静态 PV。其次persistentVolumeReclaimPolicy: Retain表示 PVC 被删除以后PV 背后的数据不会被 Kubernetes 自动清除。最后accessModes: - ReadWriteOnce表示该 Volume 可以在满足存储系统约束的条件下以读写方式挂载到一个 Node。它不等于“只能由一个 Pod 读取”也不意味着提供分布式文件系统能力。特别说明本次 hostPath 方案只用于单节点实验。这里声明的20Gi是 Kubernetes 的存储匹配容量不是对宿主机目录的实际硬盘配额限制。真正多节点部署时应使用支持节点拓扑约束的 Local PV或者合适的 CSI、共享存储方案。本篇暂时不增加这些复杂度。八、实验三先下载模型再启动服务这是本次实验一个比较重要的设计选择。最简单的方式是让 vLLM 启动时自己下载模型。但是这样Pod Start ↓ Model Download ↓ GPU Initialization ↓ Model Loading ↓ API Ready所有步骤混在一起。当启动耗时很长时我们很难判断到底是网络下载慢 还是模型加载慢 还是 vLLM 初始化慢因此这次换一种方式把模型下载与推理服务启动分开。我们先使用 Kubernetes Job 下载模型到 PVC。然后让 Deployment 直接使用这份缓存。创建vllm-model-download.yamlapiVersion: batch/v1 kind: Job metadata: name: vllm-model-download namespace: ai-infra spec: backoffLimit: 2 template: spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet restartPolicy: Never containers: - name: downloader image: python:3.11-slim env: - name: HF_HOME value: /models/hf command: - /bin/sh - -c args: - | set -e pip install --no-cache-dir \ huggingface_hub0.36.0 python - PY import time from huggingface_hub import snapshot_download start time.monotonic() path snapshot_download( repo_idQwen/Qwen2.5-7B-Instruct-AWQ ) print(Model path:, path) print( Download elapsed:, round(time.monotonic() - start, 2), seconds ) PY volumeMounts: - name: model-cache mountPath: /models volumes: - name: model-cache persistentVolumeClaim: claimName: vllm-model-pvc如果需要代理在spec.template.spec下增加hostNetwork: true dnsPolicy: ClusterFirstWithHostNet在 Container 的env中增加代理配置env: - name: HF_HOME value: /models/hf - name: HTTP_PROXY value: http://HTTP_PROXY_IP:HTTP_PROXY_IP_PORT - name: HTTPS_PROXY value: http://HTTP_PROXY_IP:HTTP_PROXY_IP_PORT - name: NO_PROXY value: localhost,127.0.0.1,.svc,.cluster.local这里的HTTP_PROXY_IP 和 HTTP_PROXY_IP_PORT需要替换成你真实的代理IP地址和端口。执行kubectl apply -f vllm-model-download.yaml查看下载过程kubectl logs -n ai-infra \ -f job/vllm-model-download如果网络正常模型文件会被下载到 PVC 对应的宿主机目录中。从宿主机查看sudo du -sh \ /srv/ai-infra/vllm-model-cache以及sudo ls \ /srv/ai-infra/vllm-model-cache/hf/hub应该可以看到类似models--Qwen--Qwen2.5-7B-Instruct-AWQ注意此时模型已经下载但模型还没有加载进 GPU这是两个不同阶段。模型下载 Job 不申请 GPU因为它只负责获取模型文件不负责执行推理。另外上面的计时主要测量snapshot_download()调用耗时不包含 Python 镜像拉取和依赖安装。如果想统计整个 Job 的完成时间需要另行记录 Kubernetes Job 的开始和完成时间。为什么不直接把模型打包进镜像其实可以。例如构建Custom vLLM Image │ ├── vLLM Runtime └── Model Weights这样启动时就不需要额外下载模型。但代价也很明显每次更新模型都可能需要构建和分发一个非常大的镜像。对于频繁变化的模型或者多个推理实例共享相同模型的情况独立保存模型文件通常更灵活。所以这次选择vLLM Image Model PVC把应用运行环境和模型数据分开管理。九、实验四部署正式的 vLLM Deployment现在模型已经准备好了。接下来才真正启动推理服务。这里使用官方的vllm/vllm-openai镜像。为保证实验可重复我们不使用latest而是明确指定版本。本次示例选择vllm/vllm-openai:v0.31.0这个版本已在 vLLM 官方发布记录中提供。实际实验时也可以替换成之前单机 Benchmark 已经验证稳定的版本避免同时引入版本升级变量。创建vllm-deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: vllm namespace: ai-infra spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: vllm template: metadata: labels: app: vllm spec: terminationGracePeriodSeconds: 60 containers: - name: vllm image: vllm/vllm-openai:v0.31.0 imagePullPolicy: IfNotPresent args: - --model - Qwen/Qwen2.5-7B-Instruct-AWQ - --served-model-name - qwen2.5-7b-awq - --host - 0.0.0.0 - --port - 8000 - --dtype - half - --gpu-memory-utilization - 0.80 - --max-model-len - 2048 - --max-num-seqs - 4 - --enforce-eager env: - name: HF_HOME value: /models/hf - name: HF_HUB_OFFLINE value: 1 - name: TRANSFORMERS_OFFLINE value: 1 - name: VLLM_CACHE_ROOT value: /models/vllm ports: - name: http containerPort: 8000 resources: requests: cpu: 2 memory: 4Gi limits: cpu: 4 memory: 10Gi nvidia.com/gpu: 1 volumeMounts: - name: model-cache mountPath: /models - name: shm mountPath: /dev/shm startupProbe: httpGet: path: /health port: http periodSeconds: 5 failureThreshold: 180 readinessProbe: httpGet: path: /health port: http periodSeconds: 10 livenessProbe: httpGet: path: /health port: http periodSeconds: 30 failureThreshold: 3 volumes: - name: model-cache persistentVolumeClaim: claimName: vllm-model-pvc - name: shm emptyDir: medium: Memory sizeLimit: 1Gi应用kubectl apply -f vllm-deployment.yaml观察kubectl get pods -n ai-infra -w正常情况下会经历Pending ↓ ContainerCreating ↓ Running ↓ Ready这里不要看到Running就认为 vLLM 已经可以提供推理。容器进程已经启动不代表模型已经加载完毕。真正能够提供 API 服务需要等应用进入 Ready 状态。1. 为什么设置replicas: 1因为这次只有一张 RTX 3060。而且运行的是 7B 量级的模型。即使上一回 Time-Slicing 已经让 Kubernetes 暴露多个 GPU Resource我们也不希望同时启动两个完整的 vLLM 实例。否则vLLM A │ ├── Model Weights └── KV Cache vLLM B │ ├── Model Weights └── KV Cache两个实例需要竞争同一张 GPU 的显存。Kubernetes 的 GPU Resource 数量并不能提前保证它们都装得下。因此单 GPU 实验先采用单 Replica。2. 为什么使用RecreateDeployment 默认采用 RollingUpdate。这种方式对普通 Web Service 非常合理。例如Old Pod Running ↓ Create New Pod ↓ New Pod Ready ↓ Terminate Old Pod它的目标是减少升级过程中的不可用时间。但是对于我们的单 GPU 大模型服务Old vLLM占用大量显存 New vLLM还要加载完整模型如果两者同时启动就可能发生 GPU 资源竞争或显存不足。因此这次明确使用strategy: type: Recreate升级时先停止旧版本再创建新版本。代价是服务会有一段不可用时间。但它更符合单 GPU 实验机的资源条件。需要注意Recreate主要约束 Deployment 更新过程并不是所有场景下都提供严格的单实例互斥保证。3. 为什么配置这些 vLLM 参数首先--gpu-memory-utilization 0.80表示 vLLM 按指定比例规划自身使用的 GPU 显存。这不是 Kubernetes 级别的硬隔离也不能保证和其他 GPU 进程安全共享。然后--max-model-len 2048先把模型支持的最大上下文长度限制在较小范围。我们暂时不需要很长的 Context降低初始化和 KV Cache 容量需求更符合本次实验目的。再看--max-num-seqs 4限制一次调度迭代中能够处理的 Sequence 数量。这里的4不是四个 GPU也不是四个 Kubernetes Pod。它属于 vLLM 内部的推理调度参数。最后--enforce-eager让 vLLM 使用 Eager 执行关闭 CUDA Graph 和相应的编译优化路径。本次目的是减少首次初始化的复杂度、节省部分显存并让启动时间更容易分析。它可能牺牲稳态推理性能。后续进行正式吞吐量 Benchmark 时应重新评估是否保留这个参数。4. 为什么加入 Startup、Readiness 和 Liveness Probe这是这篇非常值得理解的部分。三者不是同一个东西。startupProbe应用完成启动了吗它允许 vLLM 在首次加载模型时花费较长时间而不被其他探针过早判定失败。readinessProbe现在可以接收请求了吗只有 Ready 的 Pod 才会被正常加入 Service 的可用 Endpoint。livenessProbe应用是不是已经失去响应需要重启这三个 Probe 让 Kubernetes 不只是知道Container Running还能够进一步判断Inference Service 是否可用对于需要几十秒甚至数分钟启动时间的模型服务Startup Probe 尤其重要。不过探针本身不加速启动它只是帮助 Kubernetes 正确管理慢启动应用。十、实验五创建 Service真正提供 APIDeployment 已经负责运行 vLLM。但我们还需要给它一个稳定入口。创建vllm-service.yamlapiVersion: v1 kind: Service metadata: name: vllm-service namespace: ai-infra spec: type: ClusterIP selector: app: vllm ports: - name: http port: 8000 targetPort: http protocol: TCP执行kubectl apply -f vllm-service.yaml查看kubectl get svc -n ai-infra应该出现vllm-service进一步检查kubectl get endpointslice \ -n ai-infra \ -l kubernetes.io/service-namevllm-service当 Pod Ready 以后Service 才会有可用于转发请求的后端。在同一个 Namespace 中其他 Pod 可以通过http://vllm-service:8000访问推理服务。其他 Namespace 中的 Pod则可以使用http://vllm-service.ai-infra.svc.cluster.local:8000这就是 Kubernetes Service Discovery。对于一个 AI 应用来说以后调用的是稳定的 Service 名称而不需要知道模型究竟运行在哪个 Pod。本次先不配置 Ingress、Gateway 或 LoadBalancer。它们主要解决更进一步的入口流量管理问题不是这次实验的重点。十一、实验六从 Kubernetes 外部调用 vLLM API我们先使用最安全、最简单的本地测试方式kubectl port-forward \ -n ai-infra \ service/vllm-service \ 8000:8000这里的localhost指执行kubectl port-forward的机器。打开另一个 Terminal先检查服务curl http://127.0.0.1:8000/health接着查看可用模型curl -s \ http://127.0.0.1:8000/v1/models \ | python3 -m json.tool应该能够看到刚才设置的模型名称qwen2.5-7b-awq最后发送一次真正的 Chat Completions 请求curl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5-7b-awq, messages: [ { role: user, content: 请简单解释 Kubernetes Service 的作用。 } ], temperature: 0.2, max_tokens: 128 }如果一切正常会得到兼容 OpenAI Chat Completions 格式的 JSON 响应。其中通常包含id object created model choices usage而在choices[0].message.content中就可以得到模型生成的文本。到这里我们完成的不再是一个简单 GPU Pod。而是Client │ ▼ Kubernetes Service │ ▼ vLLM API │ ▼ Qwen2.5 Model │ ▼ RTX 3060 │ ▼ Inference Result这就是本篇最重要的功能验证。需要注意OpenAI-compatible 主要指 API 请求与响应接口的兼容并不表示不同模型的输出行为完全一致。另外本篇只使用本地端口转发进行实验。vLLM API 不应在没有适当访问控制的情况下直接暴露到公网。十二、观察数据我们真正应该测量什么前面的实验解决的是能不能运行但 AI Infra 还有一个很重要的问题运行起来需要付出什么代价这里最值得观察的并不是再跑一次 TTFT 和 TPOT Benchmark。这些我们已经在前面的 vLLM 专题中研究过。这一篇关注的是模型服务的启动生命周期。1. 首先把启动过程拆成几个阶段一个完整的冷启动可能包括Create Pod ↓ Scheduling ↓ Image Pull ↓ Container Start ↓ Model Loading ↓ Engine Initialization ↓ Health Check Success ↓ Ready如果没有预先下载模型还会多出Model Download如果没有关闭 CUDA Graph 和编译优化路径还可能增加相应的编译、捕获和预热时间。因此Pod Startup Time并不是一个单纯由 Kubernetes 决定的数字。它还受到镜像大小镜像是否已经缓存模型是否已经下载模型文件存储速度GPU 显存和加载速度vLLM 初始化配置CPU、内存及磁盘负载等多种因素影响。这也是为什么本篇选择先下载模型再启动 vLLM。至少可以把模型下载阶段从服务初始化过程中单独分离出来。2. 记录模型下载时间前面的下载 Job 已经记录Download elapsed这个数字可以告诉我们第一次执行snapshot_download()花费多少时间。再查看kubectl logs -n ai-infra \ job/vllm-model-download记录下载耗时。如果之后再次运行相同的下载逻辑缓存完整且模型 Revision 未变化就可以观察缓存是否复用。需要注意缓存命中也不保证完全没有网络请求。Hugging Face 仍然可能检查远端 Revision 等元数据。3. 记录第一次 Deployment 启动时间查看kubectl get pods -n ai-infra -o wide再看详细事件kubectl describe pod -n ai-infra \ -l appvllm可以重点观察Scheduled Pulling Pulled Created Started这些 Event。进一步查看 vLLM 日志kubectl logs -n ai-infra \ deployment/vllm \ --timestamps重点寻找Model Loading Weights Loading Engine Initialization API Server Started等阶段性日志。不同 vLLM 版本的具体日志文字可能不一样不需要强行依赖固定字符串。真正重要的是建立时间顺序Container Start │ ▼ Model Load │ ▼ Engine Init │ ▼ API Ready第一次镜像拉取所花费的时间应当单独记录否则容易把网络下载时间误判成模型初始化时间。4. 重启 Pod对比第二次启动接下来进行本篇最值得做的一组对比实验。记录开始时间T0$(date %s) kubectl rollout restart \ deployment/vllm -n ai-infra kubectl rollout status \ deployment/vllm -n ai-infra \ --timeout900s T1$(date %s) echo Rollout elapsed: $((T1-T0)) s这个数字代表从发起重启更新到 Deployment Rollout 完成的总体耗时。它不是纯粹的模型加载时间。但它很适合从运维角度观察一个已经部署的推理服务重启以后需要多久才能恢复到期望状态然后查看缓存sudo du -sh \ /srv/ai-infra/vllm-model-cache/hf以及第二次启动的日志。如果 HF Cache 正常复用第二次启动通常不需要重新下载完整模型权重。但它仍然需要重新完成 GPU 模型加载和 vLLM 初始化。这也是预期中的行为。5. 建议记录的数据观察项目第一次重启后vLLM 镜像是否本地缓存否下载耗时 36 min是HF 模型缓存是否已存在是已预加载是Pod 到 Ready 总耗时44.65 秒65.0 秒vLLM 引擎初始化耗时10.88 秒14.78 秒是否重新下载完整模型否使用 PVC 缓存否Ready 后 GPU 显存占用9371 MiB9371 MiBAPI 请求是否成功是是这些数据和实际镜像版本、网络、模型缓存及 GPU 状态直接相关。如果想把本次实验做得更严谨建议在对比期间保持相同模型、相同 vLLM 参数并停止其他 GPU 工作负载。这组数据真正说明什么有三个问题值得回答。第一PVC 是否真正发挥了作用如果 Pod 重建后仍然可以复用 HF Cache说明模型数据已经和容器生命周期分离。第二减少下载是否等于快速启动不等于。即使模型文件就在本地vLLM 仍然要把权重加载到 GPU并完成推理引擎初始化。第三Kubernetes 能不能知道什么时候可以接收请求可以。通过 Readiness ProbeKubernetes 可以把Container Running和Application Ready区分开。这才是把 AI 推理进程交给 Kubernetes 管理的重要收益。十三、还有一个值得观察的现象显存不是 Scheduler 能完全管理的等 vLLM 启动以后在宿主机执行nvidia-smi查看显存占用。这时一般会看到模型加载带来的明显显存变化。但是 Kubernetes 里面nvidia.com/gpu: 1仍然只是一个 GPU Resource Request。Scheduler 并不知道Model Weights多少 GB KV Cache多少 GB Runtime Buffers多少 GB这些内部数据。而且CPU Memory Limit也不等于GPU Memory Limit例如我们在 Deployment 中设置limits: memory: 10Gi nvidia.com/gpu: 1这里的memory: 10Gi约束的是容器使用的系统内存而不是把 RTX 3060 的显存限制为 10GiB。真正的 GPU 显存使用还取决于 vLLM 的模型加载方式和运行时参数。因此即使Pod Successfully Scheduled也不保证Model Successfully Loaded如果模型太大仍然可能在初始化阶段发生 CUDA OOM。这个现象恰好把前几篇的 GPU Resource Management与本篇的实际 AI Workload 联系起来。Kubernetes 负责分配资源。但应用是否能够在已分配的设备上正常运行还需要应用自己处理。十四、几个真正值得注意的坑本次实验不准备列一长串故障清单。但有三个问题确实容易影响结果。1. PVC 一直 Pending首先检查kubectl get pv kubectl get pvc -n ai-infra确认StorageClass AccessModes Requested Capacity volumeName能够匹配。由于这次使用静态 PV不能假设 Kubernetes 会自动创建磁盘和目录。宿主机上的实际存储路径必须准备好。2. 模型下载失败如果 Job 无法下载模型先检查kubectl logs -n ai-infra \ job/vllm-model-download确认究竟是DNS HTTPS Connectivity Proxy Authentication Disk Space哪一层出了问题。尤其需要注意宿主机能够访问 Hugging Face不代表 Kubernetes Pod 一定能访问。宿主机 Shell 的代理环境变量也不会自动出现在 Pod 中。如果使用代理需要为下载 Job 单独配置并正确处理NO_PROXY。对私有或需要授权的模型应通过 Kubernetes Secret 安全地提供访问令牌而不是把 Token 明文写进公开的 YAML。3. Pod Running但迟迟不 Ready这是本次最重要的故障边界。首先看kubectl logs -n ai-infra \ deployment/vllm --tail100如果正在加载模型可能只是启动尚未结束。如果出现CUDA out of memory则需要检查GPU Memory Utilization Model Size Max Model Length Max Number of Sequences 其他 GPU 进程尤其要确认上一回 Time-Slicing 实验的其他工作负载是否已经清理。如果容器不断被探针重启则需要查看kubectl describe pod -n ai-infra \ -l appvllm判断是应用真正失败还是 Startup Probe 的允许时间不足。不能因为模型启动慢就简单地把所有健康检查全部删除。正确的方向是区分启动中和启动失败十五、总结我们终于有了第一个 Kubernetes AI 推理服务回顾这一篇我们没有重新安装 Kubernetes也没有继续研究 GPU Sharing。而是把前面搭好的基础设施真正用于部署 AI Inference Service。这次串起来的完整链路是Kubernetes │ ▼ Deployment │ ▼ vLLM Pod │ ├── GPU Resource │ │ │ ▼ │ RTX 3060 │ ├── PVC │ │ │ ▼ │ HF Cache │ └── vLLM Engine │ ▼ OpenAI-compatible API │ ▼ Service │ ▼ Client我们还第一次把模型服务的生命周期拆开观察Download Model ↓ Persist Model ↓ Start Container ↓ Load GPU Weights ↓ Initialize Engine ↓ Ready ↓ Serve Requests这里最值得记住的并不是某条kubectl apply命令。而是几个清晰的边界。Deployment 管理 Pod 的生命周期。Service 提供稳定的服务访问入口。GPU Resource 让 Kubernetes 能够把推理工作负载调度到具备 GPU 的 Node。PVC 让模型文件可以跨 Pod 生命周期保存。HF Cache 减少重复下载但不等于 GPU 中的模型权重或 KV Cache。Readiness Probe 则让 Kubernetes 知道一个已经启动的容器是否真正具备服务能力。所以我们从前面几篇的GPU 能不能被 Kubernetes 使用终于走到了AI 模型能不能作为 Kubernetes Service 持续运行这就是 AI Infra 实验室从基础设施建设走向实际 AI Serving 的重要一步。当然这距离真正的生产级推理平台还有距离。目前还只有Single Node Single GPU Single vLLM Replica Single Model没有高可用也没有专门的 API Gateway、认证体系或多副本调度。但整条基本链路已经具备了。接下来我们可以在这个基础上继续增加真正的 AI Infrastructure 能力。