引言

上篇:从 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:

  1. 先选择 Decode Endpoint;
  2. 根据 Decode Endpoint 已有的 Prefix Cache,判断剩余未缓存 Prompt 是否值得使用远程 Prefill;
  3. 如果不需要拆分,则直接在 Decode Endpoint 完成请求;
  4. 如果需要远程 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
2
3
4
5
NIXL
= 推理数据传输抽象

RDMA / IB / RoCE / EFA
= 底层高性能传输能力

在支持 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
2
3
GPU HBM
↓
CPU DRAM / Shared Storage

当前 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
2
请求 -> 空闲 Pod
缓存 -> 从其他 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
2
3
4
5
6
7
8
Offloader / KV Connector
解决:KV 如何保存、如何移动

KV Cache Indexer
解决:KV 在哪里

Router / EPP
解决:请求应该去哪里

P2P Sharing 则在必要时将“请求去哪里”和“缓存在哪里”之间的差距补齐。

因此,llm-d 并不是建立一个中央 KV Cache Server,而是通过 分布式缓存 + 全局位置索引 + 请求调度 + 按需数据传输,构造出一个逻辑上的跨实例 KV Cache Infrastructure。


3. 从智能路由到弹性扩缩容

Routing 可以重新分配现有容量,但无法创造新的容量。

当某个 Endpoint 过载而其他 Endpoint 空闲时,EPP 可以通过调度解决问题;当整个 InferencePool 都趋于饱和时,无论请求如何重新分配,最终都需要增加 Model Server Replica。

因此:

1
2
3
4
5
Routing
解决:现有容量如何分配

Autoscaling
解决:总容量是否足够

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
2
3
4
5
6
7
average_queue_size 高
+ std_dev_queue_size 低
=> 大部分 Endpoint 都忙,更像是整体容量不足

average_queue_size 一般
+ std_dev_queue_size 高
=> 少数 Endpoint 成为热点,更像是 Routing 不均

这也是为什么只观察平均值是不够的。一个 Pool 的平均负载可能正常,但个别 Endpoint 已经严重拥塞。

3.2.3 用户视角:TTFT、TPOT 与 ITL

对于在线推理,最终仍应从用户体验出发。

1
2
3
4
5
6
7
8
TTFT
= Request 到 First Token 的时间

TPOT
= 平均每个 Output Token 的生成时间

ITL
= 相邻 Streaming Token 之间的时间

EPP 提供对应的 Request-level Metrics,例如:

1
2
3
llm_d_epp_request_ttft_seconds
llm_d_epp_request_streaming_tpot_seconds
llm_d_epp_request_streaming_itl_seconds

它们可以帮助快速判断问题更可能发生在哪个阶段:

1
2
3
4
5
6
7
8
TTFT 高、ITL 正常
=> 优先检查 Queue / Prefill / Prefix Cache / KV Transfer

TTFT 正常、ITL 高
=> 优先检查 Decode Saturation / Concurrency / KV Pressure

TTFT 与 ITL 同时升高
=> 更可能是整个 Pool 进入系统性饱和

3.2.4 一个排障例子:用户反馈“首 Token 明显变慢”

假设线上用户反馈:

请求发送后需要等待很久才开始输出,但开始输出后速度基本正常。

这实际上已经给出了第一个线索:

1
2
TTFT ↑
ITL ≈ normal

因此可以按照“用户指标 → 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
2
llm_d_epp_request_ttft_seconds ↑
llm_d_epp_request_streaming_itl_seconds 正常

说明问题主要发生在 First Token 之前,Decode 的持续生成阶段暂时不是首要怀疑对象。

第二步,看 EPP Flow Control:

1
2
llm_d_epp_flow_control_queue_size
llm_d_epp_flow_control_request_queue_duration_seconds

如果二者持续升高,说明请求在进入 Model Server 之前就已经开始排队。

此时再看:

1
2
3
llm_d_epp_average_queue_size
llm_d_epp_std_dev_queue_size
llm_d_epp_per_endpoint_queue_size

如果所有 Endpoint 的 Queue 都高,则更接近 整体容量不足;如果主要集中在少量 Endpoint,则应首先检查 Load-Aware Routing、Prefix Affinity 或其他 Scorer 是否把流量过度集中到少数 Pod。

第三步,如果 EPP Queue 正常,则继续观察 Model Server:

1
2
3
vllm:num_requests_waiting
vllm:num_requests_running
vllm:kv_cache_usage_perc

如果 num_requests_waiting 明显升高,而 EPP 侧没有对应 Queue,则说明请求已经进入 Model Server,但执行层无法及时消化,问题更靠近 Prefill、Continuous Batching 或 KV Cache 压力。

第四步,如果 Model Server 并不拥塞,则检查缓存复用:

1
2
3
llm_d_epp_prefix_indexer_hit_ratio
llm_d_epp_request_cached_tokens
vllm:prefix_cache_hits_total / prefix_cache_queries_total

如果 Cache Hit Ratio 突然下降,同样的 Prompt 需要重新执行更多 Prefill,即使请求量没有变化,TTFT 也可能显著升高。这时需要继续检查 KVEvents、KV Cache Indexer、Prefix Scorer,以及 Prompt Pattern 是否发生变化。

第五步,在 P/D Disaggregation 场景下,如果 Prefill 本身正常,但:

1
2
vllm:nixl_xfer_time_seconds ↑
vllm:nixl_num_failed_transfers ↑

则问题可能出现在 Prefill → Decode 的 KV Transfer 路径,而不是计算本身。

最后,如果后端指标基本健康,但:

1
2
llm_d_epp_scheduler_e2e_duration_seconds ↑
llm_d_epp_plugin_duration_seconds ↑

则应转向 EPP Scheduler、Plugin 或外部依赖,而不是继续扩容 Model Server。

因此,一个更稳定的排障顺序是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户体验
TTFT / TPOT / ITL
↓
Pool 级别
Queue / Running / Inflight / Saturation
↓
负载分布
Average + StdDev + Per-Endpoint
↓
Model Server
Waiting / Running / KV Pressure
↓
特定能力
Prefix Cache / NIXL / Offloading
↓
控制面
Scheduler / Plugin Latency

这种方式的核心不是看到一个指标异常就直接下结论,而是通过相邻层指标之间是否同时异常,逐步缩小问题范围。

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
2
TTFT <= 1 s
TPOT <= 50 ms/token

llm-d 的 SLO-aware 路径会将预测或实测的 TTFT / TPOT 与目标 SLO 比较,形成一个 Saturation Signal。

可以用简化形式理解:

1
2
3
4
5
Saturation
≈ max(
TTFT / TTFT_SLO,
TPOT / TPOT_SLO
)

当该值持续升高时,说明当前 Pool 正在消耗延迟预算;扩容后,每个实例承担的负载下降,预计延迟随之降低,从而形成闭环。

这一机制也与 P/D Disaggregation 自然结合。

通常情况下:

1
2
3
4
5
TTFT 压力
更多反映排队、Prefill 与 KV Transfer

TPOT / ITL 压力
更多反映 Decode 阶段的持续生成能力

因此 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
2
3
4
5
6
7
8
9
10
11
12
13
POST /v1/files
上传 JSONL 输入
↓
POST /v1/batches
创建 Batch Job
↓
后台处理
↓
GET /v1/batches/{id}
查询状态与进度
↓
GET /v1/files/{output_file_id}/content
获取输出

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
2
3
4
5
6
7
8
9
10
11
Queue
↓
Dispatch Gate
↓
Async Processor Worker
↓
llm-d Router
↓
EPP
↓
Model Server

Dispatch Gate 可以基于后端 Saturation、可用 Budget、本地最大 Concurrency 等条件控制流量,从而避免后台任务一次性灌满 Model Server。

因此,两者可以概括为:

1
2
3
4
5
Batch Gateway
= Job Manager + Producer

Async Processor
= Consumer + Controlled Dispatcher

这个 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
2
3
4
5
6
7
8
9
10
11
Endpoint Discovery
InferencePool
↓
Request Scheduling
EPP / Filter / Score / Pick
↓
Distributed Inference
P/D Disaggregation + KV Cache Infrastructure
↓
Operations
Autoscaling + Flow Control + Batch / Async

其中最重要的并不是某一个具体 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 所需要的 调度、缓存、数据传输、弹性与工作负载治理能力。


参考资料