跳转至

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