☰
大模型推理显存优化:KV Cache与PagedAttention实战
2026/10/10 4:00:45 网站建设 项目流程

1. 大模型推理到底在算什么账

很多人第一次接触大模型推理,脑子里想的都是“模型多大、参数多少、效果好不好”,但真正把模型跑起来之后才会发现,最卡脖子的往往不是模型本身,而是显存。你有一张24GB显存的卡,加载一个70亿参数的模型,权重本身按FP16算就要占掉大约14GB,剩下10GB要留给KV Cache、中间激活值、框架开销,稍微把并发调高一点或者上下文拉长一点,直接OOM。这就是为什么“大模型推理与内存基础”这个主题值得单独拿出来讲——它不是一个可选项,而是决定你能不能把模型真正跑起来、跑稳、跑出性价比的地基。

这一课的核心目标很明确:搞清楚大模型推理过程中显存到底被谁吃掉了,每一块内存的用途是什么,以及怎么通过合理的参数配置和推理引擎的选择,把有限的显存利用率拉到最高。适合谁看?如果你已经能把模型加载起来、跑通一个简单的generate调用,但一遇到并发请求或者长上下文就崩,或者你正在做推理服务的部署方案选型,那这篇内容就是写给你的。如果你还没跑过任何模型,也没关系,我会把基础概念用生活化的方式讲清楚,保证你能跟上。

我自己的经验是,大部分人第一次做推理部署时踩的坑都高度相似:以为显存只装权重就够了,结果被KV Cache教做人;以为batch size越大吞吐越高,结果延迟爆炸;以为量化精度越低越省显存,结果输出质量崩得没法看。这些问题背后其实都是同一件事——你没有把推理过程中的内存账算清楚。接下来我会从整体设计思路开始,一层一层把这块讲透。

2. 推理内存的整体账本与设计思路

2.1 显存到底被谁吃掉了

要理解推理的内存问题,先得把显存里的“住户”列清楚。一个典型的大模型推理过程,显存占用主要分四块:

  • 模型权重(Model Weights):这是最直观的一块。参数量乘以每个参数的字节数就是权重占用的显存。FP16下每个参数2字节,INT8下1字节,INT4下0.5字节。一个7B模型FP16权重约14GB,INT4量化后约3.5GB。
  • KV Cache:这是自回归生成过程中缓存的历史Key和Value矩阵。它的大小跟batch size、序列长度、层数、注意力头数、头维度都相关。很多人忽略这块,但它在大并发或长上下文场景下可以轻松超过权重本身。
  • 激活值(Activations):前向传播过程中产生的中间张量。推理时因为没有反向传播,激活值占用比训练小很多,但在大batch下依然不可忽视。
  • 框架与运行时开销:CUDA context、通信缓冲区、临时 workspace 等。这部分通常在几百MB到1GB左右,容易被忽略但确实存在。

注意:很多人算显存只算权重,然后按“剩余显存除以单请求KV Cache”来估算并发数,这个算法在理论上没错,但实际中框架开销和碎片化会让你能跑的并发比理论值低20%到30%。

2.2 为什么推理引擎的选择比模型本身更关键

同样一个模型,用不同的推理引擎跑,显存利用率和吞吐可以差出好几倍。这不是夸张,是我实测下来的结论。早期大家用原生PyTorch做推理,后来发现几个致命问题:没有连续批处理(continuous batching),请求只能一个一个来或者等整批凑齐;KV Cache管理粗放,预分配一大块显存但利用率极低;没有PagedAttention这类内存分页机制,长序列场景下碎片化严重。

这就是vLLM这类推理引擎要解决的核心问题。它的设计思路可以类比操作系统的虚拟内存管理:把KV Cache切成固定大小的block,按需分配,用页表来映射逻辑位置和物理位置。这样做的好处是显存碎片大幅减少,不同请求的KV Cache可以灵活共享和复用,吞吐量能提升数倍。

选择推理引擎时,我一般看几个维度:是否支持连续批处理、KV Cache管理机制是否高效、量化支持是否完善、部署和扩展是否方便。vLLM在这几个维度上目前是比较均衡的选择,尤其适合需要高吞吐在线服务的场景。当然如果你的场景是单请求低延迟,或者模型特别小,那用原生框架可能更简单直接,没必要上重型引擎。

2.3 内存优化的核心权衡:精度、速度、显存

做推理内存优化,本质上是在三个维度之间做权衡:输出质量(精度)、推理速度(吞吐和延迟)、显存占用。你不可能三个都拿满分,必须根据场景做取舍。

举个例子,量化是最直接的省显存手段。FP16换成INT8,权重显存直接减半,推理速度通常还能提升,但输出质量会有轻微下降。再往下走到INT4,显存再减半,但质量下降就明显了,尤其是对数值敏感的任务。我一般建议:如果显存够用,优先FP16;显存紧张但质量要求高,用INT8;显存极度受限且能接受一定质量损失,才考虑INT4。

另一个权衡是batch size和序列长度。增大batch能提升吞吐,但KV Cache线性增长;拉长上下文能处理更复杂的任务,但KV Cache同样线性增长。这两个参数不能同时拉满,必须根据实际请求分布来调。我的做法是先统计线上请求的序列长度分布,取P95或P99作为max_model_len的参考值,而不是无脑设成模型支持的最大值。

3. 核心细节拆解:KV Cache与PagedAttention

3.1 KV Cache的计算方法

KV Cache是推理内存里最容易被低估的部分,我把计算公式拆开讲。对于Transformer架构,KV Cache的总大小可以用这个公式估算:

KV Cache大小 = 2 × batch_size × seq_len × num_layers × num_heads × head_dim × dtype_bytes

其中前面的2是因为要同时缓存Key和Value。num_heads × head_dim通常等于hidden_size,所以公式可以简化为:

KV Cache大小 = 2 × batch_size × seq_len × num_layers × hidden_size × dtype_bytes

拿一个具体的例子来算:假设一个7B模型,num_layers=32,hidden_size=4096,FP16推理(2字节),batch_size=1,seq_len=2048。那么单个请求的KV Cache大小是:

2 × 1 × 2048 × 32 × 4096 × 2 = 2,147,483,648 字节 ≈ 2GB

也就是说,光是KV Cache,一个请求在2048上下文下就要吃掉2GB显存。如果你有16GB剩余显存,理论上最多同时处理8个这样的请求。但实际中因为碎片化和框架开销,可能只能跑到5到6个。

实操心得:在估算并发能力时,我习惯先算单请求KV Cache,然后用“可用显存 × 0.8 / 单请求KV Cache”来估算,留20%的余量给碎片和开销。这个经验值在多数场景下比较稳。

3.2 PagedAttention为什么能省显存

传统的KV Cache管理方式是预分配:每个请求一开始就按max_seq_len分配一大块连续显存,不管实际用不用得到。这就像你去餐厅吃饭,不管几个人都先占一张大桌,浪费严重。

PagedAttention的思路是把KV Cache切成固定大小的block(比如16个token一个block),按需分配,用block table来记录每个请求的逻辑block到物理block的映射。这样做有几个直接好处:

  • 减少内部碎片:请求实际用了多少就分配多少,不会因为预分配而浪费。
  • 支持内存共享:多个请求如果有相同的前缀(比如相同的system prompt),可以共享同一份KV Cache block,这在多轮对话和few-shot场景下省显存非常明显。
  • 提升并发能力:因为碎片少了,同样显存能容纳更多请求的KV Cache。

我实测过一个场景:同样的模型和硬件,用预分配方式最多跑8个并发,换成PagedAttention后能跑到14个,吞吐提升了将近一倍。这个差距在长上下文场景下会更明显。

3.3 量化对内存和精度的影响

量化是另一个省显存的大头。我把常见量化方案的对比整理成表格,方便你选型:

量化方案权重显存(7B模型)精度损失推理速度适用场景
FP16~14GB无基准显存充足,质量优先
INT8~7GB轻微通常更快显存紧张,质量要求高
INT4~3.5GB明显更快显存极度受限
GPTQ/AWQ~3.5-4GB中等较快消费级显卡部署

需要强调的是,量化不只是省权重显存,对KV Cache也有影响。如果你把KV Cache也量化到INT8,那KV Cache占用也能减半。但KV Cache量化对精度的影响比权重量化更敏感,因为KV Cache直接参与注意力计算,误差会被放大。我的建议是:权重量化可以大胆用,KV Cache量化要谨慎,除非你实测过质量可接受。

注意:量化模型的加载需要推理引擎支持对应的量化格式。不是所有引擎都支持所有量化方案,选型时要先确认兼容性,别模型量化完了发现引擎加载不了。

4. 实操过程:从零配置一个推理服务

4.1 环境准备与依赖安装

假设你已经有一张支持CUDA的显卡,驱动和CUDA工具链都装好了。第一步是创建独立的Python环境,避免依赖冲突:

python -m venv vllm_env source vllm_env/bin/activate pip install --upgrade pip pip install vllm

安装vLLM时会自动拉取PyTorch和相关的CUDA库,这个过程比较久,耐心等。如果你的显卡比较新,可能需要指定对应的CUDA版本。安装完成后可以用python -c "import vllm; print(vllm.__version__)"验证。

实操心得:我强烈建议用虚拟环境而不是全局安装,因为vLLM对PyTorch版本有要求,全局环境很容易跟其他项目的依赖打架。另外,如果你的机器有多张卡,先确认CUDA_VISIBLE_DEVICES的设置,避免vLLM默认占用所有卡。

4.2 关键参数配置与计算过程

启动一个vLLM推理服务,核心参数就那么几个,但每个都直接影响显存和性能。我用一个具体场景来演示参数计算过程。

假设硬件是单张24GB显存的卡,模型是7B FP16(权重约14GB),剩余约10GB给KV Cache和开销。目标是在保证质量的前提下最大化吞吐。

关键参数设置:

  • --max-model-len:最大序列长度。这个值直接决定单请求KV Cache的上限。我一般先看业务请求的P99长度,假设是4096,那就设4096,不要设成模型支持的最大值(比如32768),否则KV Cache预分配会浪费大量显存。
  • --gpu-memory-utilization:GPU显存利用率上限,默认0.9。意思是vLLM最多用90%的显存。这个值不要设太高,留一点给系统和其他进程,我一般设0.85到0.9。
  • --max-num-seqs:最大并发序列数。这个值决定了同时处理多少请求。设太小吞吐上不去,设太大KV Cache不够会排队。
  • --tensor-parallel-size:张量并行度,单卡设1,多卡按卡数设。

计算过程:可用显存 = 24GB × 0.9 = 21.6GB,减去权重14GB,剩7.6GB给KV Cache。单请求在4096长度下的KV Cache按前面公式算约4GB(2 × 1 × 4096 × 32 × 4096 × 2)。等等,这个算法有问题——4GB是batch_size=1时的值,但实际KV Cache是按token逐步增长的,不是一开始就占满。vLLM的PagedAttention是按需分配的,所以实际占用取决于请求的实际生成长度。

更准确的估算方式是:假设平均请求长度2048,那单请求KV Cache约2GB,7.6GB能支持约3到4个并发。如果觉得并发不够,可以考虑权重量化到INT8,权重降到7GB,KV Cache空间翻倍。

4.3 启动服务与验证

配置好参数后,启动命令大概是这样:

python -m vllm.entrypoints.openai.api_server \ --model /path/to/your/model \ --max-model-len 4096 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 8 \ --dtype float16 \ --port 8000

启动后观察日志,重点看几个信息:模型加载耗时、KV Cache block数量、可用并发数。vLLM会在启动时打印类似“GPU blocks: XXX, CPU blocks: XXX”的信息,这个block数量直接决定了能同时处理多少请求。

验证服务是否正常,用curl发一个测试请求:

curl http://localhost:8000/v1/completions \ -H "Content-Type: application/json" \ -d '{ "model": "/path/to/your/model", "prompt": "请用一句话解释什么是大模型推理", "max_tokens": 100, "temperature": 0.7 }'

如果返回正常结果,说明服务跑起来了。接下来用压力测试工具(比如locust或自己写脚本)模拟并发请求,观察显存占用和吞吐变化。

4.4 显存监控与动态调整

服务跑起来之后,监控是必须的。我一般用nvidia-smi看整体显存,用vLLM的metrics接口看细粒度的KV Cache使用率。关键指标是KV Cache使用率,如果长期接近100%,说明并发已经到瓶颈,请求在排队;如果长期低于50%,说明显存有浪费,可以调大max-num-seqs或max-model-len。

动态调整的思路:先按保守参数启动,观察实际负载下的显存和吞吐,然后逐步调大并发数直到KV Cache使用率稳定在80%到90%之间。这个区间既能充分利用显存,又留了余量应对突发流量。

注意:调整参数后要重新压测,不要凭感觉调。我见过有人把max-num-seqs从8调到16,结果延迟翻了三倍,因为请求排队导致尾延迟爆炸。吞吐和延迟是一对矛盾,要找到适合你业务的平衡点。

5. 常见问题与排查技巧实录

5.1 启动就OOM怎么办

这是最常见的问题。模型加载到一半就报CUDA out of memory,原因通常是权重本身就超过了可用显存。排查步骤:

  1. 先算权重显存:参数量 × 字节数。7B FP16约14GB,13B约26GB,70B约140GB。如果权重就超了,只能换量化版本或换卡。
  2. 检查是否有其他进程占用显存:nvidia-smi看有没有残留进程。
  3. 降低--gpu-memory-utilization,给系统留更多空间。
  4. 如果权重刚好卡在边界,试试--dtype half明确指定FP16,避免框架用FP32加载。

5.2 跑着跑着OOM怎么办

服务启动正常,但处理请求过程中OOM,这通常是KV Cache增长导致的。排查方向:

  • 检查--max-model-len是不是设太大了,导致KV Cache预分配过多。
  • 检查--max-num-seqs是不是设太大了,并发请求的KV Cache总和超了。
  • 看是否有超长请求,单个请求的序列长度远超平均值,把KV Cache吃光了。

解决办法:调小max-model-len和max-num-seqs,或者启用KV Cache量化。另外可以设置请求长度限制,拒绝超长请求。

5.3 吞吐上不去怎么调

服务不OOM但吞吐低,GPU利用率上不去。常见原因和解决:

现象可能原因解决办法
GPU利用率低batch太小调大max-num-seqs
请求排队严重并发数不够检查KV Cache是否成为瓶颈
单请求延迟高序列太长限制max_tokens或分段处理
吞吐波动大请求长度分布不均启用连续批处理(vLLM默认开启)

我踩过的一个坑是:以为调大batch就能提升吞吐,结果因为KV Cache不够,请求反而排队更严重。后来发现瓶颈在KV Cache而不是计算,调大batch没用,得先解决显存问题。

5.4 量化后质量下降怎么排查

量化后输出质量下降是常见问题,排查思路:

  • 先确认量化方案是否适合你的任务。INT4对生成任务的伤害比分类任务大。
  • 对比量化前后的输出,看是普遍下降还是特定类型问题下降。
  • 尝试混合量化:权重INT4,KV Cache保持FP16,看质量是否恢复。
  • 如果质量要求高,退回INT8或FP16,用其他方式省显存(比如减小max-model-len)。

实操心得:量化不是免费的午餐,省显存的代价是质量。我一般建议先在测试集上对比量化前后的效果,确认可接受再上生产。别为了省显存把产品质量搭进去,得不偿失。

5.5 多卡部署的注意事项

多卡部署时,张量并行(TP)是常用方案,但有几个坑:

  • TP度数和卡数要匹配,2卡就设2,4卡就设4,不要设成奇数。
  • 多卡通信有开销,TP越大通信开销越高,吞吐提升不是线性的。
  • 每张卡的显存要单独算,权重和KV Cache都会分摊到各卡。
  • 注意NVLink和PCIe的带宽差异,带宽不够会成为瓶颈。

我实测下来,2卡TP的吞吐大约是单卡的1.7倍,4卡大约是单卡的2.8倍,不是线性增长。如果卡间通信带宽低,提升会更小。所以多卡部署前先确认通信拓扑,别盲目堆卡。

6. 内存优化的进阶思路

6.1 前缀缓存与共享

多轮对话和few-shot场景下,不同请求往往有相同的前缀(比如相同的system prompt)。PagedAttention支持前缀共享,相同前缀的KV Cache block只存一份,多个请求共用。这个优化在多轮对话场景下能省30%到50%的KV Cache。

启用方式一般是在引擎配置里开启prefix caching选项。需要注意的是,前缀共享要求前缀完全一致,差一个token都不行。所以如果你的system prompt有动态内容,共享效果会打折扣。

6.2 分页与交换策略

当显存不够时,可以把部分KV Cache换出到CPU内存,需要时再换回来。这就是分页和交换的思路。vLLM支持CPU offload,但换入换出有延迟,适合对延迟不敏感的场景。

我的建议是:如果延迟敏感,优先用其他方式省显存(量化、减小序列长度),不要依赖CPU offload。如果延迟不敏感且显存极度受限,offload可以作为兜底方案。

6.3 请求调度与优先级

不同请求的重要性和延迟要求不同。可以给请求设置优先级,高优先级请求优先分配KV Cache,低优先级请求排队或降级处理。这个在混合负载场景下很有用,比如同时有实时对话和批量处理任务。

实现方式一般是在API层做调度,把请求分类后发给不同的推理实例,或者在同一实例内用优先级队列。vLLM本身支持一定的调度策略,但复杂的优先级逻辑可能需要自己在应用层实现。

6.4 模型切分与流水线并行

除了张量并行,流水线并行(PP)是另一种多卡方案。TP是把每一层切到多卡,PP是把不同层分到不同卡。PP的通信开销比TP小,但会有流水线气泡,吞吐提升不如TP直接。

选择TP还是PP,取决于你的瓶颈在哪。如果单层计算量大、通信带宽高,用TP;如果层数多、想减少通信,用PP。实际中两者可以结合使用,但配置复杂度会上升。

我在实际部署中的体会是,内存优化没有银弹,每个场景的瓶颈都不一样。有人卡在权重太大,有人卡在KV Cache不够,有人卡在通信带宽。关键是先用监控工具定位瓶颈,再针对性优化,不要盲目套用别人的配置。我见过太多人直接抄网上的参数,结果因为模型不同、硬件不同、请求分布不同,效果差很远。先测量,再优化,这个顺序不能反。

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

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

立即咨询