<aside> 🎯
调度参数其实只回答四个问题:谁先进 batch(策略),一次进多少(预算),长 prompt 会不会卡住别人(分块),显存不够了怎么办(回退)。后面这一页,我们就是顺着这四个问题,把每个参数背后的算法和代价一点点拆开。
</aside>
假设一次最多同时处理 3 个请求,有 5 个请求,需要生成的 token 数为 A=2、B=6、C=3、D=2、E=4(忽略 prefill):
静态批处理:共 10 步
步数: 1 2 3 4 5 6 | 7 8 9 10
座位1: A A . . . . | D D . .
座位2: B B B B B B | E E E E
座位3: C C C . . . | . . . .
连续批处理:共 7 步(每步结束都重新决定 batch 成员)
步数: 1 2 3 4 5 6 7
座位1: A A D D . . .
座位2: B B B B B B .
座位3: C C C E E E E
| 参数 | 作用 | 默认 | 备注 |
|---|---|---|---|
--schedule-policy |
waiting_queue 的排序策略 | fcfs | 可选 lpm、random、fcfs、dfs-weight、lof、priority、routing-key(routing-key 的含义本次文档没展开,请查 --help 或源码) |
--schedule-conservativeness |
调度的保守程度,越大越保守 | 1.0 | 看到频繁 retract 时调大;token usage 低且有排队时调小(官方建议例如 0.3) |
--max-prefill-tokens |
单个 prefill batch 的 token 上限 | 16384 | 官方说明实际上限是这个值与模型最大上下文中的较大者 |
--chunked-prefill-size |
chunked prefill 中一块的最大 token 数 | None(启动时自动确定,日志示例中是 8192) | 设为 -1 表示关闭;prefill OOM 时调小到 4096 或 2048,代价是长 prompt prefill 变慢 |
--prefill-max-requests |
一个 prefill batch 包含的请求数上限 | None,不限 | 限制的是请求个数,而不是 token 数 |
--max-running-requests |
同时运行请求上限 | None | 日志示例中自动确定为 4096 |
--max-queued-requests |
排队请求上限 | None | PD 分离模式下忽略 |
--retraction-policy |
KV 满时 decode 回退哪些请求 | length | length:优先回退输出短、输入长的请求;priority:优先回退低优先级请求 |
--enable-priority-scheduling |
开启优先级调度 | False | 默认优先级数值大的先调度;配合 --schedule-low-priority-values-first 可反过来 |
--priority-scheduling-preemption-threshold |
新请求抢占运行中请求所需的最小优先级差 | 10 | 可用 --disable-priority-preemption 关闭抢占 |
--enable-prefill-delayer |
DP attention 下延迟 prefill 减少空闲 | False | 可配 --prefill-delayer-max-delay-passes(默认 30)、--prefill-delayer-queue-min-ratio、--prefill-delayer-max-delay-ms(用于限制最坏 TTFT) |
--disable-overlap-schedule |
关闭 CPU 与 GPU 的重叠调度 | 默认开启 overlap | 诊断或兼容性问题时才关闭 |
mixed chunk(--enable-mixed-chunk) |
允许把 chunked prefill 与 decode 放进同一个 batch | 关闭 | ⚠️参数名未在本次抓取的文档片段中核对;但官方投机解码文档确认 NGRAM、DFLASH、UNO 会禁用 mixed chunked prefill |
所有策略干的事情都一样,就是在 waiting_queue 上做一次排序。排完以后,由 PrefillAdder 按这个顺序一个一个地尝试往 batch 里装。所以策略之间的差别,只在于「排序的依据」和「排序本身贵不贵」。
按到达时间排序,最简单、公平、开销最小,也是默认。
LPM 的做法分四步。第一步,对队列中每个请求,用 token 序列去 Radix Tree 里做前缀匹配,得到命中长度。第二步,按命中长度从大到小排序:命中越多,需要重新算的 token 就越少,先做它吞吐最高。
第三步是 in-batch 前缀去重,这一步第一眼看起来很反直觉:为什么要故意「少放」请求进 batch?设想 req2、req3、req4 在全局缓存里都没命中多少,但它们彼此之间共享一段 4 个 token 的开头。三个一起放进来,这段开头要算三遍;只放 req2,它算完写回 radix 树,下一轮 req3、req4 就直接命中。延迟一轮,换来少算两遍。实现上,调度器把本轮已选中的请求插进一棵临时 radix 树,用两个阈值控制(都在 schedule_policy.py,默认都是 32,可用同名环境变量覆盖):IN_BATCH_PREFIX_CACHING_CHECK_THRESHOLD 管的是全局命中已经超过 32 的请求直接放行、不做去重;IN_BATCH_PREFIX_CACHING_DEPRIORITIZE_THRESHOLD 管的是在临时树里与同批请求共享的前缀超过 32 才让路,太短的共享(比如都以 “The” 开头)不值得让它等。被让路的请求在排序时被放到队尾(key 取 inf)。
第四步是队列太长时回退。对长队列逐个做前缀匹配,本身就是 CPU 开销。WAITING_QUEUE_PREFIX_MATCH_MAX = 128,等待队列超过 128 个请求时,lpm、dfs-weight、hrrn 这些 cache-aware 策略都会临时退化成 FCFS。这一点非常关键:高负载时你以为开了 LPM,实际跑的是 FCFS。如果压测时发现 LPM 和 FCFS 没差别,先看看 #queue-req 是不是长期大于 128。
官方确认的代价是 LPM 会引入更多调度开销。此外,从算法上看,它会让前缀命中高的请求不断插队,理论上拉高命中低的请求的等待时间(尾延迟),这是我自己推导的取舍,不是官方文档的原话。
把等待请求挂到 Radix Tree 对应的节点上,节点权重等于子树下等待请求的数量;从根开始 DFS,每层优先走权重大的子节点。效果是共享度最高的前缀簇被优先调度,并且同簇的请求相邻。
random 是随机打乱,多用于基线。lof 是最长输出优先,需要预知输出长度。priority 按请求携带的优先级排序,需要配合 --enable-priority-scheduling。
hrrn(Highest Response Ratio Next)是经典操作系统调度算法的 token 版。响应比 = 1 + 等待时间 / 预计服务时间,这里把「时间」都换成 token:等待期间系统已经 prefill 了多少 token,除以这个请求未命中缓存、真正需要算的 token 数。短请求天然分母小,排在前面;长请求等得越久,分子越大,也终会被调度,不会饿死。它和 LPM 的区别,是把「公平」也考虑进来了。shortest-prefill-first 按未命中长度从短到长,最小化平均 TTFT,代价是长 prompt 可能一直被插队。routing-key 按请求的 routing key 在 running batch 里出现的频次排序,让同一个 key(比如同一个会话或租户)的请求步调一致。