llm-d 架构解析(下):从 P/D 分离到弹性扩缩与 Batch Serving
引言
上篇:从 InferencePool 到 KV Cache-Aware Routing围绕 llm-d 的 Router 展开,讨论了 InferencePool、EPP、Load-Aware Routing、Prefix Cache-Aware Routing,以及 KV Cache Indexer 如何将单实例内部的缓存状态转化为可用于全局调度的信号。
当请求调度能力建立之后,推理系统仍然需要解决三个更进一步的问题:
- Prefill 与 Decode 的计算特征不同,是否可以拆分并独立优化?
- KV Cache 不仅要“知道在哪里”,还要解决容量不足与跨实例复用问题;
- 当负载持续变化时,如何在保证 SLO 的同时完成扩缩容,并兼顾 Batch 等非实时工作负载?
这些问题共同构成 llm-d 在分布式推理阶段的核心能力。本文将重点讨论 Disaggregated Serving、KV Cache Infrastructure、Autoscaling,以及 Batch / Async Serving。
1. Prefill / Decode Disaggregation
1.1 为什么要拆分 Prefill 与 Decode
一次自回归 LLM 推理通常可以分为两个阶段:
- Prefill:一次性处理输入 Prompt,计算对应的 KV Cache;
- Decode:基于已有 KV Cache,逐 Token 生成输出。
两者的资源特征并不相同。Prefill 通常更偏向大规模矩阵计算,而 Decode 在逐 Token 生成过程中更容易受到内存带宽与 KV Cache 访问的限制。
如果 Prefill 与 Decode 始终运行在同一组 Model Server 上,会产生两个问题。
第一,长 Prompt 的 Prefill 可能干扰正在进行的 Decode。当某个实例开始处理大规模 Prefill 时,已经进入 Decode 阶段的请求可能出现更高的 Token 间延迟。
第二,两类阶段无法独立配置与扩缩容。Prefill 与 Decode 对并行策略、实例数量以及硬件资源的最优配置可能不同,将二者绑定在同一实例中会限制优化空间。
因此,P/D Disaggregation 的核心目标并不是简单地“把一个请求拆成两半”,而是:
将 Prefill 与 Decode 放到不同的 Model Server 实例中,使两个阶段可以独立调度、独立扩容,并降低相互干扰。
1.2 llm-d 如何完成 P/D 调度
在 llm-d 中,Prefill Worker 与 Decode Worker 仍可以属于同一个 InferencePool,但通过 llm-d.ai/role 等角色信息区分。
EPP 使用 disagg-profile-handler 分别运行 Decode Profile 与 Prefill Profile。默认的思路可以概括为 Decode-first:
- 先选择 Decode Endpoint;
- 根据 Decode Endpoint 已有的 Prefix Cache,判断剩余未缓存 Prompt 是否值得使用远程 Prefill;
- 如果不需要拆分,则直接在 Decode Endpoint 完成请求;
- 如果需要远程 Prefill,再运行 Prefill Profile 选择 Prefill Endpoint。
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 41}}%%
flowchart LR
R["Inference Request"] --> D["Decode Profile<br/>Filter -> Score -> Pick"]
D --> DE["Decode Endpoint"]
DE --> C{"PD Decider<br/>remote prefill?"}
C -->|No| O["Decoder-only"]
C -->|Yes| P["Prefill Profile<br/>Filter -> Score -> Pick"]
P --> PE["Prefill Endpoint"]
PE --> PD["P/D Execution"]
DE --> PD
classDef input fill:#EEE8FF,stroke:#7663C6,stroke-width:1.5px,color:#222
classDef control fill:#D8F5F2,stroke:#3A9C96,stroke-width:1.5px,color:#222
classDef result fill:#FFF2BF,stroke:#B9952A,stroke-width:1.5px,color:#222
class R input
class D,C,P control
class DE,PE,O,PD result
这里需要强调:Prefill 与 Decode 并不是由两个独立的 EPP 负责调度。它们是同一个 EPP 中的不同 Scheduling Profile,并由 Profile Handler 负责组合。
1.3 一次 P/D 请求如何执行
当 EPP 确定 Prefill 与 Decode Endpoint 后,真正的执行链路涉及一个重要组件:Decode Pod 中的 Routing Sidecar。
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 43}}%%
sequenceDiagram
participant C as Client
participant G as Gateway / Proxy
participant E as EPP
participant S as Decode Routing Sidecar
participant P as Prefill Worker
participant D as Decode Worker
C->>G: inference request
G->>E: scheduling metadata
E-->>G: Decode Endpoint + optional Prefill Endpoint
G->>S: forward original request
S->>P: run remote prefill
P-->>S: KV transfer metadata
S->>D: decode request + transfer metadata
D-->>P: pull KV Cache
D-->>S: generated tokens
S-->>G: response
G-->>C: response
Routing Sidecar 负责的是请求编排与 KV Transfer 元数据协调,而不是搬运大体量 KV Tensor。
真正的 KV 数据传输由 Model Server 的 KV Connector 与底层数据传输机制完成。
1.4 NIXL 与 RDMA 的角色
P/D 分离成立的前提之一,是 Prefill Worker 产生的 KV Cache 能够高效传递给 Decode Worker。
llm-d 当前重点使用 NIXL(NVIDIA Inference Transfer Library)完成这一数据路径。NIXL 提供统一的数据传输接口,其底层可以基于 UCX、UCCL、libfabric 等后端,并进一步使用 InfiniBand、RoCE、EFA 等网络能力。
因此,两者的关系可以概括为:
1 | NIXL |
在支持 GPUDirect RDMA 的环境中,KV 数据可以在远端 GPU 内存之间直接移动,减少 CPU 中转带来的额外开销。
需要注意,P/D Disaggregation 对网络性能较为敏感。没有高性能互联时,传输成本可能抵消拆分带来的收益,因此 P/D 是否值得启用仍需要结合模型、Prompt 长度与网络条件评估。
1.5 从 P/D 到 E/P/D
对于多模态模型,还可能存在独立的 Encode 阶段,用于将图像、视频或音频转换为后续推理所需的表示。
因此,阶段可以进一步抽象为:
1 | Encode -> Prefill -> Decode |
llm-d 的调度框架已经提供 Encode、Prefill、Decode 等角色过滤能力,使不同阶段可以继续沿用 Endpoint Role + Scheduling Profile 的方式进行组织。
P/D 是最核心、最常见的 Disaggregated Serving 形态,而 E/P/D 则是同一思想在多模态工作负载上的进一步扩展。
2. 从 KV Cache Index 到 KV Cache Infrastructure
上篇解决了一个问题:
Router 如何知道 Prefix KV 位于哪个 Endpoint?
但“知道位置”并不能解决所有 KV Cache 问题。GPU HBM 容量有限,跨实例负载均衡也可能让请求无法始终落到持有缓存的实例。因此,还需要进一步解决 存储容量 与 跨实例移动。
2.1 KV Cache Offloading:扩大有效缓存容量
KV Cache Offloading 的基本思想是:当 GPU HBM 中的 KV Block 不适合继续驻留时,不立即丢弃,而是将其保存到更低成本、更大容量的存储介质。
在概念上,可以理解为:
1 | GPU HBM |
当前 llm-d 可以与 vLLM Native OffloadingConnector 以及 LMCache、Mooncake 等外部 KV Cache Engine 集成。
其中,vLLM Native Offloading 当前可以将 KV Block offload 到 CPU RAM,也可以通过 llm-d FS backend 使用共享文件系统。这两个目标在当前 Native Path 中是独立选择;统一的 GPU → CPU → Storage 分层链路仍在持续演进。
最重要的职责边界是:
Offloading 的实际数据移动由 Model Server / KV Connector 完成,llm-d Router 并不会逐 Block 下发“请把它搬到 CPU”的指令。
Router 更关注这些缓存如何影响后续请求调度。
2.2 P2P Prefix Cache Sharing:让缓存跟随请求移动
Prefix Cache-Aware Routing 的理想情况是:
1 | 请求 -> 已经持有 Prefix KV 的 Pod |
但这并不总是可行。
如果持有缓存的 Pod 已经过载,而另一个 Pod 具有大量空闲计算能力,强制保持 Cache Affinity 会形成热点。此时更合理的方式可能是:
1 | 请求 -> 空闲 Pod |
llm-d 当前提供 Experimental P2P KV Cache Sharing。其参考实现允许一个 vLLM 实例直接从另一个实例的 CPU Offload Tier 拉取 Prefix KV Block。
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 47}}%%
flowchart LR
I["KV Index<br/>block -> endpoint / tier"] --> E["EPP<br/>choose request target"]
I --> S["P2P Source Producer<br/>choose cache source"]
A["Pod A<br/>CPU Offload Tier"] -->|"NIXL / UCX<br/>CPU -> CPU"| B["Pod B<br/>CPU Offload Tier"]
B --> G["Pod B GPU KV Cache"]
E -. "request target = Pod B" .-> B
S -. "source = Pod A" .-> A
classDef state fill:#EEE8FF,stroke:#7663C6,stroke-width:1.5px,color:#222
classDef control fill:#D8F5F2,stroke:#3A9C96,stroke-width:1.5px,color:#222
classDef cache fill:#FFF2BF,stroke:#B9952A,stroke-width:1.5px,color:#222
class I state
class E,S control
class A,B,G cache
当前参考路径中的 P2P 数据传输主要是 CPU-to-CPU,并通过 NIXL/UCX 使用 RDMA 等传输能力;源 Pod 的 GPU 不需要参与该次 Prefix Pull。
这与 P/D 中的 KV Transfer 需要明确区分:
| 场景 | 目的 | 典型方向 |
|---|---|---|
| P/D KV Transfer | 当前请求完成 Prefill 后,把新生成的 KV 交给 Decode | Prefill → Decode |
| P2P Prefix Sharing | 复用历史请求已经产生的 Prefix KV | Cache Source → 当前执行实例 |
P2P 的价值在于,它打破了“要复用缓存就必须把请求发回原 Pod”的约束,使负载均衡与 Cache Reuse 可以同时存在。
2.3 Router、Indexer 与 Offloader 的职责边界
将这些能力组合后,一个跨实例 KV Cache 系统可以拆成三个问题:
1 | Offloader / KV Connector |
P2P Sharing 则在必要时将“请求去哪里”和“缓存在哪里”之间的差距补齐。
因此,llm-d 并不是建立一个中央 KV Cache Server,而是通过 分布式缓存 + 全局位置索引 + 请求调度 + 按需数据传输,构造出一个逻辑上的跨实例 KV Cache Infrastructure。
3. 从智能路由到弹性扩缩容
Routing 可以重新分配现有容量,但无法创造新的容量。
当某个 Endpoint 过载而其他 Endpoint 空闲时,EPP 可以通过调度解决问题;当整个 InferencePool 都趋于饱和时,无论请求如何重新分配,最终都需要增加 Model Server Replica。
因此:
1 | Routing |
3.1 为什么 GPU Utilization 不是理想的扩缩容指标
传统 GPU Workload 很自然地会使用 GPU Utilization 触发扩容。但 LLM Serving 使用 Continuous Batching 后,即使系统仍有较大 Serving Headroom,GPU 也可能长时间保持较高利用率。
因此:
1 | GPU Utilization ≈ 100% |
并不能区分:
1 | 系统正在健康地持续批处理 |
与:
1 | 请求已经大量排队,系统处于饱和状态 |
llm-d 更倾向使用直接描述推理需求的信号,例如:
| Signal | 反映的问题 |
|---|---|
| Queue Depth | 有多少请求尚未得到处理 |
| Running Requests / Concurrency | 当前活跃推理压力 |
| Token Backlog | 尚未处理的 Token 工作量 |
| Pool Saturation | 当前 Pool 距离容量上限有多近 |
| Estimated Latency | 预计 TTFT / TPOT 是否接近或违反 SLO |
其中 Token Backlog 尤其适合 Prompt 长度差异较大的场景,因为“10 个请求”无法表达每个请求分别包含 500 Token 还是 20,000 Token。
3.2 Metrics:从用户体验反推系统根因
Autoscaling 依赖 Metrics,但 Metrics 的价值并不止于扩缩容。对于 llm-d 这类多实例推理系统,可观测性的核心目标是回答四个问题:
- 用户是否真的感受到延迟恶化;
- 整个 InferencePool 是否已经接近饱和;
- 是否只是少数 Endpoint 出现热点或负载不均;
- 问题发生在 Model Server、EPP 调度、KV Cache,还是 P/D 数据传输路径。
因此,Metrics 应当按照层次理解,而不是记忆一组孤立的指标名称。
3.2.1 Model Server:观察单实例执行状态
vLLM、SGLang 等 Model Server 提供最接近实际执行阶段的指标。
以 vLLM 为例,最常用的一组指标包括:
| 关注点 | 代表性指标 | 含义 |
|---|---|---|
| Active Requests | vllm:num_requests_running |
当前正在执行的请求数 |
| Queue | vllm:num_requests_waiting |
等待进入执行的请求数 |
| KV Pressure | vllm:kv_cache_usage_perc |
GPU KV Cache 占用程度 |
| TTFT | vllm:time_to_first_token_seconds |
首 Token 延迟 |
| ITL | vllm:inter_token_latency_seconds |
相邻输出 Token 之间的延迟 |
| Prefix Cache | vllm:prefix_cache_hits_total / vllm:prefix_cache_queries_total |
Prefix Cache 复用效果 |
| Throughput | vllm:prompt_tokens_total / vllm:generation_tokens_total |
输入、输出 Token 吞吐 |
这些指标主要回答:
某一个 Model Server 当前是否繁忙,以及推理执行本身是否出现异常。
在 P/D 场景中,还可以进一步观察 NIXL KV Transfer 相关指标,例如 vllm:nixl_xfer_time_seconds、vllm:nixl_num_failed_transfers,用于判断性能问题是否来自 KV 数据传输。
3.2.2 EPP / InferencePool:观察全局负载与分布
单 Pod 指标不足以描述一个 Pool 的整体状态。EPP 会进一步提供 Pool 级别的聚合与 Per-Endpoint 指标,例如:
| 关注点 | 代表性指标 |
|---|---|
| 可用容量 | llm_d_epp_ready_endpoints |
| 平均 Queue | llm_d_epp_average_queue_size |
| 平均 Running Requests | llm_d_epp_average_running_requests |
| 平均 KV Cache Utilization | llm_d_epp_average_kv_cache_utilization |
| Queue 离散程度 | llm_d_epp_std_dev_queue_size |
| 单 Endpoint Queue | llm_d_epp_per_endpoint_queue_size |
| In-flight Requests | llm_d_epp_inflight_requests |
| In-flight Tokens | llm_d_epp_inflight_tokens |
平均值用于判断 整个 Pool 是否繁忙,Standard Deviation 与 Per-Endpoint 指标用于判断 负载是否分配不均。
例如:
1 | average_queue_size 高 |
这也是为什么只观察平均值是不够的。一个 Pool 的平均负载可能正常,但个别 Endpoint 已经严重拥塞。
3.2.3 用户视角:TTFT、TPOT 与 ITL
对于在线推理,最终仍应从用户体验出发。
1 | TTFT |
EPP 提供对应的 Request-level Metrics,例如:
1 | llm_d_epp_request_ttft_seconds |
它们可以帮助快速判断问题更可能发生在哪个阶段:
1 | TTFT 高、ITL 正常 |
3.2.4 一个排障例子:用户反馈“首 Token 明显变慢”
假设线上用户反馈:
请求发送后需要等待很久才开始输出,但开始输出后速度基本正常。
这实际上已经给出了第一个线索:
1 | TTFT ↑ |
因此可以按照“用户指标 → Pool → Endpoint → 执行路径”的顺序向下排查。
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 57}}%%
flowchart TD
U["用户反馈<br/>First Token 很慢"] --> T["确认 TTFT ↑<br/>ITL 是否正常?"]
T -->|"ITL 正常"| Q{"EPP Flow-Control Queue<br/>是否持续增长?"}
Q -->|"Yes"| P{"Pool Queue 分布"}
P -->|"平均值高<br/>StdDev 低"| C["整体容量不足<br/>检查 Autoscaling / Replica"]
P -->|"平均值一般<br/>StdDev 高"| R["局部热点<br/>检查 Routing / Scorer"]
Q -->|"No"| V{"vLLM waiting<br/>是否升高?"}
V -->|"Yes"| B["Model Server 内部拥塞<br/>检查 Prefill / Concurrency"]
V -->|"No"| K{"Prefix Cache Hit<br/>是否明显下降?"}
K -->|"Yes"| X["缓存复用退化<br/>检查 Prefix Routing / KV Indexer"]
K -->|"No"| N{"P/D 场景下<br/>NIXL transfer latency 是否升高?"}
N -->|"Yes"| Z["KV Transfer / Network 问题"]
N -->|"No"| S{"EPP scheduler / plugin latency<br/>是否异常?"}
S -->|"Yes"| E["EPP 调度路径异常"]
S -->|"No"| D["继续检查模型执行、网络与上游链路"]
classDef user fill:#EEE8FF,stroke:#7663C6,stroke-width:1.5px,color:#222
classDef check fill:#D8F5F2,stroke:#3A9C96,stroke-width:1.5px,color:#222
classDef root fill:#FFF2BF,stroke:#B9952A,stroke-width:1.5px,color:#222
class U,T user
class Q,P,V,K,N,S check
class C,R,B,X,Z,E,D root
对应的 Metrics 推断过程可以进一步展开。
第一步,确认用户感知:
1 | llm_d_epp_request_ttft_seconds ↑ |
说明问题主要发生在 First Token 之前,Decode 的持续生成阶段暂时不是首要怀疑对象。
第二步,看 EPP Flow Control:
1 | llm_d_epp_flow_control_queue_size |
如果二者持续升高,说明请求在进入 Model Server 之前就已经开始排队。
此时再看:
1 | llm_d_epp_average_queue_size |
如果所有 Endpoint 的 Queue 都高,则更接近 整体容量不足;如果主要集中在少量 Endpoint,则应首先检查 Load-Aware Routing、Prefix Affinity 或其他 Scorer 是否把流量过度集中到少数 Pod。
第三步,如果 EPP Queue 正常,则继续观察 Model Server:
1 | vllm:num_requests_waiting |
如果 num_requests_waiting 明显升高,而 EPP 侧没有对应 Queue,则说明请求已经进入 Model Server,但执行层无法及时消化,问题更靠近 Prefill、Continuous Batching 或 KV Cache 压力。
第四步,如果 Model Server 并不拥塞,则检查缓存复用:
1 | llm_d_epp_prefix_indexer_hit_ratio |
如果 Cache Hit Ratio 突然下降,同样的 Prompt 需要重新执行更多 Prefill,即使请求量没有变化,TTFT 也可能显著升高。这时需要继续检查 KVEvents、KV Cache Indexer、Prefix Scorer,以及 Prompt Pattern 是否发生变化。
第五步,在 P/D Disaggregation 场景下,如果 Prefill 本身正常,但:
1 | vllm:nixl_xfer_time_seconds ↑ |
则问题可能出现在 Prefill → Decode 的 KV Transfer 路径,而不是计算本身。
最后,如果后端指标基本健康,但:
1 | llm_d_epp_scheduler_e2e_duration_seconds ↑ |
则应转向 EPP Scheduler、Plugin 或外部依赖,而不是继续扩容 Model Server。
因此,一个更稳定的排障顺序是:
1 | 用户体验 |
这种方式的核心不是看到一个指标异常就直接下结论,而是通过相邻层指标之间是否同时异常,逐步缩小问题范围。
3.3 KEDA + EPP:把 Serving Signal 转换为 Replica
llm-d 当前推荐的扩缩容路径是 KEDA + EPP Metrics。
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 53}}%%
flowchart LR
M["Model Servers"] --> E["EPP Metrics<br/>queue / running / tokens / latency"]
E --> P["Prometheus"]
P --> K["KEDA<br/>ScaledObject"]
K --> H["KEDA-owned HPA"]
H --> D["Model Server Deployment"]
D --> M
classDef runtime fill:#EEE8FF,stroke:#7663C6,stroke-width:1.5px,color:#222
classDef control fill:#D8F5F2,stroke:#3A9C96,stroke-width:1.5px,color:#222
classDef act fill:#FFF2BF,stroke:#B9952A,stroke-width:1.5px,color:#222
class M,E runtime
class P,K,H control
class D act
这里各层职责清晰:
- EPP:产生面向推理需求的 Serving Signal;
- Prometheus:采集和聚合指标;
- KEDA:将 Prometheus 指标转换为 External Metric,并管理 HPA;
- HPA:计算目标 Replica 数;
- Deployment:真正增加或减少 Model Server Pod。
KEDA 并不取代 HPA,而是为 HPA 提供外部指标并管理其生命周期。
3.4 SLO-Aware:直接围绕用户体验控制容量
Queue、Concurrency 和 Token Backlog 都属于用户延迟的代理信号。更进一步,可以直接围绕延迟 SLO 进行扩缩容。
例如定义:
1 | TTFT <= 1 s |
llm-d 的 SLO-aware 路径会将预测或实测的 TTFT / TPOT 与目标 SLO 比较,形成一个 Saturation Signal。
可以用简化形式理解:
1 | Saturation |
当该值持续升高时,说明当前 Pool 正在消耗延迟预算;扩容后,每个实例承担的负载下降,预计延迟随之降低,从而形成闭环。
这一机制也与 P/D Disaggregation 自然结合。
通常情况下:
1 | TTFT 压力 |
因此 P/D 分离后,可以分别观察 Prefill 与 Decode Pool 的压力,并独立调整 Replica 数量,而不是将整套推理实例整体扩容。
3.5 Routing 与 Autoscaling 是两个时间尺度的控制环
两者最终形成互补关系:
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 59}}%%
flowchart LR
R["Incoming Request"] --> E["EPP Routing"]
E -->|"fast: choose best existing endpoint"| M["Model Servers"]
M --> X["Serving Metrics / SLO Pressure"]
X --> A["Autoscaling"]
A -->|"slow: add or remove capacity"| M
classDef request fill:#EEE8FF,stroke:#7663C6,stroke-width:1.5px,color:#222
classDef control fill:#D8F5F2,stroke:#3A9C96,stroke-width:1.5px,color:#222
classDef runtime fill:#FFF2BF,stroke:#B9952A,stroke-width:1.5px,color:#222
class R request
class E,X,A control
class M runtime
Routing 在单次请求到达时立即生效,用于重新分配现有容量;Autoscaling 的响应更慢,但能够真正改变系统容量。
如果所有 Endpoint 都已经无法满足 SLO,仅依赖 Routing 无法解决问题;如果只是局部热点,则直接扩容又可能浪费资源。
4. Batch 与 Async Serving
在线推理关注低 TTFT、稳定 TPOT/ITL,而离线推理更关注吞吐、完成时间、失败恢复和任务生命周期。
典型 Batch Workload 包括:
- 大规模数据集推理;
- 批量 Embedding;
- 离线评测;
- 数据分类与生成;
- 夜间非实时任务。
llm-d 将这类工作负载拆分为两个不同层次:Batch Gateway 负责 Batch Job,Async Processor 负责异步请求派发。
4.1 Batch Gateway:管理 Batch Job 生命周期
Batch Gateway 对外提供 OpenAI-compatible Batch API。
这里的 “OpenAI-compatible” 指的是 API Contract 与使用方式兼容,并不意味着请求会被发送到 OpenAI。底层仍然由用户自己的 llm-d Router 与 Model Server 执行推理。
典型生命周期为:
1 | POST /v1/files |
Batch Gateway 还需要负责 Job Metadata、输入输出文件、取消、失败恢复以及过期清理。因此,它本质上是一个 Batch Job Management Layer,而不是 Model Server。
4.2 Async Processor:控制请求何时进入推理系统
Async Processor 则位于更低一层。
它从 Redis Sorted Set、GCP Pub/Sub 等消息队列消费 inference request,并通过 Dispatch Gate 判断当前是否适合继续向下游 Router 发送请求。
1 | Queue |
Dispatch Gate 可以基于后端 Saturation、可用 Budget、本地最大 Concurrency 等条件控制流量,从而避免后台任务一次性灌满 Model Server。
因此,两者可以概括为:
1 | Batch Gateway |
这个 Producer / Consumer 类比主要适用于启用异步派发的场景。Batch Gateway 也可以采用同步派发模式,此时请求无需经过 Async Processor。
4.3 Online 与 Batch 如何共享同一套 GPU
Batch Serving 的一个重要目标,是利用 Online Traffic 留下的闲置容量,而不是必须维护一套完全独立的 GPU 集群。
完整关系可以抽象为:
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 61}}%%
flowchart LR
O["Online Requests"] --> G["Gateway / Router"]
B["Batch Client"] --> BG["Batch Gateway"]
BG --> Q["Queue"]
Q --> AP["Async Processor<br/>Dispatch Gates"]
AP --> G
G -. "scheduling request" .-> E["EPP<br/>Flow Control + Scheduling"]
E -. "selected endpoint" .-> G
P["Shared InferencePool"] -. "candidate endpoints" .-> E
G --> M["Model Servers / GPUs"]
classDef online fill:#EEE8FF,stroke:#7663C6,stroke-width:1.5px,color:#222
classDef batch fill:#D8F5F2,stroke:#3A9C96,stroke-width:1.5px,color:#222
classDef runtime fill:#FFF2BF,stroke:#B9952A,stroke-width:1.5px,color:#222
class O,G online
class B,BG,Q,AP,E batch
class P,M runtime
关键并不是“所有请求完全公平地共享 GPU”,而是通过 Flow Control、Inference Objective、Priority 与 Dispatch Gate 保护 Online Workload。
当 Online Traffic 较低时,Async Processor 可以提高后台请求派发速度;当后端趋于饱和时,则减少或停止 Batch Dispatch。
因此 Batch Workload 更适合被理解为:
在不破坏在线 SLO 的前提下,利用 Serving Fleet 的 Slack Capacity。
5. 将这些能力放在同一张图中
到这里,可以将 llm-d 的核心能力重新放回完整推理链路中:
%%{init: {"look": "handDrawn", "theme": "base", "handDrawnSeed": 67}}%%
flowchart TB
C["Online / Batch Requests"] --> G["Gateway / Proxy"]
G -. "scheduling request" .-> R["EPP"]
R -. "selected endpoints" .-> G
IP["InferencePool"] -. "candidate endpoints" .-> R
subgraph SERVE["Model Serving"]
direction LR
P["Prefill Workers"]
D["Decode Workers"]
end
G --> S["Decode Routing Sidecar"]
S -->|"optional remote prefill"| P
S -->|"decode request"| D
P -->|"KV Transfer<br/>NIXL / RDMA"| D
K["KV Cache Infrastructure<br/>Indexer / Offloading / P2P"] -. "cache locality & reuse" .-> R
K -. "KV data" .-> P
K -. "KV data" .-> D
O["Observability / SLO Metrics"] --> A["KEDA / HPA"]
P --> O
D --> O
A -->|"scale P / D independently"| SERVE
classDef request fill:#EEE8FF,stroke:#7663C6,stroke-width:1.5px,color:#222
classDef control fill:#D8F5F2,stroke:#3A9C96,stroke-width:1.5px,color:#222
classDef runtime fill:#FFF2BF,stroke:#B9952A,stroke-width:1.5px,color:#222
class C,G,IP request
class R,K,O,A control
class P,D,S runtime
图中实线表示请求或数据流,以及指标采集与扩缩容关系;虚线表示调度与缓存位置关系。InferencePool 仍然是候选 Endpoint 的声明,而不是请求必须经过的代理。这张图体现了 llm-d 的整体思路:
- Router / EPP 决定请求去哪里;
- P/D Disaggregation 决定推理阶段如何拆分与组合;
- KV Cache Infrastructure 负责缓存的定位、扩展与复用;
- Autoscaling 根据 Serving Signal 调整容量;
- Batch / Async Serving 在同一套 Serving Fleet 上引入受控的后台工作负载。
这些能力并不是独立功能的简单堆叠,而是在共同解决一个问题:
如何让一组 Model Server 作为一个整体,以更高的缓存复用率、更合理的负载分配和更稳定的 SLO 对外提供推理服务。
6. 总结
如果将上下两篇合在一起,llm-d 可以被理解为四个相互衔接的层次:
1 | Endpoint Discovery |
其中最重要的并不是某一个具体 Plugin 或 CRD,而是职责边界:
- vLLM / SGLang 负责单个 Model Server 内部的高效推理;
- llm-d Router / EPP 负责跨 Model Server 的请求调度;
- KV Connector / NIXL 等数据路径 负责真正的 KV 数据移动;
- KEDA / HPA 负责将推理需求转换为实例容量;
- Batch Gateway / Async Processor 负责非实时工作负载的任务管理与受控派发。
从这一视角看,llm-d 的价值并不是提供“另一个推理引擎”,而是在 Kubernetes 环境中补齐多实例 LLM Serving 所需要的 调度、缓存、数据传输、弹性与工作负载治理能力。







