llm-d:面向Kubernetes的高性能分布式LLM推理框架 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 GatewayIGW之上核心做了三件事前缀感知路由、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-columnsNAME:.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 --imagenicolaka/netshoot --restartNever -- sleep 3600 kubectl exec nettest -- ping -c 3 另一节点IP2.2 安装 Inference Gateway 与 Gateway API CRDllm-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 处于 Runningkubectl get pods -n igw-system kubectl get gatewayclass你应该能看到一个inference-gateway的 GatewayClass。没有的话后面的 HTTPRoute 和 EPP 配置都不会生效。2.3 准备模型访问凭证TaoTokenllm-d 本身负责调度和路由但模型权重和推理后端的访问凭证需要你提前准备好。如果你用的是托管式模型服务作为后端或者需要通过统一入口访问多个模型可以用 TaoToken 来管理 Key。到控制台创建一个 API Key然后把它存成 K8s Secret不要硬编码在 YAML 里kubectl create secret generic llm-backend-credentials \ --from-literalapi-keysk-你的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 --namespacellm-d到这里前置就绪了。检查清单节点 GPU 可分配、IGW controller Running、GatewayClass 存在、Secret 已创建。四项都过再进下一步。3. 可复制配置llm-d 推理服务编排与 GPU 调度清单这一节给出可以直接 apply 的清单。我按“先跑通单副本再加智能路由最后上 P/D 解耦”的顺序组织你可以分阶段验证不用一次性全上。3.1 推理后端 DeploymentvLLM 单副本起步先起一个 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 把一组副本聚合成一个逻辑池EPPEndpoint 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: 1P/D 解耦对互联要求高先用数据中心网络验证功能再考虑上 RDMA。KV Connector 的具体参数按你用的 LMCache 版本调整。3.6 自动扩缩Variant Autoscalingllm-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 --imagecurlimages/curl --restartNever --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 --tail50 kubectl logs -n igw-system deploy/igw-controller --tail504.2 前缀缓存命中验证前缀感知路由的价值在于缓存命中。发两个共享长前缀的请求观察第二个请求的 TTFT 是否明显下降。# 第一个请求长前缀 time kubectl run curl-a --imagecurlimages/curl --restartNever --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 --imagecurlimages/curl --restartNever --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_secondsTTFT 分布。缓存命中率上不去说明路由策略没生效或者请求前缀差异太大。压测可以用hey或wrk从集群内打kubectl run hey --imagewilliamyeh/hey --restartNever --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 --imagecurlimages/curl --restartNever --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 appvllm-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 --imagecurlimages/curl --restartNever --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 URLhttps://taotoken.net/api多加/v1或末尾斜杠Keysk-开头完整字符串复制时漏字符、多了空格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 解耦、按需扩缩。先把第一个跑通收益最直接后面两个按负载特征逐步加。别一次性全上出了问题不好定位。