大模型推理过程中,KV Cache缓存机制是决定吞吐量和首字延迟的核心环节。传统推理框架在管理KV Cache时面临显存碎片化严重、实际利用率不足30%的问题,PagedAttention借鉴操作系统虚拟内存分页机制,将KV Cache划分为固定大小的block进行按需分配,在vLLM框架中将显存利用率提升至97%以上,并发处理能力提升2-4倍。本文从KV Cache底层原理出发,结合PagedAttention架构设计与vLLM框架实战配置,给出大模型推理显存优化的完整方案。
KV Cache缓存机制原理与大模型推理加速
自回归大模型生成文本时,每一步都需要对当前及之前所有token计算Self-Attention。若每步重新计算历史token的Key和Value矩阵,计算复杂度随序列长度呈O(n²)增长。KV Cache的核心思路是将每层Transformer计算出的K、V矩阵持久化存储,后续解码步骤直接从缓存读取,避免重复计算。
对于L层、H个注意力头、每头维度D的模型,单个token的KV Cache大小为:
KV Cache per token = 2 × L × H × D × 2 bytes(FP16)
以175B参数模型为例,96层、96头、每头128维,单token KV Cache约4.5MB。2048 token序列的KV Cache达到9.2GB,4096 token序列则需18.4GB,显存压力随序列长度线性增长。
在批量推理场景下,不同请求的序列长度差异巨大。传统框架为每个请求预分配最大序列长度的连续显存空间,导致大量显存碎片和内部碎片浪费。短序列请求预分配的空间利用率极低,而长序列请求又可能因预分配空间不足而截断输出。
传统KV Cache显存管理的问题分析
主流框架如HuggingFace Transformers默认采用连续分配策略,为每个请求预分配 max_sequence_length 对应的KV Cache空间。这种做法存在三类显存浪费:
第一类是外部碎片(External Fragmentation)。多个请求的KV Cache在显存中不连续分布,请求完成后释放的空间可能无法满足新请求的连续性要求,形成碎片间隙。
第二类是内部碎片(Internal Fragmentation)。请求实际生成的序列长度通常远小于 max_sequence_length,预分配但未使用的空间被浪费。实测显示,在对话场景下平均序列长度为512 token,但框架预分配2048 token空间,内部碎片率高达75%。
第三类是预分配阻塞。新请求必须等待足够大的连续显存块释放后才能进入处理,即使总剩余显存充足也无法并发处理,导致GPU利用率偏低。
PagedAttention分页显存管理架构解析
PagedAttention将KV Cache的管理逻辑从连续分配改为分页分配,核心设计包含三个层次:
Block分页层:将KV Cache划分为固定大小的block,每个block存储固定数量token的KV数据。block大小block_size通常设为16,即每个block容纳16个token的Key和Value矩阵。block在物理显存中可以非连续分布,通过block table维护逻辑block到物理block的映射关系。
Block Table映射层:类似操作系统的页表,每个请求维护自己的block table,记录逻辑block序号到物理block地址的映射。逻辑上连续的KV Cache在物理显存中可以分散在不同位置,消除了外部碎片问题。
按需分配层:请求启动时仅分配初始block,随着token生成动态追加新block。序列结束后block立即归还全局block池供其他请求复用,内部碎片仅存在于最后一个block中(最多浪费block_size-1个token的空间)。
以下为PagedAttention block管理的伪代码实现:
class PagedAttentionCache:
def __init__(self, num_blocks, block_size, num_layers, num_heads, head_dim):
self.block_size = block_size
self.num_blocks = num_blocks
# 物理block池:预分配所有block的KV存储空间
self.kv_cache = torch.zeros(
num_blocks, block_size, 2, num_layers, num_heads, head_dim,
dtype=torch.float16, device='cuda'
)
self.free_blocks = list(range(num_blocks)) # 空闲block列表
self.block_tables = {} # 每个请求的block table
def allocate_request(self, request_id):
"""为新请求分配初始block"""
block_id = self.free_blocks.pop(0)
self.block_tables[request_id] = [block_id]
def append_token(self, request_id, token_pos, k_val, v_val):
"""写入token的KV数据,按需分配新block"""
logical_block_idx = token_pos // self.block_size
offset_in_block = token_pos % self.block_size
# 逻辑block不存在时分配新物理block
if logical_block_idx >= len(self.block_tables[request_id]):
new_block = self.free_blocks.pop(0)
self.block_tables[request_id].append(new_block)
physical_block = self.block_tables[request_id][logical_block_idx]
self.kv_cache[physical_block, offset_in_block, 0] = k_val # K
self.kv_cache[physical_block, offset_in_block, 1] = v_val # V
def release_request(self, request_id):
"""释放请求占用的所有block"""
for block_id in self.block_tables[request_id]:
self.free_blocks.append(block_id)
del self.block_tables[request_id]
vLLM推理框架KV Cache配置实战
vLLM是实现PagedAttention的开源推理框架,支持Continuous Batching和PagedAttention的协同工作。以下为vLLM部署大模型的完整配置示例:
from vllm import LLM, SamplingParams
# 模型加载配置
llm = LLM(
model="/models/Qwen2-72B-Instruct",
tensor_parallel_size=2, # 2卡张量并行
gpu_memory_utilization=0.90, # GPU显存利用率上限90%
max_model_len=8192, # 最大序列长度
block_size=16, # PagedAttention block大小
swap_space=4, # CPU交换空间(GB)
enable_prefix_caching=True, # 开启前缀缓存
enforce_eager=False, # 使用CUDA Graph加速
)
# 批量推理
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=2048,
)
prompts = [
"请解释Transformer架构中多头注意力的计算流程",
"用Python实现一个LRU缓存",
]
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
print(output.outputs[0].text)
关键参数说明:block_size设为16在大多数场景下取得最佳平衡,过小会增加block table开销,过大则内部碎片率升高。gpu_memory_utilization控制KV Cache可用的显存比例,vLLM会根据该参数和模型权重大小自动计算最大block数量。swap_space允许将不活跃请求的KV Cache换出到CPU内存,在显存压力较大时避免OOM。enable_prefix_caching开启后,相同系统prompt的多个请求共享前缀KV Cache,大幅减少重复计算。
Continuous Batching与PagedAttention协同调度
vLLM的Continuous Batching机制与传统static batching的关键区别在于:static batching必须等待一个batch内所有请求完成才能处理下一批,而Continuous Batching在每次迭代中动态调整batch内的请求——已完成生成的请求立即移出batch,新请求随时加入。
PagedAttention为Continuous Batching提供了基础设施支持。由于KV Cache以block为单位管理,移出请求只需归还其占用的block,新请求从空闲block池分配,不需要等待连续显存空间释放。这使得batch size可以根据当前显存容量动态调整,而非受限于预分配策略。
调度核心逻辑如下:
class ContinuousBatchingScheduler:
def __init__(self, llm_engine, max_batch_size):
self.engine = llm_engine
self.max_batch_size = max_batch_size
self.running = [] # 正在处理的请求
self.waiting = [] # 等待队列
def schedule(self):
"""每步调度:补充新请求,移出已完成请求"""
self.running = [req for req in self.running
if not req.is_finished()]
while (len(self.waiting) > 0 and
len(self.running) < self.max_batch_size and
self.engine.has_free_blocks()):
req = self.waiting.pop(0)
self.engine.allocate_kv_cache(req)
self.running.append(req)
return self.running
KV Cache量化与多级缓存策略
在显存容量受限的部署场景下,KV Cache量化是进一步降低显存占用的有效手段。FP16 KV Cache量化到INT8可将显存需求减半,量化到INT4则可降至1/4。vLLM支持通过配置开启KV Cache量化:
llm = LLM(
model="/models/Qwen2-72B-Instruct",
quantization="fp8",
kv_cache_dtype="fp8",
)
多级缓存策略将KV Cache按访问热度分层:热数据保留在GPU显存,温数据换出到CPU内存,冷数据落盘存储。vLLM通过 swap_space 参数实现GPU-CPU两级缓存,当显存压力达到阈值时自动将不活跃请求的block换出,活跃请求需要时再换入。
对于多模型混部场景,可将多个模型的KV Cache统一纳入block池管理,根据负载动态调整各模型可用的block配额,避免单模型独占显存导致其他模型请求阻塞。这种全局调度策略在多租户推理服务平台中尤为关键。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/da-mo-xing-tui-li-kvcache-huan-cun-ji-zhi-yu-pagedattention/