先把成果说清楚:搭完之后,Kubernetes 里会跑起多个 vLLM 模型副本,外部通过一个统一 Gateway 访问;请求按路径或域名路由到不同模型;llm-d 的 Endpoint Picker Provider(EPP)在选副本时会参考前缀缓存命中、等待队列长度和 KV 缓存利用率;HPA 根据 vLLM 暴露的指标自动扩缩容。下面从单机 vLLM 出发,一步步升级到这层推理控制平面。
前置条件清单
- 一个可用的 Kubernetes 集群,GPU 节点已安装 NVIDIA 驱动与 device plugin,能分配
nvidia.com/gpu。 - 本地有
kubectl、helm,能拉取容器镜像。 - 已经能在单 Pod 或单机跑通 vLLM 的 OpenAI 兼容服务,确认模型权重可访问。
- 按官方文档安装 Gateway API CRDs 与 Gateway API AI 词典:Inference">Inference Extension CRDs。版本以官方文档当前版本为准。
- 安装 Prometheus 与 Prometheus Adapter,后面 HPA 要用自定义指标。
- 模型权重放在 PVC、对象存储或镜像中,尽量缩短冷启动时间。
- 网络策略允许 Gateway 到 EPP、EPP 到 vLLM 的流量。
- 下文镜像名、CRD 版本、Helm chart 名称均以官方文档当前版本为准,不要直接照搬旧 tag。
步骤 1:把单机 vLLM 变成 Kubernetes 可调度后端
先部署两个模型后端,这里以 qwen 为例。重点是开启前缀缓存、暴露 metrics、设置启动探针。镜像用占位符,替换成实际可用的 vLLM 镜像。
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-qwen
namespace: llm
spec:
replicas: 2
selector:
matchLabels:
app: vllm-qwen
template:
metadata:
labels:
app: vllm-qwen
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
spec:
containers:
- name: vllm
image: <vllm-image> # 以官方文档当前版本为准
command: ["python3", "-m", "vllm.entrypoints.openai.api_server"]
args:
- "--model"
- "<model-path>"
- "--served-model-name"
- "qwen"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
- "--enable-prefix-caching"
- "--max-model-len"
- "8192"
- "--gpu-memory-utilization"
- "0.90"
- "--disable-log-requests"
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: "1"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 120
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: vllm-qwen
namespace: llm
spec:
selector:
app: vllm-qwen
ports:
- name: http
port: 8000
targetPort: 8000
```
再复制一份,改成 vllm-llama、--served-model-name llama、对应模型路径。两个 Deployment 的标签要区分开,后面 InferencePool 靠标签选 Pod。
```bash
kubectl apply -f vllm-qwen.yaml
kubectl apply -f vllm-llama.yaml
kubectl -n llm get pods -w
```
确认每个 Pod 的 /health 返回正常,再继续下一步。
步骤 2:安装 Gateway API 与 llm-d 控制组件
这一步把 CRD、Gateway 控制器、EPP 装进集群。命令里的文件与仓库地址按官方文档获取。
```bash
kubectl apply -f gateway-api-crds.yaml
kubectl apply -f inference-extension-crds.yaml
helm repo add llm-d <官方 Helm 仓库地址> # 以官方文档当前版本为准
helm repo update
helm install llm-d-infra llm-d/llm-d-infra \
--namespace llm-d-system \
--create-namespace \
--set gateway.enabled=true \
--set epp.enabled=true
```
如果不用 Helm,就按官方 YAML 清单部署。核心是集群里出现 GatewayClass、Gateway、EPP 的 Deployment/Service,以及 InferencePool CRD。安装后检查:
```bash
kubectl get gatewayclass
kubectl -n llm-d-system get pods
kubectl get crd | grep inference
```
步骤 3:为每个模型建 InferencePool
InferencePool 的作用是告诉控制平面“这组 Pod 是某个模型的后端”,并绑定 EPP 做端点选择。apiVersion 以实际安装的 CRD 为准。
```yaml
apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferencePool
metadata:
name: qwen-pool
namespace: llm
spec:
selector:
matchLabels:
app: vllm-qwen
targetPortNumber: 8000
extensionRef:
name: llm-d-epp
---
apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferencePool
metadata:
name: llama-pool
namespace: llm
spec:
selector:
matchLabels:
app: vllm-llama
targetPortNumber: 8000
extensionRef:
name: llm-d-epp
```
extensionRef 指向实际部署的 EPP 服务,名称和命名空间按安装结果调整。接着配置 EPP 调度策略,启用前缀缓存感知、队列长度和 KV 缓存利用率打分。不同版本可能用 ConfigMap 或自定义 CRD,下面只是结构示意,字段名以官方文档当前版本为准。
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: llm-d-epp-config
namespace: llm-d-system
data:
config.yaml: |
plugins:
- name: prefix-cache-scorer
weight: 3
- name: queue-scorer
weight: 2
- name: kv-cache-utilization-scorer
weight: 1
schedulingProfile: default
```
改完配置后滚动重启 EPP:
```bash
kubectl -n llm-d-system rollout restart deploy/llm-d-epp
kubectl -n llm-d-system logs deploy/llm-d-epp -f
```
步骤 4:用 Gateway + HTTPRoute 做多模型路由
OpenAI 兼容接口的 model 字段在 JSON body 里,普通 L7 路由看不到,所以用路径前缀区分模型。客户端把 base_url 改成对应前缀即可。
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: llm-multi-model
namespace: llm
spec:
parentRefs:
- name: inference-gateway
namespace: llm-d-system
rules:
- matches:
- path:
type: PathPrefix
value: /qwen
backendRefs:
- group: inference.networking.x-k8s.io
kind: InferencePool
name: qwen-pool
port: 8000
- matches:
- path:
type: PathPrefix
value: /llama
backendRefs:
- group: inference.networking.x-k8s.io
kind: InferencePool
name: llama-pool
port: 8000
```
调用时这样写:
```bash
curl http://<gateway-ip>/qwen/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen","messages":[{"role":"user","content":"你好"}]}'
curl http://<gateway-ip>/llama/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"llama","messages":[{"role":"user","content":"你好"}]}'
```
如果 Gateway 是 LoadBalancer,用 kubectl -n llm-d-system get gateway 查地址。没有外部 IP 时,可以用端口转发临时验证。
步骤 5:验证缓存感知调度
先确认 vLLM 的 /metrics 里有前缀缓存相关指标。然后发相同长前缀的请求,观察 EPP 日志选了哪个副本。
```bash
for i in $(seq 1 20); do
curl -s http://<gateway-ip>/qwen/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen","messages":[{"role":"user","content":"<长前缀> 问题'"$i"'"}]}' > /dev/null
done
```
查看 EPP 日志中的 endpoint pick 记录和打分。如果请求均匀分散到所有副本,说明前缀缓存打分没生效:检查 EPP 能否访问 vLLM 的 metrics 端口,InferencePool selector 是否匹配到 Pod,以及 EPP 配置是否真正加载。
步骤 6:配置自动扩缩容
用 Prometheus 抓取 vLLM 指标,把等待队列长度或 KV 缓存利用率通过 Prometheus Adapter 注册成 Pods 指标。下面以等待队列为例,指标名以 Adapter 实际注册名为准,通常由 vllm:num_requests_waiting 转换而来。
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-qwen-hpa
namespace: llm
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-qwen
minReplicas: 1
maxReplicas: 8
metrics:
- type: Pods
pods:
metric:
name: vllm_num_requests_waiting
target:
type: AverageValue
averageValue: "4"
behavior:
scaleUp:
stabilizationWindowSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300
```
模型加载需要时间,扩容后 Pod 就绪前不能接流量;缩容太快会导致冷启动。生产环境建议结合 GPU 配额、PDB 和优雅退出一起配。
步骤 7:观测与灰度
给 vLLM、EPP、Gateway 都接 Grafana 面板,关注 TTFT、ITL、队列长度、KV 缓存使用率、前缀缓存命中率。新模型先建小流量 HTTPRoute,按权重切少量流量,再逐步放大。用 PodDisruptionBudget 保护推理副本,避免滚动更新打断长请求。
常见坑与排错
1. GPU 不可见:检查 nvidia.com/gpu 是否可分配,device plugin 是否 Running。
2. vLLM 启动失败:显存不足、模型路径错误、tensor parallel 与 GPU 数不匹配。
3. Gateway 无地址:LoadBalancer 未就绪,或 GatewayClass 没有被控制器接管。
4. InferencePool 不生效:CRD 版本与控制器不匹配,selector 没匹配到 Pod。
5. EPP 选端点失败:EPP 服务端口、命名空间、RBAC 权限、网络策略要逐项检查。
6. 前缀缓存收益不明显:请求前缀不一致,或 EPP 读不到指标;短请求收益本来就低。
7. HPA 不触发:Prometheus 没抓到指标,Adapter 规则名称不对,或指标值一直为 0。
8. 滚动更新 5xx:readinessProbe 太早通过,模型还没加载完;用 startupProbe 兜底。
9. 长请求被缩容打断:设置较长缩容稳定窗口,配合 PDB 和优雅退出。
10. 多模型路由 404:路径前缀没在 HTTPRoute 中匹配,或客户端 base_url 没带前缀。
下一步建议
- 接入 PD 分离,把 prefill 和 decode 拆成不同池,让 EPP 分别调度。
- 用 LoRA 适配器在一个基座模型上挂多个模型名,减少副本数。
- 引入 Workload Variant Autoscaler 或 KEDA,按队列和缓存指标做更细粒度扩缩。
- 多集群:把 InferencePool 扩展到多集群网关,做故障转移和就近推理。
- 给 EPP 加自定义 scorer,比如按租户、成本、延迟 SLO 选择副本。
- 所有版本、字段与安装方式,以 vLLM、llm-d、Gateway API Inference Extension 官方文档当前版本为准。
