loading

Loading

探索与发现

首页 AI 前沿

推理服务队列里的优先级反转:调度器先动手

分类:AI 前沿
字数: (2200)
阅读: (4)
0
摘要:当 KV Cache 和 batch 槽位取代了锁成为共享资源,经典优先级反转就在推理服务队列里换了副面孔:高优请求没被决,权作等不来能将它拼进去的 KV 空间,中优先级短请求反而抢走了 host 本子,槽位被它们慢慢吃满。

优先级反转在操作系统课里是个经典三段式:高优先级任务 A 等着低优先级任务 C 手里的锁,中优先级任务 B 趁机抢了 CPU,结果是 B 跑完了 A 还卡着。教科书给的解法也经典:优先级继承、优先级天花板、关中断。但在推理服务的调度器里,这套东西直接搬过来会撞墙,因为“谁在持有资源”这件事在这里变得模糊。

把三类东西映射一下:高优先级请求是按 SLA 承诺要低延迟返回的线上请求;低优先级请求是挂着批量任务或评估流量的请求;中优先级是排队里那些优先级居中、但提交频繁的常规请求。问题常出现在 KV Cache 和批处理槽位被当作共享资源的时候。

反转是怎么发生的

先问:谁占着资源?是显存里那块 prefill 到一半的长 prompt 的 KV 块。低优先级的批量请求塞进来一个两万 token 的长文档,prefill 占满了一大块显存,这时高优先级请求到达,进了准入队列。它不会被阻塞在“锁”上,而是被卡在排不进批上——调度器发现剩余 KV 空间不够下面连续 decode 需要的 block 数,连续几轮迭代都没把它拼进 batch。

与此同时,中优先级请求不断进来。如果调度的分配口径是严格的剩余 slot 优先,只看当前这一轮哪个 batch 能填满,这时中优先级请求体短、好拼,就被接纳了。高优先级请求虽然优先级高,但它在这一轮所需的空间没有人腾出来,反而是持续进站的中优先级请求把每个 batch 都占了。

这个过程中,高优先级请求被“延迟执行”,但它没报警——只是 P99 在攀。等到长 prompt decode 结束、KV 释放,它才被接纳。对应来看,操作系统里 B 先执行完,A 才拿回资源;推理服务里中优先级请求一直占着 batch 槽,直到低优先级请求自己自然收尾。

为什么优先级继承不好用

优先级继承的核心是低优先级的持锁方继承等待方的高优先级,从而不被中优先级抢占。推理服务能做类似的事:如果一个高优先级请求在等某个长请求释放 KV,就把这个长请求临时提权。但有个硬伤:低优先级长请求一次 prefill 占 2 万 token,可能再跑半分钟;把它提到最高优先级意味着“排在它后面的所有 request 都等它这一大段”。短期内腾不出空间,只是把等待换成了更慢的排队。

另一个问题是持有者的判定。推理一次的 KV 占用是逐步增长的,并非给锁付定金。一个请求在第 50 个 token 的时候锁定了前缀缓存(prefix cache)的某一页,第 300 个 token 时又释放了一部;持有者判定要随每一步每页随动,标准继承机制没法在这种粒度上接力。

现实一点的做法

更实用的路线是绕开“创建时继承”的限制,把它改造成准入时的账本控制。具体有三件事可以做。

  1. 把 max_num_batched_tokens 按优先级动态切片。每轮迭代之前,预留一块“高优先级 rescue buffer”,只允许 P0 请求使用。这块尺寸等于从单条请求的 KV 增长速率反推的值,比如 decode 阶段每条每次一步吃掉几十个 token case,按保守估计数留几十条 request 的量。P1 请求用剩余的 part。效果是高优先级即使排队,也不必和中优先级抢同一个剩余额度池子。
  2. 给低优先级单请求设 KV 预算上限。一个 2 万 token 的批量 request 预分配 KV,实际是在无任期地长期把资源揽住。把 max_model_len 对某优先级限制变小,逼它拆块提交,或者至少限制并发存活的长请求数量。这样它在高峰期不贡献排他的 KV 回填,只占各批自己承诺那份。
  3. 把等待信息放进观测。当 P0 请求排队超过阈值,必须在 next-token latency 打上标注同时标 scheduler 未提交、申请了多大 KV 申请、还要多久才被满足。只看看板计数会把“排队时间”掩盖在每个 gate 后面,因为它根本不存在 queue length 里。

取舍与代价

预留容量意味着计算率降,任何剩余空闲 slot 都在跑。batch大小降低到 87% 和降低到 92% 的时候,吞吐差别近乎可见;预留太大的话,晚高峰的排队更长。不过这项损失比把 SLA 打成 P99 飙升要便宜,如果能兑现合同,就值。

KV 预算对批量任务施加时也会反噬:本来能像长文本摘要那样一次处理进的批量输入,被切成多块会让端到端统计不可比、重现任务周期感更笨。这是要分摊的代价,不要认为没有缺点。

把操作系统里“高优先级等待资源”翻译成推理服务里“高优先级等待调度可容纳空间”,继承就变成动态预留。我们继承了半天,最后绕回到 budget,其实也符合调度器不是多线程 Runtime 的性质。

什么时候根本不该区分优先级

如果用户没有隔离预算,全部单模型一律走同一个请求队列,高优先请求实际上常常只是一种“前台页面、用户正注视”的样本,排队长度无法精确分摊,优先只代表排序占位而没有等待重定点。在推理长 prefill 恰是当前正在用所有 GPU 最大能力处理对象的时候,任何时间片保护反而是干扰。实际中选择较少,常规思路是靠多等级租队列、物理分隔资源——在前一篇文章所述推理服务多租户配额的一层,把 P0/P1 分配到不同副本上去,而非默认共享调度层。优先级反转也就没有栖身之所了。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/ai-frontier/651.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图