vLLM 的 V1 引擎最近被讲得挺玄。尤其是那个调度器,我之前绕了挺久,因为 V0 留下的刻板印象太深了。
我以前一直以为,推理嘛,一个 step 就是一条线走到底。先做 prefill,等 prompt 处理完吐出第一个 token,再进 decode 阶段一个一个往外蹦。画成图大概是这样:
一个 step 的任务:
[.prefill.....] ──等完──► [decode] [decode] [decode] ...
一条流水线,各管各的。V0 基本上就是这么干的。
但前两天我去翻了 Aleksa Gordic 那篇拆 vLLM V1 源码的文章,对着调度循环的代码看,第一版我画错了。V1 的调度器根本不这么排队。
重来。V1 的调度器能在一个 step 里把 prefill 和 decode 请求混排在一起。画出来其实是这样的:
一个 step:
┌─────────────────────────────────────────────┐
│ decode 请求们(已经在跑的) │ prefill(新来的)│
│ [d] [d] [d] [d] [d] [d] [d] │ [prefill块] │
└─────────────────────────────────────────────┘
它不是一个接一个,而是把已经在生成 token 的 decode 请求打包,再塞个刚进来的新 prefill 请求一起算。混排。
我盯着这块看了半天,这玩意儿不卡死吗?新来的长 prompt 做 prefill 不是得占算力吗?
原来如此。调度器在这里面有个绝对优先级:正在 running 队列里的 decode 请求是大爷,得先紧着它们喂。新来的 prefill 请求哪怕排着队,也不能把正在吐字的请求给卡住。
而且如果 prompt 太长,怕它一个人霸占整个 step 怎么办?V1 有个机制:直接把长 prompt 切成小块,切成那种不会一口吞掉全局算力的颗粒度。
打个比方:这就像饭店后厨。V0 是炒完一桌大菜(prefill)再去顾散客的单(decode)。V1 是锅子全开,炒着散客的快菜(优先 decode),顺手把新来的大菜切一切(分块 prefill)扔进同一个锅里一起炖。
这个比喻在这里漏风了:真实厨房可能串味,但 GPU 的计算资源是可以通过 roofline 模型切分复用的。只要 batch 没饱和,把 prefill 塞进 decode 的间隙,吞吐量就能往上拉。
懂了的那一刻,真的咔哒一下扣上了。💡 把这两个阶段混排在一个 step 里,不搞一刀切,吞吐量和延迟的坑一下全填平了。这玩意儿真的太酷了。
下期想画开哪个概念?我想去拆那个用有限状态机做 guided decoding 的逻辑,你们觉得呢?
