vLLM 请求调度拆解:一条请求的 5 个生死关卡
2026/9/13 3:21:21 网站建设 项目流程

vLLM 请求调度拆解:一条请求的 5 个生死关卡

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

vLLM 的请求调度是引擎里最容易被低估、上线后最容易被骂的模块。它要同时伺候两个互斥的目标:吞吐量——单位时间吐出多少 token;延迟——每个请求等多久拿到下一个 token。想把 batch 堆大,单请求就变慢;想把单请求喂饱,吞吐就塌下来。vLLM 的取舍逻辑很直白:以「单次 forward」为调度单位,每一步在 token 预算内重新组批,谁都不能长期霸占 GPU。下面跟着一条请求,走完它在调度器里经历的五个生死关卡:入队 → 组批 → 分块预填充 → 抢 KV 块 → 被抢占后重算。

一个 token 预算,撑起连续批处理

先给结论:vLLM 的调度粒度是 token,不是请求。

调度器在每个 forward 之前调用一次schedule(),产出物本质上是一张表:{请求ID: 这一步要处理的 token 数}。新请求可能是几百个 prompt token,生成中的请求就是 1,分块预填充中可能落在中间。骨架大致长这样:

class Scheduler: self.waiting = create_request_queue(policy) # 新请求 + 被抢占的请求 self.running = [] # 正在 GPU 上的请求 def schedule(self): token_budget = self.max_num_batched_tokens # 1. 先服务 running:每个请求本步 +1 token,扣预算 # 2. 预算还剩,就从 waiting 按策略拉新请求进来 # 3. 拉不进的留在队列,输出本步 {req: num_tokens}

这就是连续批处理的全部秘密——不等整批跑完,新请求随时插进正在跑的批次;某个请求生成完最后一个 token,它的坑位下一步就能被别人顶掉。对比静态批处理:整批一起进、整批一起出,最先完成的那个请求也得陪着最慢的等。GPU 在等的人身上空转,连续批处理把这堆空转填满了。

vLLM 的schedule()里,running 永远排在 waiting 前面。这是第一层资源隔离:已进入解码的请求每个 step 都能领到 1 个新 token,新来的 prefill 只花预算的剩余部分。预填充是计算密集型,解码是访存密集型,两者混在一个 batch 里,计算空隙被访存摊掉,GPU 利用率才稳得住。

长请求不拖垮短请求:分块预填充

一个 3 万 token 的长 prompt 进来,如果必须一口气 prefill 完,这一步的延迟就是别人的十倍,后面排队的短请求集体陪葬。分块预填充(chunked prefill)把这个长任务切成 N 片,每片只占一个调度 step 的一部分 token 预算,跨多个 step 消化完。

它的代价是首 token 延迟(TTFT)变长——prompt 要分好几步才处理完。收益是单步延迟上界被max_num_batched_tokens锁死,任何单个请求都无法击穿。所以这个参数是延迟与吞吐之间的总阀门:调大,一步吃更多,吞吐上去了,但步时变长,短请求的 ITL(token 间隔)跟着抖;调小,反之。默认 2048 只是测试兜底值,真实部署应该按硬件标定。

vLLM 还留了一个long_prefill_token_threshold(默认 0,即关闭),给长 prompt 打标记,避免多个超长请求的 chunk 同时挤进同一个 step 把预算吃满。另外值得看一眼max_num_queued_reqsmax_num_queued_tokens:这是 API server 侧的容量闸门,超了直接 503 拒绝,把压力挡在引擎外,而不是让队列无限积压。

组好批只是拿到了入场券,下一步要付钱——显存里的 KV 块。钱不够的时候会发生什么?

GPU 快不够用时:块、水位线与抢占

KV 缓存是 LLM 推理里最大的显存开销,vLLM 的管理单位是块:整块显存切成固定block_size个 token 一格,请求按 token 数领块,每多一个 token,多领的格子数就是ceil(token数 / block_size)

块写满之后,vLLM 会把它登记进前缀缓存的哈希表(默认开启):键是块内 token 的哈希,值是块 ID。之后任何请求只要前缀的哈希撞上了,直接复用已有块,对应那部分 prompt 一步不用算。共享系统提示词、多轮对话、Agent 循环场景,命中率可以非常高。

新请求(以及被抢回来的请求)准入时要过一道检查,核心是水位线(watermark)——给等待队列预留的缓冲,防止"放进来一个新请求,下一步就被迫抢占一个老请求"的抖动:

# 只对 WAITING / PREEMPTED 状态、且已有请求在跑的请求生效 watermark_blocks = int(watermark * total_blocks) if num_blocks_to_allocate + watermark_blocks > num_free_blocks: return None # 本步不放进来,等下一轮

👉 注意这条规则只拦「新面孔」。正在解码的请求追加块时不吃水位线,所以水位线不是全局配额,只是准入缓冲。

块真的耗尽时,vLLM 会抢占。这里它做了一个相当激进的选择:v1 架构干脆移除了 v0 时代的 GPU↔CPU 交换,只剩重算这一条路。选谁当受害者有讲究——FCFS 下踢 running 列表里最后一个(最晚进入的),priority 模式下按优先级挑;被抢的请求num_preemptions += 1,块全部释放,然后插回 waiting 队首,从头重算。

为什么不做交换?把整段 KV 经 PCIe 搬到 CPU 再搬回来,带宽成本经常高于直接重算一遍,何况还要占主机内存。代价是前缀缓存命中的部分也要重算——所以缓存命中率直接影响抢占的实际痛感。块被释放后回到空闲队列,前缀缓存的驱逐走 LRU:最久没被复用的块先被顶掉腾地方,缓存命中率和重算开销就在这儿平衡。

调优与监控:两个场景的参数基线

上面所有行为都由下面这张表里的几个旋钮决定。

参数作用默认值调大或调小的影响
max_num_batched_tokens单个调度 step 的 token 总预算2048(真实值在启动时按硬件重算)调大:吞吐升、TTFT 降,但单步时延变长、decode ITL 抖动;调小:延迟平稳、吞吐掉
max_num_seqs同时 running 的请求上限128调大:并发上限升,单请求分到的算力稀释;调小:长输出请求的 TTFT 更稳
gpu_memory_utilization分配给权重+KV 的显存占比0.9决定 KV 块总数,调大意味着块更多、更少抢占,但离 OOM 更近
block_size每块 token 数16调大:管理开销降、前缀缓存粒度变粗(浪费尾部);调小:分配细、命中灵活,块表更大
long_prefill_token_threshold多长的 prompt 算"长请求"0(关闭)调大:更多请求被当长请求限流;调小:更严格地压住长 prefill 的并发
policy等待队列策略fcfs换成 priority 后按优先级值排序(小者先),同级再按到达时间;无 SLA 分层需求别开
max_num_queued_reqs引擎内排队请求上限None(不限)调小:超载时直接 503 挡在门外;调大:队列积压变深,TTFT 恶化更慢但更久
enable_prefix_caching前缀缓存复用True关掉则每次全量重算,只建议在对延迟抖动极敏感且无重复前缀时考虑

场景 A:高并发短请求(客服问答、分类打标)

# 目标:吞吐拉满,同时别让 TTFT 被长 prompt 击穿 max_num_batched_tokens = 16384 # 一步吃更多 max_num_seqs = 256 max_num_queued_reqs = 512 # 排队超深直接 503,保 SLA policy = "priority" # 高优业务先走 enable_prefix_caching = True

场景 B:长序列生成(长文档问答、Agent 长上下文)

# 目标:显存扛得住长 KV,步时别被单条长请求拖垮 max_num_batched_tokens = 4096 # 压低单步预算,保住 ITL max_num_seqs = 64 gpu_memory_utilization = 0.92 enable_prefix_caching = True # 系统提示词 + 历史轮次全靠它省重算 long_prefill_token_threshold = 8192 # 长 prompt 限流,防 chunk 挤爆单步

上生产后值得盯的指标:运行中请求数与等待队列深度(调度是否饱和)、KV cache 使用率(离耗尽多近)、前缀缓存命中率(复用在不在省算力)、抢占次数(👉 它持续上涨就是块不够的信号,优先查gpu_memory_utilization和块数,其次才是并发量)、TTFT 与 ITL 分位数。

落地清单

  1. 前缀缓存默认开着,先确认没被关掉;共享系统提示词的服务这是最大的一笔免费算力。
  2. max_num_batched_tokens按硬件实测标定,别用默认值裸奔;它是延迟与吞吐的总阀门。
  3. 有延迟 SLA 就设max_num_queued_reqs/max_num_queued_tokens,超载时 503 比积压好。
  4. 抢占次数、等待队列深度进告警——它们涨起来的时候,调参窗口往往只剩几分钟。
  5. 长短请求混跑严重的服务,试试long_prefill_token_threshold,再考虑 priority 队列。

最后一个问题留给读者:v1 用"重算"彻底换掉了"交换",赌的是重算比 PCIe 往返便宜。这个赌注在你的互联带宽和你的模型尺寸下还成立吗?跨机 prefill/decode 分离把 KV 搬上了网络之后,抢占的成本函数已经不是 v1 写下的那个样子了。

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

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

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

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

立即咨询