Files
2026-05-16 17:16:51 +08:00

317 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Paged Attention
## 背景与动机
Paged Attention是vLLMVirtual Machine for LLM Serving提出的注意力机制改进旨在解决LLM推理中的KV Cache内存管理问题。
### 传统KV Cache的问题
```
问题1: 预分配固定大小,连续内存占用
问题2: 不同序列长度差异大,内存浪费
问题3: 无法动态调整,序列长度受预分配限制
```
```
传统方法内存分配:
Sequence 1: [Block 0: 512 tokens] [Block 1: 512 tokens] ...
Sequence 2: [Block 0: 512 tokens] [Block 1: 512 tokens] ...
问题短的序列仍然占用完整Block导致内存碎片化
```
## Paged Attention原理
### 核心思想
借鉴操作系统虚拟内存的页调度思想将KV Cache分页管理每页固定大小按需分配不要求连续物理内存。
```
物理内存布局:
Page 0: [Physical Block 0] ← Sequence 1 使用
Page 1: [Physical Block 1] ← Sequence 1 使用
Page 2: [Physical Block 2] ← Sequence 2 使用
Page 3: [Physical Block 3] ← Sequence 2 使用
Page 4: [Physical Block 0] ← Sequence 3 使用(共享/回收)
```
### 逻辑页与物理块映射
```python
class PagedAttentionCache:
"""KV Cache的页式管理"""
def __init__(self, block_size=16, num_blocks=1000):
self.block_size = block_size # 每块token数
self.num_blocks = num_blocks
# 逻辑页→物理块映射表
# 对于序列1: logical_pages = [0, 1] → physical_blocks = [5, 8]
# 对于序列2: logical_pages = [0, 1] → physical_blocks = [3, 12]
self.block_mapping = {} # seq_id -> list of physical_block_ids
# 物理块内存池
self.memory_pool = [None] * num_blocks
def get_physical_blocks(self, seq_id):
"""获取序列的物理块列表"""
return self.block_mapping.get(seq_id, [])
def allocate_sequence(self, seq_id, num_tokens):
"""为新序列分配物理块"""
num_blocks_needed = ceil(num_tokens / self.block_size)
physical_blocks = []
for _ in range(num_blocks_needed):
block_id = self._find_free_block()
physical_blocks.append(block_id)
self.block_mapping[seq_id] = physical_blocks
return physical_blocks
def append_tokens(self, seq_id, num_new_tokens):
"""扩展序列的KV Cache"""
current_blocks = self.block_mapping[seq_id]
current_tokens = len(current_blocks) * self.block_size
if current_tokens + num_new_tokens > len(current_blocks) * self.block_size:
# 需要分配新块
new_blocks_needed = ceil((current_tokens + num_new_tokens) % self.block_size / self.block_size)
for _ in range(new_blocks_needed):
block_id = self._find_free_block()
current_blocks.append(block_id)
```
## 物理块管理
### 块分配策略
```python
class BlockManager:
"""物理块管理器"""
def __init__(self, num_blocks=10000):
self.num_blocks = num_blocks
# 空闲块链表
self.free_blocks = list(range(num_blocks))
# 块引用计数(用于共享)
self.ref_count = [0] * num_blocks
# 块状态追踪
self.block_states = ['free'] * num_blocks
def allocate(self, num_blocks=1):
"""分配连续物理块"""
if len(self.free_blocks) < num_blocks:
raise OutOfMemoryError()
allocated = []
for _ in range(num_blocks):
block_id = self.free_blocks.pop()
self.block_states[block_id] = 'allocated'
allocated.append(block_id)
return allocated
def free(self, block_ids):
"""释放物理块"""
for block_id in block_ids:
self.block_states[block_id] = 'free'
self.free_blocks.append(block_id)
```
### 内存布局
```
GPU Memory Layout (示例):
Block 0: [KV Cache for tokens 0-15] ← Sequence A
Block 1: [KV Cache for tokens 16-31] ← Sequence A
Block 2: [KV Cache for tokens 0-15] ← Sequence B
Block 3: [KV Cache for tokens 0-15] ← Sequence C
Block 4: [KV Cache for tokens 16-31] ← Sequence C
...
Block 999: [KV Cache for tokens 496-511] ← Sequence Z
```
## Prefix Caching机制
### 共享前缀
多个请求往往共享系统提示system prompt或用户模板的前缀
```
Prompt 1: "System: You are helpful. User: What is AI?"
Prompt 2: "System: You are helpful. User: Tell me about ML"
共享前缀: "System: You are helpful. User: "
专有后缀: "What is AI?" / "Tell me about ML"
```
### 前缀哈希与缓存
```python
import hashlib
class PrefixCache:
"""前缀缓存管理"""
def __init__(self):
self.cache = {} # hash -> (block_ids, reference_count)
self.hash_prefix = 64 # 前缀长度用于哈希
def compute_prefix_hash(self, token_ids):
"""计算前缀哈希"""
prefix = token_ids[:self.hash_prefix]
return hashlib.sha256(bytes(prefix)).hexdigest()
def lookup_prefix(self, token_ids):
"""查找匹配的前缀缓存"""
prefix_hash = self.compute_prefix_hash(token_ids)
if prefix_hash in self.cache:
return self.cache[prefix_hash]
return None
def store_prefix(self, token_ids, block_ids):
"""存储前缀的KV Cache块"""
prefix_hash = self.compute_prefix_hash(token_ids)
self.cache[prefix_hash] = (block_ids, ref_count=1)
```
### 缓存命中检测
```python
def check_prefix_hit(prompt_tokens):
"""检查前缀缓存是否命中"""
# 1. 计算系统前缀哈希
system_prefix = extract_system_prefix(prompt_tokens)
system_hash = hash_prefix(system_prefix)
# 2. 检查缓存
cached_blocks = prefix_cache.lookup(system_hash)
if cached_blocks:
# 3. 验证完整前缀匹配
if verify_prefix_match(prompt_tokens, cached_blocks):
return cached_blocks
return None
```
### 缓存收益分析
| 场景 | 无Prefix Caching | 有Prefix Caching |
|------|-----------------|-----------------|
| 100个相同系统前缀的请求 | 100 × prefix_compute | 1 × prefix_compute + 99 × cache_hit |
| 前缀长度=1024 tokens | 100 × O(1024²) | 1 × O(1024²) + 99 × O(0) |
| 节省计算量 | - | ~99% |
## vLLM实现
### 核心API
```python
from vllm import LLM, SamplingParams
# 初始化引擎
llm = LLM(
model="meta-llama/Llama-2-7b-hf",
tensor_parallel_size=2, # 张量并行
block_size=16, # 每块16个token
max_num_seqs=256, # 最大并发序列数
max_num_batched_tokens=8192 # 最大批处理token数
)
# 推理请求
outputs = llm.generate(
prompts=["Hello, world!", "How are you?"],
sampling_params=SamplingParams(temperature=0.8, top_p=0.95)
)
```
### 配置参数
| 参数 | 说明 | 典型值 |
|------|------|-------|
| block_size | 每物理块的token数 | 16 |
| max_num_seqs | 最大并发序列数 | 256 |
| max_num_batched_tokens | 每批最大token数 | 8192 |
| gpu_memory_utilization | GPU显存利用比例 | 0.9 |
### 内部实现流程
```
1. 请求入队Request Enqueue
└→ 解析prompt构建Sequence对象
2. 调度Scheduling
└→ Continuous Batching决定当前batch
└→ 检查prefix cache命中
3. 前向传播Forward Pass
└→ PagedAttention计算
└→ 块管理器更新映射
4. 采样Sampling
└→ 取出新生成的token
└→ 检查序列是否完成
5. 输出Output
└→ 序列完成则输出结果
└→ 释放物理块
```
## Paged Attention与传统Attention对比
| 特性 | 传统Attention | Paged Attention |
|------|--------------|-----------------|
| 内存管理 | 连续分配 | 分页管理 |
| 内存碎片 | 高 | 低 |
| 最大序列长度 | 预分配上限 | 受总显存限制 |
| 序列扩展 | 不可变 | 动态可扩展 |
| 共享前缀 | 不支持 | 支持 |
| 实现复杂度 | 低 | 中 |
## 性能收益
### 吞吐提升
```
配置: LLaMA-7B, A100 80GB
| 方法 | Throughput (req/s) | GPU Memory Utilization |
|------|-------------------|------------------------|
| HuggingFace (naive) | 5.2 | 45% |
| vLLM (Paged Attention) | 24.8 | 92% |
| 提升 | ~4.8x | 2x |
```
### 内存效率
```
序列长度分布: [128, 256, 512, 1024, 2048]
传统方法预分配2048:
平均内存浪费 = 1 - 平均长度 / 2048
典型值 = 1 - 512 / 2048 = 75%
Paged Attention:
内存浪费 < 5%
```
## 与其他优化技术的关系
- **Continuous Batching**Paged Attention是Continuous Batching的底层支持
- **KV Cache**Paged Attention是KV Cache的物理块实现
- **Prefix Caching**依赖Paged Attention的块映射机制
- **Flash Attention**可与Paged Attention结合使用
## 注意事项
1. **块大小选择**:过大导致内存浪费,过小导致管理开销
2. **块回收**:序列完成后需及时回收物理块
3. **共享块引用计数**:共享前缀时需正确维护引用计数
4. **Prefill阶段**:新请求的首次前向传播需要特殊处理