news 2026/11/3 7:42:07

vLLM KV缓存用量26%却调度失败:PagedAttention分块机制与调度器排查调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
vLLM KV缓存用量26%却调度失败:PagedAttention分块机制与调度器排查调优

1. 这个现象到底在说什么

面板上 KV 用量显示 26%,看起来离打满还远得很,但调度器那边一条新请求都塞不进去,全部卡在等待队列里。这个场景做大模型推理服务的人大概率都遇到过,尤其是用 vLLM 部署 Qwen、DeepSeek、GLM 这类模型的时候。第一反应通常是“显存明明还有,为什么不让进”,然后开始怀疑是不是面板统计有问题、是不是调度器有 bug、是不是版本不对。

先把结论摆在前面:KV 用量百分比和调度器能不能放请求,根本不是同一个维度的指标。面板上的 26% 通常指的是“已分配的 KV 块占 KV 缓存池总块数的比例”,而调度器判断能不能放一条请求,看的是“这条请求需要的块数,在当前可用的空闲块里能不能凑出来,并且还要满足并发序列数、最大 token 数、块粒度对齐等一系列约束”。两者之间隔着一层“碎片化”和“预留策略”,所以 26% 完全可能对应“放不进去”。

这篇文章我会从 vLLM 的 EngineCore、Scheduler、Executor 交互流程讲起,把 KV 缓存的分块管理机制拆开,解释为什么会出现“面板低、调度满”的错位,然后给出可复现的排查步骤、参数调整方案,以及我在实际部署中踩过的坑。适合正在用 vLLM 做推理服务、被调度问题卡过、或者准备上生产环境的人参考。不管你是刚接触 vLLM 的新手,还是已经在调优的老手,这里面的排查思路和参数计算都能直接抄。

2. vLLM 调度链路与 KV 缓存的核心机制

2.1 EngineCore、Scheduler、Executor 三者怎么配合

vLLM 的推理引擎在架构上分成几层,理解这个分层是排查一切调度问题的前提。最上层是EngineCore,它负责接收请求、维护请求状态机、驱动整个推理循环。EngineCore 里面有一个核心组件叫Scheduler,调度器。Scheduler 每个 step 做一件事:从等待队列(waiting)和运行队列(running)里挑出一批请求,组成一个 batch,交给下面的Executor去执行。

Executor 是真正干活的那一层,它管理 GPU worker、模型权重、KV 缓存的实际显存分配。在 vLLM 的 V1 架构里,Scheduler 和 Executor 之间的交互是通过SchedulerOutput这个数据结构传递的,里面包含了这一步要跑哪些请求、每个请求分配了哪些 KV 块、要 prefill 多少 token、要 decode 多少 token。

关键点在于:Scheduler 做决策的时候,用的是自己维护的一份 KV 块账本,而不是实时去问 Executor“你现在还剩多少显存”。这份账本记录了总块数、已分配块数、空闲块数、每个请求占用了哪些块。面板上显示的 KV 用量,通常就是从这份账本里读出来的“已分配块数 / 总块数”。

所以当面板显示 26% 的时候,意思是账本上只有 26% 的块被标记为已分配。但调度器放不放请求,还要看这些“未分配”的块是不是连续的、够不够一条新请求用、以及有没有被预留。

2.2 KV 缓存为什么要分块管理

要理解“26% 却放不进去”,必须先理解 vLLM 的PagedAttention机制。传统做法是给每个序列预分配一大块连续显存,序列多长就占多长,浪费严重。vLLM 借鉴了操作系统虚拟内存分页的思路,把 KV 缓存切成固定大小的块(block),每个块存固定数量 token 的 KV 数据,默认一个块存 16 个 token。

这样做的好处是:序列的 KV 数据可以不连续地散落在显存里,通过块表(block table)来索引。显存利用率大幅提升,碎片问题也缓解了。但代价是:调度器必须按块来分配,一条请求需要多少个块,取决于它的序列长度除以块大小再向上取整。

举个例子,块大小是 16,一条请求当前序列长度是 100,那它需要 ceil(100/16) = 7 个块。如果它还要继续生成,每生成 16 个 token 就需要再要一个新块。调度器在决定放不放这条请求时,必须保证当前空闲块里能凑出它需要的块数,否则就只能让它继续等。

2.3 面板百分比的计算口径

面板上的 KV 用量百分比,不同版本、不同监控工具的口径可能不一样。常见的有两种:

第一种是已分配块数 / 总块数。这是最直观的口径,26% 意味着总块数里有 26% 被某个请求占着。但这里有个陷阱:被占着的块里,可能有一部分是“预留但未实际写入”的,比如为了应对后续 decode 而提前占的块。

第二种是实际写入 token 数 / 总容量 token 数。这个口径算出来通常比第一种低,因为块内可能有空洞。比如一个块存 16 个 token,但某个序列只用了这个块的前 3 个位置,剩下 13 个位置空着,按 token 算利用率就低,按块算就是满的。

注意:如果你看到的面板是第二种口径,那 26% 对应的实际块占用可能远高于 26%,因为块内空洞被平均掉了。排查时一定要先确认面板读的是哪个指标。

我在实际环境里见过最坑的一种情况:面板读的是“实际写入 token / 总容量”,显示 26%,但按块算已经占了 70% 多,再加上碎片,调度器自然放不进去。所以第一步永远是确认指标口径,别被百分比骗了。

3. 为什么 26% 的用量会卡住调度

3.1 块碎片化:空闲块不连续

这是最常见的原因。假设总共有 1000 个块,已分配 260 个,空闲 740 个。听起来很充裕,但如果这 740 个空闲块是零散分布的,而一条新请求需要一次性拿到 8 个块,调度器在分配时可能找不到足够的连续可用块——注意,vLLM 的块分配并不要求物理连续,但要求块表能索引到,且分配器(block allocator)的空闲列表里要有足够的块。

真正的问题出在分配器的空闲块管理策略上。vLLM 用的是类似内存池的分配器,空闲块以队列形式管理。如果大量块被长期占用且释放顺序混乱,空闲列表里可能出现“逻辑上够但实际分配失败”的情况,尤其是在开启了prefix caching(前缀缓存)之后,很多块被标记为“可共享但未释放”,调度器在计算可用块时会保守处理。

3.2 预留块与水位线机制

vLLM 的调度器有一个watermark(水位线)机制。默认情况下,调度器不会把 KV 缓存用到 100%,而是留出一部分作为缓冲,防止在 decode 过程中突然需要新块却分配不到。这个水位线默认是 0.01,也就是留 1%。但在某些配置下,或者在某些版本里,这个预留比例会更高。

更关键的是decode 阶段的块预留。一条请求在 prefill 完成后进入 decode,每生成一个 token 都可能需要新块。调度器在放请求时,不仅要算它当前需要的块,还要预估它未来一段时间需要的块。如果预估下来空闲块不够支撑到下一次调度,就会直接拒绝这条请求。

这就解释了为什么 26% 的时候放不进去:调度器算的不是“现在够不够”,而是“现在加上未来预估够不够”。如果当前运行的请求都在快速 decode,块消耗速度很快,调度器会非常保守。

3.3 并发序列数与 max_num_seqs 限制

除了块数,调度器还受max_num_seqs参数限制。这个参数控制一个 batch 里最多能有多少条序列。默认值在不同版本里不一样,V1 架构下默认可能是 256。如果你部署的时候没调,而实际请求的并发数已经接近这个值,那即使 KV 块充足,新请求也进不来。

面板上的 KV 用量不会反映这个限制,因为它统计的是块,不是序列数。所以你会看到“KV 才 26%,但调度器说满了”,实际上是序列数满了。

3.4 长序列请求的块需求被低估

还有一种情况:等待队列里排着一条超长序列的请求,比如要 prefill 32000 个 token。按块大小 16 算,它需要 2000 个块。如果总块数只有 3000,已用 780(26%),空闲 2220,看起来够。但调度器在分配时,可能因为chunked prefill(分块预填充)的配置,把这条请求拆成多个 chunk,每个 chunk 需要的块数加上当前 running 请求的块数,超过了可用量,于是这条请求一直排不进去,后面的短请求也被它堵住。

这就是典型的队头阻塞。面板只看总量,看不到队列里排的是什么。

4. 实操排查:从面板到调度器的完整链路

4.1 第一步:确认面板指标口径

不同部署方式读的指标不一样。如果你用的是 vLLM 自带的 metrics 接口(通常是/metrics),里面有几个关键指标:

指标名含义口径
vllm:gpu_cache_usage_percGPU KV 缓存使用率已分配块 / 总块数
vllm:cpu_cache_usage_percCPU KV 缓存使用率同上,针对 swap 空间
vllm:num_requests_running正在运行的请求数序列数
vllm:num_requests_waiting等待中的请求数序列数
vllm:num_requests_swapped被换出的请求数序列数

先拉一下这些指标,确认gpu_cache_usage_perc到底是多少。如果它显示 0.26,而num_requests_waiting很大,那问题就在调度侧。如果gpu_cache_usage_perc其实很高,只是你看到的面板读的是别的指标,那就是口径问题。

curl -s http://localhost:8000/metrics | grep -E "gpu_cache_usage|num_requests"

4.2 第二步:看调度器的日志

vLLM 在启动时如果加上--disable-log-requests之外的日志级别,可以看到调度器的决策过程。更直接的办法是看启动日志里的这几行:

INFO: Total number of GPU blocks: 3000 INFO: GPU block size: 16 INFO: Max number of sequences: 256 INFO: KV cache usage watermark: 0.01

这几行告诉你总块数、块大小、最大序列数、水位线。有了这些数,你就能手算:当前 26% 对应 780 个块被占,空闲 2220 个块。如果等待队列里有一条需要 2000 块的请求,那它加上水位线预留(30 块),就需要 2030 块,超过 2220 吗?不超。但如果同时还有 running 的请求在 decode,每个 step 都要新块,调度器就会保守。

4.3 第三步:检查 max_num_seqs 和 max_model_len

这两个参数是调度器的硬约束。max_num_seqs限制并发序列数,max_model_len限制单条序列的最大长度。如果max_model_len设得很大(比如 32768),而实际请求没那么长,调度器在预估块需求时会按最大长度算,导致保守。

# 启动时显式指定 python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --max-model-len 8192 \ --max-num-seqs 128 \ --gpu-memory-utilization 0.9

把max_model_len调到实际需要的长度,能显著减少调度器的保守预估。max_num_seqs根据你的并发需求调,但别超过 GPU 能承受的范围。

4.4 第四步:检查 prefix caching 的影响

如果开启了--enable-prefix-caching,很多块会被标记为可共享。这在多请求共享 system prompt 的场景下很省显存,但会让调度器的块账本变得复杂。有些块被多个请求引用,引用计数不为零,就不会释放。面板上这些块算“已分配”,但实际可能只有一份数据。

排查时可以先临时关掉 prefix caching,看问题是否消失。如果关掉就好了,说明是共享块的引用计数导致调度器误判可用块不足。

5. 参数调优与配置方案

5.1 调整 gpu_memory_utilization

这个参数控制 vLLM 能用多少 GPU 显存来放 KV 缓存。默认 0.9,意思是留 10% 给模型权重和其他开销。如果你把它调到 0.95,KV 缓存池会变大,总块数增加,26% 对应的绝对块数也增加,调度器可用的空闲块更多。

但别调太高,否则模型权重加载可能失败,或者运行中 OOM。我一般建议在 0.85 到 0.92 之间,根据模型大小和 GPU 显存来定。7B 模型在 24G 显存上,0.9 通常没问题;70B 模型在多卡上,要留更多余量。

5.2 调整 block_size

块大小默认 16,可以调到 32 或 64。块越大,块表越小,碎片越少,但块内浪费可能增加。对于长序列场景,大块更友好;对于短序列高并发场景,小块更灵活。

--block-size 32

调大 block_size 后,总块数会减少,但每条请求需要的块数也减少。关键是看碎片情况。如果之前是因为碎片导致放不进去,调大块可能缓解,也可能加剧,需要实测。

5.3 调整 max_num_batched_tokens

这个参数控制一个 batch 里最多处理多少 token。它直接影响 prefill 的吞吐和调度器的决策。默认值在不同版本里不一样,V1 架构下可能是 8192 或更高。如果设得太小,长序列请求会被拆成很多 chunk,每个 chunk 都要重新调度,增加队头阻塞的概率。

--max-num-batched-tokens 16384

调大这个值能让长序列一次处理更多,减少调度次数,但会增加单步的显存峰值。需要根据 GPU 显存和模型大小权衡。

5.4 开启 chunked prefill 的注意事项

Chunked prefill 能把长 prefill 拆成多个小 chunk,和 decode 请求混在一起跑,提升整体吞吐。但它会让调度器的块预估更复杂。如果发现长序列请求一直排不进去,可以试试关掉 chunked prefill,让长请求一次性 prefill 完,虽然单步慢,但能快速释放等待队列。

# 关闭 chunked prefill(部分版本默认开启) --no-enable-chunked-prefill

6. 常见问题与排查速查表

6.1 问题速查表

现象可能原因排查方法解决方向
KV 26% 但请求进不去块碎片化看空闲块分布调 block_size 或重启
KV 26% 但请求进不去max_num_seqs 满看 num_requests_running调大 max_num_seqs
KV 26% 但请求进不去队头长请求阻塞看等待队列长度分布调 max_num_batched_tokens
KV 26% 但请求进不去prefix caching 引用关掉 prefix caching 测试调整缓存策略
KV 26% 但请求进不去水位线预留看启动日志 watermark调低水位线(谨慎)
面板高但实际能进指标口径不同对比 metrics 接口统一监控口径

6.2 独家避坑技巧

技巧一:重启能解决 80% 的碎片问题。如果服务跑了一段时间后出现这个现象,重启 vLLM 进程通常能让块分配器回到干净状态。生产环境可以配置定期滚动重启,或者用多实例负载均衡来平滑。

技巧二:监控要看多个指标,别只看 KV 百分比。把num_requests_running、num_requests_waiting、gpu_cache_usage_perc三个指标放一起看。如果 running 接近 max_num_seqs,那就是序列数瓶颈;如果 waiting 持续大于 0 而 running 不高,那就是块分配问题。

技巧三:长序列请求单独走一个实例。如果业务里既有短对话又有长文档处理,混在一个实例里很容易队头阻塞。可以部署两个 vLLM 实例,一个配大 max_model_len 处理长序列,一个配小 max_model_len 高并发处理短请求,用网关按请求长度路由。

技巧四:日志级别调到 DEBUG 看调度决策。在测试环境把 vLLM 日志级别调到 DEBUG,能看到每个 step 调度器为什么拒绝某条请求。虽然日志量大,但排查问题时非常有用。

VLLM_LOGGING_LEVEL=DEBUG python -m vllm.entrypoints.openai.api_server ...

6.3 版本差异要注意

vLLM 的调度器逻辑在不同版本之间变化很大。V0 和 V1 架构的调度器完全不是一回事。V1 架构下,Scheduler 和 Executor 的交互更紧密,块管理也更激进。如果你从 V0 升级到 V1,之前调好的参数可能全部要重调。

另外,不同版本对gpu_cache_usage_perc的计算口径也可能变。升级后一定要重新确认面板指标的含义,别拿旧经验套新版本。

7. 我实际部署中踩过的坑

第一次遇到这个问题是在一个 Qwen2.5-72B 的多卡部署上。面板显示 KV 用量 28%,但所有新请求都超时。当时第一反应是显存不够,加了卡,没用。后来把日志调到 DEBUG,发现调度器每个 step 都在拒绝等待队列里的第一条请求,理由是“not enough blocks”。但按总量算明明够。

最后定位到是prefix caching 的引用计数问题。那个业务场景里所有请求共享一个很长的 system prompt,prefix caching 把这段 prompt 的块标记为共享,引用计数一直不归零。调度器在算可用块时,把这些共享块全部排除,导致可用块远小于面板显示的空闲块。关掉 prefix caching 后问题立刻消失,但吞吐下降了不少。最后的方案是保留 prefix caching,同时把gpu_memory_utilization从 0.9 调到 0.95,增加了总块数,缓解了可用块不足。

还有一次是在 L20 上部署 DeepSeek 蒸馏版,用的是 docker 镜像vllm/vllm-openai。面板显示 KV 26%,请求进不去。查了半天发现是max_num_seqs默认值太小,那个版本的默认值是 64,而业务并发需求是 200 多。调大到 256 后解决。这个坑的教训是:别假设默认值适合你的场景,启动参数一定要显式指定。

第三个坑是关于 chunked prefill 的。有个长文档问答的业务,请求长度动辄上万 token。开启 chunked prefill 后,长请求被拆成很多 chunk,每个 chunk 都要排队,结果短请求也被堵在后面。后来改成短请求和长请求分实例部署,长请求实例关掉 chunked prefill,问题才解决。

8. 监控与容量规划建议

8.1 建立多维监控面板

单看 KV 百分比不够,建议至少监控这几个维度:

  • 块维度:已分配块数、空闲块数、总块数、块碎片率
  • 序列维度:running 序列数、waiting 序列数、swapped 序列数
  • 延迟维度:prefill 延迟、decode 延迟、排队时间
  • 吞吐维度:每秒处理 token 数、每秒完成请求数

把这些指标放在同一个面板上,出现“KV 低但调度满”时能一眼看出是哪个维度的问题。

8.2 容量规划的经验公式

粗略估算 KV 缓存能支撑多少并发,可以用这个公式:

可用块数 = 总块数 × (1 - 水位线) 单请求块数 = ceil(序列长度 / block_size) 最大并发 = 可用块数 / 单请求块数

但这个公式假设没有碎片、没有 prefix caching、没有 decode 预留。实际并发要打 6 到 7 折。比如算出 100 并发,实际按 60 到 70 来规划比较稳。

8.3 压测验证

上线前一定要压测。用真实分布的请求长度去压,别只用固定长度的请求。压测时观察 KV 用量和 waiting 队列的变化,找到调度器开始拒绝请求的临界点。这个临界点对应的并发数,才是你实际能用的容量。

压测工具可以用 vLLM 自带的 benchmark 脚本,也可以用 locust 之类的通用工具。关键是请求长度分布要贴近真实业务。

9. 几个容易被忽略的细节

9.1 CPU swap 空间的影响

vLLM 支持把 KV 块换出到 CPU 内存,腾出 GPU 块给新请求。如果开启了 swap,面板上的 GPU KV 用量可能不高,但 CPU swap 空间满了,调度器同样会拒绝请求。排查时别忘了看cpu_cache_usage_perc。

9.2 多实例部署的负载均衡

如果部署了多个 vLLM 实例,前面的负载均衡器如果只是轮询,可能把请求打到已经满的实例上。更好的做法是根据实例的num_requests_waiting做加权路由,把新请求优先发给等待队列短的实例。

9.3 模型本身的 KV 头数影响

不同模型的 KV 头数不一样,直接影响每个 token 的 KV 大小。比如 GQA 模型(分组查询注意力)的 KV 头数比 MHA 少,同样显存能放更多 token。换模型时,KV 缓存容量会变,之前的参数可能要重调。

9.4 量化对 KV 缓存的影响

如果用 FP8 或 INT8 量化 KV 缓存,每个 token 的 KV 占用会减半,总块数翻倍。但量化会带来精度损失,需要评估业务是否能接受。vLLM 支持--kv-cache-dtype fp8,在显存紧张时可以考虑。

10. 写在最后的一点个人体会

这个问题我前前后后遇到过七八次,每次的具体原因都不一样:有碎片问题、有参数默认值问题、有 prefix caching 问题、有队头阻塞问题。但排查思路是统一的:先确认面板口径,再看调度器日志,然后逐个排查块、序列、参数三个维度。

最深的体会是,vLLM 的调度器设计是偏保守的,它宁可让请求等着,也不愿意冒险分配块导致运行中 OOM。所以“面板低但放不进去”在它看来是正常行为,不是 bug。理解这一点之后,调优的方向就清楚了:要么增加可用块(调 gpu_memory_utilization、调 block_size),要么减少单请求块需求(调 max_model_len、关 prefix caching),要么放宽调度约束(调 max_num_seqs、调水位线)。

最后分享一个我常用的快速判断方法:如果num_requests_waiting大于 0 且持续增长,同时gpu_cache_usage_perc低于 50%,那基本可以确定是调度约束问题而不是显存不足。这时候优先查max_num_seqs和 prefix caching,这两个是最常见的元凶。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 7:42:07

化工行业CRM系统:客户资源管理与企业价值提升

1. 化工行业CRM系统的特殊性与价值化工行业作为典型的B2B领域,其客户关系管理(CRM)系统与传统快消品行业有着本质区别。我在为多家化工企业实施CRM系统的过程中发现,这个行业的客户生命周期往往长达5-10年,单个客户的价…

作者头像 李华
网站建设 2026/11/3 7:41:40

前端DOM操作实战:微博发布功能实现解析

1. 微博发布功能实现解析这个微博发布案例是一个典型的前端DOM操作练习,通过HTML、CSS和JavaScript的配合,实现了一个简易的微博发布和展示功能。下面我将从技术实现角度详细拆解这个案例。1.1 页面结构与样式设计整个页面采用经典的"上发布区下展示…

作者头像 李华
网站建设 2026/11/3 7:41:15

SpinWait在高并发消息分发中的优化实践

1. 项目背景与核心挑战在即时通讯和客服系统这类高并发场景中,消息分发模块的性能直接决定了整个系统的吞吐量和响应速度。传统客服系统每秒需要处理成千上万条消息,这些消息需要在用户与客服之间快速路由,同时还要支持消息状态同步、已读回执…

作者头像 李华
网站建设 2026/11/3 7:41:15

ADS功放协同仿真:Fast Cosim一次算全ACPR、EVM与PAE

简介:面向射频与通信电路设计者的一份应用笔记,聚焦如何在Keysight ADS中借助Ptolemy协同仿真完成ACPR、EVM与PAE三项指标的评估,适合已具备一定功放设计基础、需要把仿真流程落到实处的工程师与相关专业学习者。文档以EDGE调制信号驱动待测功…

作者头像 李华
网站建设 2026/11/3 7:40:39

modbus_tk实战:RTU主站调试工具的轻量级配置与字节级排错

1. 项目概述:为什么一个MODBUS RTU主站调试工具值得花时间重做一遍你手头有一台PLC、一块温湿度传感器模块、或者一套工业现场的电表采集终端,它们都支持MODBUS RTU协议——这个在工业自动化领域扎根三十多年、至今仍在产线设备里嗡嗡运转的“老派通信协…

作者头像 李华
网站建设 2026/11/3 7:40:36

抖音推荐算法深度拆解:从完播率到流量池赛马的运营方法论

做抖音运营的,十个有九个研究过“抖音推荐算法”。这不算错,但很少有人把算法这件事真正讲明白——讲来讲去不是玄学养号,就是“三步上热门”的营销话术。我自己用几十个账号的发布数据和真实播放表现做了大半年的反向校验,今天就…

作者头像 李华