KV缓存为何成为大模型推理性能瓶颈
大语言模型在自回归生成过程中,每个token的推理都需要访问此前所有token的Key和Value向量,这些向量被缓存在GPU显存中,称为KV Cache。随着序列长度增加,KV Cache占用的显存呈线性增长,成为制约大模型推理吞吐量的核心因素。以一个70B参数模型为例,batch size为32、序列长度2048时,KV Cache可消耗超过40GB显存,远超模型权重本身的占用。
KV Cache的显存占用计算公式为:2 * batch_size * seq_len * num_layers * num_heads * head_dim * dtype_size。其中2表示Key和Value各一份。理解这个公式有助于在部署阶段精确评估显存需求。
传统KV缓存管理的内存碎片化问题
主流推理框架(如vLLM、HuggingFace TGI)在早期版本中采用类似操作系统进程内存管理的思路,为每个请求预分配一块连续的KV Cache空间。这种方案存在两个突出问题:
一是内部碎片(Internal Fragmentation)。请求的实际生成长度往往远小于预分配的最大序列长度,预分配但未使用的空间被浪费。实测中,当最大序列长度设为2048而平均生成长度仅为200时,内部碎片导致的显存浪费可达80%以上。
二是外部碎片(External Fragmentation)。不同请求的KV Cache块大小不一致,频繁分配和释放后,显存中出现大量不连续的小块空闲区域,无法被新的长请求使用。这类似于磁盘碎片的成因。
PagedAttention分页式KV缓存管理机制
vLLM框架引入了PagedAttention机制,借鉴操作系统的虚拟内存分页管理思想,将KV Cache划分为固定大小的块(Block),每个块存储固定数量token的KV向量。
分页块的基本结构
默认配置下,每个Block存储16个token的KV向量。每个请求维护一个Block Table,记录其使用的Block编号序列。这种设计带来几个直接优势:
# PagedAttention块分配示例(伪代码)
block_size = 16 # 每个块存储16个token
# 请求A需要存储100个token的KV Cache
# 100 / 16 = 6.25,分配7个块
request_A_blocks = [block_0, block_1, block_2, block_3, block_4, block_5, block_6]
# block_6中只有4个token的KV(100 - 6*16 = 4),其余空间可被共享池复用
块共享与Copy-on-Write
在Beam Search或并行采样场景中,多个输出序列共享同一个前缀的KV Cache。PagedAttention通过引用计数实现块共享,仅在序列写入不同的新token时才通过Copy-on-Write机制复制块,避免冗余的显存复制。这使得在采样温度等超参数不同但前缀相同的场景下,显存占用大幅降低。
Prefill阶段与Decode阶段的显存调度优化
大模型推理分为Prefill阶段(处理输入prompt,计算密集型)和Decode阶段(逐token生成,访存密集型)。两者对计算资源的需求差异显著,调度策略直接影响整体吞吐量。
连续批处理(Continuous Batching)
传统静态批处理需要等待同一批次的所有请求完成才能释放资源,长请求会拖累短请求的完成时间。连续批处理在每个Decode步骤的边界动态插入新请求、移除已完成请求,实现GPU利用率的稳定保持。
# 连续批处理调度逻辑示意
running_batch = []
while pending_requests or running_batch:
# 移除已完成的请求
running_batch = [req for req in running_batch if not req.finished]
# 在Decode步骤间插入新请求
while pending_requests and can_admit(len(running_batch)):
running_batch.append(pending_requests.pop(0))
# 执行一步前向推理
step_forward(running_batch)
量化KV缓存降低显存占用的方案
对KV Cache本身进行量化是另一种有效的显存优化手段。FP16格式的KV Cache可通过INT8或INT4量化将显存占用降低50%至75%。
INT8量化实现
vLLM支持KV Cache INT8量化,通过校准数据集确定量化参数,推理时动态反量化。精度损失在大多数生成任务中可接受,但对需要高精度数值运算的代码生成等任务需谨慎评估。
# vLLM启用KV Cache INT8量化
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-2-70b-chat-hf",
quantization="fp8", # 启用FP8 KV Cache量化
kv_cache_dtype="fp8",
gpu_memory_utilization=0.9
)
# FP8量化可将KV Cache显存占用降低约50%
Prefix Caching前缀缓存加速重复输入场景
当多个请求共享相同的system prompt或few-shot示例前缀时,Prefix Caching将这些前缀的KV Cache持久化存储,后续请求命中前缀后直接复用,跳过Prefill阶段的前缀计算。
适用场景与命中率评估
Prefix Caching在以下场景中效果显著:多轮对话中固定的system prompt;RAG应用中固定的检索模板;API服务中高频重复的指令前缀。命中率监控指标可通过vLLM的内部计数器获取:
# 查看Prefix Caching命中率(需启用详细日志)
# vLLM启动时添加参数 --enable-prefix-caching
llm = LLM(model="...", enable_prefix_caching=True)
# 命中率统计通过metrics接口获取
metrics = llm.collect_metrics()
print(f"Prefix cache hit rate: {metrics.prefix_cache_hit_rate}")
print(f"Saved prefill tokens: {metrics.saved_prefill_tokens}")
多GPU部署下的KV缓存分布与张量并行
在多GPU部署场景中,KV Cache需要随模型层分布到不同GPU上。张量并行(Tensor Parallelism)将每层的注意力头分配到不同GPU,每张GPU只存储自己负责的注意力头的KV Cache。这要求GPU间在注意力计算时进行AllReduce通信。
显存预算公式与GPU数量规划
单GPU的显存预算为:total_gpu_memory - model_weights - activations - workspace。将此预算分配给KV Cache后,可通过公式反推最大支持的并发请求数和序列长度:
# GPU显存规划计算
total_vram = 80 * 1024**3 # 80GB (A100)
model_weights = 140 * 1024**3 # 70B FP16 = 140GB(需2张A100)
per_gpu_kv_budget = total_vram - model_weights / num_gpus - 4 * 1024**3 # 预留4GB workspace
# 假设单请求KV Cache占用 2 * 2048 * 80 * 64 * 2 bytes (FP16, 80层, 64头, 64维)
per_request_kv = 2 * 2048 * 80 * 64 * 2 # 约3.3GB
max_concurrent = per_gpu_kv_budget // per_request_kv
监控指标与性能调优实践
推理服务的性能调优需要持续监控关键指标,包括Time To First Token (TTFT)、Time Per Output Token (TPOT)、吞吐量和GPU利用率。
TTFT反映Prefill阶段的性能,受输入序列长度和GPU计算能力影响。TPOT反映Decode阶段的性能,主要受KV Cache访存带宽影响。当TPOT过高时,优先检查KV Cache的访存效率,考虑启用量化或增大batch size以摊薄访存开销。
vLLM提供了详细的性能指标导出接口,可对接Prometheus进行长期监控。通过对比调优前后的TTFT和吞吐量变化,量化评估各项KV Cache优化策略的实际收益。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/da-mo-xing-tui-li-jie-duan-kv-huan-cun-you-hua-ce-lyue-yu/