forked from eaiadmin/rtos_llm_opt
Initial commit
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
# 方向2: KV Cache 与内存管理
|
||||
|
||||
## 1. 问题陈述
|
||||
|
||||
KV Cache是LLM推理中**最大的内存消费者**,也是RTOS内存管理的核心痛点:
|
||||
|
||||
```
|
||||
KV Cache Size ≈ 2 × N_layers × batch_size × seq_len × hidden_dim × 2 bytes
|
||||
|
||||
Example (Qwen2.5-7B, batch=32, seq_len=4096):
|
||||
= 2 × 32 × 32 × 4096 × 4096 × 2 bytes
|
||||
≈ 6.8 GB
|
||||
|
||||
Example (Qwen2.5-1.5B, batch=8, seq_len=2048):
|
||||
= 2 × 32 × 8 × 2048 × 2048 × 2 bytes
|
||||
≈ 2.2 GB
|
||||
```
|
||||
|
||||
RTOS场景下KV Cache管理的关键问题:
|
||||
1. **内存碎片** — 频繁分配/释放导致碎片化
|
||||
2. **带宽竞争** — KV读写与计算同时竞争DDR
|
||||
3. **内存带宽瓶颈** — Attention是memory-bound而非compute-bound
|
||||
4. **多请求KV Cache隔离** — 多流场景下的内存隔离与复用
|
||||
|
||||
## 2. KV Cache结构分析
|
||||
|
||||
### 2.1 KV Cache布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ KV Cache Layout │
|
||||
│ │
|
||||
│ Layer 0: [K_0 | V_0] → Shape: [batch, head, seq, hd] |
|
||||
│ Layer 1: [K_1 | V_1] → Shape: [batch, head, seq, hd] |
|
||||
│ ... │
|
||||
│ Layer N: [K_N | V_N] → Shape: [batch, head, seq, hd] |
|
||||
│ │
|
||||
│ Total = 2 × N_layers × batch × seq × hidden × dtype |
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
Attention计算公式:
|
||||
Output = Softmax(QK^T / √d_k) × V
|
||||
|
||||
Q, K, V均为实时维护的Tensor。推理过程中:
|
||||
- Prefill阶段: K, V一次性填充
|
||||
- Decode阶段: K, V逐tokenappend
|
||||
```
|
||||
|
||||
### 2.2 KV Cache的内存访问特征
|
||||
|
||||
```
|
||||
Access Pattern | Read | Write | Latency | Bandwidth
|
||||
------------------------|-------|-------|---------|----------
|
||||
Prefill K/V init | Low | High | Low | Very High
|
||||
Decode K append | High | Medium| Medium | Medium
|
||||
Attention QK^T | High | Low | Medium | High
|
||||
Attention SV | High | High | Medium | High
|
||||
```
|
||||
|
||||
**关键洞察**:Attention是 **memory-bound** 的,内存带宽比计算算力更容易成为瓶颈。
|
||||
|
||||
## 3. 内存管理策略
|
||||
|
||||
### 3.1 Memory Pool 设计
|
||||
|
||||
**核心思路**:为KV Cache预分配固定大小的内存池,避免运行时malloc/free。
|
||||
|
||||
```c
|
||||
// KV Cache Memory Pool
|
||||
typedef struct {
|
||||
uint8_t *base; // 内存池基地址
|
||||
size_t total_size; // 总大小
|
||||
size_t used_size; // 已使用
|
||||
size_t max_block; // 最大block大小
|
||||
uint32_t n_blocks; // block数量
|
||||
uint32_t *free_map; // 空闲块位图
|
||||
uint32_t alloc_count; // 当前分配次数
|
||||
} kv_cache_pool_t;
|
||||
|
||||
// Block结构 — 每个layer一个block
|
||||
typedef struct {
|
||||
struct kv_cache_pool_t *pool;
|
||||
uint8_t *data; // 数据指针
|
||||
size_t size; // 大小
|
||||
uint32_t layer_id; // 所属layer
|
||||
uint32_t batch_id; // 所属batch
|
||||
bool locked; // 是否锁定(防释放)
|
||||
} kv_block_t;
|
||||
```
|
||||
|
||||
**Pool设计决策**:
|
||||
|
||||
| 策略 | 描述 | 优点 | 缺点 |
|
||||
|-----|------|-----|-----|
|
||||
| **Fixed-size pool** | 预分配固定大小,不动态扩容 | 零碎片,O(1)分配 | 可能浪费或不够用 |
|
||||
| **Buddy system** | powers-of-2分块 | 低碎片 | 内碎片最多50% |
|
||||
| **Slab allocator** | 预分配固定类型cache | 高效同类分配 | 不同size需多个slab |
|
||||
| **Region pool** | 按请求分配region | 便于多请求隔离 | region间可能碎片 |
|
||||
|
||||
**推荐**: 在嵌入式场景用Fixed-size pool,在边缘场景用Slab + Region组合。
|
||||
|
||||
### 3.2 分层KV Cache管理
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ L1: SRAM Cache (芯片内, ~100KB) │
|
||||
│ 最近访问的KV block, 超低延迟(<10ns) │
|
||||
│ 策略: LRU, hardware-managed │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ L2: DDR Memory Pool (RAM, 2-8GB) │
|
||||
│ 所有KV Cache, 中延迟(~100ns) │
|
||||
│ 策略: Slab allocator + region per request │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ L3: Flash/SSD (持久化, 10-100GB) │
|
||||
│ 冷KV Cache, 高延迟(~100μs) │
|
||||
│ 策略: 按需swap, 预读 │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 多请求KV Cache隔离
|
||||
|
||||
```
|
||||
Request A: [Layer 0 Block | Layer 1 Block | ...] → Pool Region A
|
||||
Request B: [Layer 0 Block | Layer 1 Block | ...] → Pool Region B
|
||||
Request C: [Layer 0 Block | Layer 1 Block | ...] → Pool Region C
|
||||
|
||||
Region管理:
|
||||
┌──────────┬──────────┬──────────┬──────────┐
|
||||
│ Region A │ Region B │ Region C │ Free │
|
||||
│ 256MB │ 128MB │ 256MB │ 512MB │
|
||||
└──────────┴──────────┴──────────┴──────────┘
|
||||
|
||||
每个Region包含:
|
||||
- Header: 大小、引用计数、是否锁定
|
||||
- Data: KV Cache数据
|
||||
- Metadata: 序列长度、是否有效、引用task
|
||||
```
|
||||
|
||||
## 4. 内存带宽优化
|
||||
|
||||
### 4.1 带宽竞争模型
|
||||
|
||||
```
|
||||
总带宽 = DDR Bandwidth (e.g., LPDDR5: 6400 Mbps × 4 = 25.6 GB/s)
|
||||
|
||||
带宽分配:
|
||||
KV Cache Read (Attention): █████████████████████ 40%
|
||||
KV Cache Write (Append): ████████████ 25%
|
||||
Weight Read (FFN/Attn): ████████████████████ 30%
|
||||
Input/Output: ████████ 5%
|
||||
|
||||
问题: KV Cache读+写 = 65% 带宽
|
||||
加上Weight Read = 95% 带宽
|
||||
仅剩5% 给其他任务
|
||||
```
|
||||
|
||||
### 4.2 带宽调度策略
|
||||
|
||||
**策略1: 错峰访问**
|
||||
|
||||
```
|
||||
Time →
|
||||
KV Cache Write: [████████][ ][████████]
|
||||
Weight Read: [ ][████████████][ ]
|
||||
Input/Output: [███████][ ][ ]
|
||||
t0 t1 t2
|
||||
```
|
||||
|
||||
**策略2: DMA Batching**
|
||||
|
||||
```
|
||||
Before (no batching):
|
||||
KV Write 1 → DMA → DDR (small chunk)
|
||||
KV Write 2 → DMA → DDR (small chunk)
|
||||
KV Write 3 → DMA → DDR (small chunk)
|
||||
Total DMA overhead: 3 × overhead
|
||||
|
||||
After (batching):
|
||||
KV Write 1,2,3 → DMA burst → DDR (large chunk)
|
||||
Total DMA overhead: 1 × overhead
|
||||
|
||||
RTOS实现:
|
||||
1. KV Write task将多个小DMA请求放入队列
|
||||
2. DMA Controller task批量处理
|
||||
3. 使用RTOS queue传递DMA描述符
|
||||
```
|
||||
|
||||
**策略3: Bandwidth-aware Scheduling**
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint64_t bandwidth_allocated; // 已分配带宽
|
||||
uint64_t bandwidth_used; // 已使用带宽
|
||||
uint64_t peak_bandwidth; // 峰值带宽
|
||||
uint64_t window_ms; // 滑动窗口大小
|
||||
} bandwidth_tracker_t;
|
||||
|
||||
// 任务请求带宽时的检查
|
||||
bool can_allocate_bandwidth(task_t *task, uint64_t required_bw) {
|
||||
if (bandwidth_tracker.used + required_bw <= BANDWIDTH_LIMIT) {
|
||||
bandwidth_tracker.used += required_bw;
|
||||
return true;
|
||||
}
|
||||
// 等待或排队
|
||||
vTaskSuspend(task);
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 In-Place KV Update
|
||||
|
||||
减少KV Cache的write bandwidth:
|
||||
|
||||
```
|
||||
Before (old):
|
||||
for each new token:
|
||||
allocate new KV block // 分配+写
|
||||
write new KV values // 写
|
||||
update pointer // 写
|
||||
|
||||
After (in-place):
|
||||
for each new token:
|
||||
KV_buffer += step_size // 指针移动(O(1))
|
||||
write KV values in-place // 只写一次
|
||||
```
|
||||
|
||||
**实现要点**:
|
||||
- 预分配连续的KV内存(避免碎片)
|
||||
- 使用环形buffer管理seq_len(自然复用)
|
||||
- 维护valid长度指针,不实际移动数据
|
||||
|
||||
## 5. KV Cache替换策略
|
||||
|
||||
### 5.1 缓存淘汰算法
|
||||
|
||||
```
|
||||
当KV Cache满时,需要选择替换策略:
|
||||
|
||||
策略 | 复杂度 | 命中率 | 适用场景
|
||||
----------------------|--------|--------|---------
|
||||
LRU | O(1) | Good | 通用
|
||||
LFU | O(log n)| Better | 长对话
|
||||
Sliding Window | O(1) | Good | 固定上下文
|
||||
Hybrid (Window+LFU) | O(1) | Best | 生产环境
|
||||
```
|
||||
|
||||
**RTOS友好实现** (LRU with doubly-linked list):
|
||||
|
||||
```c
|
||||
typedef struct kv_cache_node {
|
||||
uint32_t layer;
|
||||
uint32_t token_pos;
|
||||
struct kv_cache_node *prev;
|
||||
struct kv_cache_node *next;
|
||||
uint8_t *data;
|
||||
uint64_t last_access;
|
||||
} kv_cache_node_t;
|
||||
|
||||
// LRU: 访问时移动到链表头部
|
||||
void kv_cache_access(uint32_t layer, uint32_t pos) {
|
||||
kv_cache_node_t *node = find_node(layer, pos);
|
||||
if (node) {
|
||||
node->last_access = get_tick_ms();
|
||||
move_to_head(&lru_list, node); // O(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Evict: 从链表尾部淘汰
|
||||
void kv_cache_evict(void) {
|
||||
kv_cache_node_t *victim = lru_list.tail;
|
||||
remove_from_list(victim);
|
||||
free_block(victim);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 压缩策略
|
||||
|
||||
```
|
||||
策略 | 压缩比 | 精度损失 | 带宽节省 | 计算开销
|
||||
--------------|--------|----------|----------|---------
|
||||
Float16 | 1.0x | 0% | 基准 | 无
|
||||
Int8 | 2.0x | ~2% | 50% | 低
|
||||
Int4 | 4.0x | ~5% | 75% | 中
|
||||
Sparsification| 2-8x | 1-10% | 50-87% | 中-高
|
||||
Paged KV | variable| 0% | variable | 低
|
||||
```
|
||||
|
||||
## 6. 各硬件平台的内存管理差异
|
||||
|
||||
### 6.1 MCU级 (256KB~2MB)
|
||||
|
||||
```
|
||||
约束:
|
||||
- 内存极小,无法容纳完整KV Cache
|
||||
- 通常只支持一个请求
|
||||
- 模型参数也需压缩
|
||||
|
||||
策略:
|
||||
- KV Cache固定大小(如256 token)
|
||||
- 使用连续内存池,零碎片
|
||||
- KV Cache满后触发OOM处理(丢弃 oldest)
|
||||
- 可能需要在Flash上swap
|
||||
```
|
||||
|
||||
### 6.2 SoC级 (2-8GB)
|
||||
|
||||
```
|
||||
约束:
|
||||
- 内存有限但可容纳小模型KV Cache
|
||||
- 多请求时内存竞争严重
|
||||
- NPU有独立的memory pool
|
||||
|
||||
策略:
|
||||
- 分区管理: CPU pool + NPU pool
|
||||
- 使用Paged Memory (类似vLLP)
|
||||
- NPU-Shared Memory通过DMA同步
|
||||
```
|
||||
|
||||
### 6.3 Edge盒子 (8-16GB)
|
||||
|
||||
```
|
||||
约束:
|
||||
- 内存充足但带宽可能受限
|
||||
- 多请求+大模型场景
|
||||
|
||||
策略:
|
||||
- 分层缓存: SRAM(热点) + DDR(主流) + SSD(冷数据)
|
||||
- 预读策略: 预测下一个token的KV位置
|
||||
- 多流隔离: CGroup级别内存限制
|
||||
```
|
||||
|
||||
### 6.4 Server级
|
||||
|
||||
```
|
||||
约束:
|
||||
- 内存充足(NVMe/DDR)
|
||||
- 主要问题是带宽和NUMA
|
||||
|
||||
策略:
|
||||
- NUMA-aware KV placement
|
||||
- NVMe作为KV Cache扩展
|
||||
- 多GPU间KV Cache同步
|
||||
```
|
||||
|
||||
## 7. 关键设计决策
|
||||
|
||||
| 决策点 | 选项 | 推荐 | 理由 |
|
||||
|-------|------|-----|------|
|
||||
| 内存分配器 | malloc/free / Pool / Slab | **Slab + Region** | 零碎片+多请求隔离 |
|
||||
| KV Cache布局 | 连续 / Paged / Sparse | **Paged** | 灵活+低碎片 |
|
||||
| 替换策略 | LRU / LFU / Sliding | **Sliding Window** | 计算复杂度O(1) |
|
||||
| 压缩策略 | FP16 / Int8 / Int4 | **Int8** | 精度-带宽权衡最优 |
|
||||
| 带宽管理 | 固定分配 / 动态 | **Bandwidth-aware** | 避免带宽饱和 |
|
||||
| 多流隔离 | 共享池 / Region | **Region** | 公平+可分析 |
|
||||
|
||||
## 8. 开放研究问题
|
||||
|
||||
1. **Dynamic KV Cache Sizing**: 运行时根据负载自动调整KV Cache大小?
|
||||
2. **Cross-device KV**: 在CPU-NPU间共享/分发KV Cache?
|
||||
3. **KV Cache Compression**: 有损压缩的RTOS友好实现?
|
||||
4. **Predictive Allocation**: 预测最大seq_len,预分配最优大小?
|
||||
5. **KV Cache Migration**: 请求迁移时的KV Cache热迁移?
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-09-17*
|
||||
Reference in New Issue
Block a user