vLLM 深度解析:连续批处理与内存高效推理

大语言模型(LLM)推理服务的扩展性瓶颈,往往不在计算吞吐量,而在 GPU 显存。传统推理框架将 KV Cache 以连续张量形式预分配,造成大量内存浪费,导致 batch size 被严重限制。

大语言模型(LLM)推理服务的扩展性瓶颈,往往不在计算吞吐量,而在 GPU 显存。传统推理框架将 KV Cache 以连续张量形式预分配,造成大量内存浪费,导致 batch size 被严重限制。vLLM 通过引入操作系统虚拟内存机制启发的 PagedAttention,将显存利用效率推向新高度,成为当前开源 LLM 推理引擎的事实标准之一。

一、LLM 推理的核心挑战

1.1 Prefill 与 Decode 两阶段特性

LLM 推理包含两个截然不同的计算阶段:

  • Prefill(预填充):接收用户完整 prompt,计算所有 token 的注意力,生成第一个输出 token。此阶段计算密集,可充分利用 GPU 算力,但延迟高(与 prompt 长度成正比)。
  • Decode(解码):自回归生成后续 token,每次仅处理最新一个 token。此阶段内存带宽受限,计算利用率低。

两个阶段在延迟、计算密度和内存访问模式上差异巨大,对调度系统提出了挑战。

1.2 KV Cache 的显存爆炸

Transformer 每层的每个注意力头都需要缓存 Key 和 Value 矩阵。以 FP16 精度计算,单条请求的 KV Cache 显存占用为:

2 × num_layers × num_heads × head_dim × seq_len × 2 bytes

以 Llama-2-70B 为例(80 层、8 组 GQA、128 head_dim),序列长度 4096 时:

def kv_cache_size(layers, heads, head_dim, seq_len, bytes_per_val=2):
    """KV Cache 显存占用(单位:字节)"""
    return 2 * layers * heads * head_dim * seq_len * bytes_per_val

# Llama-2-70B, GQA 8 KV heads per group
size = kv_cache_size(
    layers=80,
    heads=8,          # GQA 后的 KV head 数
    head_dim=128,
    seq_len=4096
)
print(f"单请求 KV Cache: {size / 1024**3:.2f} GiB")
# 输出: 约 1.25 GiB

若 batch_size = 16,仅 KV Cache 就需 20 GiB,再加上模型权重(140 GiB @ FP16),GPU 显存瞬间告急。

1.3 预分配导致的内存浪费

传统框架如 Hugging Face Transformers 在推理开始前就按照 (max_batch_size, max_seq_len) 分配固定大小的 KV Cache。实际场景中,序列长度差异巨大:一条请求 50 tokens,另一条 4000 tokens,但两者占用同等大小的预分配张量。这种 internal fragmentation(内部碎片) 使得显存利用率极低,batch size 往往由内存而非算力决定上限。

二、PagedAttention:虚拟内存的工程迁移

vLLM 的核心创新 PagedAttention,其思想直接借鉴操作系统虚拟内存管理:将物理内存划分为固定大小的页(page),通过页表实现逻辑地址到物理地址的按需映射。

2.1 核心设计

Block Table(块表):每张请求维护一个块表,记录逻辑 token 位置到物理内存块的映射。块大小固定(默认 16 个 token),物理块在显存池中按需分配,逻辑上连续但物理上不连续。

非连续物理存储:与传统框架要求 KV Cache 为 (batch, seq_len, ...) 的连续张量不同,vLLM 的物理 KV Cache 存储在离散的块中。注意力计算通过块表索引,按块加载 KV 数据,绕开了连续内存的约束。

Copy on Write(写时复制):当多个请求共享同一段 prompt prefix(如 system prompt、RAG context)时,它们共用同一组物理块,引用计数递增。当某条请求生成新 token 需要写入时,再复制一份私有块。这类似于操作系统中 fork() 的 COW 机制。

2.2 对比传统方案

机制传统框架vLLM PagedAttention
存储方式连续预分配张量离散固定大小块
内存分配静态,按最大序列动态,按需分配
碎片问题严重内部碎片仅尾部块有碎片
Prefix 共享复制多份COW 共享
块大小不固定(与 seq_len 相关)固定 16 tokens(可调)

三、调度器设计:Requests 的生命周期管理

vLLM 的调度器是另一个工程亮点。它通过细粒度状态机和连续批处理策略,最大化 GPU 利用率。

3.1 请求三态模型

每条请求在生命周期中处于以下状态之一:

  • Waiting:刚到达,等待 GPU 资源执行 prefill。
  • Running:正在 GPU 上执行 prefill 或 decode。
  • Swapped:因显存不足,KV Cache 被换出到 CPU 内存,等待重新调度。
[Waiting] --prefill 完成--> [Running] --decode 生成--> [Done]
    ^                            |
    |__ swapped 换入 ____________|__ 显存不足 swapped 换出 __>

3.2 连续批处理(Continuous Batching)

传统 Static Batching(静态批处理) 要求同批次内所有请求同时开始、同时结束。最短请求完成后,其 slot 也无法被新请求复用,直到批次内最长请求结束,造成大量算力空转。

Continuous Batching,又称 In-flight Batching,允许在每次 forward 之间动态增删批次中的请求:

  1. 每轮 decode 完成后,检查是否有运行中请求已满足停止条件(EOS、max_tokens),将其移出批次。
  2. 从 Waiting 队列挑选新请求加入批次,执行 prefill,生成首个 token 后立即与当前 decode token 合并为一个统一的 forward batch。
  3. 通过 Token Budget Scheduling(token 预算调度),限制每轮迭代处理的 token 总数,平衡吞吐与延迟。

3.3 抢占与重计算策略

当新请求涌入导致显存不足时,调度器采取以下策略:

  • Preemption(抢占):将部分 Running 请求降级为 Swapped,将其 KV Cache 块换出到 CPU RAM。
  • Recomputation(重计算):当换入请求重新调度时,可选择直接恢复 KV Cache,或丢弃已生成部分、重新 prefill 计算。vLLM 默认采用后者,因为 prefill 的计算开销通常远小于显存换入换出的带宽开销。

3.4 新请求如何加入运行批次

vLLM 在每次迭代(iteration-level)调度:

  1. 计算当前 GPU 剩余显存。
  2. 尝试为 Waiting 队列头部请求分配物理块执行 prefill。
  3. 若显存不足,检查是否可通过抢占低优先级请求腾出空间。
  4. Prefill 和 decode 的请求被合并为混合批次(mixed batch),在一次 CUDA kernel 启动中执行。

这种混合批次策略有效解决了纯 continuous batching 中 prefill 和 decode 无法在一个批次内合并的问题。

四、内存节省的量化分析

4.1 块分配器的碎片分析

PagedAttention 通过固定大小块分配器管理显存。内部碎片仅存在于每个 block 的尾部未用位置:一条 18 token 的请求需要 2 个块(32 token 容量),内部碎片为 14/32 ≈ 43.75%。但由于序列通常较长,碎片被摊薄。同时,显存池中的空闲块可供任何请求复用,消除传统方案中因预分配产生的 external fragmentation

4.2 引用计数与共享

vLLM 对物理块维护引用计数,支持两类共享场景:

  • Beam Search(束搜索):多条 beam 共享原始 prompt 的 KV Cache,仅在分叉时触发 COW。
  • Parallel Sampling(并行采样):对同一 prompt 生成多个不同回答,KV Cache 完全共享,直到各采样路径产生差异。

在 beam_width = 4、seq_len = 2048 的场景下,共享可减少 60%-70% 的重复 KV Cache。

4.3 横向对比

系统核心机制KV Cache 管理适用场景
vLLMPagedAttention分页+动态分配+共享通用高吞吐服务
DeepSpeed-InferenceDeepSpeed-MII连续张量,静态分配大模型单机多卡
TensorRT-LLMIn-flight BatchingPaged KV Cache(受 vLLM 启发)NVIDIA GPU 极致性能
Text-Generation-Inference (TGI)FlashAttention + Continuous分页缓存Hugging Face 生态

TensorRT-LLM 在 NVIDIA 硬件上通过 kernel 融合和编译优化可达更高 throughput,但闭源且 Nvidia-only。TGI 在功能易用性上表现突出。vLLM 以开源、通用、社区活跃著称,是大多数团队自托管 LLM 的首选起点。

五、vLLM 高级特性解析

5.1 Prefix Caching(前缀缓存)

vLLM 在 v0.4.0 之后引入 Prefix Caching,自动将 GPU 上已计算的 prompt KV Cache 保留在显存池中。当新请求的 prompt 前缀与缓存匹配时,直接复用物理块,跳过重复 prefill。

这对 RAG、Agent 等多轮对话场景价值巨大:系统 prompt 和检索文档的 KV Cache 只需计算一次,后续请求延迟从秒级降至毫秒级。

5.2 投机解码(Speculative Decoding)

vLLM 支持集成 Medusa 和 EAGLE 等投机解码方案:

  • 使用小型 draft model(或模型自身的早期层)快速生成候选 token 序列。
  • 主模型以 single forward 验证整个候选序列,接受合法前缀并回拒绝位点。
  • 在生成延迟敏感场景中,可将 throughput 提升 1.5-2.5 倍。
# vLLM 启动时启用投机解码示例
from vllm import LLM, SamplingParams

llm = LLM(
    model="meta-llama/Llama-2-7b",
    speculative_model="[ngram]",  # 或使用独立 draft model
    num_speculative_tokens=5,
)
sampling_params = SamplingParams(temperature=0.8)
outputs = llm.generate("The future of AI is", sampling_params)

5.3 流水线并行与张量并行

  • Tensor Parallelism(TP):将单层的注意力头和 FFN 切分到多张 GPU,适用于单节点内高通信带宽场景。推荐每张卡承载一个 tp_size 分片,如 8xA100 80GB 运行 70B 模型。
  • Pipeline Parallelism(PP):将模型按层切分到不同 GPU,每卡存放部分层。TP 与 PP 可组合使用,如 tp=4, pp=2 在 8 卡节点上运行 170B+ 模型。

5.4 Chunked Prefill(分块 prefill)

长 prompt 的 prefill 阶段会阻塞同批次 decode 请求,造成抖动。Chunked Prefill 将长序列 prefill 拆分为多个 chunk,与 decode step 交错执行,平滑延迟分布,提升 P99 稳定性。

六、生产部署实践

6.1 安装与启动

pip install vllm

# 单卡启动 OpenAI 兼容 API 服务
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Meta-Llama-3-8B-Instruct \
    --tensor-parallel-size 1 \
    --max-model-len 8192

6.2 多卡与多节点配置

# 单机 4 卡张量并行
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Meta-Llama-3-70B-Instruct \
    --tensor-parallel-size 4 \
    --pipeline-parallel-size 1 \
    --gpu-memory-utilization 0.90

# 多节点(需配置 ray)
ray start --head
# 在其他节点加入 cluster 后
python -m vllm.entrypoints.openai.api_server \
    --model ... \
    --tensor-parallel-size 8 \
    --pipeline-parallel-size 2

6.3 压测与调优

vLLM 自带 benchmark 工具:

python benchmarks/benchmark_throughput.py \
    --model meta-llama/Meta-Llama-3-8B-Instruct \
    --input-len 1024 \
    --output-len 256 \
    --num-prompts 1000 \
    --tensor-parallel-size 1

关键调优参数:

  • --gpu-memory-utilization:建议 0.85-0.95,预留空间给 CUDA kernel 工作区和显存碎片。
  • --max-num-seqs:控制最大并发请求数,防止调度器过度乐观导致 OOM。
  • --enable-prefix-caching:开启前缀缓存,RAG/Agent 场景必开。
  • --enable-chunked-prefill:高并发在线服务推荐开启。

6.4 CPU Offloading 兜底

对于超长上下文或超大模型,可启用 CPU offloading 将部分 KV Cache 卸载到主机内存甚至 NVMe SSD,以吞吐换容量:

python -m vllm.entrypoints.openai.api_server \
    --model ... \
    --cpu-offload-gb 32

七、局限性与替代方案

7.1 vLLM 的适用边界

  • 极短序列不友好:PagedAttention 的分页和调度开销在 seq_len < 64 时可能抵消收益,此时 overhead 占比高。
  • 非 NVIDIA 硬件支持有限:核心 kernel 以 CUDA 为主,AMD ROCm 和 Intel XPU 支持仍在追赶。
  • 复杂控制流:对于需要细粒度干预 logits、动态修改 attention mask 的场景,vLLM 的抽象层可能成为障碍。

7.2 主要替代方案

  • SGLang:由 LMSYS 开发,提出 RadixAttention,将 prefix 缓存扩展到更细粒度的 Radix Tree 结构,在多轮对话和嵌套调用场景下共享效率更高。生态较新但增长迅速。
  • TensorRT-LLM:NVIDIA 官方推理引擎,通过 torch.compile 级别的 kernel 优化在 A100/H100 上达到极致性能。缺点是闭源生态和硬件锁定。
  • llama.cpp:GGUF 格式 + CPU/混合量化,适合消费级硬件或边缘部署。在 RAM 受限场景中无可替代,但吞吐远低于 GPU 方案。
  • TGI(Text-Generation-Inference):Hugging Face 出品,与 Transformers 生态深度集成,适合快速原型和小规模服务。

结语

vLLM 的成功在于将操作系统领域成熟的虚拟内存思想引入 LLM 推理,从根本上解决了 KV Cache 管理的显存效率问题。连续批处理和迭代级调度进一步优化了 GPU 利用率,使其成为自托管大模型服务的坚实基础。在实际选型中,应结合序列长度分布、硬件约束和团队技术栈,在 vLLM、TensorRT-LLM、SGLang 之间做出权衡。对于绝大多数需要平衡性能、通用性和开源可控性的团队,vLLM 仍是当前的最优起点。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 模型量化技术详解:INT8、FP16 与混合精度推理
  2. 模型剪枝与知识蒸馏:从压缩到加速全链路
  3. 推理引擎终极对比:TensorRT vs ONNX Runtime vs OpenVINO