前段时间对于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 依次做这几件事:
解析请求:读 body 里的 model、prompt 长度,以及请求头(SLO、priority)。
准入和流控:在 admitter / flow control 阶段决定排队、拒绝还是放行。
调度:跑上一轮讲的那条插件链,filter → scorer → picker,选出一个 pod。
后处理:响应流回来时也会经过 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 流量
两种策略
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。
优先级通过
InferenceObjectiveCRD 设置。拿不到预测、或者有空闲 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,它按下面的顺序执行:
先跑 decode profile,选出 D 节点。
问 decider:这个请求要不要拆成 P/D?
要拆就再跑 prefill profile 选出 P 节点,把 P 和 D 的地址一起返回给代理。
decode pod 前面有个 routing sidecar,真正执行两段式流程:
先把请求发给 P,带
max_tokens=1,只做 prefill;D 通过 NIXL(RDMA)把 KV cache 拉过来;
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) 权重比:
一些问题
下面的一些问题是我在做SLO预测时发现的一些问题,其中有些可能是平台问题,不做准确性的保证。
各自贪心,无联合优化
EPP 先批量预测所有 pod 的 TTFT 和 TPOT,P 和 D 的预测各算各的,互不影响。
先选 D:只看预测的 TPOT,按分数加权抽签。
再选 P:只看预测的 TTFT,前缀命中率高的 pod 优先,同样加权抽签。
选好的 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 的预测始终没有超过训练集里见过的最大值(104 ms 和 176 ms),超出范围的样本 100% 被低估。线性模型沿着斜率外推,误差在几个百分点以内。这就是树模型"持平不涨"的直接证据。
Decode TPOT(训练集只含较小 batch):
两者都偏乐观,差距不大。这条证据比较弱,原因有两个:
A100 decode 在 bs ≤127 时还处在显存带宽受限的平坦区,TPOT 只从 17 ms 涨到 19.5 ms;
小 batch 加长上下文的样本已经覆盖了这个延迟区间,所以树模型没有真的超出见过的值。
评论