loading

Loading

探索与发现

首页 AI 工程化

长请求切片抢占:切多细、抢谁的、怎么收尾

分类:AI 工程化
字数: (2371)
阅读: (4)
0
摘要:长请求切片不是只要开个开关。弄明白为什么切、切多细、抢占谁才成立,可能同时赔上每次预填充块插入的延迟预算,以及还没拿到 token 的超长输出来收场时的问题。

线上推理服务里最难受的一类请求,不是大 batch,也不是高 QPS,而是一个两万 token 的 prefill 排在队列前面不动。后面的短请求 TTFT 从 300ms 涨到两秒,GPU 利用率看着很漂亮,SLA 却在报警。chunked prefill 就是为这种局面设计的:把长 prefill 切成固定大小的块,一个 decode step 往前推一块,短请求的 prefill 能插进来。

但真正上线之后,问题会变成三个:块切多细、抢占谁、以及整条被切碎的长请求怎么收尾。

chunked prefill 解决的不是排队,是干扰

看 vLLM 优化文档里关于 chunked prefill 的描述,它最大的对手是连续的 decode。一个满长度 prompt 的 prefill 会把 GPU 长时间占在算子绑定状态下,正在 decode 的请求两个 token 之间的间隔(ITL)立刻被拉开。

网上常见的表述是:continuous batching 让新请求能在每个 decode step 动态加入、完成的移除;chunked prefill 给 prefill 也加上分时。

这两件事是互补的。continuous batching 管"什么时候能进 batch",chunked prefill 管"一次能进来多少"。很多团队只上连续批处理,最后发现吞吐很稳、长 prompt 一进来还是全员卡顿。原因不在调度器要不要收人,而在这一轮的时间片分配。

块大小不是性能旋钮,是 SLA 旋钮

切得粗,单次 prefill 块大,prefill 本身算得迅速,但每块时长变长,decode 请求的 ITL 抖动随之变大。切得细,插队机会多,反过来又增加单步调度开销和解码过程被打断的次数。项目里常见做法是先把块大小设为和 max_num_batched_tokens 相关的一个参数,再对着真实流量调。

这里有一条可以先算出来的线。假设一个请求的 inter-token 延迟目标是 50ms,正常 decode 每步花 10ms,那么留给新进来的 prefill 每一步的预算只有 40ms 上下,块大小就应该压在"单卡这 40ms 能算多少 token"以内。用固定块大小直接按指令周期估算,再到线上做压测,比只看吞吐曲线有意义。

TetriInfer 那套实验也在说同一件事:它在 prefill 调度里加了 PrefillSchedBatch,用一个预排序批次变量来控制一次调度多少。整队列二十个请求,如果批次设为 10,就分两次调度,每次内部排序。目的是防止过度排序与长期饥饿。切成块之后的 prefill 本质是分时复用,需要配一个能控制每次调度宽度的闸门。

抢占的代价主要是 KV,不是算力

长请求被反复打断,最贵的成本不是重算的那部分 prefill token(现在有前缀缓存重用),而是一串带着状态的请求在等重排队:它们的 KV 块一直占着显存,又不产生输出。抢占策略要看的指标应该是"这段 KV 已经停了多久还没有新 token",而不是请求本身有多长。

FastServe 的 MLFQ Skip-Join 给出了一个思路:多级反馈队列,长时运行的请求降优先级,并支持抢占。但这种抢占用在 decode 侧时就要小心,前面文章的结论仍然成立:光靠优先级让长请求在等待中饿死,并不是。工程上就要有"逃逸阀":抢占超过 N 次或被降级到最后一档之后,长请求要么升回优先级,要么按预测剩余长度临时标记为不可抢占。

另一类实现更锋利。SCLS(slice-level scheduling)把每段最大生成长度分解成固定长度的片段,顺序处理后组合。它的好处是片段边界清晰,服务时间和内存都以片段为单位预估;代价是长回答被强制按片段结束,分段之间无法共享完整的 KV。拿它适用于哪些交互,心中要有数:一轮对话一次输出的场景更容易出问题。

收尾比切片更容易翻车

前面那条前排长请求整切片跑完 prefill 之后进入 decode,然后往往撞上另一个问题:没有合理停止条件。vLLM 论坛记录遇到过一类真实故障:请求持续一小时以上,模型一直输出重复图案。讨论的结论是 stop token、max_tokens 拦截缺失或无终止条件时模型进入重复循环,vLLM 的服务端本身不会自动识别"这内容明显异常,我要切断",主要依赖调用方设置参数,或在 API 层监控流。

切片的引入使这个问题变得更坏。块切得越细,单个切片里观测到的"已生成 token 数"和"已消耗时间"越难和最大长度的逻辑上限对齐。再加上一些调度参数以 token 或时间片为单位,可能让"完成但没退出"状态持续时间拉长。调度侧不能把无限排队当成稳态。

工程上,我偏向给每个被切的请求挂三条规定外挂:一类是单请求 token 上限、输出侧循环检测阈值、从进入候选到排空的墙钟超时。第二,抢占不超过限定次数,超过之后要进显式的慢池。第三,流式流回来的时候靠网关做模式断流,真的不行还要有流式断连接压底。

切与抢的顺序

最后给出一个粒度上的操作顺序,按这个顺序排过的方案线上不太会翻车。

  1. 先量化目标 ITL 每步预算,按预算定块大小,不把块大小当吞吐旋钮。
  2. 切片只在块粒度,避免同一请求的已缓存前缀和新的块前缀重复计算,要确保 cache 复用真实命中。
  3. 为防饥饿设重排队宽限:等待次数、被迫切换次数单独记账。
  4. 超时和循环检测做在流外侧,越接近模型越不灵活,越容易写出一个让人不敢看的参数量。

做完这一轮,通常会看到一个不太好看的结果:平均延迟改善明显,p99 基本没动。别急着回滚——长请求本来就该用尾巴来表示,翻车前必须先能看见才谈得上优化。

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

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

手机扫码阅读

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

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