各位做 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 推理服务,情况就不一样了:
- 用户发来一个 8000 token 的长文档请求,A Pod 接收并生成了部分回复。
- 用户接着问第二个问题,前缀和上一个问题相同,Service 把请求转发到了 B Pod。
- 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 预填充解码分离后的请求生命周期
完整流程可以拆成下面几步:
- Client 发送请求到 Gateway。
- Gateway 判断该请求是否已经有可复用的缓存路由。
- 如果没有,则把请求发送到 Prefill Pool。
- Prefill Pod 处理 prompt,得到 KV Cache 和首个 token。
- Prefill Pod 将 KV Cache 传输给 Decode Pool 中的某个 Decode Pod。
- Decode Pod 接收 KV Cache,继续生成后续 token。
- 最终响应由 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 列表”。常见做法是:
- 每个推理 Pod 启动时,向 Redis 或 Etcd 上报自己的标识和所在节点。
- Gateway 启动时拉取可用列表,并周期刷新。
- 当 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 缓存路由中的价值 |
|---|---|---|
| TTFT | Time To First Token,首 token 延迟 | 优化目标是压降 TTFT,尤其长 Prompt 场景 |
| TPOT | Time Per Output Token,平均每个输出 token 耗时 | 体现 Decode Pod 的吞吐能力 |
| ITL | Inter-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 |
| 相同前缀请求却打到了不同 Pod | Gateway 路由表没刷新,Pod 列表变化 | 动态同步 K8s Endpoints,增加路由 key 校验 |
| PD 分离后 TTFT 反而变高 | Prefill 到 Decode 的缓存传输耗时过大 | 优化传输通道,尽量让两类 Pod 同节点或同节点池 |
| Decode Pod 显存频繁 OOM | KV 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 分离,会影响系统性能,因此尽量采用灰度发布:
- 先搭建一个影子集群,把线上请求复制一部分到新架构。
- 对比新旧集群的 TTFT、TPOT、缓存命中率。
- 确认新架构显著优化后再切换生产流量。
- 切换顺序可以先从“支持同一前缀高复用”的业务开始,例如文档问答、代码助手,而不是先切完全没有缓存复用价值的随机对话业务。
如果业务本身的 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 机型逐步验证。动手跑起来之后,可能你会发现优化点比预想的多得多,也会对这一层架构有更直观的理解。