第 4 章 调度:谁先跑,谁等着,谁被扔掉
先修:第 1–3 章 · 深度:L1 · 预计阅读:25 分钟
一句话:排队不算模型计算,但用户不管你在排队还是在算。
十四条里,五条很长
还是那二十个同事。这次有五份 8K 合同,十五个两百字的短问题,全部同一分钟到达。
按第 2 章的账,一次能跑十四条。于是调度器编了一批:五条长的,九条短的。
短问题半秒就生成完了。可它们占的槽位不会立刻空出来,因为这一批要等最慢的那条结束才能整体收工。九个槽位干等了快四秒。
剩下六个还在队列里,它们的首字延迟从"应该 1 秒"变成"可能 5 秒"。
静态批次浪费在哪
一批请求的寿命,由最长的那条决定。
短的提前完工,却释放不了自己的位置;长的拖着整批走。batch 越大、长度差越大,这种浪费越明显。
第 3 章的账是按"十四条长度一样"算的,现实里长度从来不一样。
连续批处理:谁结束谁退出
调度器不再等整批。
每条序列生成到结束符就退出,队列里立刻补一条进来。槽位空出来的时间从"整批结束"变成"这一条结束"。
同样二十条请求,槽位利用率从六成提到接近满。多出来的不是计算能力,是不再空转。
这是现在主流引擎的默认做法,vLLM、SGLang 都有。
长 prefill 会堵住所有人
还有一种堵法,比长度不齐更狠。
一个 32K 的合同进来,prefill 一次要算 459 TFLOP。按 A100 的 312 TFLOPS 算,约 1.5 秒。这 1.5 秒里 GPU 被它一个占用,十四条正在逐字生成的请求全部停住。
用户看到的是:本来稳定出字,突然卡住两秒。
解决办法是把 prefill 切块。每次只算 512 个 token 的 prefill,算完就让 decode 走一步,再回来算下一块。
一块 512 token 的 prefill 约 23 毫秒,和一步 decode 的 34 毫秒在一个量级,交错起来用户感觉不到。
代价是这条长请求自己的首字更慢:从 1.5 秒变成若干个 23 毫秒加起来,中间还夹着别人的 decode。它成了慢的那一个,换来其余十四条不断顿。
显存不够时会扔掉谁
队列里还有六条,显存里十四条已经贴顶。这时候又来一条。
调度器有三个选择:让它等、把别人换出去、把自己丢掉。
换出是把某条请求的缓存挪到 CPU 内存,腾出显存给新来的;等它有位置了再挪回来。麻烦在于 CPU 和 GPU 之间那条总线很窄,挪一次就是几十毫秒。
丢弃更直接:删掉某条的缓存,之后它要用时重算 prefill。一条 8K 重算要几百毫秒,用户体感是一次明显的卡顿。
两种做法都是在"让一个人等"和"让一群人卡"之间选。没有免费的选项。
三个参数分别管什么
跑起来会看到一堆参数,先分清它们各管一段。
max_num_seqs 管同时跑几条,直接对应第 2 章算出的 14。
max_num_batched_tokens 管一步里最多塞多少 token,prefill 切块和 batch 大小都受它影响。
max_model_len 管单条请求最长多少,它决定了每条请求能申请多大的缓存。
三个都调大,显存就爆;都调小,GPU 就空转。它们不是独立旋钮。
什么时候调度救不了
容量明显不够时。二十条对十四条,怎么排都是六个人在等。
单条请求本身就慢时。调度只决定谁先谁后,不改变一条 8K prefill 要 1.5 秒这个事实。
到达率超过服务能力时。队列会一直涨,最后超时。这时候需要的是加卡或者限流,不是更聪明的排队。
经验:先看队列,再看参数
延迟出问题,第一步不是改参数,是看队列:waiting 有多少、running 有多少、它们随时间怎么变。
waiting 周期性涨落,是到达不均,加缓冲或者限流。waiting 单调上涨,是能力不够。running 上不去而 waiting 很大,是容量或者切块设错了。
队列的形状会告诉你该动哪个旋钮,参数表不会。
练习
六道题,先闭卷,再翻 ANSWERS.md。
- 静态批次里,一条长请求会拖慢同批的短请求,机制是什么?
- 连续批处理提高了什么?它没有提高什么?
- 32K 的 prefill 为什么会让别人的 decode 停住?切块之后代价转移到谁身上?
- 找错:"抢占就是把请求删掉,用户什么都不用付。"
- 三个参数里,哪个直接决定第 2 章算出的并发上限?
- 观察一次真实压测的 waiting 曲线,判断它属于哪一种形状,并说出你的下一步动作。
这一章就记一句
静态批次等最慢的那条,连续批处理让谁结束谁退出。长 prefill 会独占 GPU,切块把它和 decode 交错。显存不够时,换出和丢弃都是让某个人替大家挨一下。
真只记一句:延迟问题先看队列形状——waiting 涨落是到达问题,waiting 单调上涨是能力问题,running 上不去是配置问题。
下一章处理队列里那些长度差异巨大的请求:按最大值预留缓存会浪费多少,分页怎么把浪费压下去。