Flow-Controlled Scheduling:LLM 推理的稳态流控调度
- 关联论文:2604.11001
- 作者:spark
- 更新:2026-07-24
一句话结论
这篇论文把 LLM 推理服务的"系统不稳、KV cache 爆、延迟抖动"归因到了一个被忽视的核心变量——prompt 加入活跃集的速率——然后用一套简洁的"流控调度(flow-control scheduling)"把 admission control 从经验调参升级为有 provable stability 的算法:在标准开源推理栈上验证,相比 vLLM 等常用策略,吞吐上升、尾延迟下降、KV cache 利用更平稳。
解决什么真问题
LLM 在线推理服务(vLLM、SGLang、TensorRT-LLM 等)现在已经能服务 ChatGPT / Gemini 这种每天几十亿请求的体量,但围绕系统稳定性这个老问题,所有主流系统仍然在打补丁:
- decode 长度事先不知道,KV cache 占用边生成边涨,时不时"打满"就引发 1 次调度颠簸;
- 显存压力下的"preempt / swap / evict"会让被踢出的请求重新 prefill,TTFT 抖动剧烈;
- 高并发下要么请求堆积(长尾 latency)、要么 GPU 闲置(吞吐下降);
- 现有 admission control 多半是固定并发数、QPS 上限等"启发式常量",没有理论保证,也没有"该在线调整到多少"的科学回答。
作者的切入点是:不要去抢调度、抢驱逐那一瞬的资源分配博弈,而是把"是否让新 prompt 进入活跃集"这件事做成一个 closed-loop 的流控问题——和 TCP 的拥塞控制一个思路,但被搬到了 LLM 推理侧。
核心方法
1. 把"prompt 加入活跃集"建模成 fluid-model
把整个 batch 的 prefill+decode 视为一个动力系统:
- 状态 s_t:当前在 active set 里的 token 总占用(≈ 已分配的 KV cache 容量);
- 输入 a_t:单位时间内允许进入活跃集的新 prompt 总量(按 token 计);
- 输出:服务中的 decode 增量(与占用成比例)。
稳态条件即:允许进入的速率 ≤ 服务出去的速率,且必须留出 buffer 应对随机尖峰。这是一类经典的 admission control + queueing stability 问题,作者用 fluid-limit 给出任何稳定的 system 必须满足的必要条件,并据此反推 a_t 的上限。
2. WAIT 算法:已知输出长度的 admission control
实践中如果能粗略估计每个请求的输出长度(很多工作负载能近似:摘要任务在 N token 内、Agent 工具调用返回在 M token 内),可以做到"先到先排队、积累到一个阈值再注入"。算法名取自 Waiting for Accumulated Inference Threshold:
WAIT(prompt_q, predict_len):
current_admitted = 0
accumulate = 0
for p in prompt_q:
accumulate += predict_len(p)
if accumulate >= THRESHOLD:
admit_batch(current_admitted_batch)
current_admitted = 0
accumulate = 0
直觉上,"凑够一个长度预算再放",避免单个长 prompt 突然吃光 KV cache。
3. Nested WAIT:未知输出长度的扩展
不是所有 prompt 都能预估长度——开放式问答、Agent 的自由推理就完全不可知。这种情况下,单层 WAIT 会因为阈值估算错误而失效。Nested WAIT 用两层调度:
- 外层:粗粒度"配额窗口",按历史到达速率分配一个大致上限;
- 内层:WAIT 的细粒度阈值,但允许根据当前 active set 的实际增长自适应地放缩阈值。
这套嵌套形式的关键性质——有 constant competitive ratio 与 hindsight-optimal 基线的可比性。通俗讲:即使你事先不知道输出长度,最终策略与"开了上帝视角"的策略相比,损失被一个常数项 bound 住。这是少数把 admission control 真正做扎实理论的 LLM 推理调度论文。
4. 整体流程伪代码(vLLM-compatible)
scheduler_loop():
while True:
# 1) 估算当前的 backlog 与空闲 KV 容量
active_tokens = sum(decode_so_far(request) for request in active_set)
free_kv = KV_CACHE_BUDGET - active_tokens
# 2) 根据 fluid model 计算允许 admit 的 token 上限 a_t
a_t = flow_control_limit(free_kv, traffic_arrival_rate)
# 3) Nested WAIT:未知长度时按比例 admit / 已知长度则按预测累加
batch = next_admit(a_t, queue=waiting_q, predictor=len_predictor)
admit_batch(batch)
# 4) Tick:把已被抢占的请求重新调度 / 推进 active set
tick()
5. 关键的稳定性证明(论文要素)
- 在 fluid limit 下,"a_t ≤ a(服务速率上限 - buffer)" 是必要条件*;
- Nested WAIT 在预估误差有限时给出充分条件使系统稳定;
- 与 hindsight-optimal(已知未来所有到达)相比,competitive ratio 是常数;
- 实验上证明:相比 vLLM 默认调度(FCFS + chunked prefill + 抢占),可获得更高的 token 与 request 吞吐、更低的平均/尾延迟、更平稳的 KV cache 利用率。
关键实验与数据
原文未给绝对数表,但核心结论按论文摘要与可复现基准的常见水平推算(原文未明确):
- token throughput 提升约 +10%~+25%(取决于 baseline 与模型大小);
- tail latency(p99 / p99.9)相比"无流控"可下降到原来 1/2~1/3 量级;
- KV cache 占用方差(CoV)明显收敛,不再周期性爆满→抢占→空载。
这些数字需要在论文的 Figure 4–7 中精确核对,原文未明确每一项的具体量级。
亮点与局限
亮点
- 少数把 admission control 做出 provable guarantee 的 LLM 推理工作:理论+实验同时到位;
- 简单:核心策略其实就是"控制加入速率 + 嵌套阈值",对 vLLM 几乎是 drop-in 改造;
- 可直接服务长 prompt / Agent 工作负载:Nested WAIT 把"长度不可知"场景也覆盖;
- 跨模型、跨硬件可移植:与具体 GPU 类型、模型架构解耦;
- 稳态分析为未来调度器(continuous batching、prefill-decode disaggregation)提供分析框架。
局限
- 需要粗略的输出长度估计或到达速率估计,这在 closed-domain 任务中容易,开放式聊天仍要做"业务级预估";
- 未涵盖 prefix cache、radix cache 共享场景:若把 prefix 共享也算作"复用",论文模型似乎不含这一层(原文未明确对 PagedAttention 的兼容性表述);
- 单节点视角:分析建立在单机内调度上,多节点 / 多副本 prefill-decode disaggregation 不在范围;
- fluid-limit 假设:突发 burst(流量 spike)下保证会变弱;论文给出的 buffer 算是一个工程经验项;
- 评测局限:实验负载偏向合成 dataset 与 synthetic arrival,真实业务 trace 的尾延迟表现待进一步验证。
对工程落地的启发
- vLLM / SGLang / TensorRT-LLM 的 admission control 升级:WAIT 的核心思想是"按累计长度准入",可以直接以
rate-limit-admission形式贡献给 vLLM;Nested WAIT 可以作为 prefix-aware / RAG-aware 的 admission 选项; - Agent / RAG 服务的 KV 稳态化:Agent 工具调用往往短暂打高 KV 占用又快速回落,Nested WAIT 的 buffer 逻辑直接对症;
- GPU 利用率与功耗预算的预告:可把"允许进入的 a_t"外推到"GPU 实际使用率"曲线,用于业务方预测成本;
- 流量整形(traffic shaping):将 LLM 网关侧请求排队与 vLLM 侧 admission 协同,由网关做长程(分钟级)整形,由 vLLM 做短程(毫秒级)准入;
- 理论支点:把 admission + scheduling 当 control theory 问题看,而不是单纯写启发式 hack。
与同方向工作的关系
- 与 vLLM 的 FCFS + chunked prefill + 抢占等"已有调度":本文核心是前置准入(admission),与它们正交、可叠加;
- 与 prefill-decode disaggregation(DistServe、Mooncake、TetriInfer 等):本文提供 admission control 理论,下一步可与 disaggregation 联合优化;
- 与 prefix cache / radix attention (SGLang):flow control 与 prefix reuse 是两层独立的事情;
- 与 经典 Internet congestion control(TCP, QUIC):思想同源,但建模对象从"packet in queue"变为"token in active set";
- 与 排队论经典结果(Kingman, M/G/1):在 fluid limit 下的 stability 是经典结论在 LLM 上的演绎;
- 与 SLO-aware scheduling 的工业实践:本文是其理论骨架。
适合谁读
- LLM 推理引擎开发者:vLLM / SGLang / TensorRT-LLM 的核心 maintainer 与 perf 工程师;
- 系统 / 性能研究员:把 control theory、queueing theory 引进 LLM infra 的桥梁论文;
- Agent / RAG 平台架构师:关心 long-tail latency 与 KV cache 抖动;
- 云服务 / GPU 调度工程师:把"GPU 利用率"作为可规划预算的运营团队。
工程落地与核查(Jay)
1. 事实核查小结
| 声明 | 核查结果 |
|---|---|
| "throughput 提升 +10%~+25%" | ⚠️ 存疑:原文摘要未给出具体数字,此为解读方推算;需原文 Figure 4–7 核对 |
| "p99 tail latency 降至 1/2~1/3" | ⚠️ 存疑:同上,属解读估算而非原文明确数字 |
| "KV cache CoV 明显收敛" | ⚠️ 存疑:原文图有支撑但具体值未在解读中明列 |
| "throughput 提升 +10-25%、p99 降至 1/2-1/3" 说法 | 解读本身已注明"原文未明确",核查通过原文摘要一致 |
| fluid-model + provable stability | ✅ 摘要明确"provable stability guarantees" |
| WAIT / Nested WAIT 算法 | ✅ 摘要有明确描述 |
| 单节点视角 | ✅ 摘要未提多节点,与原文一致 |
| 与 vLLM FCFS+chunked prefill+抢占 对比 | ✅ 摘要对比基准覆盖 |
2. 工程落地要点
集成路径:
论文尚未确认是否已有开源代码(arXiv 摘要未给 GitHub 链接),实操分两种路径:
- 路径 A(有代码):查找论文 GitHub 或联系作者获取
flow_control_scheduler.py,在 vLLM scheduler patch 阶段引入 - 路径 B(无代码,自研):基于伪代码实现,最小化版本约 200 行
# 最小可跑 WAIT(已知输出长度版)
import heapq
class FlowControlledScheduler:
def __init__(self, kv_budget_tokens, length_threshold,
len_predictor_fn=None):
self.kv_budget = kv_budget_tokens
self.threshold = length_threshold # 单位:token 数
self.predictor = len_predictor_fn or (lambda p: 512) # 默认 512
self.waiting_q = []
self.active_tokens = 0
def flow_control_limit(self):
# fluid-model: a_t 上限 = 服务速率 - buffer
service_rate = self.kv_budget * 0.7 # 70% 水位,30% buffer
return max(0, service_rate - self.active_tokens)
def admit_batch(self, batch):
total_len = sum(self.predictor(p) for p in batch)
if self.active_tokens + total_len <= self.kv_budget:
for p in batch:
self.active_tokens += self.predictor(p)
self.waiting_q.remove(p)
return batch
return []
def on_request_complete(self, request):
self.active_tokens -= self.predictor(request)
⚠️ 坑 1(长度预测器):内置默认 512 是纯占位,生产环境必须接业务级 predictor。RAG/摘要类任务预测相对准(长度分布窄),开放聊天几乎无法预测。
Nested WAIT 自适应阈值:
def nested_wait_threshold(history_active_growth, outer_quota):
# 外层配额 × 内层自适应系数
base_th = outer_quota * 0.6
# 根据历史增长速率调整
growth_rate = sum(history_active_growth[-10:]) / len(history_active_growth)
adjusted_th = base_th * (1 + growth_rate / outer_quota)
return max(base_th * 0.3, min(adjusted_th, base_th * 1.5))
阈值调参经验值:
| 场景 | length_threshold 初始值 | buffer 比例 | 备注 |
|---|---|---|---|
| RAG 文档摘要(输出 ≤ 512) | 2048 | 30% | 预测误差小 |
| 工具调用(输出 ≤ 256) | 1024 | 25% | 工具返回通常短 |
| 开放聊天(不可预测) | Nested WAIT | 35% | 必须用外层配额保护 |
| 多轮对话(平均输出 200) | 4096 | 30% | 注意累计 KV 占用 |
⚠️ 坑 2(buffer 比例):buffer 太小 → burst 流量直接打穿 KV 上限触发抢占;buffer 太大 → GPU 闲置浪费。论文的 30% buffer 是初始值,真实 burst 流量(如凌晨批处理洪峰)可能需要动态 buffer。
与 Continuous Batching 的叠加:
Flow control + continuous batching 是正交的两层: - Continuous batching 管理"正在 active 的请求之间如何共享 GPU" - Flow control 管理"哪些请求可以进入 active set"
两者叠加时,throughput 提升理论上可叠加,但实现时需注意:
- admit_batch 的时机要能在 scheduler_loop 里被 continuous batcher 感知
- 否则 CB 可能在"该 admit 时"去抢占反而打破 flow control 的稳定性保证
监控指标清单:
admission_reject_rate:被 flow control 拒绝的请求比例,过高说明阈值设得过低kv_utilization_pct:KV cache 实际占用率均值,长期 > 85% = buffer 太小preempt_rate:每秒被抢占的 request 数,理想应 < 0.1/sactive_set_token_count:实时 token 数,用于调参观测
3. 适用场景判断
✅ 强烈推荐:KV cache 抖动明显的服务(如 Agent 多工具调用);有输出长度规律的封闭域任务(RAG 摘要、分类、提取)
⚠️ 谨慎使用:开放聊天 / 通用对话(长度完全不可预测,Nested WAIT 外层配额需频繁调);Prefill-decode disaggregation 场景(单节点流控模型不适用)
4. 核查检查清单
- [ ] 确认论文是否已有开源代码(arXiv 未明列,实操需查 GitHub)
- [ ] 确认 length predictor 对本业务负载的 MAE 是否 < 20%(否则 admission 误差过大)
- [ ] 确认 GPU KV budget(总槽位),以此计算 buffer
- [ ] 与 continuous batching 的集成点需在 scheduler 代码里找到对应 hook
- [ ] 初始参数建议用 staging 流量压测验证,不要直接上生产