跳至主要內容

vLLM核心机制

pptg大约 5 分钟

vLLMopen in new window是加州伯克利开源的LLM推理框架,核心目标是最大化推理吞吐量、降低延迟, 其核心优化机制为: PagedAttentionPrefix CacheContinuous Batching

1. PagedAttention: 分页注意力

在Transformer解码时,每个token的生成,都需要和之前所有token的KV做注意力计算,如果不缓存,每次都需要重新计算整个序列,效率极低。KV Cache通过存储所有历史token的键值对,实现了空间换时间,最终复杂度从O(N2)降低到了O(N)

传统方法

传统的缓存方法,通常为每个推理请求分配一块连续的显存空间来存储KV缓存。在多用户并发请求的场景下,会引发以下问题:

  • 碎片化严重:不同请求的上下文长度不同,请求到达和完成时间不一致,导致连续显存的频繁分配与释放,产生大量碎片。
  • 预分配冗余:为应对可能出现的超长上下文,系统会为个请求预留更多缓存空间,很可能用不上,导致显存浪费。

PagedAttention

vLLM借鉴操作系统的分页思路,来管理KV Cache。

  • 分块:将KV Cache划分为固定大小的块(Block),每个块包含固定数量token的键值。
  • 按需分配:序列的KV Cache由若干个非连续的物理块组成,通过一个逻辑块表来映射。
  • 优势
    • 非连续存储:blocks 可分散在显存任意位置;
    • 按需分配:prefill/decode 时动态申请新 block;
    • 高内存利用率:可达 90%+(传统方法通常 < 20%);
    • 支持共享:多个请求可引用同一 block(只读)。
因素Block Size较小Block Size较大
内存碎片较多较少
缓存效率粒度细,利于共享短前缀,但对长前缀来说,管理开销更大粒度粗。共享长前缀效率高,但短前缀可能浪费空间
吞吐量可能因调度开销增加而降低吞吐量通常利于提高吞吐量

vLLM为每个请求维护一个块表(block table),用于记录逻辑 token 序列中各段K/V缓存物理显存块之间的映射关系

2. Prefix Cache: 前缀缓存优化

核心机制

不仅单个请求内会有KV复用,跨请求之间也存在KV复用,如:

  • 同一个系统提示词,处理不同用户的请求
  • 同一个用户,携带历史记录进行对话
  1. 缓存触发条件
  • 启动时指定参数: enable-prefix-caching
  • block已填满(prefill完成或者decode写满)
  1. 缓存粒度
  • block为单位
  • 每个block根据以下信息生成hash key
    • block内部的Token IDS: 内容敏感
    • 对应的Position IDS: 位置敏感
    • 模型层标识(多层共享时): 同一个token,每层的KV矩阵不同

调度策略

  1. 释放策略(引用计数法):每个cached block 维护ref_count:
  • +1: 被某个请求引用(decode/prefill 使用)
  • +1: 被缓存系统自身持有(即使无活跃请求)
  • ref_count == 0时,会触发物理显存的释放
  • 显存不足时,使用LRU规则驱逐cache_block.ref_count -= 1
  1. 复用逻辑(Partial Match)
  • 新请求的 prompt 从头开始逐 block 匹配
  • 只要连续前缀匹配,就复用对应 blocks;
  • 一旦出现不匹配(如不同 token),后续部分重新计算;
  • 支持任意长度前缀复用(哪怕只共享 1 个 block)。

3. Continuous Batching: 连续批处理

在 LLM 推理中,每个请求包含两阶段:

  • Prefill(预填充):一次性处理整个 prompt(并行计算,快)
  • Decode(解码):逐 token 生成(自回归,慢)

传统静态批处理

  • 所有请求必须同时开始、同时结束
  • 但不同请求的 prompt 长度和生成长度差异巨大;
  • 结果:短请求必须等待长请求完成GPU 利用率极低(大量时间空转)。

Continuous Batching

核心机制

Continuous Batching是一种动态、异步的批处理策略:

  • 随时加入新请求(只要显存允许);
  • 每个请求的prefill 和 decode 分开调度;
  • 已完成的请求立即退出 batch,释放资源;
  • 新 token 生成阶段(decode)可与其他请求的 prefill 混合执行

vLLM按照如下步骤实现连续批处理:

  1. 讲每个请求的PrefillDecode任务分离
  2. 使用调度器(Scheduler)动态组建batch, PrefillDecode混合在一个batch里,就叫做chunked prefill
while has_free_gpu_memory():
    if new_requests_in_queue():
        add_prefill_request()   # 加入 prefill
    if decode_requests_waiting():
        add_decode_request()    # 加入 decode
  1. 请求生命周期管理
  • 请求进入 → 分配 blocks → 执行 prefill → 进入 decode 队列;
  • 每次 decode 生成 1 个 token;
  • 生成完毕(EOS 或 max_len)→ 释放 blocks → 从 batch 移除;
  • 新请求可随时插入,老请求可随时退出。

为什么Prefill和Decode可以在一个Batch里

虽然Prefill和Decode看起来不同,但它们的底层计算本质上是相似的:

  • 都使用相同的attention机制
  • 都需要矩阵乘法和attention计算
  • 都可以分解为更小的计算单元

vLLM中,将长序列的Prefill分解成多个chunk,Prefill和Decode都使用了causal mask,每个chunk的计算模式类似于Decode步骤,以此实现混合调度。

  • prefill: 直接取1 - max_length
  • decode: 其余位置用pad填充
# 内部表示
batch_tokens = [
    # Prefill chunk: 64个token
    [t1, t2, ..., t64],
    # Decode 1: 1个token + 49个填充(为了对齐)
    [t1, pad, pad, ..., pad],
    # Decode 2: 1个token + 49个填充
    [t1, pad, pad, ..., pad],
    # ... 更多decode
]

注意

使用了chunked prefill之后,由于自回归的特点,后一个 chunk 的 attention 计算依赖前一个 chunk 产生的 KV cache。因此prefill的推理方式,从完全并行,变成了chunk内并行,chunk间串行。