源码解读与实验设想缓存与调度

等待队列里的热前缀:从 LRU 刷新到 Prefill 负载均衡

读 SGLang #41593,把排队期间的缓存保留与多 P 实例的负载观测放进同一个待验证问题

我近期想在生产场景里尝试 SGLang #41593 的等待队列前缀刷新,并继续看多个 Prefill 实例之间的 cache TPS 如何均衡。现在先把机制、指标口径和可证伪的验证方案写清楚:排队时命中的前缀,未必活到真正 admission;保住更多命中,也未必让集群负载更均衡。本文是 AI 辅助的研究记录,生产实验尚未开展。

1. 请求还在等待,为什么已有前缀也值得刷新?

在 coding-agent 一类多轮工作负载里,请求常常重用系统提示、工具定义、项目上下文和之前的对话。一个请求进入等待队列时,可能已经找到较长的缓存前缀;等它终于获得 prefill 预算,这段前缀却可能被其他活动挤出 GPU。仅在入口记录一次命中率,会漏掉这段排队时间造成的损失。

我关心的是这条时间链:请求排队多久,入队时可复用的 token 有多少,admission 重匹配时还剩多少,丢失的部分最终从哪层恢复,还是重新计算。L1 GPU、L2 host 和 L3 外部存储是本文的分析分层;具体部署必须以实际后端与指标定义为准,不能假设所有层都已启用。

SGLang #41593 尝试把等待中的近期需求送回 LRU:在计算调度优先级的路径上,对等待队列前部的有界窗口刷新已有 device prefix 的访问状态。固定阅读的 head 是 6767a7a31cbf;截至 2026-10-08 03:30 UTC,该 PR 仍 open,尚未合并。它为普通 LRU 增加的是“很快可能会使用”的信号,而不是对未来命中的保证。

这个信号有明确代价。等待中的热点可能因此占住更久,其他请求的缓存被更早驱逐;队列持续很深时,刷新消耗的 scheduler CPU 也可能抵消收益。因此平均命中率上升不够,需要同时看冷请求公平性、可驱逐容量和 TTFT 长尾。没有缓存抖动、命中本来就能存活的场景,也完全可能没有收益。

这里还要区分两条相关研究线。CacheWise(arXiv:2606.16824)面向 coding-agent 的复用特征,把工具元数据、复用感知淘汰和前缀感知调度放在实例内讨论;CacheRoute(arXiv:2608.19677)讨论跨实例的 prefix-affinity 路由计划。前者帮助思考“留下什么、先服务谁”,后者帮助思考“送往哪里”。#41593 是更局部的队列期 LRU 刷新,不能据此宣称已经实现任一论文系统。

2. refresh、临时锁和持久锁分别保护哪个阶段?

新增分支位于 cache-agnostic active policy、非 decode worker 且未走既有 fast-match 分支的路径,还受 专用环境开关 与 supports_prefix_sharing() 条件约束。该提交默认启用此开关;关闭只停用新增分支,不改变既有 fast-match 行为。满足条件时只看当前 waiting_queue 的前 128 个请求,并逆序刷新这个窗口。这样当前队首的请求最后被 touch,在这次刷新中获得更新的访问顺序。刷新发生在后续部分策略的排序之前,因此“更接近 admission”适合按当前队列顺序工作的普通 FCFS,不能推广成所有 policy 最终的接纳顺序。128 是这一提交的具体常量,不代表所有负载的最优值;它也不是把整个等待队列长期 pin 住。

调用从 refresh_waiting_prefix 进入 refresh_device_prefix,更新匹配节点的 last_access_time 或相应 LRU 顺序。专用 refresh 路径不新建请求级锁,不改写请求的匹配结果字段,不触发 KV load,也不发起外部存储查找。但本地树匹配仍可能拆分 radix 节点、读取现有 host-backed 元数据,并维护已有 pending-offload 的 bookkeeping。因此准确说法是“绕开申请请求资源、加载 KV 和外部查询的通用匹配流程”;不能把它写成只改一个时间戳、绝对无副作用,或连 host/offload 元数据都完全不碰。

刷新和 admission 分属两个阶段。scheduler 真正尝试接纳请求时仍先重新匹配,然后进入 add_one_req。评估资源和预算时,用临时 lock 保护本次正在考虑的 prefix,避免评估与分配过程中它又变成可驱逐对象。只有走到提交接纳的路径,才获取由 req.lock 持有的持久锁;临时锁无论成功或未接纳都会在退出时释放;常规路径中的持久锁在资源释放时归还,streaming session 等资源归属则另循既有生命周期。临时锁和持久锁是既有 admission 生命周期的一部分,不是 #41593 给等待请求新增的机制。

锁的影响要从 allocator 和 cache 的计数看:锁定节点会改变 protected 与 evictable 的边界。假如把深队列的很多大前缀都长期锁住,留给新 token 分配的可回收空间可能不足,接纳可能停滞。这也是作者列出的替代方案风险。但“任何锁都会死锁”并不成立:是否还能推进,取决于已有活动请求何时释放、可回收容量、请求长度、chunked prefill 和当前预算检查。这里应诊断容量压力与调度停滞,不能给所有配置下普遍死锁的结论。

同样,cache 命中多不等于一次 batch 的成本为零。命中的 prefix 可以减少待计算的 token,但 chunk token budget、运行请求数、内存空间及后续 P→D 传输仍各有约束。要检查代码在哪个分支返回、哪些预算包含命中 token,不能只看一个 hit ratio 推导系统吞吐。

证据也要按版本分开。作者报告过早期端到端收益,但那轮使用的是通用 match_prefix_for_req 实现;当前 head 改为专用 refresh 路径,作者明确尚未重跑对应 E2E。当前 Base CI run 37454554618 成功,同 head 仍有 AMD、NPU、Extra 的失败记录尚未在本文归因。可以说该机制有代码与测试依据;不能说当前 head 已获本文复现、所有 CI 通过或生产收益已确认。

SGLANG_ENABLE_WAITING_PREFIX_REFRESH=0
# 仅关闭新增 refresh 分支;既有 fast-match 行为不变
# 配置生效时机须在隔离环境验证
图 1 · 等待期软保留、admission 锁生命周期与多 P 指标链;概念示意,箭头不代表实测耗时
图 1 · 等待期软保留、admission 锁生命周期与多 P 指标链;概念示意,箭头不代表实测耗时

支持横向滑动查看

3. 每个 P 的 cache TPS 怎样定义,又怎样验证负载均衡?

我接下来想看的“每个 P 的 cache TPS”,首先需要分清观测吞吐和服务能力。本文把 cached-token throughput 定义为:同一时间窗口内,该 P 在 prefill 中实际使用的有效复用输入 token 总数,除以窗口秒数。按每个请求首次实际复用的执行事件归入窗口,恢复失败、执行前取消与仅完成 admission 的预估值不计成功复用。token 按请求计,多个请求重用同一物理块可以分别计入;同一请求因重试、retraction 或多次探测产生的重复记录必须剔除。它不是独立缓存块数量、缓存占用量,也不是等待队列中预计命中的 token。

这个定义还要带来源标签。至少分别记 admission 时的 L1 resident token、从 L2 成功恢复后使用的 token、从 L3 成功恢复后使用的 token,以及最后重新计算的 token。来源计数按一次实际使用的最终来源互斥划分,避免 L3→L2→L1 的同一 token 被加三遍;传输字节则按每一条物理链路分别计;恢复与计算的实际资源成本保留 retry/retraction 的额外工作量,不能随业务去重一起抹掉。时间窗口可先用固定 30 秒作为实验参数,并同时保留逐请求追踪与更长窗口,避免突发被平均值掩盖。

如果 cache TPS 指“该实例能承受多少缓存命中请求”,那是服务能力,需要在给定模型、硬件、token 长度组合、batch、恢复带宽和 TTFT SLO 下单独校准。观测到 10 万 cached token/s,并不能证明它有 10 万 token/s 的可用余量。不同实例的额定容量不同时,直接拉齐原始 TPS 更没有意义,应比较归一化的负载与可用余量。

一个简单反例是:P-A 处理大量 L1 热前缀,几乎只计算短后缀;P-B 的 cached TPS 与它相同,却需要从 L3 拉回很大状态,或者同时计算更长的 uncached 后缀。两者的 GPU、PCIe/RDMA、存储队列和 TTFT 都可能完全不同。相反,把同一个热前缀拆到很多 P 上,虽然 cached TPS 看上去更均匀,却可能增加副本和冷启动成本。

因此路由目标应先由 SLO 和约束决定。可以提出一个待校准的预测:某 P 的预计完成首 token 时间,由已有队列等待、必要恢复、剩余 prefill 计算以及 P→D 交接等关键路径共同决定。恢复与计算可能重叠,不能简单把所有阶段耗时相加。cached TPS 用来解释复用收益,queue、uncached compute、恢复带宽、内存压力与 D 侧反压用于判断负载;不把“每个 P 的 cached TPS 一样”设成唯一优化目标。

第一步先做 shadow 观测:保持生产策略不变,只记录脱敏的请求标识、长度与 prefix fingerprint,不采集原始提示文本;对齐 route、enqueue、admission、restore complete、prefill complete 和 first token 的事件时钟。观测应复用既有事件,或使用经验证不触碰 recency、锁和外部 IO 的只读路径;不能为测量新增普通 match_prefix 探针,因为探针本身可能刷新访问顺序、拆树或发起外部查询,污染对照。按同一请求关联入队命中与 admission 命中,再按 prefix 热度、队列深度、请求长度和实例分桶。此处是未来实验方案,本文没有接入或修改任何生产系统。

第二步用隔离 replay 拆开因果。固定模型、KV 格式、到达时间、请求长度、缓存初始状态、L1/L2/L3 容量与传输约束,先仅比较 refresh off/on,路由保持不变;再在同一留存策略下比较既有路由、仅 prefix-affinity、affinity 加负载约束。数据若只有 token 数与 fingerprint,只能验证队列与缓存模型,不能冒充真实 GPU 或存储性能。需要真实硬件回放时,另行获得资源与数据使用授权。

第三步才考虑小流量 canary。应先在 replay 排除错误、停滞与公平性退化,冻结回退配置,选择可隔离的 P 池,并给热点与冷请求都保留对照。跨实例共享缓存或相同热前缀会污染逐请求随机 A/B,需要按池或时段设计对照并检查流量漂移。扩量门槛与回滚阈值应依据现有 SLO 预先填写;本文不捏造当前生产阈值,也不承诺上线时间。

至少预登记三类停止条件:错误、KV 不一致、OOM 或无进展立即停止;p95/p99 TTFT、冷请求等待/超时、scheduler CPU、恢复链路占用或内存水位超出约定预算则回滚;有命中率收益却无端到端收益时停止扩量并查明关键路径。回滚顺序是撤销新路由决策、关闭 refresh 试验开关并恢复已验证配置;具体控制点须先在隔离环境演练,不能假设当前 PR 已提供可热切换的生产开关。

可证伪标准要比“看起来更快”更严格。若在预先选定的缓存抖动与排队条件下,刷新并未减少入队到 admission 的命中损失,机制假设就缺少支持;若命中损失减少但恢复/计算成本与 TTFT 无改善,收益没有传到服务路径。对负载均衡,若只降低 cached TPS 的离散程度,却恶化最忙实例的 SLO、冷请求公平性或集群总 goodput,就应否定把 TPS 拉齐作为目标的策略。这里的 goodput 指满足约定延迟与正确性要求的有效请求量,不能混成所有完成请求的吞吐。

  • 请求链:queue wait → enqueue/admission hit token 差值 → 最终恢复来源 → uncached token compute → p95/p99 TTFT;报告整体与热/冷分组
  • 每层恢复:成功恢复字节、耗时、有效带宽、失败与取消;L3→L2 和 L2→L1 物理链路分别计,避免重复算业务复用量
  • 资源和公平性:GPU prefill 活跃时间、P→D 字节与等待、queue age、evictable/protected/free tokens、OOM、scheduler 刷新耗时及冷请求超时
  • 集群结果:SLO goodput、每 P 归一化资源负载与最差实例、cache TPS 的分布;控制相同 offered load,并说明因排队导致的未完成请求
cached_tps[p, W] = Σ effective_reused_input_tokens(request, p) / |W|
uncached_tps[p, W] = Σ actually_computed_input_tokens(request, p) / |W|
net_l1_hit_loss(request) = l1_resident_matched_tokens_at_enqueue - l1_resident_matched_tokens_at_admission
两端须固定同一 key、长度、对齐和有效性条件。差值可为负,且可能掩盖同时发生的驱逐与新填充。
精确保留率须追踪入队命中的原逻辑 token/page 区间;标量差仅是净命中损失,不是驱逐计数。

主要结论

  • 1.等待期 refresh 提供有界的近期需求信号,影响 LRU;admission 的临时保护与 req.lock 的持久保护仍有各自生命周期。
  • 2.缓存复用要沿请求追到 admission 和实际使用;L1 命中、跨层恢复与重新计算对资源的影响不同。
  • 3.多 P 负载均衡应优化约束下的 SLO 与 goodput。cache TPS 是一项有用观测,只有定义分子、窗口和来源后才可比较。
  • 4.近期关注顺序是先观测和隔离回放,再评估获授权的小流量 canary。本文只有阅读与实验设计,没有已完成的生产验证。

待验证问题

  • 固定 128 个请求的窗口在长短请求混合与热点突发时是否合适?是否应按预计近期接纳成本设预算,而不是请求数?
  • 刷新收益发生在多长的 queue wait 与多高的 L1 压力区间?冷前缀被挤出的代价是否抵消了热前缀收益?
  • 路由器掌握的 prefix 位置与 worker admission 时的真实命中之间,有多大的滞后?如何降低热点迁移时的来回摆动?
  • 同样的 cached TPS 下,是 uncached compute、L2/L3 restore、P→D 传输还是 D 侧反压决定 TTFT?容量估计多久需要重校准?

来源与核验边界

  1. [1] SGLang #41593:当前实现、作者 E2E 版本说明与替代方案
  2. [2] 固定 head:等待窗口刷新与 admission 预算及锁生命周期
  3. [3] 固定 head:scheduler 在 admission 前重新匹配
  4. [4] 固定 head:RadixCache 的 device-prefix refresh 与锁计数
  5. [5] 固定 head:UnifiedRadixCache 专用刷新与本地树维护
  6. [6] 固定 head:BasePrefixCache lock 接口
  7. [7] 固定 head:请求资源释放路径
  8. [8] Base CI 37454554618:单个成功 run,不代表全套 CI
  9. [9] CacheWise:实例内复用感知淘汰与前缀调度
  10. [10] CacheRoute:跨实例 prefix-affinity 路由计划

AI 辅助研究记录,来源核验于 2026-10-08 03:30 UTC。固定 SGLang PR #41593 head 6767a7a31cbfe165ff71a89d9d6c75d8979e021c,PR 未合并。作者早期 E2E 观察不等于当前专用 refresh 实现的重跑结果;本文未独立复现,未实施生产变更,未测量或宣称个人生产收益。cache TPS 口径、成本模型、shadow/replay/canary 与回滚门槛均为后续待校准的实验设计。

关联笔记