1. 这个面板显示 26% 却塞不进请求到底卡在哪线上跑推理服务的人大概率都撞见过这个画面监控面板上 KV Cache 用量明晃晃写着 26%看着还有大把余量可调度器那边一条新请求都排不进去客户端一直转圈日志里全是等待调度的记录。第一反应通常是面板坏了或者是不是有内存泄漏然后开始重启、扩容、加机器折腾一圈发现根本没解决问题。这个现象的本质是面板统计口径和调度器准入判断口径不一致。面板上的 26% 通常是当前已分配的 KV 块占全部 KV 块的比例而调度器决定能不能放一条请求进来看的是这条请求需要的最大 token 数对应的 KV 块能不能在剩余块里连续凑出来。一个是全局平均视角一个是单请求峰值视角两者天然会对不上。再加上分页管理、块粒度、并发上限、抢占策略这些机制叠在一起就会出现看着空、实际满的诡异状态。这篇内容适合正在用 vLLM 这类推理引擎做线上部署的工程师、负责容量规划的运维同学以及被面板和实际不一致折磨过的调度方向开发者。我会把 KV 缓存的分页机制、调度器的准入逻辑、面板指标的采集来源这三条线拆开讲清楚再给出一套可复现的排查流程和参数调整方案。看完你应该能自己判断到底是真满了还是统计骗了你以及该动哪个参数。2. 先搞懂 KV 缓存和调度器到底在算什么2.1 KV 缓存不是一整块内存而是被切成了固定大小的块很多人脑子里的 KV 缓存是一整块连续显存用多少算多少。实际主流推理引擎早就改成了分页式管理把 KV 缓存切成一个个固定大小的块block每个块能装固定数量的 token 的 key/value 向量。比如块大小设成 16那一个块就存 16 个 token 的 KV。为什么要这么切因为大模型推理的请求长度是动态的有的请求几百 token有的几万 token。如果按连续内存分配长请求会把内存切得七零八落短请求进来找不到连续空间碎片化严重。分页之后一个请求的 KV 可以分散在任意多个不连续的块里靠一张块表block table记录逻辑位置到物理块的映射跟操作系统的虚拟内存分页是一个思路。这个设计直接决定了后面所有看起来矛盾的现象。因为块是最小分配单位一个请求哪怕只多一个 token也可能要多占一整个块。块大小设得越大浪费越明显设得越小块表管理开销越大。这是第一个需要权衡的参数。2.2 调度器准入看的是最坏情况不是当前平均调度器每来一条新请求要做的事是预估这条请求最多会用到多少 KV 块然后检查当前空闲块够不够。注意这里的关键词是最多。一条请求的输入长度是已知的但输出长度是未知的。调度器必须按输入长度 最大输出长度来预留空间否则跑到一半没块了要么中断请求要么触发抢占把别的请求踢出去。所以哪怕当前只用了 26% 的块只要剩余块数凑不出一条新请求的输入 最大输出所需块数调度器就会拒绝。举个具体的数假设总共有 1000 个块块大小 16当前用了 260 个块26%剩 740 个块。如果最大输出长度设成 4096输入长度 512那一条请求最坏需要 (5124096)/16 288 个块。740 够 288理论上能放两条。但如果最大输出长度设成了 8192单请求就要 (5128192)/16 544 个块740 只够放一条第二条就进不来。这时候面板还是显示 26%但调度器已经满了。提示面板百分比低不代表能塞请求关键看剩余块数能不能覆盖单请求最坏块数。这两个数才是调度器真正在比的。2.3 面板的 26% 是怎么算出来的为什么它天生偏低面板上的 KV 用量采集来源通常是引擎内部维护的块分配计数器。常见算法是已分配块数 / 总块数。但这里有几个坑第一已分配块数只统计正在被请求占用的块不包括那些被预留但还没实际写入的块。调度器准入时会预留但预留的块在真正写入前可能不计入已用统计导致面板偏低。第二抢占和重计算会产生临时块。当显存紧张时调度器会抢占低优先级请求把它们的块释放等有空间了再重算。这个过程中块的归属频繁变化统计有延迟。第三不同版本的统计口径不一样。有的版本把空闲但未归还的块也算进已用有的不算。升级版本后同一个负载下面板数字可能差很多这不是 bug是口径变了。所以看到 26% 这个数先别急着下结论得确认它统计的到底是哪一部分块。3. 把调度器的准入逻辑拆到能自己算3.1 一条请求从进来到被调度中间经历了什么请求进到引擎后大致走这么几步先是 tokenize把文本变成 token 序列然后进等待队列waiting queue调度器每轮从等待队列里挑请求检查能不能分配 KV 块能分配就进运行队列running queue开始 prefillprefill 完进入 decode 循环每步生成一个 token每步都可能需要新块。关键卡点在检查能不能分配这一步。调度器的判断逻辑通常是需要的块数 空闲块数。需要的块数 ceil((输入长度 最大输出长度) / 块大小)。空闲块数 总块数 - 已分配块数 - 预留块数。注意这里有个容易忽略的点最大输出长度是配置项不是实际输出长度。如果你把 max_tokens 或者 max_model_len 设得很大调度器就会按这个大值预留哪怕实际输出只有几十个 token。这是面板低但塞不进最常见的元凶。3.2 块大小、最大长度、并发数三者的连锁关系这三个参数是互相牵制的动一个会影响另外两个的实际效果。参数作用调大的后果调小的后果块大小block_size每个 KV 块存多少 token块表开销小但内部碎片多短请求浪费严重碎片少但块表变大管理开销上升最大长度max_model_len单请求最长 token 数单请求预留块数暴涨并发能力骤降长请求被截断可能影响业务最大并发max_num_seqs同时运行的请求数上限显存压力大容易触发抢占吞吐上不去GPU 利用率低举个实际算例。假设显存能放 2000 个块块大小 16max_model_len 设成 8192。单请求最坏需要 8192/16 512 个块。那理论最大并发就是 2000/512 ≈ 3.9也就是最多 3 条请求同时跑。这时候面板可能只显示 30% 多但第 4 条请求就是进不来因为剩下的块不够第 4 条的最坏需求。如果把 max_model_len 降到 4096单请求最坏 256 个块并发能到 7 条。面板数字没变多少但能塞的请求翻倍。这就是为什么盲目调大 max_model_len 是并发杀手。3.3 抢占机制为什么有时候请求进去了又被踢出来当运行中的请求需要新块但空闲块不够时调度器会触发抢占。抢占策略一般是挑最不划算的请求——比如已经跑了很久但还没结束的或者优先级低的——把它的块释放让给新请求或更需要的请求。被抢占的请求不会消失它会回到等待队列等有空间了重新 prefill。但重算是有代价的已经生成的 token 白算了得从头再来。如果抢占频繁发生会出现请求反复进出、吞吐暴跌的雪崩。这也是面板 26% 却塞不进的另一种解释不是没块而是块被抢占逻辑锁住了调度器在等一个安全的时机才放新请求进来。这种情况下面板数字会来回跳不是稳定在 26%。4. 一套可复现的排查流程4.1 第一步确认面板数字的真实口径别急着调参数先把面板的统计来源搞清楚。最直接的办法是看引擎暴露的指标接口找到 KV 相关的几个计数器对比它们的值。通常会有这么几个指标总块数、已用块数、空闲块数、被抢占次数、等待队列长度。把这几个数拉出来看如果已用 空闲 ≠ 总块数说明有块被预留或统计延迟面板的百分比就是失真的。我一般会写个小脚本定时抓这些指标画成曲线。如果发现已用块数长期不动但等待队列一直涨那基本可以确定是准入判断卡住了不是真的没内存。# 伪代码抓取引擎指标并计算真实可用块 def check_kv_status(metrics): total metrics[kv_total_blocks] used metrics[kv_used_blocks] free metrics[kv_free_blocks] waiting metrics[waiting_queue_size] preempted metrics[preempted_count] # 真实空闲 总 - 已用如果 free 字段不可信 real_free total - used # 面板百分比 panel_pct used / total * 100 print(f面板显示: {panel_pct:.1f}%) print(f真实空闲块: {real_free}) print(f等待队列: {waiting}, 抢占次数: {preempted}) # 如果等待队列 0 且真实空闲块 0说明是准入逻辑问题 if waiting 0 and real_free 0: return 准入逻辑卡住检查 max_model_len 和块大小 return 正常4.2 第二步反推单请求最坏块数拿到 max_model_len、块大小、当前输入长度分布算出单请求最坏需要多少块。公式是单请求最坏块数 ceil((输入长度 最大输出长度) / 块大小)如果这个数接近甚至超过总块数那并发能力必然极低。比如总块数 2000单请求最坏 1800 块那同时只能跑一条第二条永远进不来面板再低也没用。这一步的目的是确认到底是块不够还是单请求胃口太大。很多时候问题不在总量在于单请求预留太狠。4.3 第三步检查是否有僵尸块没释放有些情况下请求已经结束了但它占的块没被正确归还导致空闲块数虚高、实际可用块少。这种僵尸块在面板上表现为已用块数缓慢增长不下降重启后恢复正常。排查方法是看已用块数在请求全部结束后是否归零。如果不归零说明有泄漏。常见原因是异常中断的请求没走完释放流程或者抢占逻辑有边界 bug。这种情况升级引擎版本通常能解决因为分页管理这块迭代很频繁。4.4 第四步用压测复现别靠猜线上环境变量太多最好的办法是在测试环境用固定负载复现。构造一批固定输入长度、固定输出长度的请求逐步加压观察从第几条请求开始被拒绝然后对比理论计算值。如果实测拒绝点和理论值对得上说明是参数配置问题调参数就行。如果对不上比如理论能放 5 条但第 3 条就进不去那可能是碎片化或者统计口径问题需要更深入看块表状态。5. 参数怎么调才能让面板和实际对上5.1 块大小16 是默认但不是万能块大小默认一般是 16。这个值在大多数场景下是平衡点块表开销可接受内部碎片不算太严重。但如果你的请求普遍很短比如平均输出几十 token可以试试调到 8 甚至 4减少浪费。如果请求普遍很长且稳定调到 32 能降低块表管理开销。调整块大小需要重启引擎而且会影响已有缓存的复用。生产环境调这个参数要谨慎最好在低峰期做并且准备好回滚。5.2 max_model_len按业务实际需求设别图省事设最大这是最容易被滥用的参数。很多人直接设成模型支持的最大长度觉得反正用不到那么多。但调度器是按这个值预留的设大了并发直接腰斩。正确做法是统计业务请求的长度分布取 P99 或者 P999 作为 max_model_len。比如 99% 的请求输出不超过 2048 token那就设 2048别设 8192。剩下 1% 的长请求可以单独走一个长文本队列用不同的配置处理。5.3 并发上限和显存预留留出安全边际max_num_seqs 控制同时运行的请求数。设太高会导致频繁抢占设太低浪费显存。经验值是根据总块数 / 单请求最坏块数算出理论上限然后取 70% 到 80% 作为实际配置留出安全边际应对突发长请求。另外显存利用率gpu_memory_utilization这个参数也别拉满。留 5% 到 10% 的余量给框架本身和临时分配能显著减少 OOM 和抢占。参数保守值激进值建议block_size832短请求多选 8长请求多选 32max_model_len业务 P99模型最大值按 P99 设长请求单独处理max_num_seqs理论上限 70%理论上限 100%留 20% 到 30% 余量gpu_memory_utilization0.850.95留 5% 到 10% 给框架5.4 开启前缀缓存减少重复占用如果业务里有大量请求共享相同前缀比如固定的系统提示词开启前缀缓存prefix caching能让这些共享部分只占一份块多个请求复用。这能显著降低实际块占用让面板数字和真实可用空间更接近。开启方式一般是在启动参数里加对应开关具体名称看引擎版本文档。开启后要注意前缀缓存的块释放策略和普通块不同统计口径可能又变面板数字要重新校准。6. 踩过的坑和常见问题速查6.1 升级版本后面板数字突变这是最常见的假故障。新版本改了统计口径比如把预留块也算进已用面板数字会突然涨一截。这时候别慌先看实际吞吐和延迟有没有变化。如果业务指标正常只是面板数字变了那就是口径问题调整监控告警阈值即可。6.2 请求进去了但一直不输出有时候请求被调度进去了但 decode 阶段卡住不动。这通常不是 KV 问题而是采样或者模型前向的问题。但如果伴随已用块数不涨可能是块分配了但没写入属于引擎内部状态不一致重启能解决根治要等版本修复。6.3 并发上不去但显存还有富余这就是本文的核心场景。排查顺序是先看 max_model_len 是不是设太大再看块大小是不是不合适然后看有没有僵尸块最后看是不是抢占太频繁。大部分情况调小 max_model_len 就能解决。6.4 常见问题速查表现象可能原因排查方法解决方向面板低但塞不进max_model_len 过大算单请求最坏块数调小 max_model_len面板数字来回跳抢占频繁看抢占计数降并发或加显存已用块数不归零僵尸块泄漏请求结束后看计数升级版本或重启升级后数字突变统计口径变化对比业务指标调整告警阈值并发低但显存空块大小不合适看请求长度分布调整 block_size提示排查这类问题永远先算数再动手。把总块数、单请求最坏块数、当前并发这三个数写下来大部分疑惑当场就解开了。6.5 一个容易被忽略的细节输入长度也在预留里很多人只盯着输出长度忘了输入长度也要占块。如果业务里有超长输入的请求比如整篇文档做摘要输入部分可能就占掉几百个块加上输出预留单请求胃口极大。这种情况下面板显示很低但就是塞不进第二条因为第一条就把块吃光了。处理办法是把长输入请求单独隔离用专门的实例或者队列处理别和普通短请求混在一起。混在一起的结果就是长请求把并发能力全占了短请求饿死。7. 我个人的几点实操体会调这类问题调多了会发现一个规律面板数字是给人看的调度器算的是另一套账。想让两者对上要么改统计口径让面板反映真实准入状态要么改参数让准入判断别那么保守。前者需要改代码后者改配置就行所以实际工作中都是从参数入手。我一般会先做一个容量账本把总块数、块大小、max_model_len、业务长度分布这四个数固定下来算出理论并发上限然后拿这个上限去对比实际能跑的量。差距在 20% 以内算正常超过 50% 就一定有配置问题。还有一点别迷信默认值。默认的 max_model_len 往往是模型支持的最大长度对大多数业务来说都太大了。我见过太多案例把 max_model_len 从 8192 降到 2048并发直接翻三倍面板数字反而更真实了。这个调整成本极低收益极高值得每个部署的人都试一次。最后分享一个小技巧在监控面板上除了 KV 百分比再加一条等待队列长度和一条单请求最坏块数的曲线。这三条线放一起看什么时候是真满、什么时候是假满一眼就能分辨。光看一个百分比永远会被它骗。
阅读完成 · 觉得有帮助?