大模型面试题60:vLLM中PageAttention的实现原理
·
一、小白入门:先搞懂核心概念(无技术门槛)
1. 为什么需要PageAttention?
LLM(大语言模型)推理时,KV Cache 是核心(存储每一层的Key和Value矩阵,避免重复计算),但传统KV Cache有两个致命问题:
- 内存碎片化:每个请求的KV Cache占连续内存,长请求/多请求时,显存里全是“小碎片”,利用率极低;
- 内存浪费:即使请求只生成几个token,也要分配一整块大内存,空着的部分无法复用。
PageAttention的核心思路:借鉴操作系统的“内存分页”思想,把KV Cache切成固定大小的“小方块(Block)”,像书架上的固定尺寸书格——每个书格放固定数量的书(token),空书格可以给其他请求用,彻底解决碎片化和浪费问题。
2. 小白视角的Block(块)
- 本质:GPU显存里的“固定大小内存单元”,是KV Cache的最小管理单位;
- 存储内容(基础):每个Block存
N个token的K和V数据(N叫block_size,vLLM默认64)。比如一个Block能存64个token的Key矩阵+64个token的Value矩阵; - 类比:把KV Cache看成一本厚书,Block就是这本书的“页码”——每页固定64行(对应64个token),每行存该token的K和V数据,空行可以留给后续内容。
二、基础进阶:PageAttention的核心实现逻辑(懂基础编程即可)
先明确两个关键术语(承上启下):
- 物理Block:GPU显存中实际分配的内存块(真正存数据的地方);
- 虚拟Block:每个请求的KV Cache被抽象成“虚拟Block列表”,映射到物理Block(类似电脑的“虚拟地址→物理地址”)。
1. PageAttention的核心工作流程(4步)
步骤1:初始化Block池
- 启动vLLM时,先在GPU显存里预分配一批物理Block(比如1000个),每个Block大小固定(block_size=64);
- 每个Transformer层都有独立的Block池(因为每层的KV Cache是独立的);
- 这些Block初始状态是“空闲”,形成一个“Block池”,供所有请求复用。
步骤2:处理请求(存储KV Cache)
假设你发送一个请求:“你好,今天天气怎么样?”(对应8个token):
- 模型计算前8个token的K和V,从Block池里申请1个物理Block(8<64,够用);
- 把8个token的K/V存在这个Block的前8个“位置(Slot)”里,剩下的56个Slot空着;
- 记录“虚拟-物理映射表”:比如“请求A的第0个虚拟Block → 物理Block ID=10”;
- 当请求继续生成token(比如生成第9个token),直接存在这个Block的第9个Slot里,直到64个Slot占满;
- 占满后,再申请新的物理Block,更新映射表(比如“请求A的第1个虚拟Block → 物理Block ID=25”)。
步骤3:Attention计算(PageAttention的核心差异)
传统Attention需要加载“连续的整块KV Cache”,而PageAttention只加载“有效Block”:
- 先查当前请求的映射表,拿到所有用到的物理Block ID(比如请求A用到ID10、ID25);
- 只加载这些Block里的有效Slot(ID10用了64个,ID25用了12个),忽略空Slot;
- 用“稀疏索引”标记有效Slot,只对这些Slot做Attention计算(Q@K^T → softmax → 乘V),减少计算量和内存带宽。
步骤4:释放Block
请求生成完成后,它占用的物理Block会被标记为“空闲”,放回Block池,供其他请求复用(比如下一个请求可以直接用ID10的Block)。
2. 基础版Block结构(伪代码)
# 小白能看懂的Block类(简化版)
class KVBlock:
def __init__(self, block_id, block_size=64, num_heads=32, head_dim=128):
self.block_id = block_id # Block唯一ID(比如10、25)
self.block_size = block_size # 每个Block能存的token数(默认64)
# 核心存储:K和V矩阵(GPU显存上的张量,FP16格式节省内存)
self.k = torch.zeros((block_size, num_heads, head_dim), device="cuda") # Key
self.v = torch.zeros((block_size, num_heads, head_dim), device="cuda") # Value
self.num_valid_tokens = 0 # 该Block已使用的token数(比如8、64)
self.is_free = True # 是否空闲(True=空闲,False=被占用)
self.seq_id = None # 所属请求ID(比如请求A的ID=1)
三、深度进阶:PageAttention的技术细节 + Block完整结构(源码级)
1. PageAttention的底层实现(CUDA核函数级)
vLLM的PageAttention是基于CUDA核函数(GPU并行编程)实现的,核心优化点:
- 稀疏Block索引:用数组存储每个请求的物理Block ID列表,以及每个Block的有效token数,避免加载空数据;
- 分块矩阵乘法(Blocked MatMul):将Q/K/V按Block大小分块,利用GPU的共享内存(Shared Memory)加速计算(共享内存比全局内存快100倍+);
- 动态批处理:多个请求的Block可以混合计算,最大化GPU并行性(比如同时处理请求A的ID10和请求B的ID15)。
核心伪代码(接近vLLM源码逻辑)
import torch
import math
# Block池管理
class BlockPool:
def __init__(self, num_blocks=1000, block_size=64, num_heads=32, head_dim=128):
self.block_size = block_size
self.blocks = [KVBlock(i, block_size, num_heads, head_dim) for i in range(num_blocks)]
self.free_block_ids = set(range(num_blocks)) # 空闲Block ID集合
def allocate_block(self):
"""分配一个空闲Block"""
if not self.free_block_ids:
raise RuntimeError("GPU显存不足:无空闲Block")
block_id = self.free_block_ids.pop()
self.blocks[block_id].is_free = False
return self.blocks[block_id]
def free_block(self, block_id):
"""释放Block(标记为空闲,重置有效token数)"""
block = self.blocks[block_id]
block.is_free = True
block.num_valid_tokens = 0
block.seq_id = None
self.free_block_ids.add(block_id)
# PageAttention核心计算函数(CUDA加速的核心逻辑)
def page_attention(
q: torch.Tensor, # 当前token的Query:[1, num_heads, head_dim]
request_block_map: dict, # 请求的Block映射表:{虚拟BlockID: 物理Block对象}
block_size: int = 64
) -> torch.Tensor:
"""
核心逻辑:只加载有效Block的有效Slot,计算Attention
"""
all_scores = [] # 存储所有Block的Attention分数
all_v = [] # 存储所有Block的Value
# 遍历该请求的所有物理Block
for virtual_block_id, physical_block in request_block_map.items():
# 1. 加载该Block的有效K/V(只取有数据的部分)
valid_num = physical_block.num_valid_tokens
valid_k = physical_block.k[:valid_num] # [valid_num, num_heads, head_dim]
valid_v = physical_block.v[:valid_num] # [valid_num, num_heads, head_dim]
# 2. 计算Q@K^T(Attention分数)
# 转置K的最后两个维度:[valid_num, num_heads, head_dim] → [valid_num, head_dim, num_heads]
score = torch.matmul(q, valid_k.transpose(-1, -2)) # [1, num_heads, valid_num]
all_scores.append(score)
all_v.append(valid_v)
# 3. 拼接所有Block的分数和Value
all_scores = torch.cat(all_scores, dim=-1) # [1, num_heads, total_valid_tokens]
all_v = torch.cat(all_v, dim=0) # [total_valid_tokens, num_heads, head_dim]
# 4. Softmax + 乘V(标准Attention最后两步)
attn_weights = torch.softmax(all_scores / math.sqrt(q.shape[-1]), dim=-1)
output = torch.matmul(attn_weights, all_v) # [1, num_heads, head_dim]
return output
2. vLLM源码中Block的完整结构(KVBlock)
在vLLM的vllm/core/kv_cache.py中,Block(KVBlock)存储的信息远不止基础版的K/V,核心字段如下:
| 字段名 | 数据类型 | 核心作用 |
|---|---|---|
k_data |
GPU张量(torch.Tensor) | 存储Block内所有token的Key,维度[block_size, num_heads, head_dim],默认FP16/FP8格式 |
v_data |
GPU张量(torch.Tensor) | 存储Block内所有token的Value,维度同上 |
block_id |
int | 物理Block的唯一标识(全局唯一) |
state |
Enum(Free/Used/Pinned) | Block状态:空闲/被占用/固定(前缀缓存用) |
ref_count |
int | 引用计数(前缀缓存时,多个请求共享一个Block,计数>0时不释放) |
seq_id |
int/None | 所属请求ID(None=空闲) |
slot_mapping |
List[int]/None | 记录Slot→token的映射(前缀缓存时,标记哪些Slot属于共享前缀) |
device |
torch.device | Block所在设备(GPU/CPU,异构内存管理用) |
3. 进阶优化:前缀缓存(Prefix Caching)
PageAttention的延伸优化,进一步提升内存利用率:
- 多个请求如果有相同的前缀(比如都以“今天天气”开头),可以共享同一个Block;
- 此时Block的
ref_count会记录有多少个请求引用它(比如2个请求共享→ref_count=2); - 只有当
ref_count=0时,Block才会被释放,避免重复存储相同前缀的KV Cache。
总结
核心关键点回顾
- 小白核心:PageAttention是把KV Cache切成固定大小的Block(像分页内存),解决传统KV Cache的碎片化和浪费问题;每个Block存固定数量token的K/V。
- 进阶核心:PageAttention通过“虚拟-物理Block映射”管理内存,计算时只加载有效Block/有效Slot,用CUDA核函数加速;Block不仅存K/V,还包含状态、引用计数等管理信息。
- 关键优化:Block池复用、稀疏计算、前缀缓存是PageAttention高效的核心,也是vLLM比传统推理框架快的关键原因。
Block核心信息总结
- 基础存储:固定数量token的Key和Value矩阵(核心);
- 管理信息:Block ID、使用状态、有效token数、所属请求ID;
- 进阶信息:引用计数、Slot映射、设备信息(异构内存管理)。
更多推荐


所有评论(0)