
注:建议在开始学习本专栏前先完成对 推理优化工程师快速入门指南 专栏中所有内容的学习。
LLM Serving 的排队模型
经典排队论的服务台假设是:顾客到达后立即被服务,服务时间彼此独立且与系统状态无关。LLM Serving 的场景------请求按序列生成 token,一个 batch 中的多个请求共享 GPU 计算资源,输出长度在到达时完全未知------恰好系统性破坏这三个假设。因此,这节的任务不是照搬教科书公式,而是建立一套可推导、可测量、能预测拐点的近似模型,让后续的 admission、batching、preemption 决策有统一的数学坐标。
3.1 一切从 Little 定律开始:在重负载下寻找锚点
Little 定律是排队论中极少数不依赖到达过程和分布假设的普适结论,它构成了所有 LLM Serving 排队分析的起点:
L = λ ⋅ W L = \lambda \cdot W L=λ⋅W
其中 L L L 为系统内平均请求数(含正在服务的), λ \lambda λ 为平均到达率, W W W 为单个请求在系统内的平均停留时间(含排队等待和服务时间)。模型可以展开为:
L = λ ⋅ ( W q + W s ) L = \lambda \cdot (W_q + W_s) L=λ⋅(Wq+Ws)
W q W_q Wq 是排队等待时间, W s W_s Ws 是服务时间。对 LLM Serving 而言, W s W_s Ws 包括 prefill(首 token 计算)和 decode(逐 token 生成)的全部时间,而后端框架的调度器(如 vLLM、TensorRT-LLM 的 scheduler)直接控制 W q W_q Wq 和调度间隔。
在系统设计中,Little 定律的价值在于:给定 SLA 目标 W ≤ W m a x W \le W_{max} W≤Wmax,可以反推出系统能承受的最大到达率 λ m a x \lambda_{max} λmax,进而确定需要多少 GPU 实例。例如,目标平均响应时间 2s,每实例承载的并发请求数上限 K K K,则每个实例的排队模型允许的到达率上限为 λ i n s t ≤ K / 2 req/s \lambda_{inst} \le K / 2 \text{ req/s} λinst≤K/2 req/s。实例数 N N N 可估算为:
N ≥ ⌈ λ t o t a l λ i n s t ⌉ N \ge \left\lceil \frac{\lambda_{total}}{\lambda_{inst}} \right\rceil N≥⌈λinstλtotal⌉
Little 定律的前提是系统处于稳态(平均到达率≈平均完成率)。LLM 工作负载的突发性意味着不能直接用瞬时 λ \lambda λ 求 W W W,但用滑动窗口 1 分钟的平滑 λ ˉ \bar{\lambda} λˉ 计算,仍能得到工程上足够准确的容量估计。作为对照,这是调度器中最可靠的锚点------在不知道分布假设的情况下,Little 定律依然成立,它是所有复杂模型出错时的回归基准。
3.2 M/G/1 是基线近似,而非精确模型
经典排队论中最接近 LLM Serving 的可解析模型是 M/G/1(泊松到达、一般服务时间分布、单服务台),其 Pollaczek-Khintchine 公式给出了平均排队等待时间:
W q = λ ⋅ E S 2 2 ( 1 − ρ ) W_q = \frac{\lambda \cdot \mathbb{E}S\^2}{2(1 - \rho)} Wq=2(1−ρ)λ⋅ES2
其中 S S S 是服务时间(随机变量), ρ = λ ⋅ E S \rho = \lambda \cdot \mathbb{E}S ρ=λ⋅ES 为服务强度(utilization), E S 2 \mathbb{E}S\^2 ES2 是服务时间的二阶矩。该公式揭示两个核心事实:排队延迟主要由服务时间方差 驱动,而不是均值。当输出长度分布呈重尾时, E S 2 \mathbb{E}S\^2 ES2 会远大于 E S 2 \mathbb{E}S^2 ES2,导致 W q W_q Wq 急剧上升。
但 M/G/1 有三层局限,使其只能作为基线参考而非定量工具:
- 泊松到达假设不成立。LLM 流量有明显的潮汐性(工作日白天高、深夜低),且 burst 是常态。
- 单服务台假设 不成立。GPU 并行处理多个 batch 中的请求,本质上是 m m m 个服务台并行。
- 最关键的是:服务时间与系统负载相关 。在 M/G/1 中, S S S 是外生变量,不随 ρ \rho ρ 变化。但在 LLM Serving 中,当系统繁忙时,调度器会增大 batch size 来提高吞吐,实际上改变了 E S \mathbb{E}S ES 和 E S 2 \mathbb{E}S\^2 ES2 。GPU 算力固定时,batch 越大,每个请求分得的算力越少、单请求的 token 生成速率下降,服务时间变长。因此 S S S 是 ρ \rho ρ 的函数。
尽管如此,M/G/1 的价值在于它为可视化收益提供了一个清晰的目标:任何调度优化的本质都是降低 E S \mathbb{E}S ES(更好利用 GPU)并抑制 E S 2 \mathbb{E}S\^2 ES2(减少长尾拖累)。
3.3 batching 在模型中的准确位置:service rate 随负载变化
batching 是 LLM Serving 性能的核心杠杆,它的数学本质必须精确表达。设 GPU 的峰值计算吞吐为 T p e a k T_{peak} Tpeak(tokens/s,包含 prefill 和 decode 的混合),在时刻 t t t 的 batch 大小为 b ( t ) b(t) b(t)(该 batch 中所有请求的总 token 数,近似与请求数成正比),则每个请求的有效生成速率为:
r g e n ( t ) = T p e a k b ( t ) r_{gen}(t) = \frac{T_{peak}}{b(t)} rgen(t)=b(t)Tpeak
服务的完成速率 μ ( t ) = r g e n ( t ) ⋅ E 剩余输出长度 − 1 \mu(t) = r_{gen}(t) \cdot \mathbb{E}\\text{剩余输出长度}^{-1} μ(t)=rgen(t)⋅E剩余输出长度−1,即单位时间完成的请求数。更直观的是:batch 大小直接改变 service rate 与服务时间分布。
具体而言,当单条序列单独运行时,生成速率可能为 50 tokens/s;当 batch size 增至 8 时,每条序列的有效速率可能降为 20 tokens/s(GPU 的算术强度提高后总吞吐增加,但每条序列获得的算力份额下降)。最终,整个 GPU 的总吞吐 T G P U ( b ) T_{GPU}(b) TGPU(b) 不是线性的,而是呈边际递减的增长曲线------典型的吞吐-延迟权衡。
定义实测总吞吐 T G P U ( b ) T_{GPU}(b) TGPU(b),并令 α ( b ) = T G P U ( b ) / T p e a k \alpha(b)=T_{GPU}(b)/T_{peak} α(b)=TGPU(b)/Tpeak。通常 α ( 1 ) \alpha(1) α(1) 小于 1,不能同时称其为"相对峰值"又规定 α ( 1 ) = 1 \alpha(1)=1 α(1)=1。若 batch 中各请求每轮各生成一个 token,单请求完成时间还取决于动态离队与调度份额:
E S i ∣ b ≈ E G i ⋅ t i t e r ( b ) , t i t e r ( b ) = b T G P U ( b ) \mathbb{E}S_i\\mid b\approx \mathbb{E}G_i\cdot t_{iter}(b),\qquad t_{iter}(b)=\frac{b}{T_{GPU}(b)} ESi∣b≈EGi⋅titer(b),titer(b)=TGPU(b)b
这个公式的工程含义值得反复品味:当系统负载上升(排队长度增加导致 b b b 增大),单请求服务时间反而变长 。这意味着 ρ ≈ λ ⋅ E S ( ρ ) \rho \approx \lambda \cdot \mathbb{E}S(\\rho) ρ≈λ⋅ES(ρ) 变成了一个非线性方程 。调度的目标可以重述为:在一个批次中组合哪些请求,使得在满足 W S L O W_{SLO} WSLO 的同时让 b b b 尽可能大来摊薄 GPU 开销,又不至于让任一请求超过 latency deadline。
batching 效率应同时报告 Goodput、每轮时间、总 tokens/s 与资源 SOL;"GPU 空闲算力占比"不是 FLOPs 利用率的定义。目标函数也未必凸:shape bucket、kernel dispatch、KV 容量和 SLO indicator 会造成离散与非凸边界。实际做法是对合法 batch/token budget 做离散扫描或在线控制,而不是未经证明地宣称凸优化。
3.4 重尾输出:真正让模型失效的推手
输出长度分布是 LLM 服务时间分布的函数。实际测量表明,GPT 类模型在自由对话场景中的输出长度呈明显的重尾分布(长尾来自代码生成任务、长文摘要、思维链推理)。使用 Pareto 分布近似:
P ( L > x ) ≈ ( x m i n x ) α P(L > x) \approx \left(\frac{x_{min}}{x}\right)^\alpha P(L>x)≈(xxmin)α
其中 α ∈ ( 1 , 2 ) \alpha \in (1, 2) α∈(1,2),均值存在但方差趋于无穷。这直接导致 W q W_q Wq 公式中的 E S 2 → ∞ \mathbb{E}S\^2 \to \infty ES2→∞。在有限样本中,这意味着一次极其长的输出会占据 GPU 资源数分钟,将所有后续请求的排队时间推高几个数量级。
重尾带来的工程后果远不止数值计算:它导致平均延迟不再是可靠的服务质量指标------p99 和 p999 与均值相差可达 10-20 倍。设计调度策略时,必须做到:
- 以 percentile(p95/p99)作为 SLO 约束而非均值。
- 对超长请求施加输出长度上限(token cap)。
- 建立主动截断机制:一旦检测到输出长度超过 p95 分位点,降低其优先级或强制截断。
模型层面, E S 2 \mathbb{E}S\^2 ES2 大到一定程度时,M/G/1 给出的 W q W_q Wq 预测与实际偏差可达 4-5 倍。因此数据驱动的方法成为必要:通过在服务端记录每个请求的 S S S 分布,拟合出 E S \mathbb{E}S ES、 E S 2 \mathbb{E}S\^2 ES2 的滚动估计值,再用 M/G/1 做实时容量预测------这比任何静态模型都可靠。
3.5 饱和拐点:调度器的生死线
将上述因素综合起来,LLM Serving 系统的吞吐-延迟曲线并非线性,而是存在一个清晰的饱和拐点 。在负载较低时,GPU 算力冗余,服务时间主要由单请求的计算量决定,延迟随负载增幅平缓;当负载超过某一阈值 λ s a t \lambda_{sat} λsat 后,排队延迟呈超线性增长,而 batching 带来的 throughput 增益因 E S 2 \mathbb{E}S\^2 ES2 的膨胀而被抵消,系统进入自我强化的恶性循环------延迟飙升导致请求超时重试,重试进一步增加负载。
工作负载从低负载过渡到高负载时,系统的稳定状态出现突变。这一现象在数学上可表述为:在低负载区 ρ ≪ 1 \rho \ll 1 ρ≪1,系统稳定;当 ρ → 1 \rho \to 1 ρ→1 时, W q ∝ 1 1 − ρ W_q \propto \frac{1}{1-\rho} Wq∝1−ρ1 发散。但 batching 效应的存在使实际模型比经典公式更早进入拐点------因为 E S \mathbb{E}S ES 也随 ρ \rho ρ 增大。
精确识别拐点是调度策略的前提条件。实践中依赖两个手段:
- 离线压力测试:在部署前,对目标模型和 GPU 型号做基准测试,记录不同并发度下的 p50/p99 延迟与吞吐,拟合出拐点位置。
- 在线实时监控 :通过指标(GPU 利用率、queue depth、平均 token 生成速率)的组合推断当前负载位置。当
queue depth × 平均服务时间接近 p99 SLO 时发出告警。
饱和拐点的存在决定了无状态调度策略的极限:在不考虑请求特性和 SLO 差异的情况下,所有请求共享同一队列,拐点出现后将集体恶化。这也正是后续引入「SLO 感知调度」的意义所在------通过区分高 SLO 和低 SLO 任务、有差别的 admission control,把硬性 SLO 请求隔离在拐点之前,软性任务允许超时重试。
3.6 小结:模型的边界与可操作空间
至此建立了一个三层近似模型------第 1 层是 Little 定律作为总容量锚点,第 2 层是 M/G/1 作为延迟预测基线(必须用真实分布参数校准),第 3 层是 batching 对 service rate 的耦合修正 。重尾特性决定了 SLO 必须用百分位数据表达。饱和拐点意味着任何调度算法都必须能够在负载逼近拐点前做出反应------要么拒绝新请求(admission control),要么提前降低低优先级请求的资源占用,要么主动终止超长请求以拉低 E S 2 \mathbb{E}S\^2 ES2。
有了这个模型,我们才可计算一个调度的关键指标:在给定到达率分布、给定模型输出长度分布和 GPU 型号下,系统在不违反 p99 SLO 前提下的最大吞吐。下一节将进入这个优化问题的在线形态------当请求长度无法预知时,如何动态决定哪些请求进入下一个 batch 以及如何配置 batch 大小------这正是以 SLO 为约束的在线调度问题的核心框架。
Token Budget 与准入控制
上一节的排队模型回答了"系统什么时候会过载"------当到达率逼近服务率、且服务时间方差随负载放大时,响应时间曲线将出现拐点。但模型本身并不回答"谁可以被放进系统"。这一节把问题推向决策层:给定一个正在运行的 GPU 集群,每个请求在到达时如何被接受或拒绝,才能让吞吐与延迟同时可预期?
答案的核心是一个词:预算(Budget)。GPU 的显存、算力和 KV 缓存空间都是有限资源,准入控制本质上是一个**资源承诺(Commitment)**问题------系统必须在请求到达时就决定:我是否愿意为这个请求的未来行为承担资源开销?
4.1 max_num_batched_tokens:算力的硬上限
LLM Serving 中有一个核心引擎参数:max_num_batched_tokens。它定义了在一次 forward pass 中,所有活跃请求的 token 总数上限。这个参数直接决定了单次迭代的计算量------因为 Transformer 的注意力计算量大致与 batch 内 token 总数成正比。
python
# 伪代码:一次 decode 迭代的约束
def can_accept_new_request(current_tokens: int,
new_request_tokens: int,
max_batched: int = 4096) -> bool:
"""判断是否可以将新请求加入下一轮迭代"""
# current_tokens: 当前 batch 中所有请求的已生成 token 数之和
# new_request_tokens: 新请求的预估出口长度(通常用 max_tokens 保守估计)
return current_tokens + new_request_tokens <= max_batched
这个参数至关重要,因为它将排队论中的"服务能力"具体化为一个可调控的数值。过大的 max_num_batched_tokens 意味着单次迭代的延迟(time-to-first-token)会升高;过小则意味着 GPU 利用率不足。vLLM 和 TensorRT-LLM 等主流推理引擎都将其作为核心调优参数,原因就在于它直接决定了吞吐与延迟的平衡点。
排队论视角下,这个参数扮演的是"服务台容量"的角色------它限制了每个服务周期内能处理的工作量上限,从而控制了队列的爆发性增长。
4.2 显存承诺:KV Cache 的预约制
第二个预算维度是显存,尤其是 KV cache 的占用。每个请求的 KV cache 大小与两个因素成正比:序列长度和模型层数。一个 7B 模型的 GQA 配置中,每个 token 大约需要 0.5--2 MB 的 KV 空间(取决于精度和层数);而 70B 模型则可能达到 4--8 MB/token。
问题在于:到达时请求的最终长度未知。因此,准入控制必须做出一个保守的承诺------"我为这个请求预留最大可能长度的 KV 空间"。这个承诺的代价是:如果请求实际很短,预留的空间就是浪费;如果预留不足,请求可能在运行中途因显存耗尽而失败。
业界的主流做法是按最大输出长度(max_tokens)做显存预留。这本质上是一种最坏情况下的资源承诺。它的代价是显存碎片化和利用率下降,但换来了确定性的安全边界。
┌─────────────────────────────────────────────────────┐
│ 显存分配策略对比 │
├─────────────────┬─────────────────┬─────────────────┤
│ 按需增长 │ 按 max_tokens │ 按预测长度 │
│ (动态分配) │ (静态预留) │ (预测+修正) │
├─────────────────┼─────────────────┼─────────────────┤
│ 利用率高 │ 利用率低 │ 中等 │
│ OOM 风险高 │ 零 OOM 风险 │ 需要预测模型 │
│ 实现简单 │ 实现最简单 │ 实现复杂 │
└─────────────────┴─────────────────┴─────────────────┘
4.3 过量订阅:接受不确定性的经济学
如果严格按最坏情况做资源承诺,那么 GPU 的利用率将被"短请求"拖垮。现实中,大多数请求的实际输出长度远小于其 max_tokens。因此,现代推理系统引入了过量订阅(Oversubscription)机制------允许 admitted 请求的总承诺资源超过物理资源上限,但通过运行时调度来动态调节。
这个思想与云计算的超卖(overcommitment)一脉相承,但有一个关键区别:LLM 的 token 生成是计算密集且实时性要求极高的。过量订阅的收益是在统计意义上提升吞吐,风险则是在极端情况下(所有请求同时输出到最长长度)导致 OOM 或严重的 queueing delay。
过量订阅的数学基础是:如果请求的实际输出长度服从某个分布(实践中近似对数正态),那么系统可以基于历史统计来估计"同时处于峰值消耗状态的请求比例",并据此设定一个安全的超卖倍数。例如,若历史数据显示只有 10% 的请求会真的到达 max_tokens 长度,那么 1.5--2 倍的超卖通常是安全的。
但安全的前提是系统必须有优雅的降级路径------这正是第 5 节要展开的 preemption 与 abort 策略。
4.4 早拒绝优于 OOM:失败的代价不对称
现在考虑一个关键的设计原则。当系统资源紧张时,有两个选择:
- 准入阶段拒绝(admission rejection):请求到达时拒绝,客户端立即收到明确的错误码。
- 运行期失败(runtime OOM):请求已开始生成,运行到一半因显存耗尽而崩溃。
前者的代价是确定性的、可预期的 :一个 HTTP 4xx/5xx 响应,客户端可以立刻选择重试或降级。后者的代价是不可预测且往往是灾难性的:已生成的 token 全部丢失,客户端需要重新发起请求,且系统的整体状态可能已损坏。
这正是"早拒绝优于 OOM"的经济学直觉:失败的时间点越早,损失越小。用排队论的术语说,早拒绝发生在队列入口,而 OOM 发生在服务过程中------后者不仅浪费了已投入的计算资源,还破坏了系统内部的稳定性。
早拒绝 vs 运行期失败
──────────────────────────────────────────────────────
准入检查点 运行期
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 请求到达 │───▶│ 准入控制 │───▶│ 执行中 │
└──────────┘ └──────────┘ └──────────┘
│ │ │
│ ▼ ▼
│ 明确的拒绝 OOM / 超时
│ (低代价) (高代价)
▼
客户端立即重试/降级
这种不对称的代价结构决定了准入控制器的设计哲学:宁可保守地拒绝一个可能成功的请求,也不冒险接受一个可能拖垮系统的请求。
4.5 优先级租户:多租户下的资源隔离
当多个租户共享同一个 GPU 集群时,准入控制还必须考虑优先级。SLO 是分级别的:交互式应用(如 ChatGPT)要求 p95 延迟 < 2 秒;批处理任务(如离线摘要)允许更长的等待时间。这两类请求不能简单地用同一个 FCFS 队列。
**优先级租户(Priority Tenants)**机制将资源的承诺权按租户级别差异化:
| 租户级别 | SLO 类型 | 准入策略 | 超卖倍数 |
|---|---|---|---|
| 交互式 | p95 < 2s | 严格准入,预留容量 | 1.0(不超卖) |
| 标准型 | p95 < 10s | 中等准入,可超卖至 1.3x | 1.3 |
| 批处理 | 无严格 SLO | 宽松准入,依赖重试 | 1.5--2.0 |
实现上,准入控制器维护每个租户的已承诺资源量,将其与租户的**配额(quota)**比较,只有当请求需要的资源+已承诺量不超过配额时,才允许进入。这样,一个租户的突发流量不会挤占其他租户的 SLO 保障。
这种机制的排队论本质是:多类顾客、多服务台、优先级服务规则。在 M/G/1 模型的框架下,这表现为每个优先级类别有独立的等待时间分布------高优先级队列获得的服务优先权越高,其尾部延迟(tail latency)越有保障。
4.6 小结:准入控制是系统的"闸门"
本节建立了一个三层预算模型:算力预算 (max_num_batched_tokens)控制单位时间内的服务能力上限;显存预算 (KV 容量预留)控制并发请求的内存承诺;SLO 预算(优先级租户的延迟容忍度)控制不同流量类型的服务质量差异。这三层预算通过准入控制器统一执行,形成一个"闸门"------决定哪些请求进入系统、以什么代价进入系统。
闸门放行之后,问题并没有结束。请求进入了系统,但运行过程中可能发现资源比预想更紧------超卖的后果开始显现。这就引出了下一层次的决策:当系统已经超载时,谁能存活、谁被牺牲、如何切换? 这正是 preemption 策略的舞台。
抢占、重算与交换的代价
当准入控制放行了一个请求,系统便与之建立了资源承诺。但承诺可以被打破------当高优先级请求到达、或 batch 的 SLO slack 消耗殆尽时,系统必须从已在执行的请求中回收资源 。回收的方式有四种:abort (丢弃)、recompute (杀掉后从头重算)、CPU swap (将 KV 换到内存)、KV migration(迁移到另一台 GPU)。本节回答一个工程决策问题:在什么条件下,哪种回收方式代价最低?
5.1 已生成长度:一切代价函数的自变量
四种回收方式的代价,全部是已生成长度 的函数。设请求当前已生成 k k k 个 token,预计总长为 L L L,则剩余需生成的 token 数为 r = L − k r = L - k r=L−k。四种方式的代价可统一表达为:
| 回收方式 | 付出的代价 | 收益(释放的资源) | 代价与 k k k 的关系 |
|---|---|---|---|
| abort | 丢弃 k k k 个 token 的全部计算;若客户端可重试则需端到端重来 | KV 全部释放 | 代价随 k k k 线性增长 |
| recompute | 丢弃 k k k 个 token 的 KV,但保留 prompt 的 prefill 结果(若缓存命中) | KV 全部释放,但需重新执行 decode | 代价随 k k k 线性增长,但通常比 abort 便宜一个 prefill 的开销 |
| CPU swap | 将 k k k 个 token 的 KV 拷贝到主存,后续需拷贝回 GPU | 仅释放显存,不释放计算 | 代价随 k k k 近似线性,但受 PCIe 带宽约束 |
| KV migration | 将 k k k 个 token 的 KV 经 RDMA 拷贝到另一台 GPU | 释放本机显存和算力,但目标机需有富余容量 | 代价随 k k k 线性,受 RDMA 带宽和远端队列深度约束 |
核心结论 :当 k k k 很小(比如 k < 50 k < 50 k<50),abort 或 recompute 几乎免费;当 k k k 很大(比如 k > 1000 k > 1000 k>1000),swap 或 migration 更划算------因为重新生成 1000 个 token 的 GPU 时间远超把这些 KV 搬走的传输时间。这也意味着,抢占决策必须发生在请求生命周期的早期,一旦请求运行超过某个阈值,任何形式的回收都变得昂贵。
5.2 PCIe/RDMA 的物理成本:带宽不是唯一约束
CPU swap 走 PCIe,KV migration 走 RDMA。两者的带宽差异是量级的:PCIe 4.0 x16 的理论带宽约 32 GB/s,实际可用约 25 GB/s;RDMA(InfiniBand HDR)单链路可达 200 Gb/s(约 25 GB/s),多链路聚合后通常翻倍。但带宽不是唯一约束。
Swap 的真正瓶颈是延迟。一次 swap 需要经历:显存 KV 序列化 → DMA 拷贝到主机内存 → 目标请求被重新调度 → 需要时再拷贝回去。这期间的拷贝延迟取决于 KV 大小。一个 2048 上下文长度的请求,KV 显存占用约为:
KV 大小 ≈ 2 (K和V) × 层数 × 头数 × 头维度 × 上下文长度 × 精度字节数
≈ 2 × 32 × 8 × 128 × 2048 × 2 ≈ 268 MB
在 25 GB/s 的 PCIe 下,一次 swap 的纯传输时间约 10 ms。但别忘了:swap 期间 GPU 算力完全闲置 ,而 GPU 一秒钟能生成的 token 数(decode 吞吐)通常为几百到数千。10 ms 的闲置意味着损失了约 10-50 个 token 的生成机会。因此 swap 的机会成本 不是传输带宽,而是 GPU 空闲时间的吞吐损失。
KV migration 的延迟更低:RDMA 的端到端延迟通常在微秒到几十微秒级别(取决于报文大小和队列深度),更重要的是,migration 可以在请求运行时流式传输 (先传已生成的 KV,边生成边传新的),从而将传输延迟与计算重叠。这条差异让 migration 在延迟敏感场景下往往优于 swap,但前提是目标节点有足够的空闲显存和算力------这是一个分布式资源匹配问题。
5.3 未来服务时间估计:贪婪策略为何失败
如果只比较已生成长度 k k k,一个简单的策略是: k k k 小时 abort, k k k 大时 swap。这个策略忽略了未来服务时间 。设请求的剩余生成时间为 t r e m t_{rem} trem,系统当前 decode 吞吐为 B B B tokens/s,则 t r e m ≈ r / B t_{rem} \approx r / B trem≈r/B。
考虑一个具体场景:请求 A 已生成 1000 个 token( t r e m ≈ 1 t_{rem} \approx 1 trem≈1 秒),请求 B 刚到达,高优先级,需要立即执行。如果 abort A:
- 代价 = A 已消耗的 GPU 秒数 ≈ k / B a v g k / B_{avg} k/Bavg ≈ 1 秒
- 如果客户端重试,还需加上重新 prefill 的时间 ≈ 0.2 秒
而 swap A 到 CPU:
- 代价 ≈ 传输 1000 token 的 KV ≈ 131 MB / 25 GB/s ≈ 5 ms(几乎可忽略)
- 但 swap 期间 GPU 闲置 ≈ 5 ms,损失 ≈ 0.005 秒的生成量
从算式看 swap 明显更优。但如果 A 的剩余长度 r r r 很大(比如还有 8000 token 要生成),swap 后 A 被放回 GPU 时,它的 batch 位置已经改变,可能打乱后续 batch 的组成 ,导致整体吞吐下降。更关键的是:我们不知道 r r r 是多少。
因此,工程实现常用的方法是将 t r e m t_{rem} trem 替换为可估计的上界 。这个上界来自 token budget:准入时请求声明了 max_tokens,则 r m a x = m a x _ t o k e n s − k r_{max} = max\tokens - k rmax=max_tokens−k,于是 t r e m U B = r m a x / B t{rem}^{UB} = r_{max} / B tremUB=rmax/B。抢占决策使用上界而非期望值------这会高估代价(保守),但保证不会出现"抢占了却发现几乎快跑完"的浪费 。实践中的优化是维护一个 token 生成速度的 EWMA(指数加权移动平均) ,用近期的实测吞吐来动态调整 B B B,让 t r e m U B t_{rem}^{UB} tremUB 更贴近真实。这本质上是一个在线估计问题:暴露的信息越多,决策越准。
5.4 饥饿保护:可撤销决策的公平性约束
任何抢占策略都必须回答:如果一个请求总是被抢占,它是否永远无法完成? 这被称为饥饿(starvation)。纯优先级抢占(高优先级永远先执行)必然导致低优先级请求的饥饿。
工程上常用的保护机制有三种:
- 老化(Aging):请求的优先级随时间增长。每等待一秒,优先级的权重加一,直到超过当前最高优先级的请求。
- 抢占预算:每个请求在生命周期内被抢占的次数上限。一旦达到上限,系统禁止再抢占该请求,即使它的优先级较低。
- 完成度阈值 :当一个请求的已生成长度 k k k 超过 L × θ L \times \theta L×θ(如 θ = 0.8 \theta = 0.8 θ=0.8),系统保证不再抢占它------因为此时 abort 或 recompute 的代价已经超过任何可能的收益。这个阈值直接对应 5.1 节中代价函数的拐点。
这三种机制可以叠加使用。实际系统中,老化 是最常用且最容易被工程师错误实现的一个:如果老化是线性增长的,高优先级请求密集到达时可以击败老化,导致低优先级仍然饥饿;正确做法是使用指数老化(等待时间翻倍则优先级翻倍),确保任何优先级差距都能在有限等待时间内被追赶。
5.5 可撤销决策:从事件驱动的抢占到预算驱动的抢占
把这四节的讨论整合,可以提炼出一条准绳:抢占决策必须可撤销,且撤销的代价是可计算的。具体来说:
- 决策时刻:当新请求到达、batch 的 SLO slack 低于阈值、或 GPU 利用率低于目标值时,触发抢占评估。
- 决策函数 :对每个候选请求,计算四种回收方式的综合代价 ------已生成长度 k k k(历史成本)、剩余时间上界 t r e m U B t_{rem}^{UB} tremUB(未来成本)、传输成本(swap/migration)、以及饥饿风险(老化值)。
- 决策动作 :选择综合代价最小的方式,且保证系统有回退路径------如果 swap 后目标 GPU 的负载上升导致 swap 回来的延迟不可接受,系统需要能在中途切换为 recompute。
这带来的一个关键设计原则是:抢占不是一次性事件,而是一个连续的、可逆的决策过程。一个请求可以被 swap 出去,如果它长时间未被调度回来,系统可以升级决策------将其 abort 并通知客户端重试,而不是无限期地占着 CPU 内存。反过来,一个被 abort 的请求如果客户端的重试请求立刻到达,系统可以复用已缓存的 prefill 结果,将 recompute 的成本降低为仅重新 decode。
如此,抢占策略的四个维度------已生成长度、传输带宽、未来服务时间、饥饿保护------就统一到了一个可实现的决策框架中。下一节,我们将把这些决策逻辑与前两节的排队模型和准入控制串起来,形成一个从「请求到达」到「结果返回」的完整在线调度闭环。
SLO-aware 目标函数
前两节分别回答了"系统何时过载"与"谁能进入系统",第三节则给出了四种回收方式的代价函数。但调度器至今还缺一个关键组件------优化的方向 。抢占决策需要比较"杀掉 A 保 B"是否划算,准入决策需要判断"拒绝 C 换取 D 的延迟达标"是否值得,这一切都必须建立在统一的数值语言之上:目标函数。本节的目的是回答一个看似简单的问题:一批请求得到了服务,怎么衡量调度得好不好?
6.1 双 Deadline 结构:TTFT 与 TPOT 分别约束
LLM 请求的延迟体验并非单维指标。用户在输入提示词后,首先等待的是第一个 token 的生成------这段时间过长会让人觉得"服务没反应";而在首 token 到达后,用户感受到的是逐 token 生成的节奏------如果这个节奏慢于人类的阅读速度,对话将变得难以忍受。两者感知不同,在调度器中也必须被分别建模、分别约束。
形式化地,设请求 i i i 在时刻 t 0 i t_0^i t0i 到达,其 SLO 由一对 deadline 定义:
- TTFT deadline : d t t f t i d_{ttft}^i dttfti,要求首个 token 必须在 t 0 i + d t t f t i t_0^i + d_{ttft}^i t0i+dttfti 之前完成生成。
- TPOT deadline : d t p o t i d_{tpot}^i dtpoti,要求从首 token 到最后一个 token 的平均生成间隔不超过此值。
注意 TTFT 是绝对时间约束(从到达时刻起算),而 TPOT 是速率约束(单位 token 的平均间隔)。两者的区别是本质性的:TTFT 约束的是调度器的响应速度 ,TPOT 约束的是持续吞吐能力。一个 batch 配置可以满足 TTFT 但违反 TPOT(首 token 快,后续衰减),也可能相反(批量大导致排队慢但生成稳定)。
在实际系统中,TPOT 约束通常转化为对 batch 大小的隐式限制:设单卡单请求的生成速度为 v s i n g l e v_{single} vsingle(tokens/s),共享同一 batch 的 n n n 个请求各自分得的速率约为 v s i n g l e / n v_{single}/n vsingle/n(在均匀分割的近似下),则 TPOT deadline 直接给出 batch 大小的上界:
n ≤ v s i n g l e TPOT_deadline − 1 ⋅ 1 1 + overhead n \leq \frac{v_{single}}{\text{TPOT\_deadline}^{-1}} \cdot \frac{1}{1 + \text{overhead}} n≤TPOT_deadline−1vsingle⋅1+overhead1
这个式子将延迟约束翻译成了调度器可直接执行的资源约束,是本节后续所有讨论的基础。
6.2 Slack:SLO 的富余量
Deadline 只是硬边界,调度器真正需要的是一门"软货币"------slack ,即请求当前进度与 deadline 之间的距离。定义请求 i i i 在时刻 t t t 的 slack 为:
slack i ( t ) = 剩余可用时间 剩余生成所需时间 + 排队时间估计 \text{slack}_i(t) = \frac{\text{剩余可用时间}}{\text{剩余生成所需时间} + \text{排队时间估计}} slacki(t)=剩余生成所需时间+排队时间估计剩余可用时间
slack > 1 表示有富余,slack < 1 表示濒临违约。这个比值的妙处在于它是一个无量纲标量,不同请求之间可以直接比较------这正是调度器需要的排序键。
Slack 是动态变化的:请求在 batch 中每被抢占一次,排队时间估计上升,slack 下降;请求被优先调度时,slack 回升。调度器的抢占决策因此可以表述为极简的形式:只抢占 slack 最大的请求,去保护 slack 最小的请求。这就是"slack-based scheduling"的核心思想------不是抽象地比较优先级,而是比较每个请求离 deadline 有多远。
实践中,slack 的估计需要谨慎处理两个细节。第一,剩余生成所需时间的估计依赖输出长度预测,常用做法是取 max_tokens 作为上限估计(悲观),或用历史分布的分位数(如 P90)作为估计(中性)。第二,排队时间估计需包括当前 batch 完成时间、等待中的高优先级请求数等因素,通常用排队模型中的预期等待时间 W q W_q Wq 近似。
6.3 多目标:从单指标到 Pareto 前沿
调度器面对的并非单一目标。简单地最小化平均 JCT(job completion time)会导致长请求被无限饿死;最大化 SLO attainment 会过度保守,牺牲吞吐;最大化 goodput(在 SLO 内完成的请求占比)又可能让系统在重负载下过早拒绝请求。现实中的调度器必须同时优化多个指标。
将目标函数形式化为加权组合是最常用的方法:
O = λ 1 ⋅ Attainment + λ 2 ⋅ Goodput − λ 3 ⋅ J C T ‾ + λ 4 ⋅ Fairness \mathcal{O} = \lambda_1 \cdot \text{Attainment} + \lambda_2 \cdot \text{Goodput} - \lambda_3 \cdot \overline{JCT} + \lambda_4 \cdot \text{Fairness} O=λ1⋅Attainment+λ2⋅Goodput−λ3⋅JCT+λ4⋅Fairness
其中:
- Attainment = 在 SLO 内完成的请求数 / 总请求数,衡量可靠性
- Goodput = 完成的请求中满足 SLO 的比例 × 吞吐量,衡量"有效产出"
- J C T ‾ \overline{JCT} JCT 为平均完成时间,衡量速度
- Fairness 为各租户/用户获得的资源份额的公平性指标
这四者天然存在冲突:追求高 Attainment 意味着保守准入、预留资源,Goodput 和吞吐会下降;追求低 JCT 意味着短请求优先,长请求被饿死,Fairness 受损。因此目标函数不是"选一个数然后最大化",而是在四维空间中寻找Pareto 最优的配置点------即无法在不牺牲至少一个指标的情况下改善任何其他指标的操作点。
调参 λ 1 : λ 2 : λ 3 : λ 4 \lambda_1: \lambda_2: \lambda_3: \lambda_4 λ1:λ2:λ3:λ4 是商业决策而非技术决策:生产系统通常将 Attainment 设为硬约束(如"P99 的 TTFT 必须 < 800ms"),其余指标在约束内优化。
6.4 租户权重:公平与优先的调和
多目标函数虽然能刻画系统整体表现,但无法回答一个实际部署中的问题:当两个租户的请求同时到达,谁的请求先进去? 多租户场景下,调度器还必须将目标函数分解到租户维度。
设系统中有 K K K 个租户,每个租户 j j j 有一个权重 w j w_j wj(反映其在 SLA 中的付费等级和服务优先级)。租户级别的目标函数为:
O j = α j ⋅ Attainment j + β j ⋅ Goodput j \mathcal{O}_j = \alpha_j \cdot \text{Attainment}_j + \beta_j \cdot \text{Goodput}_j Oj=αj⋅Attainmentj+βj⋅Goodputj
系统总目标即 ∑ j w j ⋅ O j \sum_j w_j \cdot \mathcal{O}_j ∑jwj⋅Oj。这种分解带来两个直接好处:
第一,slack 比较时可按租户加权------一个 premium 租户的请求即使 slack 较大,也可能优先于普通租户 slack 较小的请求。这实际上是在公平性维度上引入了"金票"机制。
第二,准入控制可按租户独立作决策 ------每个租户有自己的 token budget,超额部分直接拒绝,互不干扰。这种隔离策略避免了"一个租户的突发流量挤占其他租户 SLO"的经典问题。权重 w j w_j wj 的取值通常与租户的 SLA 等级直接挂钩,并在部署时通过配置管理。
6.5 离线 Oracle:目标函数的评估基准
有了目标函数之后,随之而来的问题不是"如何优化",而是"优化到多好才算好"。离线 oracle 提供了一个理论上的上界------它回答了"如果调度器拥有上帝视角(事先知道所有请求的输出长度、到达时刻),最优解是什么样的"。
小规模 oracle 可用动态规划、整数规划或离散事件搜索构造:给定全部请求的完整信息,在明确的 batch 服务曲线、显存容量、抢占成本和 SLO 约束下求最优或可证明有界的调度。大规模实例通常只能求松弛上界、截断搜索或使用强启发式,因此必须区分"最优解""上界"和"目前找到的最好解"。在线策略应报告相对 gap,但不存在脱离工作负载仍成立的"至少达到 oracle 90%"门槛。
实践中,oracle 的价值不仅在评估,更在于发现模型缺陷。如果在线调度器与可证上界差距很大,需要把差距拆成三部分:在线信息缺失造成的不可避免损失、求解器/启发式造成的决策损失、服务时间与迁移成本建模误差。只有第三类能通过修正模型直接消除;因此 oracle 更像"校准仪",不能自动证明是哪一项出了问题。
最终,这一节给出的不是一个具体的调度器,而是一套语言:TTFT/TPOT 双 deadline 定义了什么叫做"好",slack 给出了可比较的排序键,多目标和租户权重解决了冲突,oracle 提供了校准的天花板。有了这套语言,前面的排队模型、准入控制、抢占决策才能在同一个坐标系中被评估和权衡------下一节将把这套语言组装为可执行的分层调度器,展示它在真实负载下的行为。
7. 实验设计:实现并对比四种调度策略
数学模型的价值不在于其优雅,而在于其可证伪性。前三节分别建立了排队模型、准入预算与回收代价函数,如果它们只是停留在纸面上,那和黑板上的粉笔字没有区别。本节将四种策略------FCFS (先到先得)、SRPT 近似 (按已服务时间最短优先)、deadline 驱动 (按 SLO slack 排序)与 cost-aware(综合回收代价与剩余价值)------放进同一个实验框架,让它们在完全相同的负载下竞争,用数据回答三个问题:各策略的吞吐-延迟曲线长什么样?谁在尾延迟上表现最好?策略本身的决策开销是否值得?
7.1 实验设计:同一个 trace,同样的不可预知性
实验的可比性建立在完全相同的输入序列与服务曲线 之上。负载生成器产生 10,000 请求的 open-loop trace,并扫描多档到达率;输入长度覆盖 100 , 2000 100,2000 100,2000 tokens,输出长度同时采用真实日志的经验分布与重尾 log-normal 分布。指定算术均值 500、标准差 300 时,配置生成器先换算 log-space 的 μ , σ \mu,\sigma μ,σ,再把随机种子写入 manifest。manifest 还包含 GPU 型号与数量、可用于 KV 的实际字节、block size、最大 batch token 数、模型、精度和服务曲线版本。
关键设计决策在于不可预知输出长度 :调度器到达时只知道输入、max_tokens 与在线预测特征,trace 中的真实输出长度只供模拟器推进和事后评分。实验分别实现预测式 SRPT 与 LAS/Foreground--Background:前者按预测剩余时间排序,后者按已获得服务量排序。两者语义不同,结果中同时报告长度预测误差,才能判断收益来自调度原则还是预测器质量。
7.2 复现四种策略
四种策略在同一个可插拔 scheduler 接口下实现,核心调度循环每 10ms 触发一次:
| 策略 | 排序键 | 准入逻辑 | 抢占依据 |
|---|---|---|---|
| FCFS | 到达时间 | 容量不足则拒绝 | 不抢占 |
| SRPT 近似 | 预测剩余 token/时间(升序);无预测器时改名 LAS | 容量不足则拒绝 | 预测剩余时间最大或按显式策略选择 |
| Deadline | 预测完成时刻与 deadline 的差(升序) | 预测可按时完成且预算允许才接纳 | slack 最大且回收代价可接受者优先让出资源 |
| Cost-aware | 回收代价 / 剩余价值 | 总承诺 ≤ 可用容量 | 代价最小者先被抢占 |
其中 deadline 策略 必须区分绝对截止时刻和服务预算。对尚未完成 prefill 的请求,可定义 s T T F T = d T T F T − C ^ f i r s t s_{TTFT}=d_{TTFT}-\widehat{C}{first} sTTFT=dTTFT−C first;对 decode 请求则维护未来一个控制窗口内的 s T P O T = d n e x t − C ^ n e x t s{TPOT}=d_{next}-\widehat{C}_{next} sTPOT=dnext−C next。排序可取当前阶段的 slack,负值表示预测违约。平均吞吐不足以估计完成时刻,预测器至少要查询当前队列、batch 服务曲线、prefill/decode 阶段和抢占迁移成本。2s/100ms 是实验参数,不是通用 SLO。
cost-aware 策略 应比较可执行的增量成本,而非把秒、字节和"价值"直接相加。可先归一化为 C p r e e m p t = w r T ^ r e c o m p u t e / S L O r + w s T ^ s w a p / S L O r + w v Δ V r C_{preempt}=w_r\widehat{T}{recompute}/SLO_r+w_s\widehat{T}{swap}/SLO_r+w_v\Delta V_r Cpreempt=wrT recompute/SLOr+wsT swap/SLOr+wvΔVr,其中重算与迁移时间由当前带宽和 KV 字节预测, Δ V r \Delta V_r ΔVr 是拒绝或延后造成的归一化效用损失。已经消耗的计算属于沉没成本,只有它会导致未来重算、丢失已生成结果或 SLA 赔付时才进入决策;"已经跑了 3 秒所以必须保留"本身不是正确优化原则。
所有策略的共性约束是:决策开销必须计入结果。每次调度记录 CPU wall time、扫描请求数、锁等待和 GPU 空泡,并计入对应请求的排队/step 延迟。FCFS 通常更便宜,cost-aware 的复杂度则取决于堆、增量更新和候选集裁剪。模拟器中的决策耗时与生产语言/运行时中的决策耗时分开报告,避免把算法复杂度和实现开销混在一起。
7.3 三组结果:从曲线识别机制
吞吐与 p50 延迟。第一张图同时画 offered load、goodput、p50 与 p99,并从服务曲线的拐点定位饱和区。在低、中、高三档负载分别报告完成、拒绝和超时请求,防止通过大量拒绝维持低延迟。预测式 SRPT 与 LAS 再按输出长度分桶,展示短请求收益是否由长请求尾延迟换来。
尾延迟与反饥饿。四个统计量从原始 per-request 记录聚合,并附 bootstrap 置信区间、有效样本数和至少三个随机种子:
| 统计量 | 聚合范围 | 主要用途 | 易误读点 |
|---|---|---|---|
| p99 TTFT | 全部到达请求与已接纳请求各一份 | 衡量排队与 prefill 调度 | 拒绝请求不能从全量口径消失 |
| p99 TPOT | decode token 间隔,按长度桶分组 | 衡量迭代稳定性 | 不能用请求平均值掩盖尖峰 token |
| 最大/高分位完成时间 | 长请求 cohort | 检测饥饿 | 最大值需同时给样本数和超时上限 |
| starvation rate | 明确定义阈值与观察窗口 | 比较 aging/抢占机制 | 阈值改变会显著改变比例 |
解释结果时先验证机制:预测式 SRPT 可能牺牲长请求,LAS 对逐渐暴露为长作业的请求也可能不友好;deadline 策略只有在不可挽救请求的处置、aging 和准入规则正确时才抑制饥饿;cost-aware 是否改善结果取决于成本预测误差。最长完成时间、条件 p99(按长度桶)和 starvation rate 应共同报告,且被拒绝请求不能从分母中悄悄消失。
过载行为与决策开销。负载扫描至少延伸到饱和点以上 25%--100%,同时画 acceptance rate、goodput、SLO attainment、排队长度、GPU 利用率和 scheduler CPU 时间。比较准入策略时采用两套视图:一套按全部到达请求计分,另一套仅分析已接纳请求;前者防止"拒绝换低延迟"的统计欺骗。决策开销按每 step 与每 request 两种口径报告,并通过关闭策略计算的 A/B 实验测量其对 GPU 空泡的因果影响。
7.4 完成复现后应如何解释结果
四条曲线放在一起,结论不是预设"哪个策略最好",而是估计每个决策维度的边际价值。FCFS→预测式 SRPT/LAS 的差异检验长度信息,加入 deadline 检验 SLO 信息,加入 cost-aware 再检验迁移/重算成本。只有置信区间、长度分桶和拒绝率均支持时,才能声称某一维度带来增益;若 TTFT 改善而 TPOT 恶化,应呈现 Pareto 前沿,不能压成单一胜负。
验证链由离散事件模拟、不同 seed、不同到达过程和长度分布的敏感性分析组成。Little 定律不预测固定临界到达率;饱和点来自服务曲线与调度约束,Little 定律只在稳态联系平均在途数、到达率与响应时间。每个曲线点都关联 trace、配置、seed 和原始事件日志,使结论能够回放。
部署决策还要计入实现复杂度、可解释性和预测器漂移。若 deadline 策略在多种 trace 上已接近 oracle 上界,继续增加 cost-aware 复杂度可能不划算;若 swap/recompute 暴露时间主导尾延迟,成本项才可能具有显著价值。更稳妥的发布顺序是离线回放、shadow decision、少量租户灰度与自动回滚。调度器的数学内核最终指向的不是一个永恒最优解,而是一个可解释、可调节、可观测且能被反例推翻的决策空间。