vLLM 请求调度一篇讲透:高并发下如何平衡吞吐量与延迟
2026/9/13 7:51:05 网站建设 项目流程

vLLM 请求调度一篇讲透:高并发下如何平衡吞吐量与延迟

【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm

vLLM 是一个以高吞吐、低显存著称的 LLM 推理引擎,它的请求调度是整个引擎的性能中枢:连续批处理、分块预填充、抢占式的 GPU 资源分配,都发生在调度器这一步。这篇只讲三件事:调度器在管什么、优先级怎么排、显存不够时它怎么救场,最后给你能直接抄的参数配方。

为什么 LLM 服务需要专门的调度器

想象一个场景:100 个用户同时向你的推理服务提问,GPU 只有一块。此时吞吐量(单位时间处理的请求数)和延迟(每个请求从发起到拿到首 token 的时间)是打架的:让一个长 prompt 一口气跑完 prefill,其他 99 个用户的首 token 就得干等;反过来每步只让每个请求算一点点,单个请求的延迟又被拉长。

传统服务框架用静态批处理:凑满一批,全部跑完,再开下一批。批次里哪怕有请求提前结束,GPU 也得空转到批次收尾。vLLM 的连续批处理把粒度压到了"每一步":

说白了,完成的请求每步立即离场,新请求立即补进来,GPU 几乎没有空转。代价就是每一步都要重新算一遍"这一批放谁、各放多少 token"——这件事必须由一个专门的调度器来做,这正是 Scheduler 存在的理由。

vLLM 调度器里到底在管什么

vLLM 请求调度围绕三个对象展开,关系如下:

  • 调度器:维护 waiting(等待)和 running(运行)两个队列,每个 step 决定处理哪些请求、每个请求推进一步多少 token。
  • KV 块管理器(KV 缓存即模型"记住"前文内容的笔记本,kv_cache_manager.py):把显存切成固定大小的块,跟踪块被哪个请求占用,并支持相同前缀的请求直接复用已算好的块,省掉重复计算。
  • 块池(block_pool.py):维护空闲块队列和 LRU 驱逐顺序,是"最后一块砖从哪拆"的决策者。

请求的生命周期可以浓缩成一张状态图:

这里有个坑:vLLM 的 V1 引擎默认走"重算式"抢占——显存不够时牺牲一个请求、丢它的 KV 缓存、移回等待队列,而不是把 KV 缓存搬到 CPU 内存再换回来(老版 V0 才默认交换式)。所以图里没有 SWAPPED 状态,如果你按老教程找它,会找不到的。

请求进来了,vLLM 怎么排优先级

排优先级其实是三层机制在协同。

第一层是队列出队顺序,由scheduling_policy控制,取值fcfs(默认,先来先服务)或priority(按请求携带的优先级数值插队)。第二层是 step 内的处理顺序:每个 step 先照顾 running 队列里的请求(decode 的 token 优先拿到预算),再把剩余额度分给 waiting 里捞上来的新请求。第三层是 token 预算,两个参数一句话版本:

  • max_num_batched_tokens:单个 step 允许处理的 token 总预算,默认 2048,直接决定吞吐与延迟的天平倒向哪边;
  • max_num_seqs:单个 step 最多并发多少个请求,默认 128,决定批的"宽度"。

真正让长短请求和平共处的机制是分块预填充(chunked prefill):长 prompt 不再一口气 prefill 独占 GPU,而是每步只推进一小段,和其他请求的 decode 混在同一个批次里算。配套参数long_prefill_token_threshold给"长请求"单步推进的 token 数设上限,防止一条超长 prompt 把整个 step 的预算吃光,把其他请求的首 token 延迟顶爆。

从源码注释看,V1 调度器内部甚至没有独立的 prefill/decode 阶段:每个请求只记一个"已计算 token 数",调度器把预算分给"还没追平"的请求。这个统一视角让分块预填充、前缀缓存、投机解码全部天然兼容,不用为每种特性写特殊分支。

GPU 显存不够时,vLLM 会做什么

KV 缓存被切成固定大小的块(默认 16 或 32 个 token 一块),请求的"块表"维护着逻辑位置到物理块的映射。vLLM 的 GPU 资源分配,本质上就是对这些块做分配、复用和驱逐三件事。

前缀缓存复用是块管理最大的收益来源:相同前缀的请求直接挂载已缓存的块,多轮对话、Agent、RAG 这类场景命中率通常很高。块池按 LRU 顺序管理空闲块:带哈希的已缓存块排到队尾(保留久一点),无哈希的普通空闲块排队首(优先被复用)。当新请求来了而空闲块不够,就从队头驱逐最久没被用过的缓存块腾位置——这个动作叫缓存驱逐,频繁发生就说明缓存命中率在往下掉。

水位机制则预留一小撮块作缓冲,避免"每分配一次都触发一次驱逐"的抖动。预留太少显存贴边运行、抖动频繁;预留太多白白浪费容量。

抢占发生时,被牺牲的请求有两类退路:

  • 交换式(SWAP):把它的 KV 块逐块搬到 CPU 内存,资源空出来后搬回去续跑。省的是算力,代价是内存带宽和一笔 CPU 内存占用,KV 越长搬运越贵。
  • 重算式(RECOMPUTE):直接释放 KV 块,请求回等待队列,资源空出来时从零重新 prefill。省 CPU 内存,代价是白算一遍;请求已经生成了上千 token 时心疼,但胜在简单、不依赖主机内存。

资源流向一张图说清:

V1 引擎默认只用重算式:显存不够就从 running 队列尾部挑一个牺牲者,释放它的块、移回 waiting 重新排队。抢占次数是一个值得盯紧的信号——偶尔发生正常,持续发生说明容量根本不够,该扩容或限流,而不是继续拧参数。

落地调优:三个场景的参数配方

下面这张表覆盖三类典型负载,参数取值都给了方向性理由,实际压测时以你的硬件为准:

参数高并发短问答长文档生成混合负载
max_num_batched_tokens8192(拉高,多请求同跑)4096(压低,保护长请求)8192
max_num_seqs256(吃满并发)64(长请求占块多,别贪)128
block_size32(块少,管理开销低)16(碎片小,长文本友好)16
enable_prefix_caching开(多轮对话命中高)开(长前缀复用省算力)
long_prefill_token_threshold0(短 prompt 用不上)1024(长 prefill 分块限速)512(中间值,两边都不难受)
max_num_queued_reqs512(防洪峰压垮队列)64(长请求排队成本太高)256

三个踩坑提醒,各一句:

  • 短问答:别把max_num_batched_tokens一路拉到 16384,首 token 延迟会跟着水涨船高,TTFT 和吞吐是拿一个换另一个。
  • 长文档:并发一高显存先爆,抢占风暴比"慢"更难看,max_num_seqs宁小勿大。
  • 混合负载:long_prefill_token_threshold设小了长请求卡住,设大了短请求陪跑,先用 512 起调,再按监控微调。

怎么判断调度是否健康

参数调完不看指标等于盲调。四个指标按优先级排:

  • 前缀缓存命中率:低于 10% 说明负载前缀差异太大,调参空间有限,该考虑请求侧路由(按前缀分组打到同一实例);高于 50% 说明命中良好。
  • 累计抢占次数:健康状态应接近 0。持续上涨说明容量不足,往扩容、降max_num_seqs或收紧max_num_queued_reqs方向调,而不是加大 token 预算。
  • 运行中请求数:长期贴着max_num_seqs不动,说明批被卡宽了,往上加并发。
  • 等待队列长度:持续正增长说明吞吐跟不上到达速率,先加max_num_batched_tokens,再加实例;队列长期为空则说明容量冗余,可以降配。

写在最后

调度是 vLLM 性能的核心杠杆:连续批处理决定 GPU 利用率,分块预填充守住延迟下限,抢占与块池兜住显存边界,三者拧在一起才是完整的推理服务调优。建议的下一步很朴素:先用默认配置压一遍,记录 TTFT、TPOT 和吞吐的 baseline,然后每次只改一个参数、用相同负载重跑对比——参数之间是耦合的,单看某一个的"直觉收益",多半会被其他参数吃掉。

【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询