1. 为什么标准 K8s 负载均衡跑不动 LLM 推理
如果你已经在 Kubernetes 上部署过 vLLM 或 TGI,大概率遇到过这种场景:三个副本,Service 用默认的轮询,结果一个副本排队排到爆,另外两个 GPU 利用率还不到 30%。这不是配置写错了,而是 LLM 推理的请求形状和传统 Web 服务根本不是一个物种。
传统微服务的请求耗时基本稳定,副本之间可以随便轮询。但 LLM 推理不一样:一个 RAG 请求可能塞进去 8000 个 token 的上下文、只输出 200 个 token;另一个代码补全请求输入 300 token、输出 1500 token。这两种请求对 Prefill 算力和 Decode 显存带宽的压力完全不同。轮询分发等于把不同重量的包裹随机扔给快递员,有的累死有的闲着。
更麻烦的是 KV Cache。多轮对话和 Agent 场景里,第二轮请求如果被路由到没有缓存第一轮 KV 的副本上,就得从头算一遍 Prefill。首 token 延迟(TTFT)直接从几百毫秒飙到几秒。标准 K8s Service 的负载均衡完全不知道哪个副本缓存了什么前缀,它只会看连接数。
llm-d 就是冲着这几个问题来的。它是 Kubernetes 原生的分布式 LLM 推理框架,构建在 vLLM、Kubernetes 和 Inference Gateway(IGW)之上,核心做了三件事:前缀感知路由、Prefill/Decode 解耦、分布式前缀缓存。说白了,它让 K8s 终于“看懂”了 LLM 推理请求的形状,然后按形状把请求送到最合适的副本上。
这篇文章面向的是已经在自有 K8s 集群上跑推理服务、想进一步压榨 GPU 利用率的人。我会从集群就绪检查开始,给出可复制的部署清单、连通性验证命令和吞吐观测方法。你不需要先成为调度专家,跟着步骤走就能把链路跑通。
适合谁看:ML 平台工程师、负责推理服务的运维、以及自己维护 GPU 集群的开发者。如果你还在单机跑 vLLM 没上 K8s,这篇可以先收藏,等集群就绪了再回来。
2. llm-d 部署前置:集群就绪检查与 TaoToken 接入准备
在往集群里 apply 任何 YAML 之前,先把地基检查一遍。llm-d 对集群有几个硬性要求,缺一个后面就会卡在奇怪的报错上。
2.1 集群与 GPU 就绪检查
先确认节点和 GPU 状态。你需要一个能跑 GPU 工作负载的 K8s 集群,节点上装好 NVIDIA Device Plugin 或对应厂商的插件。
# 确认节点 Ready 且带 GPU 资源 kubectl get nodes -o custom-columns=NAME:.metadata.name,STATUS:.status.conditions[-1].type,GPU:.status.allocatable.'nvidia\.com/gpu' # 确认 GPU Operator 或 Device Plugin 的 Pod 在跑 kubectl get pods -n kube-system | grep -E 'nvidia|gpu|device-plugin' # 看一张卡的健康状态 kubectl describe node <gpu-node-name> | grep -A5 'Allocatable'如果nvidia.com/gpu那一列是<none>,说明 Device Plugin 没装好,先解决这个再往下走。llm-d 的调度器依赖 K8s 能正确上报 GPU 资源,否则副本根本起不来。
网络方面,如果你打算用 P/D 解耦并走 RDMA/IB,需要确认节点间的高性能互联可用。先用普通数据中心网络也能跑,只是 Prefill 和 Decode 之间的 KV 传输会慢一些。
# 检查节点间基础连通性(示例,按你的 CNI 调整) kubectl run nettest --image=nicolaka/netshoot --restart=Never -- sleep 3600 kubectl exec nettest -- ping -c 3 <另一节点IP>2.2 安装 Inference Gateway 与 Gateway API CRD
llm-d 的智能路由依赖 Inference Gateway。IGW 又构建在 Gateway API 之上,所以 CRD 要按顺序装。
# 安装 Gateway API CRD(版本按官方最新稳定版调整) kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml # 确认 CRD 就位 kubectl get crd | grep gateway.networking # 安装 Inference Gateway(以官方 Helm chart 为例) helm repo add inference-gateway https://inference-gateway.github.io/inference-gateway helm repo update helm install igw inference-gateway/inference-gateway -n igw-system --create-namespace装完后确认 IGW 的 controller Pod 处于 Running:
kubectl get pods -n igw-system kubectl get gatewayclass你应该能看到一个inference-gateway的 GatewayClass。没有的话,后面的 HTTPRoute 和 EPP 配置都不会生效。
2.3 准备模型访问凭证(TaoToken)
llm-d 本身负责调度和路由,但模型权重和推理后端的访问凭证需要你提前准备好。如果你用的是托管式模型服务作为后端,或者需要通过统一入口访问多个模型,可以用 TaoToken 来管理 Key。
到控制台创建一个 API Key,然后把它存成 K8s Secret,不要硬编码在 YAML 里:
kubectl create secret generic llm-backend-credentials \ --from-literal=api-key='sk-你的Key' \ -n llm-d如果你需要先确认模型 ID 和可用模型列表,可以在模型对话页面直接试跑一下,确认 Key 有效、模型名拼写正确,再写进配置。这一步能省掉后面 401 报错的排查时间。
接入文档里有 Base URL 和鉴权头的完整说明,配置推理后端时对照着填。Base URL 用https://taotoken.net/api,注意不要多加路径后缀。
2.4 命名空间与基础资源
给 llm-d 单独开一个命名空间,把 Secret、ConfigMap、推理服务都放进去,方便清理和权限控制。
kubectl create namespace llm-d kubectl config set-context --current --namespace=llm-d到这里前置就绪了。检查清单:节点 GPU 可分配、IGW controller Running、GatewayClass 存在、Secret 已创建。四项都过,再进下一步。
3. 可复制配置:llm-d 推理服务编排与 GPU 调度清单
这一节给出可以直接 apply 的清单。我按“先跑通单副本,再加智能路由,最后上 P/D 解耦”的顺序组织,你可以分阶段验证,不用一次性全上。
3.1 推理后端 Deployment(vLLM 单副本起步)
先起一个 vLLM 副本,确认基础推理链路通。这个 Deployment 申请 1 张 GPU,暴露 OpenAI 兼容接口。
apiVersion: apps/v1 kind: Deployment metadata: name: vllm-backend namespace: llm-d spec: replicas: 1 selector: matchLabels: app: vllm-backend template: metadata: labels: app: vllm-backend spec: containers: - name: vllm image: vllm/vllm-openai:latest args: - "--model" - "你的模型ID" - "--port" - "8000" - "--enable-prefix-caching" ports: - containerPort: 8000 env: - name: VLLM_API_KEY valueFrom: secretKeyRef: name: llm-backend-credentials key: api-key resources: limits: nvidia.com/gpu: "1" readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 10--enable-prefix-caching是后面前缀感知路由能生效的前提,别漏。模型 ID 填你在模型列表里确认过的那个。
3.2 Service 与 InferencePool
普通 Service 只做基础发现,真正的智能路由交给 InferencePool 和 EPP。
apiVersion: v1 kind: Service metadata: name: vllm-backend-svc namespace: llm-d spec: selector: app: vllm-backend ports: - port: 8000 targetPort: 8000 --- apiVersion: inference.networking.x-k8s.io/v1alpha2 kind: InferencePool metadata: name: vllm-pool namespace: llm-d spec: selector: matchLabels: app: vllm-backend targetPortNumber: 8000 extensionRef: name: vllm-eppInferencePool 把一组副本聚合成一个逻辑池,EPP(Endpoint Picker Protocol)负责在这个池里做前缀感知和负载感知的选择。
3.3 EPP 配置(前缀感知路由)
EPP 是 llm-d 智能调度的核心。下面这份 ConfigMap 开启前缀缓存感知和负载感知两个策略。
apiVersion: v1 kind: ConfigMap metadata: name: vllm-epp-config namespace: llm-d data: epp.yaml: | scheduler: profile: prefix-aware plugins: - name: prefix-cache weight: 0.7 - name: load-aware weight: 0.3 kvCache: enabled: true telemetrySource: vllm loadBalancing: policy: least-request maxInflightPerReplica: 8prefix-cache权重给高一些,因为缓存命中带来的 TTFT 收益通常比单纯均衡负载更大。maxInflightPerReplica控制单副本并发上限,防止某个副本被压垮。
3.4 HTTPRoute 暴露入口
通过 Gateway API 的 HTTPRoute 把外部请求引到 InferencePool。
apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: llm-route namespace: llm-d spec: parentRefs: - name: igw namespace: igw-system rules: - matches: - path: type: PathPrefix value: /v1 backendRefs: - group: inference.networking.x-k8s.io kind: InferencePool name: vllm-pool3.5 P/D 解耦配置(进阶)
当你要跑 Prefill 密集型负载(比如长输入短输出)时,把 Prefill 和 Decode 拆到独立实例组。下面用两个 Deployment 示意,通过 KV Connector 连接。
# Prefill 实例组:算力优先 apiVersion: apps/v1 kind: Deployment metadata: name: vllm-prefill namespace: llm-d spec: replicas: 2 selector: matchLabels: app: vllm-prefill template: metadata: labels: app: vllm-prefill role: prefill spec: containers: - name: vllm image: vllm/vllm-openai:latest args: - "--model" - "你的模型ID" - "--kv-connector" - "lmcache" - "--kv-role" - "prefill" resources: limits: nvidia.com/gpu: "1" --- # Decode 实例组:显存带宽优先 apiVersion: apps/v1 kind: Deployment metadata: name: vllm-decode namespace: llm-d spec: replicas: 2 selector: matchLabels: app: vllm-decode template: metadata: labels: app: vllm-decode role: decode spec: containers: - name: vllm image: vllm/vllm-openai:latest args: - "--model" - "你的模型ID" - "--kv-connector" - "lmcache" - "--kv-role" - "decode" resources: limits: nvidia.com/gpu: "1"P/D 解耦对互联要求高,先用数据中心网络验证功能,再考虑上 RDMA。KV Connector 的具体参数按你用的 LMCache 版本调整。
3.6 自动扩缩(Variant Autoscaling)
llm-d 为 HPA 提供精准的负载指标。下面这份 HPA 基于自定义指标扩缩 Decode 组。
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-decode-hpa namespace: llm-d spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: vllm-decode minReplicas: 2 maxReplicas: 8 metrics: - type: Pods pods: metric: name: vllm_pending_requests target: type: AverageValue averageValue: "4"指标名vllm_pending_requests需要你的 vLLM 暴露对应遥测,llm-d 的调度器会消费这些数据。没有的话先用 CPU/GPU 利用率兜底。
配置清单到这里就齐了。apply 顺序:Secret → Deployment → Service → InferencePool → EPP ConfigMap → HTTPRoute → HPA。每步 apply 后kubectl get确认资源创建成功再继续。
4. 验证请求与吞吐观测:确认推理链路真的通了
配置写完不代表链路通了。这一节用具体命令验证连通性、前缀缓存命中和吞吐表现。
4.1 基础连通性测试
先确认 Gateway 拿到了地址,HTTPRoute 状态正常。
kubectl get gateway -n igw-system kubectl get httproute -n llm-d kubectl get inferencepool -n llm-dHTTPRoute 的STATUS应该是Accepted。然后从集群内发一个请求:
kubectl run curl-test --image=curlimages/curl --restart=Never --rm -it -- \ curl -s http://igw.igw-system.svc/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{ "model": "你的模型ID", "messages": [{"role": "user", "content": "用一句话解释什么是 KV Cache"}], "max_tokens": 64 }'返回里应该有choices字段和正常的文本内容。如果卡住不动,先看 EPP 和 vLLM 的日志:
kubectl logs -n llm-d deploy/vllm-backend --tail=50 kubectl logs -n igw-system deploy/igw-controller --tail=504.2 前缀缓存命中验证
前缀感知路由的价值在于缓存命中。发两个共享长前缀的请求,观察第二个请求的 TTFT 是否明显下降。
# 第一个请求:长前缀 time kubectl run curl-a --image=curlimages/curl --restart=Never --rm -it -- \ curl -s http://igw.igw-system.svc/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{"model":"你的模型ID","messages":[{"role":"system","content":"'"$(python3 -c 'print("背景信息。"*500)')"'"},{"role":"user","content":"总结上面"}],"max_tokens":32}' # 第二个请求:相同前缀,不同问题 time kubectl run curl-b --image=curlimages/curl --restart=Never --rm -it -- \ curl -s http://igw.igw-system.svc/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{"model":"你的模型ID","messages":[{"role":"system","content":"'"$(python3 -c 'print("背景信息。"*500)')"'"},{"role":"user","content":"提取关键词"}],"max_tokens":32}'第二个请求的耗时应该明显低于第一个。如果没差别,检查 vLLM 是否带了--enable-prefix-caching,以及 EPP 的kvCache.enabled是否为 true。
4.3 吞吐观测
用 vLLM 自带的 metrics 端点看吞吐和缓存命中率。
kubectl port-forward -n llm-d deploy/vllm-backend 8000:8000 & curl -s http://localhost:8000/metrics | grep -E 'vllm:prefix_cache|vllm:num_requests|vllm:time_to_first_token'关注三个指标:vllm:prefix_cache_hit_rate(缓存命中率)、vllm:num_requests_running(并发请求数)、vllm:time_to_first_token_seconds(TTFT 分布)。缓存命中率上不去,说明路由策略没生效或者请求前缀差异太大。
压测可以用hey或wrk从集群内打:
kubectl run hey --image=williamyeh/hey --restart=Never --rm -it -- \ hey -n 200 -c 20 -m POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{"model":"你的模型ID","messages":[{"role":"user","content":"写一个二分查找"}],"max_tokens":128}' \ http://igw.igw-system.svc/v1/chat/completions看输出的 P95 延迟和 QPS。对比一下关掉 EPP 前缀感知(把权重改成 0)的基线,你能直观看到路由策略带来的差异。实测下来,共享前缀明显的负载下,P95 TTFT 的改善是肉眼可见的。
4.4 结果解读
如果 QPS 上不去但 GPU 利用率也不高,多半是 EPP 的maxInflightPerReplica设太小,请求在排队。如果 TTFT 波动大,检查是否有副本过载,看vllm:num_requests_running是否某个副本明显偏高。P/D 解耦场景下,还要看 Prefill 和 Decode 之间的 KV 传输延迟,这个在 LMCache 的日志里能看到。
5. 本篇常见报错排查:401、local proxy failed 与 OAuth
部署过程中最容易卡住的几个报错,我按实际遇到的频率排一下。
5.1 401 Unauthorized
最常见。请求返回{"error":{"message":"Unauthorized"}}或类似。
排查顺序:先确认 Secret 里的 Key 没写错,再确认请求头格式是Authorization: Bearer sk-xxx,注意 Bearer 后面有空格。然后确认 Base URL 没多加路径,https://taotoken.net/api后面直接接/v1/chat/completions,不要写成/api/v1/v1/...。
# 直接验证 Key 是否有效 kubectl run curl-auth --image=curlimages/curl --restart=Never --rm -it -- \ curl -s -o /dev/null -w "%{http_code}" \ https://taotoken.net/api/v1/models \ -H "Authorization: Bearer sk-你的Key"返回 200 说明 Key 没问题,问题在集群内的配置传递。返回 401 就是 Key 本身的问题,去控制台重新生成一个。
5.2 local proxy failed
这个报错通常出现在 EPP 或 IGW 尝试连接后端副本时。日志里会看到local proxy failed或upstream connect error。
原因一般是 InferencePool 的 selector 没匹配到任何 Pod,或者 targetPort 写错了。检查:
# 确认 selector 能匹配到 Pod kubectl get pods -n llm-d -l app=vllm-backend # 确认 InferencePool 状态 kubectl describe inferencepool vllm-pool -n llm-d如果 Pod 列表为空,说明 Deployment 的 labels 和 InferencePool 的 selector 对不上。如果 Pod 在但端口不对,检查targetPortNumber是否和容器实际监听端口一致。
5.3 reading choices 报错
返回体解析失败,日志里出现error reading choices或unexpected end of JSON input。这通常是后端返回了非 OpenAI 格式的响应,或者流式响应被中途截断。
先确认 vLLM 版本和 OpenAI 兼容接口的版本匹配。然后检查是否有中间层修改了响应体。如果用了流式(stream: true),确认 EPP 和 Gateway 都支持流式转发,有些旧版本 IGW 对 SSE 支持不完整。
# 非流式请求验证后端本身是否正常 kubectl run curl-nostream --image=curlimages/curl --restart=Never --rm -it -- \ curl -s http://vllm-backend-svc.llm-d.svc:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"你的模型ID","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'绕过 Gateway 直连 Service,如果正常,问题在 Gateway/EPP 层;如果也报错,问题在 vLLM 本身。
5.4 OAuth / 鉴权链路问题
如果你在 Gateway 层加了 OAuth 或外部鉴权,可能出现OAuth token validation failed或鉴权头被覆盖。
检查 HTTPRoute 的 filter 配置,确认没有重复添加 Authorization 头。Gateway 的鉴权 filter 和 EPP 的鉴权是两层,别让它们互相干扰。如果用了外部鉴权服务,确认它的响应格式符合 Gateway API 的 ExternalAuth 规范。
5.5 三件套检查法
遇到任何连接类报错,先核对三件套:Base URL、Key、Model ID。这三个有一个不对,报错信息往往指向别处,浪费排查时间。
| 配置项 | 正确值 | 常见错误 |
|---|---|---|
| Base URL | https://taotoken.net/api | 多加/v1或末尾斜杠 |
| Key | sk-开头完整字符串 | 复制时漏字符、多了空格 |
| Model ID | 模型列表里的准确名称 | 大小写错误、用了别名 |
如果用了 CC Switch 或 Cline MCP 这类工具做本地调试,它们的配置文件里同样要填全这三件套。Codex 的auth.json里 Base URL 和 Key 的字段名和标准 OpenAI 配置略有不同,对照文档填,别想当然。
6. 把链路跑稳之后:从单副本到分布式推理的下一步
链路跑通只是起点。真正在生产里跑,还有几件事值得做。
第一,把 EPP 的权重调优当成一个持续过程。prefix-cache和load-aware的权重不是固定的,取决于你的负载里共享前缀的比例。共享前缀多就加大缓存权重,请求形状差异大就加大负载权重。用第 4 节的压测方法,每次调完对比 P95 TTFT 和 QPS。
第二,P/D 解耦不要一上来就全量切。先用一个 Prefill 组加一个 Decode 组跑灰度,确认 KV 传输稳定、没有丢请求,再逐步扩副本。KV Connector 的版本要和 vLLM 版本对齐,升级时一起升。
第三,自动扩缩的指标要选对。vllm_pending_requests比 GPU 利用率更灵敏,因为 GPU 利用率高不一定代表在有效产出 token。如果拿不到这个指标,退而求其次用队列长度。
第四,监控要覆盖三层:Gateway 层的请求延迟和错误率、EPP 层的路由决策分布、vLLM 层的缓存命中率和 TTFT。任何一层出问题,另外两层的数据能帮你快速定位。
如果你还在选型阶段,想先确认模型 ID 和接口行为,可以在模型对话页面直接试跑几个典型请求,把请求形状摸清楚再设计路由策略。接入细节看接入文档,里面有 Base URL、鉴权头和常见参数说明。长期跑编码类或 Agent 类负载的话,Coding Plan 的配额和并发策略值得提前了解,避免上线后才发现并发不够。
最后一句实在话:llm-d 的配置项不少,但核心就三个——前缀感知路由、P/D 解耦、按需扩缩。先把第一个跑通,收益最直接,后面两个按负载特征逐步加。别一次性全上,出了问题不好定位。