ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

KV缓存感知路由与预填充解码分离:K8s上优化LLM推理延迟的实用架构

KV缓存感知路由与预填充解码分离:K8s上优化LLM推理延迟的实用架构

各位做 AI Infra、推理平台和大模型应用交付的工程师,应该都有过类似经历:模型好不容易跑起来了,但线上延迟忽高忽低,用户反复问同一个大型文档或者长上下文问题时,GPU 资源反而被重复消耗;明明扩容了更多 Pod,吞吐量却没有线性提升;同一套模型服务,在某些请求上首字延迟(TTFT)很高,但在另一些请求上却快得奇怪。

这些现象背后,往往都和“KV Cache 没被有效利用”以及“预填充和解码两种阶段混跑”有关。本文围绕 Kubernetes 上的 KV 缓存感知路由,以及预填充解码分离(PD Disaggregation)展开,先讲清楚这两个优化点是什么,再给出可落地的架构设计、路由网关示例、K8s 配置思路和运维建议。

适合读者:正在做大模型推理平台、想把 vLLM / SGLang 这类推理框架更好地部署到 Kubernetes 上的工程师;也包括需要理解推理延迟构成、想优化线上服务成本的后端开发者。读完你可以掌握一个核心思路:把“请求该送到哪个 Pod”和“Pod 内部有哪个缓存片段”这两件事,从 Kubernetes Service 的默认随机负载均衡里解放出来,变成更可控的方式。

1. 背景与核心概念

1.1 KV Cache 为什么能加速,又为什么难路由

讲 KV Cache 感知路由之前,先回顾 KV Cache 是什么。

Transformer 模型做增量解码时,每生成一个 token,都需要重新计算当前 token 对历史 token 的注意力。为了避免每次重复计算所有历史 token 的 Key 和 Value 矩阵,推理框架会把已经计算过的 Key、Value 结果保存下来,这就是 KV Cache。

KV Cache 的价值很直观:命中越多,重复计算越少,服务端生成 token 的速度就越快。

但 KV Cache 也有两个特点:

  • 和输入内容强相关。不同请求的前缀如果不同,KV Cache 内容就不同。
  • 占用显存和内存。长上下文场景下,KV Cache 可能比模型权重还大。

于是出现了一个非常典型的 K8s 问题:当多个请求带同一个长前缀访问服务时,如果网关把它们随机调度到不同的 Pod,那么每个 Pod 都要重新计算一次前缀,缓存命中率极差;如果能把相同前缀的请求,尽量路由到同一个 Pod,那么第二个、第三个请求就可以复用缓存,整机吞吐和延迟都会显著改善。

这就是“KV 缓存感知路由”的意义:在路由层感知到请求的语义特征,并且尽量把关联请求送到同一个推理实例。

1.2 预填充和解码为什么需要分离

大模型服务一个请求时,计算过程大致分为两个阶段:

  • 预填充阶段(Prefill):把用户输入 prompt 一次性处理掉,并行计算所有输入 token 的注意力,生成第一个输出 token。
  • 解码阶段(Decode):拿到第一个 token 后,逐个生成后续 token,每一轮计算量小,但轮数取决于输出长度。

这两个阶段对 GPU 资源的压力完全不一样:

  • Prefill 是计算密集的,可能会把 GPU 利用率瞬间打满,并且单请求耗时主要由 prompt 长度决定。
  • Decode 是访存密集的,GPU 算力往往用不满,但每一刻都在占着显存和 KV Cache。

如果让同一个 Pod 同时做 Prefill 和 Decode,慢速长请求会阻塞后续请求。如果调度到同一个 Pod 的请求太多,整个 Pod 的吞吐都会下降。

预填充解码分离,通常也叫 PD Disaggregation,核心思路是:把 Prefill 和 Decode 拆成两批 Pod,甚至两个独立节点池,分别配置不同机型、不同副本数、不同调度策略。请求先进 Prefill Pod 处理 prompt,然后把计算出的 KV Cache 传给 Decode Pod,后续生成全部在 Decode Pod 完成。

这样做的好处也很直观:

  • Prefill Pod 可以专注“吃”长 prompt,批量大也不怕,高峰期可以单独扩 Prefill 实例。
  • Decode Pod 可以稳定维持并发,不会因为几个长 prompt 被频繁打断。
  • 两类实例的监控、扩缩容、故障恢复策略可以分别定义。

2. Kubernetes 上的默认负载均衡,为什么不适合 KV 缓存路由

2.1 Service 的随机转发会破坏缓存亲和性

Kubernetes 中最常见的请求接入方式是 Service + Deployment。Service 默认会做代理,常见的 ClusterIP 基于 iptables/IPVS 转发,转发规则类似“轮询或随机哈希”;单个请求到底去哪个 Pod,通常不由业务逻辑决定。

对于无状态 Web 服务,这种默认行为很合理,因为哪个 Pod 处理都一样。

但对于带 KV Cache 的 LLM 推理服务,情况就不一样了:

  1. 用户发来一个 8000 token 的长文档请求,A Pod 接收并生成了部分回复。
  2. 用户接着问第二个问题,前缀和上一个问题相同,Service 把请求转发到了 B Pod。
  3. B Pod 没有这部分前缀的 KV Cache,只能重新处理 8000 token,TTFT 瞬间变高,GPU 计算时间翻倍。

在这种模型下,随机负载均衡不是“避免热点”,而是“主动制造缓存未命中”。

2.2 为什么不能只依赖 vLLM/SGLang 内部缓存

现在主流推理框架,比如 vLLM,Self 自带 Prefix Caching;SGLang 有 RadixAttention 机制,它能很好地解决“同一个引擎实例内”的缓存复用。

但如果 K8s 网关把请求随机分散到多个实例,框架内部的缓存优化就变成了“局部最优”:

  • 每个 Pod 只能缓存自己见过的前缀。
  • 同前缀请求分散后,每个 Pod 都要各自缓存一份,显存浪费。
  • 当 Pod 重新调度或重建时,本地缓存也随之清空。

所以,框架内部缓存能力越强,路由层越需要做“缓存感知路由”。两者配合,才能让命中率最大化。

2.3 Kubernetes 路由能力的相关选项

在 K8s 生态里,能影响请求转发策略的组件不少,但很多默认能力并不直接支持“按 KV 前缀路由”:

  • Service:默认负载均衡,可以配置 sessionAffinity,但维度是客户端 IP,不是 Prompt 前缀。
  • Ingress/Nginx:可以按 Header、Path 转发,但 LLM 请求的 Prompt 通常在 Body 里,Nginx 原生解析起来不便。
  • Gateway API:支持更丰富的流量治理,但要实现 Prompt 级路由,仍需要定制扩展。
  • 自定义网关:比如 Go/Python 实现的轻量转发层,由它读取请求内容并决定后端。

综合来看,最适合 LLM 推理场景的路线是:在 Service 前面加一层“语义路由器”或“缓存感知代理”,由代理计算请求的路由键,再精确转发到对应后端 Pod。

3. 整体架构设计

3.1 目标架构

整个架构不复杂,核心是四层:

Client / 上层应用 ↓ LLM Gateway(路由层) ↓ Prefill Pool ←→ Decode Pool ↓ KV Cache / 共享存储

解释一下每一层的作用:

  • LLM Gateway:接收请求,解析 prompt,计算 Prefix Hash 或从 Header 中取出路由 key,再结合缓存状态,决定转发到哪个 Prefill Pod 或 Decode Pod。
  • Prefill Pool:专门处理长 prompt 预填充,生成首个 token 和 KV Cache。
  • Decode Pool:接收从 Prefill 传来的 cache 和 prompt 状态,持续生成后续 token。
  • Cache Channel:KV Cache 从 Prefill 传向 Decode 的通道。如果采用同节点部署,可以使用本地传输;如果跨节点,需要走网络传输或共享内存机制。

3.2 为什么网关要自己计算“路由键”

很多人会问:能不能直接让 K8s Service 根据 Pod IP 做哈希?

不行,因为 KV Cache 和“业务请求的前缀”强相关,而 Service 看不到业务请求。网关层的作用,是把“KV 缓存存在哪个 Pod”这一信息,映射成“哪类请求应该发到哪个 Pod”。

最基础的做法是,用下面某一种信息作为路由键:

  • 用户 ID。
  • Prompt 前 N 个字符的哈希。
  • Prompt 的全局唯一前缀 ID。
  • 请求头里由上层业务透传的分享会话 ID。

对于 Chat 应用,通常用 Prompt 前缀哈希更实用,因为同一个文档、同一个系统提示被多个问题重复引用时,很容易碰撞到同一个 Pod。

3.3 预填充解码分离后的请求生命周期

完整流程可以拆成下面几步:

  1. Client 发送请求到 Gateway。
  2. Gateway 判断该请求是否已经有可复用的缓存路由。
  3. 如果没有,则把请求发送到 Prefill Pool。
  4. Prefill Pod 处理 prompt,得到 KV Cache 和首个 token。
  5. Prefill Pod 将 KV Cache 传输给 Decode Pool 中的某个 Decode Pod。
  6. Decode Pod 接收 KV Cache,继续生成后续 token。
  7. 最终响应由 Client 直接获取,或者由 Gateway 汇聚返回。

这里要注意,“首次 token”到底在 Prefill 生成还是在 Decode 生成,不同框架实现不同。有些框架为了减少一次跨节点传输,会在 Prefill 阶段直接生成首个 token,然后把缓存传给 Decode;有些做法是只把 KV Cache 传过去,首个 token 也由 Decode 生成。实际架构中需要和所用推理框架对齐。

4. KV 缓存感知路由的实现思路

4.1 基础版:基于 Prefix Hash 的 LLM Gateway

先看一个最简网关实现。这个网关的角色,是接收 OpenAI 风格的请求,读取 messages 里的内容,计算 Prompt 前缀哈希,然后转发到 K8s 内部的一组后端 Pod 之一。

后面这段代码以 Python + FastAPI 为例,用于演示路由思路,线上落地时需要围绕并发、超时、重试、优雅关闭做加固。

# 文件路径: gateway/main.py import hashlib import httpx from fastapi import FastAPI, Request, Response app = FastAPI() # 假设你有三个带固定编号的推理服务,每个服务都是一组 Pod BACKENDS = [ "http://llm-pod-0.llm.svc.cluster.local:8000", "http://llm-pod-1.llm.svc.cluster.local:8000", "http://llm-pod-2.llm.svc.cluster.local:8000", ] def extract_prefix(body: dict) -> str: """从请求体中提取用于计算路由键的前缀片段。""" prompt = body.get("prompt") if prompt: return str(prompt)[:256] messages = body.get("messages", []) if messages: content = messages[-1].get("content", "") return str(content)[:256] return "default" def route_key(body: dict) -> int: """计算路由键:取前缀哈希,并映射到后端索引。""" prefix = extract_prefix(body) digest = hashlib.sha256(prefix.encode("utf-8")).hexdigest() return int(digest[:8], 16) % len(BACKENDS) @app.post("/v1/chat/completions") async def chat_completions(request: Request): body = await request.json() idx = route_key(body) backend = BACKENDS[idx] async def proxy(): async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client: resp = await client.post(f"{backend}/v1/chat/completions", json=body) return Response(content=resp.content, status_code=resp.status_code, media_type="application/json") return await proxy()

这段代码虽然简单,但它体现了感知路由的核心逻辑:

  • 没有把请求平均分散到所有后端。
  • 相同前缀的请求会被哈希到同一个后端。
  • 在哈希桶内,负载还是会均匀落到的,但每个桶服务一类特征请求。

实际项目里,不会只有 3 个固定的 Pod 地址,而是需要通过 Kubernetes API 或者服务发现,动态维护“Pod 列表”。常见做法是:

  1. 每个推理 Pod 启动时,向 Redis 或 Etcd 上报自己的标识和所在节点。
  2. Gateway 启动时拉取可用列表,并周期刷新。
  3. 当 Pod 故障或扩容缩容时,更新路由表。

4.2 在 Kubernetes 中固定后端编号

上面代码假设后端 Pod 有固定编号,比如llm-pod-0、llm-pod-1。这在 K8s 中可以通过 StatefulSet 或固定数量 Deployment + 独立 Service 实现。

如果使用 Deployment,每扩一个实例,就对应一个 Service。举一个 3 实例的例子:

# 文件路径: k8s/llm-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: llm-pod-0 namespace: llm spec: replicas: 1 selector: matchLabels: app: llm-spot instance: "0" template: metadata: labels: app: llm-spot instance: "0" spec: containers: - name: llm image: vllm/vllm-openai:latest args: - --model - Qwen/Qwen2.5-7B-Instruct - --max-model-len - "8192" ports: - containerPort: 8000 resources: limits: nvidia.com/gpu: "1" --- apiVersion: v1 kind: Service metadata: name: llm-pod-0 namespace: llm spec: selector: app: llm-spot instance: "0" ports: - port: 8000 targetPort: 8000

同理创建llm-pod-1、llm-pod-2。这样网关侧不需要感知 Pod IP,只要固定访问三个 Service 就行。

但这种做法也有缺点:实例数量固定,扩缩容不灵活。所以很多团队会使用第三种方案:不仅动态维护 Pod 列表,还引入 Pod 标签和拓扑路由。

4.3 扩展:路由键 + 粘性调度

只做前缀哈希并不够,因为扩容、重启都会改变 Pod 集合。如果某个 Pod 被重新调度,它的本地 KV Cache 会丢失,这时已经排给它接下来的请求就全部变成冷缓存请求。

为了让缓存更稳定,建议在应用层增加“请求级路由键”透传机制:

  • 上层应用根据业务语义生成一个route-id。
  • Gateway 根据route-id转发,而不是每次都从 Prompt 里重新计算。
  • 如果同一个route-id的请求正在占用某个 Pod,后续同类请求尽量继续转发到该 Pod。

K8s 中还有一种常见做法,就是给推理 Pod 增加“亲和性标签”:

spec: template: metadata: labels: app: llm cache-bucket: bucket-0

然后通过 Gateway 把bucket-0的请求转发到cache-bucket: bucket-0对应的 Pod。如果 Pod 重建,新 Pod 仍然带bucket-0标签,路由逻辑保持不变。

这样可以避免“Pod IP 变化导致路由表失效”的问题。

4.4 安全与隔离细节

在 KV 缓存感知路由时,需要考虑一个重要问题:同一个 Pod 会缓存多个用户的 Prompt 内容。

如果缓存没有隔离,恶意用户可能故意构造相同前缀来探测其他人的缓存状态,间接推断出某些 Prompt 信息。正常业务中,至少应该做到:

  • 用租户 ID + 前缀哈希一起作为路由键,减少跨租户缓存复用。
  • 敏感数据场景下,对缓存内容做加密,或者直接关闭跨用户前缀缓存。
  • 不要公开 Gateway 的路由信息,避免被批量探测。

如果你的场景安全要求极高,那么比起“追求缓存命中率”,更应该优先考虑隐私合规,宁可牺牲部分性能。

5. 预填充解码分离的具体设计与实现

5.1 网络拓扑规划和节点池划分

PD 分离在物理架构上通常需要两类节点池:

  • Prefill 池:机器可以选择计算型 GPU,例如 A100、H800、H20 等偏算力的型号。它主要处理长 Prompt。
  • Decode 池:机器可以偏显存型 GPU,例如大显存版本。它主要持续生成 Token,需要更多 KV Cache 容量。

在 K8s 中,可以用节点亲和性区分:

# 文件路径: k8s/nodepool-pre.yaml apiVersion: apps/v1 kind: Deployment metadata: name: prefill-pool namespace: llm spec: replicas: 3 selector: matchLabels: app: llm phase: prefill template: metadata: labels: app: llm phase: prefill spec: nodeSelector: node-type: gpu-compute containers: - name: prefill image: vllm/vllm-openai:latest args: - --model - Qwen/Qwen2.5-7B-Instruct - --max-model-len - "16384" ports: - containerPort: 8000
# 文件路径: k8s/nodepool-decode.yaml apiVersion: apps/v1 kind: Deployment metadata: name: decode-pool namespace: llm spec: replicas: 5 selector: matchLabels: app: llm phase: decode template: metadata: labels: app: llm phase: decode spec: nodeSelector: node-type: gpu-memory containers: - name: decode image: vllm/vllm-openai:latest args: - --model - Qwen/Qwen2.5-7B-Instruct - --max-model-len - "16384" ports: - containerPort: 8000

注意,实际能不能直接通过同一个模型服务同时承担 Prefill 和 Decode,取决于你用的推理框架是否支持 PD 分离。vLLM、SGLang 等框架在演进过程中对相关能力支持程度不同,部署前务必查阅对应版本的文档,不要把上面 YAML 当作直接可运行的最高配置。

5.2 KV Cache 传递的挑战

PD 分离最微妙的部分,是 KV Cache 如何从 Prefill Pod 传递到 Decode Pod。

KV Cache 本质上是一组高维 Tensor,大小和模型结构、Prompt 长度、Batch 大小强相关。对一个 7B 模型,一段 4K token 的 Prompt,其 KV Cache 可能占几十 MB 到几百 MB 不等;模型更大、上下文更长时,这个数字会急剧上升。

跨节点传递方式通常有两种方向:

第一,共享显存/共享内存方式。如果 Prefill 和 Decode 再跑同一台物理机,可以通过 host 内存或 NVLink 等通道直接共享,速度最快,但调度灵活度低。

第二,网络传输方式。把 KV Cache 序列化后在节点之间传输,比如通过 RDMA、InfiniBand 或普通 TCP。优点是可以跨节点弹性调度,缺点是需要额外网络带宽和传输延迟。

在 K8s 上落地时,常用以下组合:

  • Prefill Pod 和 Decode Pod 通过 CRD 编排,让同一批请求尽量被调度到同一节点。
  • 增加一个协调组件,负责缓存注册和传递。比如 Prefill 完成缓存后,把缓存信息写入 Redis,记录“某些前缀的 Cache 在哪个 Pod”,然后 Decode Pod 按信息拉取。
  • 跨节点传输时,如果缓存数据较大,可以考虑压缩,但压缩本身的 CPU 开销要评估。

5.3 请求流向示例

下面用一个伪代码表示 Gateway 在 PD 分离模式下如何处理请求:

# 文件路径: gateway/pd_router.py def route_request_to_prefill_or_decode(): # 1. 从请求中获得路由键和是否已完成 prefill 的信息 request_id = get_request_id() prefix_key = get_prefix_key() # 2. 检查解码池中是否已经有可用的 KV Cache cache_info = kv_cache_registry.get(prefix_key) if cache_info and cache_info.ready: # 3. 如果缓存已存在于某个 Decode Pod,直接绕过 Prefill backend = f"http://decode-{cache_info.pod_id}.llm.svc.cluster.local:8000" return send_to_backend(backend) # 4. 否则先进入 Prefill Pool prefill_backend = select_from_prefill_pool(prefix_key) result = send_to_prefill(prefill_backend, request_id) # 5. Prefill 完成后,将缓存调度到某个 Decode Pod decode_pod = allocate_decode_pod() transfer_kv_cache(prefill_backend, decode_pod, prefix_key) # 6. 将请求送进 Decode Pod,继续生成 return send_to_decode(decode_pod, result)

实际项目中,这六个步骤可能分散在多个服务里,不是一个函数可以完成的。但逻辑大体一致:

核心是避免所有请求都走“完整 Decode 链路”,而是让已经有 KV Cache 的请求直接进入 Decode Pod,跳过 Prefill。

5.4 为什么不能拿普通 Kubernetes Service 直接做 PD 分离

你可能想到,直接把 Prefill 和 Decode 分成两个 Deployment,然后 Service 分别暴露不就行了吗?

这里有个问题:用户的请求不会自己知道“需要到哪个阶段”。如果没有 Gateway,那么 Prefill Service 和 Decode Service 就只是两个互相独立的端点,缺少“从 Prefill 到 Decode 的调度衔接”。

因此,PD 分离一定要搭配:

  • 一个控制面组件,负责分配请求 ID。
  • 一个 KV 注册组件,负责记录缓存和 Pod 的位置。
  • 一个转发决策组件,负责判断请求是直接进入 Decode,还是先走 Prefill。

这三者可以合并进一个 LLM Gateway,也可以拆成多个微服务。早期实验阶段,App 项目更推荐先合并进 Gateway,减少排查链路复杂度。

6. 监控指标与数据验证

6.1 核心性能指标

上线前就要确定好观察指标,不然很难知道优化有没有生效。

指标含义在 KV 缓存路由中的价值
TTFTTime To First Token,首 token 延迟优化目标是压降 TTFT,尤其长 Prompt 场景
TPOTTime Per Output Token,平均每个输出 token 耗时体现 Decode Pod 的吞吐能力
ITLInter-Token Latency,相邻 token 间隔流式输出场景更关注
缓存命中率Prefix Cache Hit Rate衡量 KV 缓存路由是否生效
Token 级吞吐每秒生成 token 数评估扩容策略
GPU 利用率Prefill Pod 与 Decode Pod 分开看判断两类资源配置是否合理
KV Cache 占用显存中 KV 缓存的比例防止 OOM,指导路由策略调整

缓存命中率是最关键的指标。如果命中率很低,说明 Gateway 的路由键设计有问题,或者 Pod 经常被重新调度导致缓存丢失。

6.2 采集方式建议

  • Prefill 和 Decode 的日志打到同一套 Prometheus 中,用job或phase标签区分。
  • 在 Gateway 层增加一条鉴权埋点,记录每个请求的路由键和一个布尔值cache_hit。
  • 在 vLLM 框架侧开启 prometheus 端点,把引擎内部缓存命中指标暴露给 Grafana。

如果发现cache_hit为 true 的请求仍然需要走 Prefill,那么大概率是“路由键不敏感”或“缓存注册失败”,需要从控制面日志看到底是谁分配了后端。

6.3 一个简单的压测思路

压测时不要只测并发吞吐,还要测这几种典型场景:

  • 同一 Prompt 前缀重复请求,观察第二次请求的 TTFT 是否明显下降。
  • 不同前缀混合请求,观察缓存命中率和 GPU 利用率是否稳定。
  • 多个 Pod 同时扩缩容,观察缓存命中率是否出现断崖式下降。
  • 请求中途 Pod 被驱逐,观察系统是否会把未生成的请求自动切换到新 Pod。

压测数据一定要和架构变更前做对比。如果只是扩大副本数,却不同时调整路由策略,缓存命中率很可能会下降。

7. 常见问题与排查思路

问题现象常见原因解决思路
缓存命中率长期很低路由键没有区分业务前缀,请求被随机打散改用 Prompt 前缀哈希,或透传业务 route-id
相同前缀请求却打到了不同 PodGateway 路由表没刷新,Pod 列表变化动态同步 K8s Endpoints,增加路由 key 校验
PD 分离后 TTFT 反而变高Prefill 到 Decode 的缓存传输耗时过大优化传输通道,尽量让两类 Pod 同节点或同节点池
Decode Pod 显存频繁 OOMKV Cache 过大,预分配显存不足限制 max-model-len,控制并发,使用显存显控策略
Pod 重启后线上延迟抖动本地缓存丢失,所有请求重新 Prefill缩短 Pod 扩容周期,启用 PDB 防止节点驱逐
Gateway 转发超时后端推理耗时远超普通 HTTP 超时设置把超时调到 300 秒以上,并支持流式响应

这里重点说一个最容易忽略的场景:Pod 被驱逐。

K8s 集群在做节点维护、缩容、抢占时,可能会把推理 Pod 重新调度到新节点。对普通服务来说,这影响不大;但对带大量 KV Cache 的推理服务来说,缓存一旦清零,接踵而至的一波请求会全部变成“冷启动”,TTFT 可能上升几倍。

建议给推理负载配置 PodDisruptionBudget,确保同一时间只有少数 Pod 被驱逐:

# 文件路径: k8s/llm-pdb.yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: llm-pdb namespace: llm spec: minAvailable: 2 selector: matchLabels: app: llm

同时配合拓扑分布约束,避免同类 Pod 挤到同一台节点上。节点挂了不至于所有缓存同时丢失。

8. 最佳实践与上线建议

8.1 路由层设计注意项

网关路由键不等于模型缓存键。应用层可以选择业务语义更强的键,例如文档 ID、会话 ID。不要把随机 UUID 作为前缀路由键,否则每个请求都会分散到不同 Pod。

缓存感知路由也不等于简单哈希。在共识层面,可以通过“最近最少使用”策略维护一张活跃键表,让最近访问的 Prompt 前缀总是优先路由到缓存最热的 Pod。具体可由 Gateway 维护一份热点缓存状态,定期从推理框架拉取。

8.2 命名与配置规范

建议给每个推理实例增加三个标签:

  • app:标识模型应用。
  • phase:prefill或decode。
  • cache-bucket:标识缓存桶编号。

网关侧不要硬编码 Pod IP,尽量按标签选择后端。这样扩容缩容时,路由逻辑不需要改动。

8.3 安全与权限

  • 不要让所有业务直接访问推理 Service,只允许 Gateway 访问。
  • Gateway 到推理 Pod 之间建议开启 mTLS 或至少使用内部网络策略限制来源。
  • 如果使用跨节点 KV 传输,存储端和传输端都要控制访问权限,避免缓存内容被非授权组件读取。
  • 生产环境下的节点驱逐、镜像升级、配置修改,都要通过审计和变更流程执行,并先在小规模验证。

8.4 配置变更与灰度发布

引入 KV 缓存感知路由和 PD 分离,会影响系统性能,因此尽量采用灰度发布:

  1. 先搭建一个影子集群,把线上请求复制一部分到新架构。
  2. 对比新旧集群的 TTFT、TPOT、缓存命中率。
  3. 确认新架构显著优化后再切换生产流量。
  4. 切换顺序可以先从“支持同一前缀高复用”的业务开始,例如文档问答、代码助手,而不是先切完全没有缓存复用价值的随机对话业务。

如果业务本身的 Prompt 几乎没有重复前缀,那 KV 缓存路由和 PD 分离的收益有限。先判断业务特征,再决定是否投入。

8.5 资源评估

上线前评估一下单个 KV Cache 片段的大小。最简单的方式是在框架日志中查看:当一个长 Prompt 完成 Prefill 后,KV Cache 占用了多少显存。

估一个粗略模型:假设 7B 模型,单 token 的 KV Cache 大小约为 0.1 MB 到 0.2 MB,上下文 4096 时,一个请求可能占 600 MB 以上;如果批量处理 8 个请求,KV Cache 可能占去 5 GB 左右。

Decode 池的副本数,应该按“并发用户数 × 每个用户的平均输出长度 × 平均 KV Cache 大小”来估算。Pre Fill 池的副本数,则按“每分钟新增长 Prompt 请求数 × 单个 Prefill 耗时”估算。提前预留 30% 的显存余量,避免显存碎片导致 OOM。

9. 下一步学习方向

KV 缓存感知路由和 PD 分离是两个相互促进的优化手段,但它们都不是“装一个组件就完事”的配置项,而是一整套需要持续调优的架构。

如果接下来你想深入,可以从这几个方向入手:

  • 熟悉自己项目所用的推理框架。比如 vLLM 的前缀缓存、SGLang 的 RadixAttention,它们的缓存命中逻辑各不相同。搞清楚缓存键是怎么生成的,才能设计更匹配的网关路由键。
  • 了解 K8s 的高级调度能力。包括节点亲和性、Pod 拓扑分布约束、PodDisruptionBudget、HPA 自定义指标扩容。这些能力直接影响 KV Cache 的稳定性。
  • 研究 KV Cache 的跨节点传输方案。包括共享显存、RDMA、分布式缓存状态管理等。关注社区里相关实现的演进,判断是否需要自己造轮子。
  • 建立完善的压测和监控闭环。不要只盯着 GPU 利用率,前面提到的 TTFT、TPOT、缓存命中率、KV Cache 占用才是判断优化是否有效的关键。

最后提醒一句:不管方案设计得多好,一定要先在测试环境跑通完整链路,再逐步灰度到生产。推送配置前确认路由键、缓存注册、PD 调度三个环节不会互相打架。如果你的业务存在大量同前缀复用场景,这套架构的收益会非常明显;如果业务请求几乎完全不重复,那么也许先调整业务设计,再决定要不要上这套复杂架构。

这篇内容偏架构落地向,希望能帮你把“大模型推理性能优化”从一个模糊概念,变成一个可以照着设计、部署和排障的实操方案。如果你正在 K8s 上做 LLM 推理平台,可以参考上面的路由思路和 PD 分离流程,结合具体框架版本与 GPU 机型逐步验证。动手跑起来之后,可能你会发现优化点比预想的多得多,也会对这一层架构有更直观的理解。

RELATED READING

延伸阅读

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