很少有一个体验能像聊天AI那样制造出如此固定的节奏:输入一长段文本,点击发送,光标转圈,等待一两秒后第一个字蹦出来,然后后面的内容像泄洪一样哗哗往外冒。很多人的第一反应是,第一个token慢是因为模型算力太重,它在“思考”怎么回答;一旦想明白了,输出自然就顺了。这个解释听着合理,但方向基本是错的。那个让首token变慢的东西,和让后续输出变得丝滑的东西,其实是同一件事——KV缓存。今天我想把这件事彻底讲清楚:为什么首token要等,为什么后面又这么快,以及KV缓存到底在服务的哪个环节,扮演了什么样的角色。
这篇文章适合两类人。一类是刚开始接触大模型推理的开发者,想搞懂服务端部署时显存为什么总不够用;另一类是纯用户视角、只是好奇聊天AI底层机制的朋友。我会尽量把公式和术语都翻译成人话,同时保留足够的工程细节,保证读完你能自己估算一个模型的KV缓存开销,也能在部署时准确判断服务到底是卡在算力还是卡在缓存。
1. 从点击发送到第一个字:那几秒钟服务端到底在忙什么
我们先还原一下服务端那一刻的状态。模型权重已经加载在显存里,用户把一段可能长达几千字的prompt发过来,推理引擎需要做的事情,并不是“从第一个字开始慢慢想”,而是一次性把整段prompt跑完一整轮前向传播。这个过程在工程上叫prefill,也叫预填充阶段。
prefill阶段要做的事情很重:模型要把prompt里的每一个token都过一遍所有层,算出一组对后续生成至关重要的中间结果。这组中间结果就是所谓的Key和Value,也就是KV。每一层Transformer都会给当前序列里的每个位置计算一份K向量和V向量,然后把这些向量缓存起来。首token的延迟,几乎全部消耗在这个阶段上。
用户感知到的“第一个字”,其实是prefill跑完之后,从最后一个位置的logits里采样出来的结果。也就是说,在你看到第一个字之前,服务端已经默默把整段prompt“读”完了,并且把它压缩成了一张可检索的索引表。这张索引表,就是KV缓存。后面每个字的生成,都是在这张表的基础上进行的。
1.1 所有对话AI都逃不开的两段式生成
很多人以为AI写答案就像人写文章,是一边想一边写,写到哪算哪。实际上大模型生成是严格的自回归过程:先有第一个token,把它拼回输入序列,再预测第二个token,再拼回去,如此循环。每一步只能生成一个token,整个句子是一个token一个token“挤”出来的。
这带来一个工程上的必然结论:生成阶段天然分成两大段。第一段是你把整段prompt一次性给模型,计算所有位置的状态,这叫prefill;第二段是从已经算好的状态出发,逐token往外蹦,这叫decode。两段的工作量完全不同,表现出来的速度也完全不同。
prefill阶段的特点是“一次算一大片”,可以充分压榨GPU的矩阵计算能力;decode阶段的特点是“一次只算一个点”,但每一步都要查阅之前所有的KV缓存。所以如果你把首token耗时和后续每token耗时分别测出来,会发现一个很有意思的现象:prompt越长,首token越慢;而后续token的速度在上下文不太长的时候基本稳定。这就是两段式生成最直观的印记。
1.2 真正可复用的“索引”:KV缓存的雏形
prefill阶段为什么要专门去算K和V?因为它们是后续所有生成的“素材”。在注意力机制里,K可以理解为“这个token能被什么特征检索到”,V是“这个token真正要提供给下游的信息”。当一个新token产生时,它需要拿着自己的查询Q,去和历史所有token的K做匹配,找到哪些历史信息值得参考,再把这些信息的V加权求和。
关键在于:历史token的K和V只取决于它们自己,不取决于后面谁会来查它们。所以一旦算出来,就可以长期保存。服务端每生成一个新token,都会为这个新token也算一份K和V,追加到缓存里。对话越长,缓存就越厚;缓存越厚,后面每一步要翻阅的资料就越多。聊天AI前期像在读一本新书,后期则像在一摞越来越厚的笔记里快速翻找答案。
理解了这一点,就能明白为什么标题说“KV缓存才是丝滑输出的秘密”。如果没有缓存,每生成一个新token,模型都得把历史所有token重新算一遍K和V,那计算量会随着对话长度线性飙升,别说丝滑了,多轮对话能做到不卡死都是奇迹。
2. 自回归的笨办法:没有中间缓存,每个字都得把历史重算一遍
先做一个思想实验:假设我们完全不做KV缓存,模型每一步生成时,把截至目前的所有历史token重新喂进模型,从头跑一遍完整前向。这样能不能工作?能,但代价极其离谱。
每一步解码都要重新计算之前所有token在各层的K和V,而这些K和V在上一步刚算过,内容一模一样。等于你每写一个字,都要把已经写过的每一个字重新读一遍、重新理解一遍。第一轮对话可能还好,到了第十轮,模型每次吐一个字都要过一遍上万token的历史,GPU再强也扛不住。
复杂度上说,无缓存解码的总计算量大致是O(T²)乘以模型参数量级的常数,T是生成的总token数。而带了KV缓存的方案,prefill仍然要算一遍O(T_prompt²)级别的前向计算,但decode阶段每步只新增一个token,单步计算量基本恒定。这就是缓存带来数量级提升的原因。
2.1 注意力公式里那三个字母,决定了什么能被复用
要理解KV缓存,最好把注意力公式展开看一眼。标准点积注意力的计算是:Attention = softmax(Q × K^T / sqrt(d)) × V。这里Q是当前查询向量,K是所有候选位置的键向量,V是这些候选位置的值向量,d是向量维度。
Q、K、V这三个字母的语义差异,直接决定了缓存策略。Q代表“我现在想找什么”,它只跟当前正在生成的那个token有关。K代表“我这里有某某信息可供查阅”,是历史token自身属性的编码。V代表“我实际提供的内容是什么”,同样是历史token的固有信息。
当一个新token产生时,它需要拿自己的Q去和历史全部K做点积。注意,历史token的K不需要随新token变化,V也一样。所以它们可以被算一次、存起来、反复用。Q则不一样,每个新token的Q都是新的,旧token的Q不会再有其他token拿它来当查询,因此完全没有缓存的必要。
还有一个很多人问过的问题:为什么不缓存attention矩阵,也就是softmax之后的权重?因为权重是Q和K交互的结果,新token一来,Q变了,所有位置的权重都要重新算,旧的权重对任何后续token都无效。缓存权重既占空间又没用,而缓存K和V才是既能复用、又体积可控的做法。
2.2 无缓存推理的复杂度灾难:一次O(n²)的表演
把无缓存的代价说得更具体一点。假设对话累积到了1万token,模型有32层,每层都要为这1万个token重新计算K和V,每个token的K/V维度是几百到上千。这一步涉及的总计算量,大约等于prefill阶段为同样长文本付出的计算量,而且每生成一个token都要重复一次。
换句话说,如果没有KV缓存,一个1000 token的回复,等于要把同样长度的前向计算做1000遍。哪怕GPU算得再快,这种重复劳动也会把单token延迟推高到不可容忍的程度。实际部署时,所需的显存带宽和算力都会膨胀到硬件完全无法支持。
所以KV缓存不是一种可有可无的优化,而是自回归生成能在今天落地的先决条件之一。工程上所有推理框架的第一优先级,基本都围绕着同一件事:把KV缓存管好。显存分配、批处理策略、长上下文支持,本质上都是在给KV缓存腾地方、省空间、提高命中率。理解了这一点,你再看各种推理框架的文档,会发现每条配置都变得清晰了。
3. KV缓存的一生:从一次前向传播到长对话里的隐形成本
接下来我们跟着KV缓存走完它的一生。从它诞生的那一刻,到它被反复读取,再到它挤爆显存,每个阶段都对应着用户可以感知到的性能特征。
假设现在有一条用户消息进来,长度是n个token。prefill阶段会做这样几件事:把n个token的嵌入向量喂进第一层,算出这一层每个位置的Q、K、V;把K和V存入该层的KV缓存,Q用完即弃;接着进入下一层,重复同样操作,直到所有层都完成。最后从最后一层输出里算下一个token的概率分布。
注意,prefill阶段就已经把整段prompt的KV全部写进了缓存。所以等到decode第一个token时,模型并不需要重新处理用户输入的每个字,只需要处理当前这个新token,然后去缓存里查历史K/V。这就是为什么首token虽然等了半天,后续每个字却出得很快。
3.1 Prefill阶段:把整个prompt写成一张“检查表”
prefill阶段可以被理解成“做检查表”的过程。模型读完整段输入,为每一个位置提炼出它的话题索引(K)和具体内容(V),整整齐齐排好。这些索引和内容接下来会被后续所有新生成的token查阅。
这个过程本身很贵,因为它涉及两层复杂度。一层是模型宽度带来的:每一层都要对n个token做矩阵乘法,计算量和模型参数量成正比。另一层是注意力机制带来的:每个token都要和其他所有token做注意力交互,计算量和n²成正比。prompt越长,prefill阶段的耗时增长越接近超线性。
所以“第一个token慢是因为算力太重”这个说法,从现象上不算全错。prefill确实算力需求巨大,但它不是因为模型在“想答案”,而是因为模型必须先为整段输入建立完整的KV缓存。从产品体验上讲,这段等待时间本质上是“建索引”的时间。
还有一点容易被忽略:prefill阶段虽然计算量大,但它是高度并行的矩阵运算,GPU利用率通常很高。真正决定首token延迟的,往往不是模型token数多少,而是prompt里有多少token。短问题秒回,长文档慢,这个规律基本就是prefill复杂度的直接体现。
3.2 Decode阶段:每吐一个字,缓存就厚一分
进入decode阶段后,模型每一步只生成一个新token。这个token会被拼接进序列,计算它自己的Q、K、V,然后通过注意力机制读取历史所有位置的K和V,得到输出向量,最后预测下一个token。每完成一步,新token的K/V也会被追加进缓存。
所以KV缓存是动态增长的。对话从第10个token到第1000个token,缓存体积涨了100倍。也正是因为这种增长,长对话后期即使输出速度没变慢,显存压力也会越来越大。如果你的服务端没有做好缓存管理,聊着聊着就可能OOM,表现为突然断掉或报错。
decode阶段的单步计算量不大,真正的瓶颈通常在于读取缓存的显存带宽。想象一下,每生成一个新token,模型都要把当前已累积的所有历史KV从头到尾读一遍。上下文长度越长,这一步需要搬运的数据就越多。很多人以为对话变长后模型回答变慢是因为“模型累了”或者“找不到思路”,其实更常见的原因是KV缓存膨胀导致带宽吃紧。
3.3 为什么“第一个token慢”的锅不能全甩给算力
回到标题里的那个误解。大多数人以为第一个token慢是因为模型算力太重,尤其当模型宣称自己有几千亿参数时,这句解释听上去特别合理。但实际做推理服务的人会告诉你,算力“重”和“慢”之间需要重新算一笔账。
首token慢的直接原因是prefill必须处理完整段输入,而这段输入对应的计算量和显存访问量都在同一时刻爆发。模型参数只是被读取和参与计算,真正让首token延迟随输入线性增长的,是那n个token之间两两交互的注意力矩阵,以及为每个token生成并缓存K/V的代价。模型参数大小影响的是单次前向的基座成本,而prompt长度影响的是KV缓存建立的边际成本。
换句话说,算力太重解释的是“为什么哪怕输入很短,也会有一个基础延迟”;KV缓存解释的则是“为什么输入越长,首token越慢,而后续输出却能相对丝滑”。两者一个管底线,一个管斜率,缺了KV缓存的概念,你对首token延迟的理解就是不完整的。
4. 一笔显存账单:从7B到70B,KV缓存是上下文长度的隐形价格
知道KV缓存存在是一回事,知道它有多占显存是另一回事。很多部署大模型的人第一次跑服务时,都会遇到一个困惑:模型权重明明只占十几GB,怎么一跑推理显存就见底了?答案往往是:你忘了给KV缓存留预算。
KV缓存的体积可以精确估算,公式是:KV缓存大小 = 2 × 层数 × KV头数 × 每头维度 × 序列长度 × 批大小 × 精度字节数。公式里的2来源于K和V各一份。这个公式拿任何模型的配置都能套,误差很小。
下面我给出一个简单的Python脚本,输入模型配置就能估算出单条序列的KV缓存占用:
def kv_cache_bytes(layers, kv_heads, head_dim, seq_len, batch_size, dtype_bytes=2): # dtype_bytes: fp16为2,bf16为2,int8为1 return 2 * layers * kv_heads * head_dim * seq_len * batch_size * dtype_bytes # 示例1:某7B级模型,32层,8个KV头,head_dim=128,fp16精度 print(kv_cache_bytes(32, 8, 128, 4096, 1, 2) / 1024**3, "GB per sequence") print(kv_cache_bytes(32, 8, 128, 4096, 16, 2) / 1024**3, "GB for batch=16") # 示例2:某70B级模型,80层,8个KV头,head_dim=128,fp16精度 print(kv_cache_bytes(80, 8, 128, 4096, 1, 2) / 1024**3, "GB per sequence") print(kv_cache_bytes(80, 8, 128, 128000, 1, 2) / 1024**3, "GB at 128K context")我们按常见的GQA配置估算,会发现一个惊人的事实:一篇4000 token的短文档,7B级模型单条序列的KV缓存大约是128MB,这还不算多。但如果同时服务16个用户,这批KV缓存就涨到2GB,开始有点疼了。到了70B级模型,同样4000 token的上下文,单条序列的KV缓存轻松超过1GB,而如果上下文的长度拉到128K,单条序列的KV缓存可以逼近几十GB。也就是说,长上下文能力本质上是用显存换来的。
4.1 KV缓存体积的计算公式
上一节的公式其实已经说明了一切。为了让大家有更直观的感受,我把几个典型配置塞进一张表里,按fp16精度估算,单位是GB。
| 模型规模 | 层数 | KV头数 | head_dim | 上下文长度 | 单序列KV缓存 | 16批KV缓存 |
|---|---|---|---|---|---|---|
| 7B级 | 32 | 8 | 128 | 4K | 0.125 | 2 |
| 7B级 | 32 | 8 | 128 | 32K | 1 | 16 |
| 13B级 | 40 | 8 | 128 | 4K | 0.156 | 2.5 |
| 70B级 | 80 | 8 | 128 | 4K | 0.5 | 8 |
| 70B级 | 80 | 8 | 128 | 128K | 16 | 256 |
注意,这张表用的是GQA配置下常见的8个KV头。如果是MHA,每个注意力头都有独立的K/V,KV头数可能冲到32甚至更多,表里的数字还要翻几倍。这也就是为什么如今新发布的大模型几乎都在往GQA方向走:KV缓存实在太贵了。
4.2 不同模型规模与序列长度下的账单明细
部署时只看权重显存必然翻车。一张消费级显卡通常也就16GB到24GB显存,一个7B模型权重在fp16下就要占用14GB左右,看起来勉强够跑。可真要跑起来,你还要预留激活值、计算缓冲区,以及随对话长度不断膨胀的KV缓存。上下文稍微开长一点,或者并发用户一多,立刻爆显存。
对比一个更极端的场景:70B模型配合128K上下文,单条序列的KV缓存就是16GB起步。也就是说,光是一个用户的长文档对话,KV缓存就能吃满一张专业显卡的全部显存,模型权重反而要放在另一张卡上。这时候你会发现,所谓“能处理百万token上下文”的模型,显存成本高得离谱,硬件不堆上去,长上下文只能看看参数文档。
这也是为什么量化模型、量化KV缓存在部署中越来越常见。模型权重量化是把“模型参数”这个大头变小,而KV量化则是把“中间缓存”这个动态大头变小。两者解决的问题不同,但最终都是为了让同样的显存能塞进更多并发和更长上下文。
4.3 部署中最容易翻车的三个显存场景
我见过不少项目在部署推理服务时翻车,基本都逃不出三个场景。第一个是只按模型权重规划显存,没给KV缓存留预算,上线一测并发就OOM。第二个是盲目拉长上下文窗口,把max_length从4K调到32K,却忘了KV缓存会线性膨胀,结果显卡直接爆掉。第三个是没注意批处理大小和KV缓存之间的联动,批大小一增加,KV缓存成倍增长,在长上下文的场景下尤其致命。
这类问题的最佳解法,是在上线前就按公式把KV缓存预算算清楚。把“模型权重 + 激活值 + KV缓存峰值 + 推理框架自身开销”列成一个显存规划表,再根据峰值并发反推单卡能承受的上下文长度。宁可把max_length设得保守一点,也不要让服务在用户聊到一半时突然断掉,那种体验基本等于功能事故。
5. 工业界的“KV瘦身运动”:GQA、量化、分页与窗口
KV缓存既然这么贵,工业界自然想尽办法给它瘦身。这些年围绕KV缓存诞生了一系列优化方案,从模型结构上的改动,到推理框架的显存管理,再到量化压缩和稀疏化,各有适用场景。把它们放在一起看,其实就是一场针对显存和带宽的持久战。
最根本的思路是从模型结构下手,直接减少KV的体积。MQA让所有注意力头共享同一份K/V,体积直接缩到接近1/头数,但会牺牲一定的表达力。GQA是MQA的折中,把注意力头分成若干组,组内共享K/V。它比MQA保留更多信息,又比MHA省下大量显存,所以成了近年大模型的主流配置。你看到某个模型配置里写“GQA”,基本可以确认它是为了控制KV缓存成本而设计的。
5.1 GQA/MQA:让多个注意力头共享同一份K/V
为什么共享K/V可行?因为注意力头虽然各有所长,但它们处理的都是同一个序列里的信息。不同头的主要差异体现在“用什么样的Q去查询”,而K/V作为被查询的对象,存在大量冗余。让几个Q头共享同一份K/V,会损失一些“查询视角”的丰富度,但换来的是KV缓存大幅缩小,工程收益远大于精度损失。
从部署角度看,GQA带来的好处非常直接:相同显存下可以开更大的批处理,或者支持更长的上下文。尤其是长上下文场景,KV头和层数是决定缓存增长率的核心变量,KV头数减半,缓存直接减半。所以新模型宁可加深层数,也要压KV头数,本质就是在做这个权衡。
还有一种更新颖的压缩思路,是把K/V做低秩映射,先投影到一个很小的潜在空间再缓存。这样KV缓存体积可以压得更狠,但实现复杂度更高,也属于“KV瘦身运动”里比较前沿的方向。理解GQA之后,看这类方案会容易很多:它们都在想方设法让“每一个历史token在缓存里占的空间”更小。
5.2 KV量化:把缓存从fp16压到int8甚至更低
除了动结构,还可以动精度。KV缓存默认用fp16或bf16存储,如果把它量化成int8,显存占用直接减半;极端情况下量化到4bit,缓存体积能压到原来的四分之一。代价是量化误差会影响注意力计算的精度,极端情况下会出现回答质量轻微下降。
我个人的实践感受是,KV量化在中短上下文场景下影响不明显,但在很长上下文的场景里要谨慎。上下文越长,需要累积的量化误差越多,注意力分数的细微偏差可能会在长距离依赖上被放大。如果你要处理128K上下文这种极限场景,建议先做一轮基准测试,对比量化前后模型在长文档问答上的准确率,再决定是否开启KV量化。
另外要注意,KV量化的收益在并发场景会被放大。单条序列省下几十MB不算什么,但几百条并发时省下的就是几GB甚至几十GB显存。量化后的KV缓存还能降低带宽压力,让decode阶段的每token延迟更稳定,这在长上下文服务中往往比显存收益更明显。
5.3 分页式缓存与连续批处理:把显存利用率抠出来
模型结构和精度之外的另一个方向,是缓存管理本身。传统推理框架会给每条请求预分配一块连续的KV缓存空间,但请求长度是不可预测的,预分配多了浪费显存,预分配少了又要迁移。这个问题在并发场景下尤其明显,显存碎片化能白白浪费掉两到三成空间。
近年来流行起来的思路是给KV缓存做分页管理:把缓存切成固定大小的块,按需分配,逻辑上连续、物理上可以不连续。这就像操作系统里的虚拟内存分页,按页管理,碎片率大幅下降,显存利用率显著提升。配合连续批处理,新请求可以在旧请求即将结束时立刻插入空档,吞吐量又能上一个台阶。
连续批处理本身不减少单个请求的KV缓存,但它会动态调整不同请求占用的缓存空间,避免因为一个慢请求卡住整批请求。把这两项技术一起用,等于在有限的显存里“抠”出了更多并发槽位。选推理框架的时候,可以重点看它有没有类似的KV管理机制,这往往决定了高并发场景下的实际吞吐差距。
5.4 最激进的一类:对历史KV做稀疏化或丢弃
最激进的KV瘦身思路是:有些历史信息,也许根本不值得缓存。滑动窗口注意力只保留最近若干token的KV,更早的上下文直接忽略,非常适合流式对话。有些方案则通过启发式规则,先给历史KV打分,认为当前最相关的部分保留,不相关的丢弃。这种稀疏化能大幅压缩缓存,但存在信息丢失风险,属于拿质量换性能的典型做法。
在实际项目里,我建议把优化手段分层使用。如果只是想让服务跑起来,先上GQA模型和分页式缓存;如果显存仍然紧张,再考虑KV量化和连续批处理;只有当你真的需要超长上下文且能接受效果波动时,才去碰稀疏化和丢弃策略。从工程稳妥性的角度讲,越晚尝试越安全,先把最容易预判收益的手段用足。
6. 部署实战:用三招判断你的服务是卡在算力还是卡在KV缓存
原理讲完,最后分享一点实战经验。很多人在部署推理服务时,遇到“生成慢”就想升级显卡,但慢的原因未必是算力不够,很可能是KV缓存把显存带宽吃满了,或者显存容量不够导致批处理上不去。判断清楚瓶颈在哪,才能把钱花在刀刃上。
我自己常用的方法有三招,不需要太复杂的工具,靠着日志和系统监控就能给出比较可信的判断。核心思路是把“算力瓶颈”和“缓存瓶颈”分开来看,因为它们表现出的特征完全不同。
6.1 第一招:观察首token耗时对输入长度的敏感度
先说最简单的观察方法:固定batch大小为1,把prompt长度从几百token拉到几千token,记录首token延迟的变化。如果首token延迟随prompt长度明显上升,说明你正处在prefill计算密集阶段,模型算力是当前的主要约束;如果首token延迟基本平稳,后续每token延迟却很高,那更像显存带宽或缓存管理出了问题。
这个方法不需要做复杂profiling,跑一组短、中、长的请求就能看出趋势。有一次我排查一个“首token要等三秒”的问题,测下来发现prompt只有几十token时首token也要两秒多,明显不是prefill算力的问题。查了一圈,发现是推理框架在每次请求时都会重新分配KV缓存块,分配逻辑有性能缺陷。换掉分配策略之后,首token延迟立刻降了下来。
所以别急着归因到GPU算力。先做长度扫描,确认延迟是否随输入长度线性增长。如果不增长,问题大概率出在框架调度、缓存分配或者其他固定开销上,换显卡是解决不了的。
6.2 第二招:盯显存增量,看它跟并发与上下文的关系
第二招是打开显存监控,观察KV缓存增长模式。在推理过程中用监控工具连续采样显存占用,会发现当并发数上升或上下文变长时,显存占用会跟着涨,这部分增量基本就是KV缓存。对比模型权重占用的固定值,KV缓存是动态项,它的增速直接反映了缓存管理效率。
如果显存占用已经接近上限,同时显存带宽利用率很高,但GPU计算单元利用率却不高,那基本可以判定瓶颈在KV缓存的读取和存储上。这种情况下,单纯堆算力没用,你更应该做的是开KV量化、缩小上下文长度、减少并发,或者换一个缓存管理更好的推理框架。
我自己习惯把这些数据画成曲线:横轴是时间,纵轴是显存占用和GPU利用率。当GPU利用率掉下来而显存曲线还在涨时,大概率就是缓存已经把显存吃满,计算单元在等数据搬运,典型的带宽受限特征。
6.3 第三招:把一个批次塞满,再看两个阶段的耗时分布
第三招适用于判断batch大小和KV缓存的联动关系。固定同样的输入长度,逐步加大batch,记录prefill阶段和decode阶段的平均耗时。如果batch加大后,prefill耗时的增速明显高于decode耗时,说明算力在预填充阶段是瓶颈;如果decode耗时的增速更猛,说明decode阶段读取KV的带宽不够了。
这个测试能帮你定出一条“最大安全并发线”。我在实际项目里通常把batch逐步翻倍,每一档都观察两个阶段的耗时比。当decode阶段单token耗时开始明显爬升时,就停止增加batch,把它作为容量上限。再往后硬加并发,用户体感会断崖式变差,表现为每个字都要卡顿半天。
这种测试也能顺便验证你做的KV优化是否有效。比如开启KV量化后,如果decode耗时曲线的斜率明显缓了,说明优化确实降低了带宽压力;如果没什么变化,那就要重新考虑问题是不是出在别处。
6.4 一次真实排障的复盘:从“换显卡”到“换缓存策略”
最后复盘一个我印象很深的项目。当时我们跑一个对话服务,模型不大,但用户聊到中后段时输出速度肉眼可见地下降。最初大家都怀疑显卡算力不够,计划升级到更大的GPU。我先把首token耗时做了长度扫描,发现prompt很短时首token很快,根本没到算力极限。然后看了显存曲线,发现随着对话轮数增加,显存占用稳步爬升,同时GPU利用率在下滑。
真正的问题在KV缓存预留策略上:每条会话都提前分配了能支持最大上下文长度的连续显存块,但绝大多数会话根本聊不到那么长,于是显存被大量闲置预留占据,实际能并发的会话数远低于预期。并发一高,每条会话能分到的带宽和算力都被稀释,用户感知到的就是越聊越慢。
后来我们把连续预留改成按需分块分配,同时给KV缓存开了量化,单条会话的显存占用降了一半多,并发能力直接翻倍。整个过程没有动模型、没有换显卡,只改了KV缓存的管理方式,问题就解决了。这件事之后,我在看任何推理性能问题时,都会先问一句:KV缓存是怎么管理的?十次里有七次,答案都藏在里面。