文章 技术

llm-d SLO延迟预测路由一些研究

llm-d SLO延迟预测路由一些研究

前段时间对于LLM inference的SLO部分设计做了一些研究,又看了一下llm-d的这里,发现他们对SLO延迟预测也一些研究成果,这里就总结一下。

概览

Predicted Latency-Based Routing,SLO-aware 是其中一种模式。llm-d 的路由组件 EPP(Endpoint Picker)旁边挂了几个 XGBoost 小服务,用线上流量实时训练。每来一个请求,就预测它在每个候选 pod 上的 TTFT(首 token 延迟)和 TPOT(每个输出 token 的时间),再按预测值路由。它要替代手工调权重的 load+prefix 打分器(队列长度、KV 占用、前缀命中三项加权)。

EPP

EPP 全称 Endpoint Picker,是 llm-d router 的大脑。

做什么?

一个请求进来,EPP 依次做这几件事:

  1. 解析请求:读 body 里的 model、prompt 长度,以及请求头(SLO、priority)。

  2. 准入和流控:在 admitter / flow control 阶段决定排队、拒绝还是放行。

  3. 调度:跑上一轮讲的那条插件链,filter → scorer → picker,选出一个 pod。

  4. 后处理:响应流回来时也会经过 EPP。predicted-latency 用这一步测 TTFT/TPOT,再回传训练样本。

与传统的CNI关系

这里我们拿Istio来举例,Istio和EPP两者并不冲突,这里的一个推荐架构为让Istio的Ingress Gateway承担proxy角色,而只让EPP选择Pod

Client
  │
  ▼
Istio Ingress Gateway(Envoy,istiod 下发配置)
  │  ① HTTPRoute 匹配到 backendRef = InferencePool
  │  ② ext-proc gRPC → EPP:"选哪个 pod?"
  │  ③ 拿到 pod IP,直连
  ▼
vLLM pod

同一个Gateway可以同时挂两类路由

rules:
- matches: [{path: {type: PathPrefix, value: /v1/}}]
  backendRefs: [{kind: InferencePool, name: qwen-pool}]   # 交给 EPP
- matches: [{path: {type: PathPrefix, value: /api/}}]
  backendRefs: [{kind: Service, name: web-backend}]        # 普通 Istio 流量

两种策略

只做路由

SLO-aware

配置文件

predicted-latency.values.yaml

predicted-latency-slo.values.yaml

插件链

loose-affinity-filter → latency-scorer → weighted-random-picker

latency-slo-admitter(准入)+ strict-affinity-filter(0.99) → slo-headroom-tier-filter → loose-affinity-filter(0.8) → latency-scorer → weighted-random-picker

训练目标

只训练 ttft,实际记录的是端到端延迟

ttft + tpot

streamingMode

false

true,所有请求都必须 "stream": true | | SLO 来源 | 不需要 |

SLO 来源

不需要

请求头 x-llm-d-slo-ttft-ms / x-llm-d-slo-tpot-ms

streamingMode这里指的是客户端的stream,如果为False,那么实际上TTFT就是在预测端到端延迟,TPOT不训练。

核心机制

  • 预测器部署:EPP pod 里跑 4 个容器:epp、envoy、training-server(:8000)、prediction-server(:8001)。

    • 训练端每 10s 重训一次。新起的 pod 攒够 10 个样本就开始训练;如果启动时加载了已保存的模型,要等 100 个样本。

    • 预测端每 30s 从训练端拉一次新模型。

    • 训练数据是分层分桶的滑动窗口:KV 占用按 10% 一档,前缀命中按 0.25 一档,每桶最多 500 条,避免模型忘掉最近没出现的负载区间。

    • 10% 样本留作测试集。官方说 MAPE(平均绝对百分比误差)约 5%。

  • TTFT 特征:KV 占用%、输入长度、队列深度、正在跑的请求数、前缀命中%、在途输入 token 数。

  • TPOT 特征:KV 占用%、输入长度、队列深度、正在跑的请求数、已生成 token 数。

  • headroom 打分:headroom = SLO − 预测值。

    • 默认策略 least 是 best-fit 装箱:挑余量最小、但仍能满足 SLO 的 pod,把空闲 pod 留给突发流量。

    • 另一个策略 most 是尽量分散。

    • 博客说 TTFT 和 TPOT 合成一个分数时默认按 80%/20% 加权。

  • tier filter:把 pod 分成能满足 SLO 和不能满足两档,以 epsilonExploreNeg=0.01 的概率去探索不能满足的那档,让正在恢复的 pod 也能分到流量。

  • admitter(准入):所有 pod 都满足不了 SLO 时:

    • priority < 0 的可丢弃请求直接拒绝;

    • priority ≥ 0 的照样放行,发给差得最少的 pod。

    • 优先级通过 InferenceObjective CRD 设置。

    • 拿不到预测、或者有空闲 pod 时,fail-open(直接放行)。

  • 前缀亲和闸门:99% 的请求只在前缀命中 >0.8 的 pod 里选;1% 的请求随机探索全部 pod。如果命中 pod 的预测 TTFT 比全局最好的差 5000ms 以上,就放弃亲和。

  • 降级:预测服务挂了,就退回 KV/队列/前缀的组合启发式打分,不会丢请求。

骨架仍然是预选(Filter)→ 优选(Score)→ 选定(Pick),但预选不再是硬性过滤,而是软性带概率的。

PD分离

prefill pod 和 decode pod 在同一个 InferencePool 里,靠标签 llm-d.ai/role=prefill/decode 区分。

一个请求要调度两次,选出一个 P 和一个 D。EPP 里负责这件事的是 disagg-profile-handler,它按下面的顺序执行:

  1. 先跑 decode profile,选出 D 节点。

  2. 问 decider:这个请求要不要拆成 P/D?

  3. 要拆就再跑 prefill profile 选出 P 节点,把 P 和 D 的地址一起返回给代理。

  • decode pod 前面有个 routing sidecar,真正执行两段式流程:

    1. 先把请求发给 P,带 max_tokens=1,只做 prefill;

    2. D 通过 NIXL(RDMA)把 KV cache 拉过来;

    3. D 开始 decode,把结果经 sidecar 回传。

    • 如果 P 返回服务端错误,sidecar 会退化成只用 D(本地自己做 prefill)。

实验数据(来自博客)

成场景:10 台服务器,每台 2×H100、TP2,跑 A–D 四个前缀共享场景加 ShareGPT。4 个场景占优,前缀缓存频繁被逐出的场景 C 打平。这组实验没设 SLO,只是挑预测延迟最低的 pod。生产仿真负载:Qwen3-480B,13 台服务器,每台 8×H200。跟手工调过的 (3,2,2) 权重比:

指标

改善

E2E p50

13.42s → 9.06s(−43%)

TTFT p50

3.38s → 0.97s(−70%)

TPOT p50

28ms → 22ms

一些问题

下面的一些问题是我在做SLO预测时发现的一些问题,其中有些可能是平台问题,不做准确性的保证。

各自贪心,无联合优化

  1. EPP 先批量预测所有 pod 的 TTFT 和 TPOT,P 和 D 的预测各算各的,互不影响。

  2. 先选 D:只看预测的 TPOT,按分数加权抽签。

  3. 再选 P:只看预测的 TTFT,前缀命中率高的 pod 优先,同样加权抽签。

  4. 选好的 D 会写进请求属性,但 predicted-latency 的 P/D 配置里没有任何插件去读它。所以选 P 时完全不管配的是哪个 D。

核心问题

  • 只做了局部最优:P、D 各选各的"看起来最快",但这一对之间的 KV 传输成本没人算。各自最优的组合,合在一起可能更慢。

  • 传输时间不在模型里:预测器没有拓扑、带宽、KV 数据量这类特征。传输时间只是被平均地混进了 TTFT 训练标签,模型分不清同机架和跨机架。

  • 现有的补救手段比较粗糙:可选插件 topology-affinity-scorer 用的是写死的常数(同主机 1.0、同机架 0.2、同可用区 0.05、同地域 0.02)。

    • 不能配置,不是学出来的,也不随 prompt 长度或 KV 大小变化。

    • 它和延迟分数按权重直接相加,两边单位对不上,权重还得手调。

    • 同机架给得太低,压不住几百毫秒的传输差;同主机又给得太高,能压过好几秒的排队差。

    • 1.0 的依据是"同主机走 NVLink"。但 llm-d 的 P/D 文档写明 NVLink 不能跨 pod 使用,所以这个前提很可能不成立。

  • 抽签是随机的:即使打分正确,也只是提高命中好组合的概率,不能保证选中。

  • 可选的执行顺序也有限制:prefill-first(先选 P 再选 D)虽然存在,但这个模式下不能同时用 prefill decider。

XGBoost预测

XGBoost对于边界外推的情况表现的并不是很好,换句话说,如果我们要预测的准,我们必须见过类似的。
我们做了一个测试:三种方案对比(A100 两次回放,离线评估)

Prefill 执行时间(训练集只含较短 prompt):

训练范围

测试范围

真实中位数

线性偏差

XGBoost 偏差

XGBoost 最大预测值

≤1024 token

1025–2048

146 ms

+4.4%

−42%

97 ms

≤1024 token

>2048

282 ms

+2.9%

−71%

97 ms

≤2048 token

2049–4096

282 ms

−1.2%

−48%

175 ms

XGBoost 的预测始终没有超过训练集里见过的最大值(104 ms 和 176 ms),超出范围的样本 100% 被低估。线性模型沿着斜率外推,误差在几个百分点以内。这就是树模型"持平不涨"的直接证据。

Decode TPOT(训练集只含较小 batch):

训练范围

测试范围

线性偏差

XGBoost 偏差

bs ≤32

65–127

−7.0%

−8.5%

bs ≤64

65–127

−4.3%

−3.6%

两者都偏乐观,差距不大。这条证据比较弱,原因有两个:

  • A100 decode 在 bs ≤127 时还处在显存带宽受限的平坦区,TPOT 只从 17 ms 涨到 19.5 ms;

  • 小 batch 加长上下文的样本已经覆盖了这个延迟区间,所以树模型没有真的超出见过的值。

评论