vLLM 请求调度完全指南:高并发下如何接住几百个并发请求?
2026/9/13 2:17:19 网站建设 项目流程

vLLM 请求调度完全指南:高并发下如何接住几百个并发请求?

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

GPU 利用率钉在 95%,新请求的首 token 却要等 8 秒。vLLM 的请求调度正是为解决这类高并发痛点而生:用连续批处理做负载均衡、用 KV Cache 块做资源分配、靠抢占在内存吃紧时兜底。下面从一个请求的完整旅程拆解。

一屏看懂 vLLM 调度系统的全貌

把 vLLM 想象成一家只有一口灶(GPU)的餐厅:调度器是传菜员,每来一个请求就像一张点单;它每“步”看一眼预算还剩多少,决定哪些菜立刻下锅、哪些先记号。而每个请求占用的 KV Cache 块 🅿️,就像车位——车(请求)走不了一定能立刻腾出车位,车在就得占着。

调度器每个引擎步骤只做一件事:在“本步最多算多少 token”的预算内,把 waiting 和 running 两个队列里的请求装进一个批。V1 引擎中不再有独立的 swapped 队列,等待、运行、被抢占三条线全部收敛到这条主线上。宏观架构可参考下图:

跟踪一个请求:从进队到被抢占的完整旅程

请求生命周期只有三个主干状态(定义在vllm/v1/request.pyRequestStatus):

  • 到达 → 排队:请求进入 waiting 队列。队列策略由policy决定:fcfs先到先服务,priority按优先级数值小的先走(vllm/config/scheduler.py)。这就是第一层负载均衡——队头顺序决定了谁先拿到 GPU。
  • 排队 → 预填充:调度器先遍历 running 队列保证老请求不断档,再看 waiting 队列。新请求要通过两道闸门:本步剩余令牌预算、KV Cache 剩余块数。通过后才分配块、进入 running。
  • 预填充 → 解码:长 prompt 不会一口气算完,而是被切成多段(分块预填充),每段和一批解码请求混在同一个 forward 里。
  • 完成或被抢占:满足停止条件就释放块、结束;若后续请求挤不进来了,调度器会挑 running 里的请求踢回 waiting,清空它的 KV 块并重置进度,腾地给新请求。

源码入口在 vllm/v1/core/sched/scheduler.py,其中schedule()方法就是上面这条线的实现。

三大核心机制逐个讲清

连续批处理与分块预填充:长 prompt 为什么不再卡死短请求

一句话类比:传菜员不盯着“一桌菜齐了才端”,而是“每口灶空出来就塞下一道菜”。连续批处理(Continuous Batching)让批次不再是固定名单——每步结束有请求退场,新请求立刻补位;分块预填充(Chunked Prefill)则把长 prompt 切成小块,和别人的解码一起跑,避免 2 万 token 的 prompt 独占一整个前向。

# 每步调度伪逻辑(自拟概括,见 scheduler.py 真实实现) token_budget = max_num_batched_tokens for req in running + waiting: # 先保 running,再填 waiting n = min(req.remaining_tokens, token_budget) if kv_manager.can_allocate(req, n): schedule(req, n) token_budget -= n

这段循环就是“预算装箱”:max_num_batched_tokens是每步的总容量,谁排在前面谁先装。

调它会发生什么:调大max_num_batched_tokens,吞吐上升、单步延迟略增;调小则短请求 TTFT(首 token 延迟)更稳。默认已开启分块预填充,long_prefill_token_threshold可给单个长请求设每步上限,防止它一次吃满整锅预算。

KV Cache 块管理与水印:怎么在内存边缘稳住不抖动

一句话类比:KV Cache 是停车场,车位按 16 个 token 一格编号,请求的车停哪几格由块表记录;水印(Watermark)就是“永远空出最后几格,不让停车场贴满”。

块分配逻辑在 vllm/v1/core/kv_cache_manager.py。水印的关键细节:它只对 waiting/preempted 请求“放行”时起作用,已在 running 的解码请求不受影响——保护的是“新客入场”,不是“老客续住”。

# 水印准入检查伪逻辑(自拟概括) free = kv_manager.num_free_blocks() need = blocks_for(req) if req in (waiting, preempted): ok = free - need >= watermark_blocks # 留余量 else: # running 请求 ok = free >= need

watermark是 KV 块总数的比例,默认 0.0(关闭)。

调它会发生什么:内存偏紧时开watermark=0.01~0.05,能显著减少“刚放行就驱逐、接着连锁抢占”的抖动;开到 0.1 以上则空闲块被长期占用,吞吐明显下降。

抢占怎么选:V1 为什么用“重算”而不是 Swap

一句话类比:Swapping 像把车临时挪去楼上的机械车库(CPU 内存),回来再挪回楼下;而 vLLM V1 的选择是“清场重停”——直接释放车位,请求回队首,等下次进场时重新预填充。

V1 的抢占路径很短(伪代码概括自_preempt_request):

def _preempt(req): # 自拟伪代码 free_blocks(req) # 释放全部 KV Cache 块 req.num_computed_tokens = 0 # 进度清零 req.status = PREEMPTED # 放回 waiting 队首

为什么放弃 Swap?GPU↔CPU 的块搬运带宽是瓶颈,长序列挪一次的成本常常比重算还高;而且 V1 抢占后重新预填充时能命中前缀缓存,实际重算量远小于表面进度(前缀缓存见 docs/features/automatic_prefix_caching.md)。V0 时代的双队列 + swap 机制已被这条更简单的路径取代。

调它会发生什么:抢占次数可看监控里的preemptions指标。频繁抢占说明 KV 容量或准入太激进——优先加大 KV 池、开水印,而不是加机器;被抢占请求回到 waiting 队首,所以它恢复时不会被新请求插队。

高并发下的关键调优参数速查表

参数含义默认值建议范围
max_num_batched_tokens每步前向最多算的 token 数(批处理容量)20482048–16384
max_num_seqs单步最多并发的序列数12864–512
block_size一个 KV 块容纳的 token 数1616–64
watermark准入时预留的 KV 块比例0.00–0.1
long_prefill_token_threshold单步内单个长请求的预填充上限0(关闭)512–2048
policy队列策略:fcfs 或 priorityfcfs按业务
max_num_queued_reqs在途请求上限,超限直接 503 拒绝不限约 DP×max_num_seqs
enable_chunked_prefill是否开启分块预填充True保持 True

两组场景化配置:

# 场景一:高并发短请求(问答类) LLM(model=m, max_num_batched_tokens=8192, max_num_seqs=256, policy="fcfs") # 大预算装满批,fcfs 保证公平 # 场景二:长序列生成(文档类) LLM(model=m, max_num_batched_tokens=4096, max_num_seqs=64, long_prefill_token_threshold=512, watermark=0.02) # 限制单请求每步占用,水印防驱逐抖动

短请求场景把容量拉满换吞吐;长序列场景收窄单请求每步占用并开水印,用一点容量换稳定。

结语

vLLM 请求调度的本质是两道预算(令牌数、KV 块)加一条兜底线(抢占重算),把负载均衡和资源分配拆到了每一步、每个块。先盯住max_num_batched_tokens与 KV 容量两个旋钮,再看抢占指标,绝大多数高并发问题都能定位到参数层。更多设计细节见 docs/design/arch_overview.md 与 docs/usage/faq.md。

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

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

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

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

立即咨询