Files
agentic-pd-hybrid/docs/KVC_ROUTER_ALGORITHM.md
kzlin 9ccd853066 docs(kvc): correct reseed cost decomposition + flag D->P sync gap
After an independent Opus-agent forensic audit, the previous "(c) 增量
fetch (工程量较大,未实现)" line in V2_DEEP_ANALYSIS §4.2 was understating
the gap. The audit confirmed:

- No D->P KV transfer code exists in the framework at any layer
  (agentic_pd_hybrid orchestration, vendored SGLang disaggregation,
  or mooncake transport).
- Mooncake MooncakeKVManager has a hard role split: PREFILL = sender,
  DECODE = receiver-only loop. `add_transfer_request` asserts the
  disaggregation_mode is PREFILL.
- The BaseKVSender / BaseKVReceiver abstraction has no bidirectional slot.
- session_aware_cache.release_session only calls kv_pool_allocator.free()
  on eviction -- no serialization, no outbound network call.
- _commit_prefill_backup_residency is only called from the seed/reseed
  path (_invoke_kvcache_seeded_router). direct-to-D path never updates
  P-side backup state.
- "capacity-backup" policy semantics: it only skips the close on P after
  reseed -- the backup is the seed-time static snapshot, never refreshed
  by D-side append-prefill activity.

V2_DEEP_ANALYSIS §4.2:
- Decomposed the 3-7s reseed cost into the P-side re-prefill segment
  (1.5-3s, dominant) and the P->D mooncake transfer segment (1.5-4s).
- Quantified the realistic effect of enabling RDMA: only the transfer
  segment shrinks, reseed reduces to 1.7-3.2s, TTFT p99 ~0.7s, still
  loses to DP's 0.43s.
- Replaced the throwaway "(c) incremental fetch" line with a full
  paragraph explaining what D->P sync would require, why it's the
  largest engineering gap, and that the blocker is SGLang's radix-tree
  single-producer assumption, not the network layer.

KVC_ROUTER_ALGORITHM §9:
- Refined Open Question 3 (RDMA) to clarify it only helps the transfer
  segment, not the re-prefill segment.
- Added Open Question 4: D->P incremental KV sync as the central
  future-work contribution gap, with cited evidence for why it doesn't
  currently exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:07:14 +08:00

22 KiB
Raw Blame History

KVC-Router面向 Agentic 多轮 LLM Serving 的 Session-Aware 调度算法

性质:论文级形式化规范——用于团队内部对齐 + 外部读者 onboarding。 对象:项目团队(统一术语);论文 reviewer算法定义最近更新2026-05-11。

本文给出本项目所开发的 KVCache-Centric Router(以下简称 "KVC-Router")调度算法的形式化、与实现无关的定义。本文设计为可直接被论文引用,并作为"KVC 到底在谈论什么调度算法"的标准回答。

对应的参考实现位于:

  • src/agentic_pd_hybrid/policies.pyKvAwarePolicyRoutingState
  • src/agentic_pd_hybrid/replay.py — orchestrationadmission RPC、reset-on-success、fallback chain
  • third_party/sglang/python/sglang/srt/managers/scheduler.py — D-worker 端的 admission 决策

1. 问题定义

我们要服务一群多轮 agentic LLM session如 Claude Code、Codex、Cursor 等 coding agent底层是异构 worker 池,分成:

  • Prefill workersPGPU 常驻的模型副本,针对长输入 prompt 的 batched prefill 做了优化。
  • Decode workersDGPU 常驻的模型副本,配备 session-aware KV cache"SessionAwareCache"),具备:(i) 跨 turn 保留 session 的 KV 状态;(ii) 在本地已缓存的 prefix 上做 append-prefill无需绕回 P

在一个 agent turn 内,请求 r 到达时其对话 prefix 已经从前序 turn 累积;新增的 tokens工具输出、用户消息等构成小规模 append。驱动 KVC 设计的根本观察是:

当 prefix KV 已经驻留在将要解码该请求的 D worker 上,请求的 first-token 延迟仅由 append 大小决定(典型 O(10²10³) tokens而非完整 prompt 大小(典型 O(10⁴10⁵) tokens

Router 的工作就是最大化满足上述条件的请求占比,同时尊重容量约束、不造成 session 无限饿死。

1.1 优化目标

给定来自 S 个 session 的请求流 R = (r_1, r_2, ...),最小化 SLO 加权的 TTFT 与端到端延迟混合:

   minimize   E[ w_ttft · TTFT(r) + w_lat · E2E_Latency(r) ]
   subject to  capacity[d] ≤ K_d   对任意 D worker d 在任意时刻 t,
               没有 session 被永久拒绝服务.

参考实现中通过 measurement 隐式取 w_ttft = 1, w_lat = 1per-D KV 池预算 K_d 取 SGLang 启动时上报的 max_total_num_tokens


2. 系统模型与记号

2.1 集合

符号 含义
`P = {p₁, …, p_ P
`D = {d₁, …, d_ D
S Session 标识符集合(由上游 agent runtime 分配)
H KV block hash 的全集(本实现中每 BLOCK_TOKEN_BUDGET = 24 tokens 对应一个 hash

2.2 请求

一个请求 r 是一个元组:

   r = ⟨ s(r),  t(r),  prefix_hashes(r),  append_len(r),  input_len(r) ⟩

其中:

  • s(r) ∈ S — session id
  • t(r) ∈ — 该 session 内的 turn index0 = 首轮)
  • prefix_hashes(r) ⊂ H — 覆盖请求输入 prefix 的 block hash 集合
  • append_len(r) ∈ — 新到达、不在 prefix_hashes(r) 中的 token 数
  • input_len(r) = (|prefix_hashes(r)| · 24) + append_len(r) — 总 token 数

2.3 Router 状态 (Σ)

Router 跨请求维护的全局状态:

字段 类型 语义
resident[d] set[H] Router 估计的 D d 当前 SessionAwareCache 中常驻的 block hash 集合router 端估计,真值在 worker 上)
pin[s] D {⊥} Session s 最近一次成功服务的 D 表示从未见过
inflight[d] 当前已派发给 d 但尚未完成的请求数
assigned[d] 累计派发到 d 的路由决策次数(负载 tie-breaker
rejects[s,d] per-(session, D) 的 admission 拒绝计数v2 引入的 migration 机制)

2.4 超参数

符号 默认值 描述
αsticky_bonus 1 匹配 pin[s] 的 D 在评分中获得的 bonus
τ_rejectmigration_reject_threshold 3 (s, d) 被拒绝达此次数后d 对 s 进入 blacklist
τ_appendkvcache_direct_max_uncached_tokens 8192v2 走 Direct-to-D 路径允许的最大 append 长度
K_d 取自 SGLang max_total_num_tokens per-D 的 KV 池预算
ρ 0.95 容量高水位线(隐式由 SGLang 强制)
ε(最大 fallback 重试数) ` D

2.5 路由结果

路由决策 δ(r) 取以下四种之一:

Mode 含义 KV transfer
Direct(d) r 完全在 D d 上执行D 在其常驻 KV 上做 append (快路径)
Seed(d) Session 首轮P 做完整 prefillKV 通过 mooncake 传到 d 完整 input
Reseed(d) Session 之前在某个 D' 上,但已不再常驻;按 Seed 处理 完整 input
Fallback(p, d) Vanilla pd-disagg 路径(其它 D 均被 blacklist 或拒绝) 完整 input

3. 算法

KVC-Router 由三个相互配合的过程组成:

  • Algorithm 1 (Route)router 端基于评分的候选选择。
  • Algorithm 2 (Admit)D-worker 端的 admission 决策(在 D scheduler 中执行,非 router
  • Algorithm 3 (Dispatch):端到端 orchestration把 Route + Admit + reset-on-success 串起来。

3.1 Algorithm 1Route(r, Σ) — 基于评分的候选选择

输入:请求 r状态 Σ
输出:候选 d* ∈ D若所有 D 都被过滤后仍无候选,退化分支兜底返回最少被拒的 D

 1.  blacklisted ← { d ∈ D : Σ.rejects[s(r), d] ≥ τ_reject }
 2.  C ← D  blacklisted                                  // 候选 D 集合
 3.  if C = ∅ :                                           // 退化
 4.       return argmin_{d ∈ D} Σ.rejects[s(r), d]        // 选最少被拒的 D
 5.  for each d ∈ C :
 6.       overlap(d)  ← |prefix_hashes(r) ∩ Σ.resident[d]|
 7.       sticky(d)   ← 1 if Σ.pin[s(r)] = d else 0
 8.       infl(d)     ← Σ.inflight[d]
 9.       assn(d)     ← Σ.assigned[d]
10.       score(d)    ← ⟨ overlap(d) + α·sticky(d),       // 主项
                          sticky(d),                       // tie-1
                          infl(d),                        // tie-2负载小者占优
                          assn(d) ⟩                       // tie-3
11.  return argmax_{d ∈ C} score(d)                       // 按字典序最大

说明

  • 评分是 4 元组按字典序比较,不是单个标量——这样避免在不同维度之间调权重。
  • 第 10 行的主项 overlap + α·sticky 同时奖励 KV 复用与 session stickiness。取 α=1overlap 以 block24 tokens为单位时任何一次 hash 命中都压制纯 sticky 的候选
  • 第 14 行的 blacklist 过滤防止永久绑死在已饱和的 D 上;与 Algorithm 3 的 reset-on-success 配合,限定了 migration 频率。

3.2 Algorithm 2Admit(d, r, M, K) — D-worker admission 决策

在 D worker 自己的 scheduler 内部执行(非 router这是 KVC 的机制核心:每个 D 自治判断能否把 r 当作 Directappend-only服务还是必须改走 P 路径。

输入D worker d请求 rd 上本地常驻的 session 集合 M_dKV 池预算 K_d
输出⟨can_admit ∈ {True, False},  mode ∈ {Direct, Seed, Reseed, ⊥},  reason⟩

 1.  used_tokens ← Σ_{s' ∈ M_d} resident_tokens(s', d)     // D 自己的 bookkeeping
 2.  cap_ok ← (used_tokens + input_len(r)) ≤ ρ · K_d        // 高水位线 ρ ≈ 0.95

 3.  if s(r) ∈ M_d :                                        // session 在 d 上有常驻
 4.       if append_len(r) ≤ τ_append  and  cap_ok :
 5.           return ⟨True, Direct, ∅⟩                      // → 快路径
 6.       elif append_len(r) > τ_append :
 7.           return ⟨False, ⊥, "real-large-append"⟩
 8.       else :
 9.           return ⟨False, ⊥, "no-d-capacity"⟩

10.  else :                                                 // session 在 d 上无常驻
11.       if cap_ok :
12.           mode ← Seed if t(r) = 0 else Reseed
13.           return ⟨True, mode, ∅⟩                        // → 经 P 做 KV seeding
14.       else :
15.           return ⟨False, ⊥, "session-not-resident-no-capacity"⟩

说明

  • 该过程通过同步 HTTP RPC/admit_direct_append)从 router 调用。RPC 阻塞直到 D scheduler 给出权威答复——这是 v5 引入的 "worker-mode admission",替换了更早的 router-端容量估算(系统性偏乐观)。
  • reason 字符串被回传给 router用于(i) 在 Algorithm 3 中驱动 fallback chain(ii) 标注 execution_mode 字段便于分析。

3.3 Algorithm 3Dispatch(r, Σ) — 端到端 orchestration

输入:请求 r状态 Σ
输出:执行模式 μ ∈ {Direct, Seed, Reseed, Fallback}

 1.  retries ← 0
 2.  tried ← ∅
 3.  while retries < ε :
 4.       d* ← Route(r, Σ \ {对 tried 中的 d 已 bump 过的 rejects})
 5.       if d* = ⊥ : break                                  // 无候选
 6.       resp ← Admit(d*, r)                                // RPC 到 D scheduler
 7.       if resp.can_admit :
 8.           Σ.rejects[s(r), d*]  ← 0                       // ◀ reset-on-successv2
 9.           Σ.pin[s(r)]          ← d*
10.           Σ.inflight[d*]       ← Σ.inflight[d*] + 1
11.           if resp.mode = Direct :
12.                在 d* 上完整执行 rappend-prefill + decode
13.                return Direct
14.           else :                                          // Seed 或 Reseed
15.                p ← round_robin_next(Σ, P)
16.                在 p 上做 r 的 prefill
17.                经 mooncake 把 KV(r) 从 p 传到 d*
18.                在 d* 上 decode r
19.                return resp.mode
20.       else :
21.           Σ.rejects[s(r), d*]  ← Σ.rejects[s(r), d*] + 1
22.           tried ← tried  {d*}
23.           retries ← retries + 1
24.
25.  // ε 次重试耗尽——退化 Fallback 到 vanilla pd-disagg
26.  p ← round_robin_next(Σ, P)
27.  d ← round_robin_next(Σ, D)
28.  通过 ⟨p, d⟩ 走 pd-disagg(r)
29.  return Fallback

维持的关键不变量

  1. 不会静默过载:一个 D 永不接受会让 used_tokens > ρ · K_d 的请求Algorithm 2 第 2 行)。
  2. 不存在永久饿死:对任意 session s,只要曾在某 D d* 上成功过一次,之后 Σ.rejects[s, d*] = 0Algorithm 3 第 8 行)。因此 blacklist 计数器不会对仍在某处成功获得服务的 session 累积——这阻止了 v1 的 thrashing 病理:原本 blacklist 计数器单调增长 + 退化 fallback 形成自放大的 round-robin 死循环。
  3. migration 有界:一个 session 从 D a 迁移到 D b 必须经过连续 τ_reject 次在 a 上失败、期间无任何成功。每个 session 生命周期内的最坏 migration 次数 ≤ (|D| 1) · τ_reject

3.4 Reset-on-success为什么这是关键修复v1 → v2 演化)

v1 实现省略了 Algorithm 3 第 8 行——一旦 (s, d) 累积 τ_reject 次拒绝d 对该 session 整个 run 永久 blacklist。实测Migration v1docs/MIGRATION_V1_FINDINGS_ZH.md)触发了自放大的失效模式:

session s 在 d 上稳定服务 70 个 turn
       ↓ 瞬时 burst 让 d 短暂饱和
3 次到 d 的 admission 被拒 → rejects[s,d] = 3 → d 对 s 永久 blacklist
       ↓ s 迁到 d'd' 也在负载中 → 被拒 → blacklist
       ↓ d'' 同理
所有 D 都 blacklist → 退化 fallback round-robin → 每次重试都 bump 一次计数器
                   → s 永远在 D 之间 thrashing每次都丢失 KV residency

reset-on-success 关上了这个回路:只要 s 在任一 d 上真正完成一次 Direct针对该 session 的 blacklist 立刻清零。该机制只对持续性(不是瞬时性)容量压力触发。


4. 性质

4.1 Theorem 1在有界 ε 下无永久饿死)

假设 τ_reject ≥ 1 且每个 D worker 的容量非零。则对任意能在 admission 时容下的 session sAlgorithm 3 在至多 |D| · τ_reject 次重试内返回 {Direct, Seed, Reseed} 之一;之后任意一次 Direct 成功即可清空 s 的所有 blacklist。

证明概要每次循环要么成功return、要么恰好让某个 rejects[s, d] 计数器 +1第 21 行)。经过 |D| · τ_reject 次迭代后,每个 D 要么对 s 已被 blacklistRoute 第 1 行会过滤),要么已成功(已终止)。在所有 D 都被 blacklist 的饱和点,Route 第 3 行返回最少被拒的 D打破对称性强制取得进展。∎

4.2 Theorem 2fast-path 命中下限)

假设 session s 在 D d 上已积累 KV residency R_s ⊂ H,且在某 turn t > 0 提交的请求 r 满足 prefix_hashes(r) ⊆ R_sappend_len(r) ≤ τ_append 且 admission 容量充足。则 Algorithm 3 将 r 路由为 Direct(d)。

证明概要:由 Algorithm 1overlap(d) = |R_s| 取得最大值;结合 α·sticky(d) ≥ 1d 的字典序得分严格高于任何 prefix_hashes(r) ⊈ R_{s,d'} 的 d'。故 Route 返回 d。Admit(d, r) 进入 s ∈ M_d ∧ append ≤ τ_append ∧ cap_ok 分支,返回 Direct。∎

这是 支持架构设计的机制级保证:只要 residency、append 大小、容量三者同时成立,快路径就被确定性地选中KVC 在典型场景下的 TTFT 优势是结构性属性,不是概率性。

4.3 复杂度

每个请求:

  • RouteO(|D|)(每个候选 D 算一次 score。生产规模下 |D| ≤ 8,主要开销在 Python 层,≪ 1 ms。
  • AdmitD scheduler 内部 O(1)(查自己的 bookkeeping无全局锁
  • Router 层的单请求总开销:O(|D|) 计算 + 1 次到目标 D 的 HTTP RTTloopback 亚毫秒,跨机数据中心约 1 ms

5. 与 baseline 的对比

性质 Vanilla pd-disagg DPcache-aware KVC-Router(本文)
P/D 分离 是(` P +
跨 turn cache locality 无(每个请求都 P→D 传 KV 仅在单 fused worker 内部走 hash prefix 路由 session 钉在某 D 上,本地 append-prefill
同 session cache 集中度 散到 ` D
最坏 turn-2 prefill 工作量 完整 input 经 P→mooncake→D 在目标 worker 上做完整 prefill带 prefix cache 命中) 本地 append_len ≤ τ_append tokens
容量感知 admission router 盲发) 隐式靠 worker 队列深度 显式的 per-D Admit() 决策
Migration 机制 N/A N/A 带 reset-on-success 的 reject-counter blacklist
Idle prefill 成本 是——P 永远在算 是——P 只在 cache miss 时启用(本工作 SWE-Bench 评测下约 8% 请求)

KVC 的关键架构权衡:用 P 端 GPU 闲置换 D 端 TTFT 稳定性。在 per-session cache 复用率高的 agentic workload 上Inferact 的 Codex trace 报告 94.2% cache hit我们的 SWE-Bench replay 实测 91.6% Direct 命中),这个交换显著有利。在 session 短或 cache hit 低的 workload 上权衡反转、DP 胜出。


6. 符号速查表

符号 含义
P, D Prefill / Decode worker 池
s(r), t(r) 请求 r 的 session id 与 turn index
prefix_hashes(r) r 输入 prefix 的 KV block hash
append_len(r) r 中新增(未缓存)部分的 token 数
Σ.resident[d] Router 对 d 缓存 block 集合的估计
Σ.pin[s] session s 最近一次成功的 D
Σ.rejects[s,d] per-(s,d) 的 admission 拒绝计数
α sticky bonus 权重(默认 1
τ_reject migration 阈值(默认 3
τ_append Direct 路径允许的 max append 大小v2 默认 8192
K_d D worker d 的 KV 池预算
ρ 容量高水位(默认 0.95
ε fallback 重试上限(默认 `
δ(r) 路由决策:Direct(d) / Seed(d) / Reseed(d) / Fallback(p, d)

7. 本工作评测中实际使用的默认参数

参数 取值 说明
` P ,
α 1
τ_reject 3
τ_append 8192 v2 调优后取值v0/v1 用 2048
K_d 92104 tokens SGLang 按 mem_fraction_static=0.835 自动算出
ρ 隐式 ~0.95 由 SGLang 的 max_total_num_tokens 强制
ε 2 `
每次 run 的 session 数 52 SWE-Bench 50sess trace
总请求数 4449
Time-scale 1.0(真实 trace 时序)
并发 32

8. Anti-patternsKVC 是什么)

  1. KVC 不仅仅是 kv-aware routing。DP 和 KVC 都可以跑 kv-aware policyKVC 在此之上加了三件事:(i) session 钉定,(ii) worker 端 admission(iii) 带 reset-on-success 的 migration。如果在比较 "KVC vs DP" 时缺这三个要素的任何一个,测的就不是 KVC 与 DP 的差异

  2. KVC 在 policy 项里不直接感知容量Route 不查 per-D 容量;容量感知完全经由 Admit 拒绝来传导。我们刻意做了这层分层——把容量判断放进 Route 会引入"换 D"的决策空间,导致 orphan KV 滞留问题。

  3. KVC 不保证 load balance。一个 session 若能舒服地装在某个 D 上,可能永远钉在那里,而其它 D 大部分时间空闲。在低容量压力下这是设计意图;高压力下 Theorem 1 的 migration 会触发再均衡。

  4. Fallback 不是"降级路径"。它和 vanilla pd-disagg 请求结构性等价延迟特征相同。KVC 的价值在于让 Fallback 占比在典型 agentic workload 下 ≪ 10%。


9. 公开问题reviewer 关注点)

以下问题在当前评测中尚未解决,主动列出以保持透明:

  1. Session 钉定相对于纯 P/D disaggregation 的边际贡献是多少? 需要 naive 1P3D 对照实验vanilla SGLang xPyD不带 KVC 层)——仓库当前缺失(见 docs/V2_DEEP_ANALYSIS_ZH.md §4.7)。

  2. Algorithm 3 在更高压下行为如何(例如 ts=10 加速、session 数 ≫ |D|·K_d/peak_input当前 ts=1 评测对应真实 agentic 区间,但算法在更高负载下的鲁棒性未经实验验证。

  3. 真 RDMA 下的 reseed 代价:本次评测的 37 s reseed 延迟由两段组成——P 端 re-prefill1.5-3s+ P→D mooncake transfer1.5-4s。当前 sweep 用的是 TCP loopback启用 IB/RoCE节点有 mlx5_0/_1 @ 200 Gb/s × 2 active需在 sweep 加 --force-rdma --ib-device mlx5_0)只能压缩 transfer 段到 ~200ms不动 re-prefill 段。预期 TTFT p99 从 1.28s 降到 ~0.7s(仍输 DP 0.43s)。待独立验证。

  4. D→P 增量 KV 同步(核心 future-work 缺口)reseed 长尾的真正消除需要让 P 端 backup 跟上 D 的 direct-to-D append 增长。经独立 forensic 审查,当前代码、vendored SGLang、mooncake 三层均无 D→P KV transfer 实现mooncake MooncakeKVManager 是 PREFILL=sender / DECODE=receiver 的硬角色分支(add_transfer_request 上有 assert disaggregation_mode == PREFILL 硬约束),BaseKVSender / BaseKVReceiver 抽象无 bidirectional slotsession_aware_cache.release_session 在驱逐时只调 kv_pool_allocator.free() 无出站,_commit_prefill_backup_residency 唯一 caller 是 seed/reseed 路径;capacity-backup policy 的真实语义只是"reseed 完不关 P streaming session"——backup 是 seed-time 的静态快照,不随 direct-to-D append 同步。要实现 D→P 增量同步,工程量 ~1-2 周,最难的不是 mooncake 加 D-sender / P-receiver 角色(~400 LOC而是 SGLang radix tree 改成允许从外部 worker 喂数据——radix cache 当前假设单一生产者(本 worker model 输出)。这是论文里最值得做的 contribution 之一。

  5. v2 代码路径下的确定性v0 代码库的 ts=1 N=3 categorical 确定性已经证实;新增的 reset-on-success 分支与 threshold=8192 路径未被独立 re-validate。两个额外的 N=1 run 即可解决。


10. 论文引用建议

论文中提到本算法时建议表述:

"We use the KVC-Router scheduling algorithm (Algorithms 13 of [our paper], formally defined in our supplementary materials). The router selects a decode worker by lexicographic scoring on (overlap+α·sticky, sticky, inflight, assigned) (Algorithm 1), defers the admission decision to the chosen worker via a synchronous RPC (Algorithm 2), and maintains a per-(session, decode worker) rejection counter that is reset on every successful Direct admission (Algorithm 3). This last detail — reset-on-success — is what distinguishes our v2 from the unstable v1 implementation that exhibits self-amplifying session thrashing."


附录 A — 算法步骤到代码实现的对照

算法步骤 文件 符号
Route 第 511 行 policies.py:189202 KvAwarePolicy.select 内层循环
Route 第 14 行blacklist 过滤 + 退化分支) policies.py:182187, 204211 migration_reject_thresholdselect 的 fallback
Admit third_party/sglang/python/sglang/srt/managers/scheduler.py handle_admit_direct_append_request
Dispatch 第 8 行reset-on-success replay.py: _run_request finish 路径中的 reset
Dispatch 第 21 行(记录 reject replay.py: _run_request state.record_admission_reject(...)
超参数 τ_append CLI flag --kvcache-direct-max-uncached-tokens
超参数 τ_reject CLI flag --kvcache-migration-reject-threshold