3. vLLM核心特性:PagedAttention、Continuous Batching、优化目标

3.1 为什么传统推理系统难以支撑大模型高并发?

在深入 vLLM 的核心特性之前,我们先要理解传统推理框架(如简单的 Hugging Face Transformers 的 generate 接口)在高并发场景下遇到的瓶颈。大模型推理的显存占用主要由两部分构成:模型权重和KV Cache(键值缓存)。模型权重是静态的,而 KV Cache 是动态的,它随着生成的 Token 数量线性增长。

传统系统通常采用静态显存预分配策略:为每个请求预先分配一块连续的最大长度显存(例如 max_length=2048)。这会导致两个严重问题:

  • 显存碎片化:不同请求的生成长度差异巨大,预分配的长短不一,经过多次调度后,显存中会出现大量无法利用的碎片。
  • 显存浪费:即使请求只生成了 50 个 Token,系统也占用了 2048 个 Token 的 KV Cache 空间,利用率极低。

这种“按最大长度预留”的方式,使得系统能同时处理的并发请求数(Throughput)非常有限。vLLM 正是为了解决这一问题而设计的,其核心是 PagedAttention 机制。

直观理解:传统方式类似于在内存中为每个进程分配一整块连续物理内存,容易产生外部碎片;而 PagedAttention 类似于操作系统的虚拟内存分页机制,将逻辑上连续的 KV Cache 映射到不连续的物理显存块中,按需分配。
传统 vs PagedAttention 显存管理对比 传统:连续预分配 请求A:预分配 2048 tokens 实际使用 120 tokens 请求B:预分配 2048 tokens 实际使用 800 tokens 内部碎片(浪费) 并发上限 ≈ 显存总量 / 2048 PagedAttention:分页管理 逻辑视图(连续) Token 0-3 页表映射 物理显存块(不连续) 块7 块3 块1 块9 按需分配,无内部碎片 • 仅分配实际需要的块 • 块大小固定(如 16 tokens/块) 并发上限 ≈ 显存总量 / 实际使用量 结论:PagedAttention 可将显存利用率提升至接近 100%,并发吞吐量提升 2-4 倍 (数据来源:vLLM 论文及官方 Benchmark)

3.2 PagedAttention:核心原理与实现

PagedAttention 是 vLLM 的基石,它借鉴了操作系统虚拟内存的分页思想。在传统的 Attention 计算中,KV Cache 被存储为连续的张量。而 PagedAttention 将每个序列的 KV Cache 划分为固定大小的块(Block),每个块可以存储在物理显存的任意位置,通过块表(Block Table)来维护逻辑块到物理块的映射。

具体实现包含以下关键数据结构:

  • 逻辑块(Logical Block):每个请求的 KV Cache 在逻辑上被划分为连续的块,编号从 0 开始。
  • 物理块(Physical Block):显存中实际分配的块,每个块大小为 block_size(通常为 16 或 32 个 Token)。
  • 块表(Block Table):一个映射数组,记录每个逻辑块对应的物理块 ID。

在 Attention 计算时,PagedAttention 不是一次性计算整个序列的 Attention,而是逐块(Block-wise)进行计算。对于每个查询 Token,它通过块表找到对应的物理块,然后在该块内执行标准的 Attention 计算,最后将结果累加。这种方式虽然增加了循环次数,但避免了显存拷贝,且能充分利用 GPU 的并行能力。

# 伪代码:PagedAttention 核心逻辑
def paged_attention(query, block_table, kv_cache, block_size):
    scores = []
    for logical_block_id in range(len(block_table)):
        physical_block_id = block_table[logical_block_id]
        key_block = kv_cache[physical_block_id]  # 获取物理块
        # 计算当前块内的 attention score
        block_scores = query @ key_block.T
        scores.append(block_scores)
    # 拼接所有块的 scores 并 softmax
    all_scores = concat(scores)
    probs = softmax(all_scores)
    # 加权求和 value
    output = 0
    for logical_block_id in range(len(block_table)):
        physical_block_id = block_table[logical_block_id]
        value_block = kv_cache[physical_block_id]
        output += probs[:, logical_block_id*block_size:(logical_block_id+1)*block_size] @ value_block
    return output

3.3 Continuous Batching:动态调度与吞吐优化

传统的 Batching 是静态的:一个 Batch 中的所有请求必须全部完成生成后,才会释放资源并接收新的请求。这会导致 GPU 在 Batch 尾部出现大量空闲等待(因为短请求已经结束,长请求还在生成)。

vLLM 实现了 Continuous Batching(连续批处理),也称为 Iteration-level Scheduling(迭代级调度)。其核心思想是:在每个 Decoding 迭代(Iteration)结束后,立即将已经完成的请求移出 Batch,并加入新的等待请求。

这种机制带来了显著收益:

  • GPU 利用率最大化:每个迭代都保持 Batch 满载,不会因为个别长请求而阻塞整个 Batch。
  • 降低请求延迟:新请求无需等待当前 Batch 完全结束,只需等待下一个迭代即可开始处理。
  • 提高吞吐量:在相同显存下,可以处理更多的并发请求。
调度策略细节:vLLM 的调度器(Scheduler)在每个迭代会执行以下步骤:
  1. 检查完成:扫描当前 Batch 中所有请求,标记已生成 EOS 或达到 max_tokens 的请求为完成。
  2. 释放资源:回收已完成请求的 KV Cache 物理块,加入空闲块池。
  3. 加入新请求:从等待队列中取出请求,为其分配物理块,加入当前 Batch。
  4. 执行迭代:对更新后的 Batch 执行一次 Decoding。

3.4 优化目标:吞吐量、延迟与显存效率的平衡

vLLM 的设计围绕三个核心优化目标展开,它们相互制约,需要在实际部署中权衡:

优化目标 核心指标 vLLM 的应对策略 典型场景
吞吐量(Throughput) Tokens/s(每秒生成的 Token 数) PagedAttention 提高显存利用率 → 更大 Batch;Continuous Batching 保持 GPU 满载 离线批量处理、数据标注
延迟(Latency) TTFT(首 Token 时间)、TPOT(每 Token 时间) 动态调度避免排队等待;并行采样(Parallel Sampling)加速单请求 在线对话、实时交互
显存效率 KV Cache 利用率、最大并发数 分页管理消除碎片;按需分配;支持 gpu_memory_utilization 参数调节 单卡部署、低成本推理

在实际使用中,vLLM 提供了多个参数来帮助用户在这三个目标之间进行调节:

# 通过参数调节优化目标
from vllm import LLM, SamplingParams

# 高吞吐模式:增大 batch,使用更大的 gpu_memory_utilization
llm_throughput = LLM(
    model="meta-llama/Llama-3-8B-Instruct",
    gpu_memory_utilization=0.95,      # 尽量多分配显存给 KV Cache
    max_num_batched_tokens=8192,      # 增大单次迭代的 token 数
    max_num_seqs=256,                 # 增大并发序列数
)

# 低延迟模式:限制 batch,优先保证响应速度
llm_latency = LLM(
    model="meta-llama/Llama-3-8B-Instruct",
    gpu_memory_utilization=0.80,      # 留出余量
    max_num_batched_tokens=2048,      # 减小 batch
    max_num_seqs=32,                  # 减少并发
)

3.5 案例与误区复盘

案例:从 8 并发到 80 并发的提升

某团队使用 Hugging Face Transformers 部署 Llama-2-7B 模型,在单张 A100-40GB 上,最大并发数仅为 8(因为每个请求预分配了 2048 Token 的 KV Cache)。切换到 vLLM 后,通过 PagedAttention 和 Continuous Batching,在保持相同延迟(TTFT < 200ms)的前提下,并发数提升至 80,吞吐量从 120 tokens/s 提升至 1100 tokens/s,提升了近 9 倍。

常见误区

误区 1:认为 PagedAttention 会降低计算效率。
实际上,虽然 PagedAttention 引入了块级循环,但 vLLM 通过高度优化的 CUDA Kernel 和块内并行计算,使得计算效率接近甚至超过传统的连续 Attention。在长序列场景下,由于减少了显存拷贝,效率反而更高。
误区 2:盲目调大 gpu_memory_utilization 到 1.0。
如果设置为 1.0,可能会导致没有足够显存加载模型权重或进行 CUDA 上下文初始化。建议保留 5%-10% 的显存余量,设置为 0.90-0.95 较为安全。
误区 3:忽略 max_num_seqs 与显存的匹配关系。
即使设置了很大的 max_num_seqs,如果显存不足,vLLM 会自动降低实际并发数。反之,如果设置过小,则无法充分利用 PagedAttention 的优势。建议根据模型大小和显存容量进行估算,并通过 /metrics 端点监控实际的 KV Cache 使用率。

复盘总结

vLLM 的成功在于它将操作系统的成熟思想(分页、虚拟内存)引入到了大模型推理领域,解决了显存管理的根本性痛点。PagedAttention 解决了“空间”问题,Continuous Batching 解决了“时间”问题,两者结合,使得 vLLM 在吞吐量和显存效率上远超传统框架。理解这三个核心特性及其背后的权衡,是正确配置和使用 vLLM 的基础。