ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

KV Cache感知路由与PD分离:Kubernetes下的LLM推理优化实践

KV Cache感知路由与PD分离:Kubernetes下的LLM推理优化实践

近两年大模型推理已经从“能跑”卷到“跑得便宜、跑得快”。单卡部署一个 7B 模型只是入门操作,真正拉高成本瓶颈的是多用户高并发场景下的显存浪费、前序重复计算和吞吐抖动。这次我们讨论一个非常实操的方向:在 Kubernetes 上做 KV Cache 感知路由,再加一层预填充(Prefill)与解码(Decode)分离,把推理集群按计算特征拆开管理。

KV Cache 感知路由解决的是“请求该送到哪个副本”的问题。传统 Kubernetes Service 做的是四层轮询或最小连接数转发,它对每个 Pod 里的 KV Cache 状态一无所知。于是两个都带相同 system prompt 或相同 RAG 上下文的请求,可能被分发到两个不同副本,同一批 Attention 中间结果被重复算了两遍。KV Cache 感知路由会识别请求前缀的哈希,把前缀相同的请求尽量送到已经缓存过这些 KV 的副本上,用缓存命中替代重复计算。

预填充与解码分离是更进一步的架构调整。Prefill 阶段要处理长序列并生成预填充阶段的 KV Cache,计算密集;Decode 阶段每一步只生成一个 Token,显存带宽密集。把两者混在同一组 GPU 上,长上下文的 prefill 会周期性抢占 decode 资源,造成在线响应时间波动。PD 分离就是拆成 Prefill 节点和 Decode 节点,各自用不同的实例类型、不同的显存分配策略,节点之间通过 KV Cache 传输机制接力。

本文会把这两件事拆开讲:先看核心能力与适用边界,再给一套 Kubernetes 环境下的部署与验证流程。因为没有绑定某个特定开源版本,所以命令和 YAML 会给出通用模板,实际使用时按你选定的推理框架、镜像和模型文件位置替换即可。

1. 核心能力速览

能力项说明
项目类型LLM 推理服务架构方案:路由策略 + 服务拓扑调整
核心机制 1KV Cache 感知路由:按请求前缀哈希将请求转发到已有对应缓存的推理副本
核心机制 2Prefill/Decode 分离:将推理节点划分为 Prefill 节点组和 Decode 节点组
部署环境Kubernetes 集群,需要 GPU 节点与 NVIDIA 设备插件
硬件门槛取决于模型参数规模;7B~13B 模型可在 24G 显存级别节点测试,70B 及以上需多卡或大显存节点
显存占用由模型权重 + KV Cache + 激活值共同组成,需要按 batch size、上下文长度实测
启动方式Kubernetes YAML / Helm Chart 部署
API 能力通常提供 OpenAI 兼容的 chat/completions 接口
批量任务支持并发请求队列与批量测试,但路由收益在并发重复前缀下更明显
适合场景多用户聊天、RAG 问答、长文档分析、多轮对话、需要稳定 TTFT 与解码吞吐的在线服务
主要收益降低 prefill 重复计算、提升 GPU 利用率、减少端到端延迟抖动

这里的“KV Cache 感知路由”有两种落地形态:一是在 Ingress 或自定义 Router 层维护前缀与副本的映射关系;二是完全交给支持 Prefix Caching 的推理框架,由框架内部的调度器做缓存感知分配。两种方式不冲突,实际生产系统往往是网关层先做粗粒度路由,推理框架内部再做细粒度缓存复用。

2. 适用场景与使用边界

PD 分离和 KV Cache 感知路由并不是所有推理项目都必须上。它适合以下情况:

  • 同一批客户端反复使用相同 system prompt、对话模板或 RAG 前缀,例如客服机器人和文档助手。
  • 在线请求有长上下文,prefill 占比高,用户能明确感受到首 Token 延迟。
  • 并发度需要稳定,不想因为某个长请求的 prefill 拖垮整体 decode 吞吐。
  • 集群规模已经大到需要区分“计算型 GPU”和“吞吐型 GPU”的采购和调度策略。

如果你的场景是单用户本地调试、一次性生成任务,或是请求之间完全无公共前缀,KV Cache 感知路由带来的收益很小。PD 分离还会引入 KV Cache 传输开销,节点越多,网络复杂度越高。在小规模测试阶段,建议先保留传统部署,把两条优化路径放到集群层面验证。

使用边界必须明确三点:

  1. 前缀缓存是内存里的状态。Pod 重启、扩容缩容、模型版本切换都会导致缓存失效,路由表和框架缓存状态需要随之更新。
  2. KV Cache 本身包含用户上下文的中间状态。在多租户场景中,缓存复用必须限定在同一模型版本和同权限级别内,不能跨租户泄露前缀信息。
  3. 涉及人脸、声音、版权文本等数据时,处理流程需要获得合法授权。这不仅适用于生成类应用,同样适用于对文档库、聊天记录建立索引和缓存。

3. 为什么路由与架构分离能提升推理吞吐

要理解 KV Cache 感知路由,需要先回顾解码过程。Transformer 生成每个 Token 时,注意力层要对当前 Query 与历史所有 Key 做点积,再与全部 Value 加权求和。如果不缓存,每一步都要重新计算历史所有 Token 的 K/V,复杂度随上下文长度线性上升。KV Cache 就是把这些中间结果存下来,用显存换算力。

在多副本部署时,KV Cache 天然是“Pod 本地”的。每个推理副本只缓存自己处理过的请求前缀。如果 Service 随机轮询,公共前缀会均匀分散到所有副本,每个副本都存了一份,但每个副本的命中率都不高。如果路由层能用一致性哈希把“相同前缀”的请求稳定送到“同一个副本”,缓存命中率会显著提高。请求进来时,命中缓存的 Token 部分直接跳过 prefill 计算,TTFT 下降,GPU 计算时间下降。

PD 分离解决的则是另一种资源冲突。Prefill 阶段输入长度可能上千 Token,矩阵乘规模很大,需要高算力;Decode 阶段每一步只算一个 Token,主要瓶颈在显存带宽和访存延迟。在同一块 GPU 上,一个 2048 Token 的 prefill 请求会长时间占用计算单元,而正在流式解码的几十个用户请求只能排队。PD 分离后:

  • Prefill 节点组:专门处理长输入、生成预填充阶段的 KV Cache,可以用算力更强的卡,跑大 batch。
  • Decode 节点组:接收 Prefill 阶段产出的 Token 序列状态和 KV Cache,只做自回归解码,批量吞吐高。
  • 节点之间通过高速链路(NVLink、RDMA 或共享缓存服务)传递 KV Cache。

在 Kubernetes 里,这体现为两套 Deployment 或两套 StatefulSet。通过 nodeSelector、污点与容忍度把 Prefill 和 Decode 固定到不同 GPU 机型上,再用标签选择器做服务发现。

这里需要明确:KV Cache 跨节点传输是 PD 分离能否落地的关键。常用方案包括:

  • 推理框架原生支持,例如部分框架通过分布式 KV Cache Manager 在节点间转发。
  • 自研 Router 把请求状态和 KV Cache 引用转发给 Decode 节点。
  • 基于共享文件系统或内存缓存服务,实现跨节点 KV Cache 读取。

不同框架支持程度不同,落地前先确认你所用的框架在当前版本是否具备跨节点 KV Cache 传输能力。缺少这一层的话,PD 分离就退化成逻辑上的“分节点部署”,仍然要绕回网络传一遍 K/V 状态。

4. 环境准备与前置条件

这套架构的验证环境建议按以下清单准备:

  • Kubernetes 集群:推荐 1.28 及以上版本,需要确认 GPU 节点支持。
  • GPU 节点:NVIDIA 显卡,安装驱动并部署 NVIDIA Device Plugin,使节点上报nvidia.com/gpu资源。
  • 推理框架:建议选择支持 Prefix Caching、连续批处理和 KV Cache 统计导出的框架,例如 vLLM、SGLang 或同类服务。不同框架对 PD 分离的支持差异较大,需要先行确认。
  • 模型文件:将模型权重放在共享存储(NFS、CephFS、云盘 PVC)或提前放到各节点本地目录。
  • 网络:节点间通信建议万兆或 RDMA 环境,PD 分离下 KV Cache 传输对带宽和延迟敏感。
  • 监控:部署 Prometheus 和 Grafana,采集 GPU 利用率、显存使用量和推理框架自带的指标,例如缓存命中率、TTFT、TPOT。
  • 目录规划:准备模型目录、日志目录、输出目录,分别挂载,防止日志和缓存写满系统盘。

在验证阶段,不必一开始就拆出大量节点。可以用一个 GPU 节点跑两个推理工作负载,通过 Kubernetes 标签模拟 Prefill 池和 Decode 池,先验证请求链路通不通。确认 KV Cache 能在两个池之间传递后,再扩展到物理节点隔离。

5. 推理服务部署与启动

下面是通用部署流程。实际镜像、启动参数、端口需要按所选框架替换。

5.1 部署 NVIDIA 设备插件

如果你的集群还没有 GPU 资源上报,需要先部署设备插件。以 DaemonSet 方式运行。

apiVersion: apps/v1 kind: DaemonSet metadata: name: nvidia-device-plugin-daemonset namespace: kube-system spec: selector: matchLabels: name: nvidia-device-plugin-ds template: metadata: labels: name: nvidia-device-plugin-ds spec: tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule containers: - name: nvidia-device-plugin image: nvcr.io/nvidia/k8s-device-plugin:v0.15.0 securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins

如果集群已经能通过kubectl describe node看到nvidia.com/gpu资源,可以跳过这一节。

5.2 推理工作负载定义

KV Cache 感知路由场景下,建议先用 Headless Service 暴露推理 Pod,让自定义路由器可以直接探测后端。下面是一个通用 Deployment 模板,镜像和启动命令需要替换为实际框架。

apiVersion: apps/v1 kind: Deployment metadata: name: llm-inference labels: app: llm-inference role: prefill-decode spec: replicas: 2 selector: matchLabels: app: llm-inference template: metadata: labels: app: llm-inference spec: containers: - name: inference image: your-registry/your-llm-server:latest args: - --model - /models/your-model - --host - 0.0.0.0 - --port - "8000" - --gpu-memory-utilization - "0.85" - --max-model-len - "8192" ports: - containerPort: 8000 name: http resources: limits: nvidia.com/gpu: 1 volumeMounts: - name: model-storage mountPath: /models - name: dshm mountPath: /dev/shm volumes: - name: model-storage persistentVolumeClaim: claimName: model-pvc - name: dshm emptyDir: medium: Memory

启动后确认服务状态:

kubectl apply -f llm-inference.yaml kubectl get pods -l app=llm-inference -o wide kubectl logs -l app=llm-inference --tail=50

看到日志输出监听地址和模型加载完成的标志后,下一步验证基础推理接口。

5.3 访问服务与基础请求验证

临时端口转发验证:

kubectl port-forward pod/llm-inference-xxxx-xxxx 8000:8000
curl -X POST http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "your-model", "messages": [ {"role": "system", "content": "你是中英文翻译助手。"}, {"role": "user", "content": "把 Kubernetes 翻译成中文。"} ], "max_tokens": 64, "temperature": 0 }'

能正常返回文本,说明推理服务和 OpenAI 兼容接口这一层已经打通。接下来进入路由与 PD 分离的专项验证。

6. KV Cache 感知路由验证流程

验证目标只有一个:相同前缀的请求能不能被稳定送到同一个拥有缓存的副本上。衡量指标主要是两点:

  • 缓存命中率是否随重复请求提升。
  • 命中后 prefill 阶段耗时是否下降,也就是 TTFT 是否缩短。

6.1 路由设计

在 Kubernetes 中实现 KV Cache 感知路由有几种方式:

  • 方式一:利用推理框架内部的 Prefix Caching。框架在单副本内部自动做缓存复用,不需要外部路由介入。适合单副本、单实例场景。
  • 方式二:部署自研或开源路由器,路由器维护“前缀哈希 → Pod IP”映射,转发时用一致性哈希选择后端。
  • 方式三:在 Ingress 层按请求特征(比如 system prompt 哈希)做会话保持,减少跨副本漂移。

从材料看,本主题强调“Kubernetes 上的 KV 缓存感知路由”,最典型的落地是方式二:在 Service 前面增加一层有状态路由,或者在 Ingress 里挂 Lua/插件逻辑。下面是通用路由配置示意,具体插件语法按实际网关调整:

annotations: nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"

方式一适合快速验证,方式二适合深入了解路由逻辑,方式三适合改造最小的场景。三者可以组合使用。

6.2 构造可命中缓存的测试请求

用一组固定 system prompt + 变化用户问题来模拟真实会话:

SYS_PROMPT="请基于以下产品文档回答用户问题。产品文档:我们的API网关支持限流、熔断、灰度发布、SSL卸载、自定义插件等功能。" for i in 1 2 3; do curl -X POST http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d "{ \"model\": \"your-model\", \"messages\": [ {\"role\": \"system\", \"content\": \"$SYS_PROMPT\"}, {\"role\": \"user\", \"content\": \"第 ${i} 个问题:什么是灰度发布?\"} ], \"max_tokens\": 128 }" done

连续三次请求使用相同 system prompt,只要请求被路由到同一个副本,并且该副本开启了 Prefix Caching,第二、三次请求就会复用第一次产生的 KV Cache。

6.3 观察指标

观察点落在两处。

第一处:推理框架自带指标接口。如果服务开启了 Prometheus 指标端口,通常可以找到缓存命中相关的指标,例如vllm:cache_hit_ratio或等价指标。

curl -s http://127.0.0.1:8000/metrics | grep -i cache

第二处:TTFT。发送同样请求时,用 curl 的-w参数观察耗时:

curl -X POST http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"your-model","messages":[{"role":"system","content":"固定前缀测试"}]}' \ -w "\nTTFT-TOTAL_TIME: %{time_total}s\n"

在相同网络条件下,第二次请求的 total_time 明显低于第一次,代表 prefill 阶段被部分跳过。如果耗时没有变化,需要检查两点:一是路由是否真的把请求送到了同一个 Pod;二是请求的 system prompt 和对话模板在字节级别是否完全一致。很多情况下,一致性前缀会因转义符、换行符、空格不同而失效。

6.4 路由效果对比实验

为了验证路由层的价值,可以做一个对比实验:

  • 对照组:普通 Service 轮询转发到两个推理副本。
  • 实验组:KV Cache 感知路由器按前缀哈希转发。

两组都发 200 个带公共前缀的请求,对比指标:

指标对照组预期实验组预期
前缀缓存命中率约 1/N,N 为副本数接近 80% 以上
Prefill 平均耗时稳定偏高明显下降
GPU 算力消耗每个副本都在重复算只有一个副本承担首次计算
端到端吞吐受重复计算拖累随命中率提升

上面预期数值不是绝对标准,需结合框架和模型实测。关键在于对比逻辑要成立:前缀重复度越高的流量,KV Cache 感知路由的收益越大。

7. PD 分离负载测试与 API 调用

PD 分离验证的核心是:prefill 阶段和 decode 阶段的负载有没有被拆开,两者是否还能协同完成一个完整的生成请求。

7.1 部署 Prefill 与 Decode 工作负载

在 Kubernetes 中定义两套工作负载,分别打标签。

Prefill 池:

apiVersion: apps/v1 kind: Deployment metadata: name: llm-prefill labels: app: llm-prefill spec: replicas: 1 selector: matchLabels: app: llm-prefill template: metadata: labels: app: llm-prefill spec: nodeSelector: gpu-type: prefill containers: - name: prefill image: your-registry/your-llm-server:latest args: - --model - /models/your-model - --host - 0.0.0.0 - --port - "8000" resources: limits: nvidia.com/gpu: 1

Decode 池:

apiVersion: apps/v1 kind: Deployment metadata: name: llm-decode labels: app: llm-decode spec: replicas: 1 selector: matchLabels: app: llm-decode template: metadata: labels: app: llm-decode spec: nodeSelector: gpu-type: decode containers: - name: decode image: your-registry/your-llm-server:latest args: - --model - /models/your-model - --host - 0.0.0.0 - --port - "8000" - --role - "decode" resources: limits: nvidia.com/gpu: 1

这里的--role参数是示意,具体以推理框架支持的启动模式为准。部分框架本身不直接支持角色分离,需要借助额外的调度组件,部署方式会有差异。

7.2 完整的 Chat 请求链路

PD 分离下,接口层应当仍然是 OpenAI 兼容接口。客户端请求进入入口服务后,由 Router 识别请求状态,将 prefill 任务发给 Prefill 池,当预填充完成再转交 Decode 池。用户侧感知不到内部链路。

接口验证仍然用 curl 和 Python。下面是一个 Python 请求脚本,适合放入批量测试中。

import json import time import requests url = "http://127.0.0.1:8000/v1/chat/completions" payload = { "model": "your-model", "messages": [ {"role": "system", "content": "你是数据分析助手。请根据给定的背景资料回答问题。"}, {"role": "user", "content": "解释 PD 分离为什么能降低在线推理延迟波动。"} ], "max_tokens": 256, "temperature": 0.2, "stream": False } start = time.time() resp = requests.post(url, json=payload, timeout=120) latency = time.time() - start print("HTTP 状态码:", resp.status_code) print("总耗时:", latency, "秒") if resp.status_code == 200: data = resp.json() print("回复内容:", data["choices"][0]["message"]["content"][:200])

注意:PD 分离与普通单服务在接口层面几乎没有区别,区别在 Pod 拓扑和请求状态传递。如果接入后接口行为异常,优先检查 KV Cache 传递组件是否正常工作。

7.3 观察 Prefill 与 Decode 节点负载

在验证过程中,同时打开两个终端的资源观察窗口。

kubectl exec -it pod/llm-prefill-xxxx -- nvidia-smi kubectl exec -it pod/llm-decode-xxxx -- nvidia-smi

或者使用kubectl top观察 CPU 内存,又或者通过 DCGM 导出器把 GPU 指标接到 Prometheus:

kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes

正常状态下:

  • Prefill 节点在请求输入阶段 GPU 算力明显上升,显存中写入大量 KV Cache。
  • Decode 节点在解析阶段持续输出 Token,GPU 利用率可能不高,但显存带宽消耗大。
  • 如果长 Tokens 输出全部集中在同一个节点,说明 Decode 池没有参与,链路可能仍是单体模式。
  • 如果 Prefill 节点也持续高占用整个输出阶段,说明架构上没有真正拆开。

7.4 常见验证场景

测试场景输入特征重点观察成功标准
短输入长输出用户提问简短,max_tokens 较大Decode 节点 GPU 显存带宽长输出在 Decode 池完成
长输入短输出文档内容很长,回答简短Prefill 节点算力长文档处理集中在 Prefill 池
多轮对话上下文持续增长KV Cache 跨节点传递多轮回复延迟稳定
高并发多个请求同时进入吞吐、响应延迟无长时间阻塞

8. 资源占用与性能观察方法

推理服务的资源占用主要由三部分组成:模型权重、KV Cache 和激活值。模型权重固定,激活值随 batch 和上下文长度波动,KV Cache 是最需要监控的动态部分。

KV Cache 大小的估算方式如下:

KV Cache 显存 ≈ 2(K 和 V) × 层数 × hidden_size × 序列长度 × batch_size × 精度字节数

以常见参数配置估算,当序列长度从 1024 翻到 8192 时,KV Cache 显存会线性增长。这也是为什么把多副本的缓存分散在各个 Pod 会导致显存浪费:每个 Pod 都缓存了一堆重复前缀,集群整体显存里堆满了同一个 system prompt 的中间状态。

观察资源占用时建议关注四个维度的指标:

1. 显存总量:nvidia-smi 或 DCGM 指标。 2. KV Cache 占用率:推理框架自带的 metrics。 3. GPU 利用率:区分计算利用率和显存带宽利用率。 4. 平均批大小:反映连续批处理的聚批效果。

降低显存占用的常见手段:

  • 调低max-model-len,限制单请求最大上下文长度,防止缓存异常膨胀。
  • 调低gpu-memory-utilization,为运行时激活值预留余量。
  • 限制单请求max_tokens,避免解码阶段持续占用显存。
  • 开启前缀缓存复用,让重复公共前缀只保存一份。
  • 对长超时请求设置队列超时,避免卡死的请求长期占用缓存。

9. 常见问题与排查方法

问题现象可能原因排查方式解决方案
缓存命中率始终为 0请求前缀字节不一致对比两次请求完整 JSON,检查换行、转义统一 chat template,去除冗余换行
命中率低但前缀一致路由没有稳定哈希查看 Router 日志确认后端 IP改用一致性哈希,并保证副本列表稳定
Pod 重启后缓存全丢失KV Cache 是进程内存态观察框架指标、检查 Pod 生命周期保留稳定副本,避免频繁滚动更新
显存不足KV Cache 持续膨胀观察 nvidia-smi 显存曲线调低 max-model-len 和 batch 上限
GPU 利用率忽高忽低Prefill 与 Decode 互相抢占检查两个阶段请求分布使用 PD 分离或错峰调度
长请求卡住不返回Decode 节点未收到 KV Cache查看 Router 与节点间网络日志检查 KV Cache 传递通道和网络带宽
接口报 400 或超时请求参数超出限制检查 max_tokens、上下文长度在前端限制输入长度,后台增大超时
Python 批量脚本并发崩溃未做连接池与重试查看异常栈信息增加重试和信号量控制并发

10. 最佳实践与使用建议

第一,路由层和推理框架的关系要理清。外部路由做的是“前缀一致性”,框架内部做的是“缓存复用”。外部路由再好,也需要框架的 Prefix Caching 开关保持开启,否则请求虽然在同一个 Pod,缓存还是会被当作未命中处理。

第二,PD 分离不要从第一天就推全量。先在测试环境中用两套 Deployment 跑通请求链路,确认 KV Cache 跨节点传输正常后再做流量切换。如果当前框架版本不支持 KV Cache 跨节点传输,PD 分离的价值会大打折扣,优先在预发环境完成能力验证。

第三,缓存状态是脆弱的状态。业务上要明确:Pod 重启、扩缩容和滚动更新都会清空缓存。如果希望缓存长期有效,关键 Pod 不要频繁重建,同时用 PDB(PodDisruptionBudget)控制自愿中断。

第四,监控优先于调优。在调整路由策略之前,先把下面三个指标接好:

- 前端请求的 system prompt 重复率 - 推理框架的缓存命中率 - 不同阶段请求的 TTFT 和 ITL

没有这些指标,所有优化都是在盲调。重复率低,说明你的流量根本不适合前缀缓存路由;命中率低,说明路由或 chat template 有问题;TTFT 高而 ITL 正常,说明瓶颈在 prefill 阶段,PD 分离或缩小上下文是合理方向。

第五,安全与合规必须落到配置里。多租户场景下,KV Cache 命中不能跨租户,必要时在路由层增加租户标识作为哈希因子;访问日志中如果包含 prompt 原文,要按数据分级做脱敏或清理;模型和中间结果文件统一放到受控存储,防止 PVC 泄露。

11. 总结与下一步

总结成一句话:KV Cache 感知路由适合前缀重复率高的在线推理流量,PD 分离适合 prefill 与 decode 资源冲突明显的规模化集群。两者可以独立使用,也可以组合。对于大多数中小团队,先开 Prefix Caching,再统一 chat template,把重复计算压下来,这一步投入小、见效快。对于已经在运行多副本 GPU 推理服务的团队,按本文第四节到第七节流程搭建测试环境,确认 KV Cache 跨节点能力后再规划 PD 分离节点池。

建议从一个小实验开始:固定三到五个带长 system prompt 的请求,连续发十遍,观察缓存命中率和 TTFT 变化。这个实验能最快验证你的框架和路由配置是否真的把 KV Cache 用起来了。能命中缓存后,再逐步加大系统提示词长度、增加副本数量、接入高并发流量,直到摸清这套架构在你自己集群上的吞吐上限。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进