Nano vllm
LLM的高效推理面临一些问题
- KV-cache随着序列长度线性增长
- 一个Batch中不同请求的序列长度不同,导致碎片化的问题
1. PagedAttention
pagedAttention就是在上述背景下提出来的方案,其背后的理念就是操作系统领域的分页机制,来实现高效的GPU内存使用
- 物理内存 -> GPU 显存
- 物理页帧 -> KV Cache 块 (Block)
- 页表 -> 块表 (Block Table)
- 进程 -> 单个请求 (Sequence)
KV-Cache块被分成了固定大小的block,请求使用的KV-Cache逻辑上是连续的,但是在物理存储上是离散的
graph TD
subgraph "Sequence A (Logical View)"
direction LR
A1["Token 1-16"] --> A2["Token 17-32"] --> A3["Token 33-48"]
end
subgraph "Sequence B (Logical View)"
direction LR
B1["Token 1-16"] --> B2["Token 17-32"]
end
subgraph "Physical KV Cache Blocks on GPU"
P1["Block #1"]
P2["Block #2"]
P3["Block #3"]
P4["Block #4"]
P5["Block #5"]
P6["Block #6"]
P7["Block #7"]
end
A1 -- "maps to" --> P5
A2 -- "maps to" --> P2
A3 -- "maps to" --> P6
B1 -- "maps to" --> P1
B2 -- "maps to" --> P4
style P5 fill:#f9f
style P2 fill:#f9f
style P6 fill:#f9f
style P1 fill:#bbf
style P4 fill:#bbf
为这个请求(在 nano-vllm 中称为一个 Sequence)创建一个空的块表 (block_table)。在 prefill 阶段,根据其提示(prompt)的长度,按需分配相应数量的 Block,并将这些 Block 的 ID 填入其 block_table。在 decode 阶段,每当序列长度增长,需要一个新的 Block 时,再从全局的空闲块池中取一个,并更新其 block_table。
除了分页的内存管理机制以外,nano-vllm还引入了很多os中的技巧,比如抢占式的进程调度工具,在vllm中就表现为抢占式的prefill,这部分由Scheduler完成,这也是vllm实现持续批处理的关键
2. ModelRunner
这是nano-vllm最核心的部分之一,涉及到核心的计算部分,PagedAttention主要应对的是KV-Cache的部分,而ModelRunner就是处理参数,激活值等部分
内部实现了多种机制,包括TP,Cuda Graph
3. 一个请求的流程
graph LR
subgraph User
U["User calls llm.generate()"]
end
subgraph LLMEngine
G["generate() loop"] --> AR["add_request()"]
end
subgraph Scheduler
W["waiting_queue"]
end
U --> G
AR -- "seq = Sequence(prompt)" --> AR
AR -- "scheduler.add(seq)" --> W