
1. 从“算得快”到“装得下”为什么推理服务会卡在内存上做过大模型推理服务的人应该都有体会一个模型刚部署上去单看算力指标觉得挺乐观能同时跑几十路并发。可一压测显存直接被打满服务报OOM请求排队排到超时。真正跑过线上的人会告诉你大模型推理的瓶颈从来不是“算不过来”而是“装不下”。这里的“装不下”大部分时候就是KV Cache在作祟。KV Cache全称是Key-Value Cache是大模型自回归解码过程中一个绕不开的中间产物。简单理解模型每生成一个token都要重新“看一遍”前面已经生成过的所有token如果没有缓存机制每生成一个token就要把前面所有的历史信息重新计算一遍算力浪费非常夸张。KV Cache就是把历史token的Key和Value向量缓存下来生成新token时只计算新的部分直接命中缓存省掉大量重复计算。这个task我拆成了四个部分来聊先讲清楚KV Cache到底是怎么算出来的、占了多少显存再讲它和整个推理服务内存管理的关系然后给出一套可以直接落地的配置和监控方案最后把我在实际项目中踩过的坑和排查思路整理成清单。不管你是刚接触推理优化的新人还是已经被显存问题逼疯的运维老手这篇内容应该都能帮你把“KV Cache”这个概念从纸面落到工程里。2. 先算一笔账一个7B模型KV Cache到底吃掉多少显存2.1 KV Cache的占用公式其实就一行KV Cache的显存占用核心就是一行公式显存占用 2Key和Value两组 × 层数 × 序列长度 × 隐藏层维度 × 精度字节数 × 并发数实际计算的时候按照上面这个式子逐项乘进去就行。这里我用一个目前最主流的7B规模模型来举例层数num_hidden_layers32层隐藏层维度hidden_size4096精度FP16也就是每个数值占2字节假设并发请求数32路平均序列长度2048个token代入公式算一下2 × 32 × 2048 × 4096 × 2字节 × 32并发这个算出来大约是34GB。对你没看错单单是KV Cache这一项在32路并发、2048序列长度的情况下就吃掉了34GB显存。这是什么概念一张A100 80G的卡光KV Cache就占掉了接近一半。而模型权重本身在FP16精度下大概是14GB左右要知道KV Cache占的显存已经远远超过了模型权重本身。这也是为什么很多人在单卡部署7B模型的时候总觉得“模型才14G80G的卡应该绰绰有余吧可以开很高的并发”结果一压测就傻眼。模型权重只是一个底数真正让你显存爆掉的是KV Cache这个随并发数线性膨胀的大头。2.2 为什么KV Cache不能省掉看到这里也许有人会问既然这么占显存能不能直接把缓存功能关掉省下这部分开销答案是可以关但代价极其惨痛。没有KV Cache的情况下每生成一个新token模型都要把所有之前的历史token重新计算一遍Key和Value。这个过程的时间复杂度是O(n²)序列越长计算量增长越快。举个例子生成第1000个token时相当于要把前999个token的注意力全部重新算一遍这已经不是浪费算力的问题了而是直接导致推理速度慢到无法接受。我做一个量化对比你就清楚了。假设一个7B模型平均每个token的生成时间是50ms。有KV Cache的情况下生成1000个token大概就是50秒左右不考虑首token延迟首token的prefill也需要算一遍。没有KV Cache的情况下生成到第1000个token时单步计算量已经是第一批token的1000倍整个生成过程会慢几个数量级几分钟都未必能跑完一段1000字的内容。所以KV Cache不是“优化手段”而是自回归解码的必需品。我们真正要优化的是如何在保证命中率的前提下让KV Cache占用的显存尽可能少、管理尽可能高效。3. 内存管理的核心矛盾显存是死的序列是活的3.1 动态增长 vs 静态分配KV Cache的管理为什么难难就难在它的占用是动态增长的。模型权重是静态的加载完占多少显存是固定的这个很好预估。但KV Cache不一样它随着生成过程不断增长——序列每多生成一个tokenKV Cache就要多出一块新的显存。而且不同的请求最终生成的序列长度完全不一样有的请求可能只生成几十个字就结束了有的请求要生成长文跑几千个token。这就带来一个尴尬的问题如果按最大可能长度给每个请求预留KV Cache空间那么短序列请求会浪费大量显存并发能力被严重拉低。如果按平均长度预留长序列请求随时可能OOM。这就好比餐厅给每个客人固定提供一个包厢大小的位置不管他就餐时间长短显然不合理但如果不预留又可能遇到客人坐不下的情况。这个问题在早期的大模型推理框架里非常突出。很多框架的KV Cache是用静态二维张量预先分配的最大序列长度是写死的。你配了2048那每个请求最多就只能处理2048的上下文超过就报错你希望支持更长的序列就需要在启动时预留更多显存给KV Cache并发数就跟着下降。这种“静态池”方式的显存利用率和灵活性都不理想。3.2 从操作系统的页表到PagedAttention这个问题真正得到解决是在PagedAttention出现之后。PagedAttention的思路其实是从操作系统内存管理那里借鉴过来的——学过操作系统的朋友应该对“内存管理单元包含页号页框号”这个概念不陌生。CPU的内存管理里为了让大程序跑在小内存上引入了虚拟内存和分页机制把进程的地址空间切成固定大小的页每个页可以映射到物理内存的任意位置甚至可以暂时换出到磁盘。这样物理内存再零碎也都能被利用起来不再要求程序占用的空间是连续的。PagedAttention就是把这个思路用到了GPU的KV Cache管理上。它把KV Cache切分成固定大小的块block每个块可以存储在显存的任意位置通过块表来记录逻辑块和物理块的映射关系。当一个请求需要的KV Cache空间增长时系统只需要给它分配新的空闲块即可不需要在显存里找一大块连续空间。这个方案的直接好处有两个。第一显存碎片被消灭了。连续内存分配最大的麻烦是外部碎片——明明总空闲空间够但没有足够大的连续区域。块式分配不存在这个问题所有块都是一样大小只要还有空闲块就能分配。第二多个请求可以共享同一个物理块。注意这个功能才是真正提升吞吐的关键。比如多个请求都带相同的前缀如相同的System Prompt、相同的few-shot示例那么这些请求的KV Cache前几块完全可以指向同一块物理内存不用每个请求都复制一份。这个机制被称为Copy-on-Write能显著降低大量相似请求的内存开销。这块内容我在后面讲vLLM的连续批处理时还会具体展开因为PagedAttention带来的不只是显存管理效率提升它直接改变了推理服务的调度方式。3.3 MHA之外GQA让KV Cache直接减半再减半除了通过系统层面的内存管理手段提升显存利用率还有一个更底层的优化方向是——从模型结构上减少KV Cache的量。很多人知道Transformer有多头注意力MHA但可能没有关注到一个趋势新发布的模型几乎都在从MHA转向GQAGrouped Query Attention。这个变化最直接的收益就是KV Cache的规模被大幅压缩。MHA的机制是每个注意力头都有自己的Key和Value矩阵。一个7B模型假设有32层、32个头那每个token每层就要缓存32组KV向量。GQA的做法是让多个Query头共享一组Key和Value比如8个Query头分成4组每组共享1个Key/Value头。这样KV的数量直接减半甚至减少到原来的1/4KV Cache也相应缩小。市场上很多模型选择GQA比如LLaMA 2 70B、Mistral系列包括国内很多开源模型都在用这个设计。KV Cache变小之后同样的显存可以支持更长的上下文、更高的并发数推理服务的吞吐自然上去了。所以做推理优化不能只盯着框架层面的显存管理还要关注模型本身的结构设计。如果你的应用场景非常依赖长上下文在选型阶段就优先选用了GQA的模型这比后期在显存管理上各种打补丁要省心得多。4. 推理服务里KV Cache到底是怎么被管理的4.1 从静态预分配到动态调度现在主流的推理服务框架比如vLLM、TensorRT-LLM、SGLang在KV Cache管理思路上已经非常趋同KV Cache不是按请求预先分配好最大长度的而是动态分配、随用随取。以vLLM为例它在启动或者第一次加载模型的时候会预留一块显存专门给KV Cache。这块峰值区域的大小是可以配置的关键参数是gpu_memory_utilization。默认值是0.9意思是GPU显存的90%用于模型加载和KV Cache剩下10%留给CUDA context、计算图之类的杂项。这里面有一个容易踩的坑如果同时有多个服务共享一张卡或者前端有CUDA算子申请额外的临时显存10%的余量可能不够。我遇到过不止一次模型加载成功了kv cache也分配好了但跑到某个特殊算子时就突然OOM。后来我的习惯是在做容量规划时预留15%-20%的余量gpu_memory_utilization设置在0.8左右牺牲一点并发换稳定。框架在预留出这块KV Cache池之后通过前面讲的PagedAttention机制把这块显存的头部空间划分成若干个block。每个block默认可以存16个token的KV数据。推理过程中的KV Cache像“水”一样流经这个池子请求结束了块就归还给池子供其他请求复用。4.2 Continuous Batching让KV Cache池“转”起来传统推理服务的批处理方式是静态的比如我一次接收64个请求这64个请求必须全部处理完才能释放资源接收下一批。但问题在于每个请求的生成速度不一样有的已经生成完了有的还在慢慢吐字。静态批处理会导致已完成的请求占着资源空等GPU利用率上不去。vLLM这类框架引入了Continuous Batching机制也是Iteration-level Scheduling。核心逻辑是每次迭代生成一个token之后如果某个请求已经生成完毕就立刻把它从当前批次中移除释放它占用的KV Cache块同时把新的待处理请求排进当前批次。这个机制和KV Cache的动态管理配合得非常精妙。对于新加入的请求框架会立即为它分配KV Cache块进行prefill然后再进入decode阶段和批内其他请求一起并行生成token。整个过程是流水线式的KV Cache池始终处于高效流转状态显存的利用率比静态批处理高出一大截。实际对比数据也很直观。我在一个部署Llama-3-8B-Instruct的服务上做过压测同样是单卡A100 80G静态批处理模式下GPU利用率大约40%-50%换算成吞吐大概800 tokens/s切到vLLM的Continuous Batching之后GPU利用率能到70%-80%吞吐翻了一倍多。KV Cache池没有再成为明显的瓶颈这就是动态调度带来的收益。4.3 请求级视角一次完整推理中KV Cache经历了什么为了更清晰理解整个流程我拆解一个请求从进入到返回KV Cache在其中经历的完整生命周期第一阶段请求到达服务端。框架需要为这个请求创建推理上下文这个上下文里就包含KV Cache的管理状态一般是逻辑块表记录这个请求当前占用了哪些物理块。第二阶段prefill阶段。模型对请求的prompt做一次性并行计算为每个token生成Key和Value然后写入KV Cache。这个阶段通常会显存占用激增因为输入的prompt是一次性全部处理的KV Cache块会在短时间内迅速增加。这也是为什么长prompt的prefill容易触发显存峰值。第三阶段decode阶段。模型逐token生成输出。每生成一个token新产生的Key和Value会被追加到KV Cache中同时框架会检查当前请求是否还有足够的空闲KV Cache块。如果不够就需要申请新的块。如果整个KV Cache池已经被耗尽那么新的请求就只能排队等待。第四阶段请求结束。生成EOS标记或达到最大生成长度服务端释放该请求占用的所有KV Cache物理块归还给全局块池。CPU上的逻辑块表销毁整个生命周期结束。这个链路看下来你会发现KV Cache的管理本质就是一个基于显存池的“申请-使用-释放”过程。理解这个生命周期比你死记任何参数配置都重要因为后续遇到任何显存问题你都能定位到是哪个环节出了问题。5. 让KV Cache更“省”量化、前缀复用与显存规划5.1 KV Cache量化FP16到INT8显存直接减半KV Cache量化是目前落地效果最直接、性价比最高的一类手段。前面算过KV Cache在FP16精度下每个数值占2字节。如果量化到INT8每个数值占1字节显存直接减半。量化到INT4则进一步减半只有0.5字节。但量化不是白拿的它涉及到精度损失。KV Cache量化的难点在于Key和Value的数值分布并不均匀。如果直接用简单的线性量化某些离群点会把整个量化范围拉得很大导致绝大多数值的精度严重下降生成质量受损。实际应用中我的建议是优先试INT8的KV Cache量化。在绝大多数场景下INT8 KV Cache带来的质量损失几乎感知不到但显存收益很明确。如果是INT4量化需要仔细做效果评估特别是长文本生成、人和模型多轮对话这些对上下文敏感的场景质量可能会下降。不同框架的量化实现不一样。vLLM里的quantization参数设置了fp8或者int8之后KV Cache也会跟着做相应类型的量化。TensorRT-LLM则专门提供了kv_cache_quantization相关的配置项。在你的服务上线前建议做一次AB对比测试同一组测试集量化前后各跑一遍对比生成结果的rouge或者人工评分确认质量可接受再上线。5.2 前缀复用Prefix Caching让相同开头的请求共享KVPrefix Caching技术的原理简单说就是把请求的KV Cache按序列前缀切块缓存起来。如果新请求的prompt前缀和之前某个请求相同框架就可以直接复用缓存的前缀KV值不需要重新计算这部分。这个机制在哪些场景下特别有用最典型的就是Agent应用。现在很多AI应用在调用大模型时会附带一段很长的System Prompt内容包含工具定义、角色设定、使用限制等。假设这段System Prompt固定不变、有1000个token如果有100个并发请求都在用这段prompt没有前缀缓存的情况下这1000个token的KV Cache每个请求都要单独计算一遍、单独存一份。有了前缀缓存计算一次、存储一份所有请求共享节省的时间和显存都非常可观。再比如多轮对话场景。用户在对话中每隔几轮把前面的历史一起发上来作为上下文如果没有前缀缓存每一轮的prefill都要重新处理所有历史消息。有了前缀缓存只有新增的那几轮消息需要真正计算历史部分的KV Cache直接命中。SGLang、vLLM这类新框架都实现了比较成熟的前缀缓存机制。vLLM中的参数是enable_prefix_caching默认开启。需要注意的一点是前缀缓存的命中需要prompt前缀完全一致差一个token都命中不了。所以如果你的应用在prompt里塞了动态生成的内容比如时间戳、随机数、用户昵称尽量把这些动态信息放在前缀的末尾这样能最大化前缀复用的命中率。5.3 显存规划模型权重、KV Cache和临时内存的比例怎么调最后聊一个运维层面的问题显存到底怎么分配才合理我一般按照这个顺序来排查和设置。第一步明确模型权重的大小。以7B模型FP16为例权重约14GB。如果你的模型是BF16和FP16大小一致如果是FP8或INT8量化版本则减半约7GB。第二步估算CUDA context和其他框架开销。这一步很多人会漏CUDA context一般会占500MB到1GB左右显存框架依赖的算子库比如cuBLAS、cuDNN也会吃几百MB。第三步确定剩余显存空间。假设你用的是80GB的A100模型14GBCUDA context预留1GB剩余约65GB。这一步剩下的显存基本就是可以分配给KV Cache池的空间了。第四步配置gpu_memory_utilization。在单卡80G、模型14G的场景下设置0.85相当于预留68GB给“模型KV Cache”扣除模型权重的14GBKV Cache池大约有54GB。这个参数不是越高越好如果你的算子有较大的临时显存需求比如某些flash attention实现会在前向计算时申请额外的临时buffer预留空间不足就会OOM。我做容量规划时有个习惯按照KV Cache池大小 / 单请求平均KV Cache占用来估算最大并发数。比如KV Cache池是54GB平均每个请求生成1024token、prompt长度512单请求KV Cache占用大概为按7B 32层 4096维度 FP16计算2 × 32 × (5121024) × 4096 × 2字节 ≈ 0.77GB。54除以0.77大概可以支撑70路并发。虽然实际并发受限于其他因素但这个估算给了你一个明确的容量天花板不会凭感觉瞎配。6. KV Cache优化进阶内存换速度还是速度换内存6.1 滑动窗口与增量缓存长对话场景的特殊处理长对话场景下KV Cache的增长几乎是线性的。你和一个Agent聊了50轮每轮平均生成200个token那到最后一轮KV Cache里已经存了上万个token的缓存。这些历史信息总量很大但模型真正在计算注意力时真的需要全部用上吗不一定。这也是滑动窗口注意力Sliding Window Attention存在的原因。它的思路是只缓存一个固定窗口内的历史token比如最近2048个token更早的历史直接丢弃。这样KV Cache的占用被限制在一个常数范围内不会随对话轮数无限增长。这种方案适合什么场景适合那些上下文太久了、模型注意力本来就覆盖不到的对话场景。对于大部分实际应用比如客服机器人、短任务对话早期的历史信息确实已经没什么用了丢弃它们换取更低的显存占用和更稳定的推理性能是划算的。但要注意滑动窗口会损失长距离依赖能力。如果你的应用依赖模型“记住”很久以前的细节比如用户在对话初期明确表达过某个偏好50轮之后模型需要根据这个偏好做出回应那滑动窗口可能就不适用了。这个取舍只能由实际业务场景决定没有统一答案。6.2 稀疏注意力与Key-Value压缩对模型可干预的优化再往底层一点有研究者尝试在模型推理时动态判断哪些历史token的KV Cache是“重要”的只缓存重要的部分不重要的直接扔掉这就是稀疏注意力Sparse Attention的思路。这种方案的问题在于计算开销和效果不稳定实际落地还没有形成特别统一的标准。另一种更稳健的方向是KV压缩模型层面的优化。比如通过建立低秩投影把KV Cache维度“压扁”存一个维度更小的表示用的时候再还原。这种方案相对复杂而且对模型结构有侵入性需要针对性训练。目前的主流开源框架对这类方案的支持也还比较有限更多是研究性质的应用。从我的实践来看如果是线上生产项目优先考虑框架层成熟支持的手段比如KV Cache量化和前缀复用如果需要更深度的优化建议从上文提到的GQA模型的选型、上下文长度限制、窗口管理这些维度入手这些手段更可控、可回退、好维护。6.3 跨请求与跨服务的KV Cache复用前面提到的Prefix Caching本质上是一种跨请求的KV Cache复用。如果你有多个服务它们共用同一个基础模型但是挂载不同的业务prompt理论上你甚至可以做到跨服务的KV Cache复用——服务A已经算过的那段基础prompt服务B可以直接拿来用。不过跨服务复用KV Cache在工程实现上要考虑的东西更多。比如不同的推理框架之间KV Cache的存储格式可能不兼容即使同一框架模型的版本稍有变化缓存就需要全部失效。生产环境里跨服务共享KV Cache的场景还很少大多数团队还是在单个服务内部做好前缀缓存优化。如果你有多个服务都用同一个基础模型能做的最实际的事是确保每个服务内部的Prefix Caching都开启同时尽量保证用户请求的prompt前缀一致。比如统一System Prompt的内容和格式、把动态信息放到prompt靠后的位置。这些从应用层就能做到的事情比你在基础设施层做各种花式优化要简单可靠得多。7. 实操环节以vLLM为例完整配置一次KV Cache管理7.1 基础启动配置逐项拆解我在生产环境里部署推理服务主要用的是vLLM。它的开源生态好、API直观、KV Cache和批处理调度都做得比较完善。这里直接给出一份我常用的启动参数模板并对KV Cache相关的关键项做拆解python -m vllm.entrypoints.openai.api_server \ --model /data/models/llama-3-8b-instruct \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --max-num-seqs 128 \ --enable-prefix-caching \ --kv-cache-dtype fp8 \ --swap-space 0 \ --max-num-batched-tokens 4096这些参数的核心逻辑我再逐一说明。--model不用多解释模型路径需要是HF格式转换好的。--tensor-parallel-size如果单卡显存足够就设为1。多卡推理时KV Cache是分布在多张卡上的此时KV Cache池的总容量是所有GPU显存叠加后的总量但是每个请求的实际KV Cache占用也会被切分需要充分利用多卡。--gpu-memory-utilization刚才说了0.85是我在单卡80G且算子临时内存吃紧时的选择。如果你的服务并发不高、请求也不长可以更高比如0.9如果服务中涉及长上下文请求或者模型有多种动态输入保守一点设0.75-0.8更稳妥。--max-model-len决定了一个请求最多能处理多长的上下文prompt 生成的输出。这个值的设置会直接影响KV Cache池的适合程度。设得过大一个请求就可能占掉大量KV Cache其他请求只能等待并发被锁死设得过小长请求会被切断影响用户实际体验。值得注意的一点是在静态显存模式下框架会按此长度预留KV Cache的峰值显存需求因此这个值的大小对显存占用有直接的乘数效应。--enable-prefix-caching开启前缀缓存。如果你服务的prompt前缀固定一定要开。--kv-cache-dtype fp8开启了KV Cache的FP8量化。如果你的显存真的非常紧张或者上下文特别长这个参数很实用如果你对生成质量的稳定性要求极高、且显存并不太缺可以不开使用FP16。7.2 动态监控、并发限制和OOM防线配置好之后更要关注的是运行时的状态。vLLM暴露的监控指标里最值得盯的是这两个vllm:num_requests_running表示当前正在请求中解码实际占用KV Cache的请求数vllm:cache_usage_percentage表示当前KV Cache池使用率。我自己常用的监控方法很简单Prometheus Grafana。vLLM本身会暴露/metrics端点直接在Prometheus里采集KV Cache池使用率画成曲线阈值超过90%就告警。KV Cache池使用率持续走高说明服务已经接近它的容量天花板了要么扩容要么降低并发。另一个重要配置是--max-num-seqs它限制了单个批次中最多同时处理的请求数。即使有空闲KV Cache块也不能无限塞请求进当前批次因为每个请求的模型计算开销不一样塞得太多会造成单次迭代时间变长整体吞吐下降。8B模型在A100上我一般设置128左右实际效果可以压测来验证。额外再说一个参数--swap-space。vLLM支持把KV Cache换到CPU内存类似操作系统的swap。但在线上服务中我基本不会开这个参数或者设为0。KV Cache换到CPU之后每次生成token都要做PCIe传输延迟会显著上升吞吐下降非常明显。除非服务器GPU显存已经小到完全撑不住基础并发否则不建议用swap兜底正确做法是降低并发数或者做模型量化。# 启动后可以通过vLLM自带的API查看当前运行状态 curl http://localhost:8000/metrics | grep -E vllm:(num_requests_running|cache_usage)7.3 一条简单的容量规划路径最后把这个过程固化成一条可执行的路径方便你直接抄作业。第一步确认模型的结构参数层数、隐藏维度、精度。模型config.json里面都有。第二步明确你的目标期望的并发数、最大上下文长度、单请求平均输出长度。第三步按照第二节的公式算每个请求在平均输出长度下的KV Cache占用再乘以目标并发数得到KV Cache池的理论需求。第四步拿目标GPU的总显存减去模型权重按精度换算、CUDA context预留1GB得到实际可用的显存总量。如果实际可用小于第三步的理论需求则需要做减法降低并发、降低上下文长度、开启KV Cache量化或考虑模型结构优化。第五步用--gpu-memory-utilization把KV Cache池配到实际可用水平上线后用监控验证。理想的状态是KV Cache池使用率维持在70%-90%之间太低说明并发没有打满太高则说明长尾风险大稍有波动就OOM。这套路径我每次新上一个模型服务都会走一遍虽然不能保证绝对精准但至少不会出现上线半天就被OOM打爆的尴尬情况。8. 踩坑实录KV Cache相关的常见问题与排查方案8.1 显存OOM模型能加载一压测就崩这是我见过最多的问题。现象是模型启动成功单请求推理也正常但是并发稍微一上来服务立即崩掉报CUDA OOM。排查优先级依次是第一看gpu-memory-utilization是不是太高了。比如单卡80G模型14G设了0.95看起来是从80G里扣掉5%也就是4G应该够吧但实际CUDA context、算子库、flash attention的临时buffer、中间激活值这些都要从“剩余”里面扣。我遇到过不少算子临时显存占几个GB的情况0.95的设置非常危险。第二检查max-model-len是不是设置过大。这个值决定了KV Cache池的预分配上限如果你设成了32768即使你实际只处理几千token框架也会按32768的峰值去分配KV Cache空间显存占用迅速膨胀。第三看看到底是哪个环节OOM。vLLM的日志一般会打印当前的KV Cache pool大小、已使用大小。如果日志显示是KV Cache预留阶段就OOM说明gpu-memory-utilization相对模型权重和其他开销来说过高如果日志显示是CUDA memory error可能是算子临时内存导致降低并发或者调低gpu-memory-utilization都能缓解。第四检查多卡部署但未显式指定tensor-parallel-size。这种情况会导致模型只加载在单卡上单卡显存很容易被模型和KV Cache同时打满。8.2 吞吐上不去KV Cache池明明还有空间并发却打不上去有时候KV Cache池使用率只有50%但并发请求一上来吞吐就是上不去。这是为什么首先要检查是不是落在了prefill阶段。框架的Continuous Batching调度会在prefill阶段占用大量KV Cache块而且prefill阶段显存波动比decode阶段大得多。如果请求普遍很长prefill阶段需要一次性写入整个prompt的KV Cache即使KV Cache池整体使用率不算高单个请求在prefill瞬间的显存申请也可能快速拉高水位。其次是max-num-seqs以外的另一个限制参数max-num-batched-tokens。这个参数限制的是单次迭代中所有请求的总token数包括prompt token和generated token。如果设得太小大量请求集中在几个batch里排队如果设得太大单次迭代显存申请过大容易触发OOM。建议按显存余量和模型结构来调在小规模压测下先找到安全上限再上线生产。还有一点容易被忽略CPU的调度能力。如果每个请求生成的token数非常多、频率非常高CPU在分配和释放KV Cache块上也会成为瓶颈。这个场景下需要关注Python进程的CPU使用率如果CPU被打满而GPU利用率不高优先做请求合并或者减少并发。8.3 前缀缓存命中率太低明明开了但缓存总是miss前缀缓存是一个非常实用的功能但也有一些使用误区。最容易踩的坑是把动态内容放在了prompt前缀中。很多团队为了让prompt具备时间特性或其他动态特性会在System Prompt里加上时间戳、随机ID等信息。这在没有Prefix Caching的时候完全没问题但一旦开启前缀缓存任何细小的前缀差异都会导致整个prompt无法复用之前的缓存。我见过一个团队的配置System Prompt每次生成时都会带上当前的时间比如“今天是2025年6月1日”就这么几个字的差别前缀缓存基本形同虚设明明所有请求的System Prompt逻辑完全一致但缓存命中率却低得可怜。解决思路就是把所有动态信息尽量移到用户输入之后。比如System Prompt固定不变时间信息跟用户消息一起发送。如果业务逻辑上确实需要在System Prompt中体现动态信息那只能接受前缀缓存命中率低的现实。另一个提高命中率的思路是让请求的prompt结构化。把固定部分放在最前面变化部分放后面。比如Agent场景把“工具定义 系统指令 历史对话 当前问题”按顺序拼接历史对话和当前问题是变化的但工具定义和系统指令如果完全一致且位置在前前缀缓存仍然能命中固定部分这就是收益来源。8.4 长上下文场景下显存碎片依然存在PagedAttention解决的是KV Cache的外部碎片问题但如果你的服务大量使用长上下文内部碎片依然可能拖慢效率。因为KV Cache是按block分配的一个block如果只用了50%就释放那另一半空间是无法供其他请求使用的。vLLM默认的block大小是16个token。如果你的服务中请求特别短比如平均就二三十个token用默认配置会造成大量内部碎片。可以尝试调小block size减少内部碎片。相反如果请求都很长block增大到32或64可以减少块表的管理开销。这个参数调整直接影响的是显存利用率和调度开销的平衡没有绝对最优值。我建议是做一次小规模的并发压测分别设置16和32看吞吐和显存使用率的变化选效果好的一组。9. 一些个人经验做推理优化这几年我自己最深的一个感受是KV Cache问题不是一个“调参数”就能彻底解决的事情它需要你对模型结构、推理框架、操作系统内存管理原理都有一定的理解才能在面对具体问题时快速定位到根因。如果你正在做一个新项目我建议在模型选型阶段就开始思考KV Cache的问题。同样做7B规模选了GQA结构的模型你的KV Cache可能只有MHA结构的1/4。这个从源头上的差距比你在推理框架里做任何量化和管理优化都来得轻松。如果你已经上线的服务频繁遇到KV Cache相关的问题不要急着换框架或者换模型先按照前面第七节那条路径做一次完整的容量规划再确认一下前缀缓存是否开启、动态信息是否放到了prompt后缀这两个基础操作往往能解决掉80%的问题。最后分享一个小技巧上线新模型前先用一个包含长prompt和长输出的测试脚本把KV Cache池打到90%以上的使用率观察一段时间确认没有内存泄漏和异常增长再放量进入生产。稳定的KV Cache管理是整个推理服务高并发稳定运行的基础。