vLLM:让大模型推理快 24 倍的秘密武器

4 阅读9分钟

引言:从"等待"到"实时"的跨越

当你在 ChatGPT 中输入问题并按下回车,后台发生了什么?一个拥有数百亿参数的大模型开始工作:加载权重、计算注意力、生成词汇……如果你曾经部署过自己的 LLaMA 或 Mistral 模型,可能会发现一个尴尬的现实:

  • 显存不够用:一个 7B 模型的 KV Cache 就能吃掉数十 GB
  • 吞吐量太低:GPU 利用率只有 30%,大部分时间在"等待"
  • 成本居高不下:同样的硬件,商业服务能处理 10 倍的并发

这些痛点的根源是什么?为什么传统推理框架(如 HuggingFace Transformers)在生产环境中表现不佳?vLLM 的出现,为这些问题提供了一套系统化的解决方案。


传统推理框架的三大瓶颈

在深入 vLLM 之前,让我们先理解传统方案的局限性:

1. 显存碎片化:看得见却用不上的资源

问题场景: 假设你的 GPU 有 40GB 显存,模型权重占用 15GB,理论上还剩 25GB 可用于推理。但实际运行时:

  • 处理 10 个并发请求时顺畅
  • 第 11 个请求到来时突然 OOM(Out of Memory)
  • 此时显存监控显示:还剩 8GB "空闲"

根本原因: 传统框架为每个请求静态预分配固定大小的 KV Cache(键值缓存)。想象你去图书馆借书,每个读者都被分配一个固定大小的书架,即使只借了 2 本书,也要占用整个书架的空间。结果是:

请求 A:预分配 2048 tokens 空间,实际只用 512  浪费 75%
请求 B:预分配 2048 tokens 空间,实际只用 1024  浪费 50%
请求 C:想要分配空间,但连续的大块内存已不存在  OOM

这种浪费在短文本和长文本混合的真实场景中尤为严重。

2. 批处理困境:无法同时服务不同进度的请求

技术挑战: 大模型推理是自回归的(Auto-regressive)——每个词需要等前一个词生成完才能开始。传统框架的批处理(Batching)策略是:

时刻 0[请求 A, 请求 B, 请求 C] 同时开始
时刻 5:请求 B 生成完毕(早结束)
        → 但必须等待 A 和 C 完成才能释放资源
时刻 10:请求 A 完成
时刻 15:请求 C 完成
        → 此时才能接受新请求

实际后果

  • 短请求被长请求"拖累"
  • GPU 利用率随时间下降(从 100% 降到 33%)
  • 平均延迟增加

3. 调度僵化:资源分配缺乏全局视角

传统框架通常采用先到先服务(FCFS)策略,但这在大模型场景中存在问题:

场景:4 个请求同时到达
- 请求 1:需要生成 2000 tokens(长文本生成)
- 请求 2-4:各需要 50 tokens(快速问答)

FCFS 调度结果:
├─ 请求 1 独占 GPU 执行 2000 步
├─ 请求 2-4 等待数十秒
└─ 平均响应时间:25 秒

理想调度结果:
├─ 动态拆分请求 1 的执行(分段生成)
├─ 穿插执行短请求
└─ 平均响应时间:8 秒

vLLM 的核心创新:PagedAttention

vLLM 的突破性贡献是PagedAttention——一种受操作系统虚拟内存启发的注意力计算机制。

概念类比:从操作系统到 AI 推理

传统内存管理 vs KV Cache 管理

维度操作系统内存大模型 KV Cache
资源物理内存(RAM)GPU 显存
碎片化进程固定分配 → 外部碎片请求固定分配 → 显存浪费
解决方案分页机制(Paging)PagedAttention
关键思想虚拟地址 → 页表 → 物理页逻辑块 → 块表 → 物理块

PagedAttention 的工作原理

步骤 1:将 KV Cache 切分为固定大小的"块"

传统方式

请求 A 的 KV Cache:[连续的 2048 tokens 空间]

PagedAttention

请求 A 的 KV Cache:
├─ 块 1:tokens 0-15
├─ 块 2:tokens 16-31
├─ 块 3:tokens 32-47
└─ ...按需分配,不预占空间

每个块通常包含 16 个 token 的键值数据,块的大小在初始化时固定。

步骤 2:用"块表"管理映射关系

类似操作系统的页表,vLLM 为每个请求维护一个块表(Block Table):

请求 A 的块表:
逻辑块 0 → 物理块 7(GPU 显存地址 0x1A4000)
逻辑块 1 → 物理块 3(GPU 显存地址 0x0C8000)
逻辑块 2 → 物理块 12(GPU 显存地址 0x2F0000)

关键优势

  • 物理块可以非连续存储(消除外部碎片)
  • 请求实际使用多少就分配多少(消除内部碎片)
  • 块可以在不同请求间共享(后文详述)

步骤 3:修改注意力计算流程

在标准 Transformer 的注意力计算中,需要访问所有历史 token 的键值:

传统实现

Attention(Q, K, V) = Softmax(Q × K^T / √d) × V
其中 K, V 是连续存储的完整矩阵

PagedAttention

对于查询 Q:
├─ 根据块表找到所有相关物理块
├─ 逐块读取键值数据
├─ 分块计算注意力分数
└─ 聚合结果

伪代码逻辑:
for 逻辑块 i in 块表:
    物理块地址 = 块表[i]
    K_i, V_i = 从 GPU 显存读取(物理块地址)
    score_i = Q × K_i^T
聚合所有 score_i 并计算最终输出

性能考量: 虽然增加了间接访问开销(查块表 + 分块计算),但通过以下优化保持高效:

  • 块表查找在寄存器/L1 缓存完成(纳秒级)
  • 分块计算可并行(利用 GPU 的 SIMD 能力)
  • 省下的显存让批量大小增加 2-3 倍,抵消单次计算的额外开销

从内存管理到极致优化:vLLM 的系统设计

1. 动态批处理:Continuous Batching

核心思想:让不同进度的请求"无缝接力"。

传统静态批处理

批次 1[请求 A, B, C] 全部完成后
批次 2[请求 D, E, F] 才能开始

vLLM 的连续批处理

时刻 0[请求 A, B, C] 开始
时刻 3B 完成 → 立即将请求 D 加入批次
       当前批次:[A, C, D]
时刻 5A 完成 → 立即将请求 E 加入
       当前批次:[C, D, E]
...持续动态调整

实现关键

  • 每个请求维护独立的块表
  • 调度器在每个生成步(Iteration)后检查:
    • 是否有请求完成?→ 移出批次
    • 是否有空闲显存?→ 加入等待队列中的请求
  • GPU Kernel 适配可变批量大小

收益

  • GPU 利用率接近 100%
  • 平均等待时间减少 50%+

2. 块共享:零成本的前缀复用

应用场景

  • Few-shot 提示:多个请求共享相同的示例前缀
  • 多轮对话:后续轮次复用历史上下文
  • 并行采样:同一提示生成多个候选(Beam Search、Temperature 采样)

技术实现: 类似操作系统的写时复制(Copy-on-Write):

场景:两个请求共享相同的系统提示(500 tokens)

请求 A 的块表:
├─ 块 0-31:指向共享物理块(系统提示)[引用计数 = 2]
└─ 块 32+:独立物理块(用户输入 + 生成内容)

请求 B 的块表:
├─ 块 0-31:指向相同的共享物理块 [引用计数 = 2]
└─ 块 32+:独立物理块(不同的用户输入)

当请求 A 完成时:
└─ 共享块的引用计数 -1 = 1(继续保留)

显存节省计算: 假设 100 个并发请求,每个请求有 500 tokens 的共享前缀:

  • 传统方式:100 × 500 = 50,000 tokens 的 KV Cache
  • vLLM 块共享:500(共享)+ 100 × 平均独立 tokens
  • 节省比例:70%-90%(取决于独立部分长度)

3. 调度策略:全局最优 vs 局部公平

vLLM 提供多种调度策略:

FCFS(First-Come-First-Serve)

  • 默认策略,保证公平性
  • 适合延迟敏感的交互式应用

优先级调度

  • 为请求分配优先级(如 VIP 用户、紧急任务)
  • 高优先级请求可抢占低优先级请求的显存块

最短作业优先(SJF)

  • 根据估计生成长度排序
  • 最小化平均响应时间(但可能饿死长请求)

性能数据:量化的突破

吞吐量提升

基准测试(ShareGPT 数据集,NVIDIA A100 80GB):

模型框架吞吐量(tokens/秒)vs HF Transformers
LLaMA-7BHuggingFace1,200
LLaMA-7BvLLM14,40012×
LLaMA-13BHuggingFace640
LLaMA-13BvLLM10,24016×
LLaMA-70BvLLM3,50024×

显存利用率

实验场景:LLaMA-13B 处理 256 个并发请求

传统框架(HuggingFace):
├─ 显存占用:76GB
├─ 实际利用率:42%(大量碎片)
└─ 最大并发:128 个请求

vLLM:
├─ 显存占用:68GB
├─ 实际利用率:86%
└─ 最大并发:256 个请求(2× 提升)

延迟对比

场景:生成 100 tokens 的响应

指标HuggingFacevLLM改善幅度
首 token 延迟450ms380ms16% ↓
平均 token 延迟18ms12ms33% ↓
P99 延迟85ms45ms47% ↓

适用场景与局限性

✅ 最适合的场景

  1. 高并发服务

    • 在线 API 服务(如 OpenAI API 的替代部署)
    • 多租户推理平台
    • 批量内容生成
  2. 长上下文应用

    • 文档问答(RAG 系统)
    • 代码补全(需要整个文件上下文)
    • 多轮对话(历史对话越长,优势越明显)
  3. 资源受限环境

    • 显存有限的 GPU(如 RTX 4090 24GB)
    • 需要最大化单卡吞吐量的场景

⚠️ 不适合的场景

  1. 训练任务

    • PagedAttention 的间接访问开销在训练的多次迭代中累积
    • 训练通常使用固定批量大小,动态批处理无优势
  2. 极短文本推理

    • 如果平均生成长度 < 20 tokens,块管理的开销占比增加
    • 首 token 延迟(TTFT)相比优化的静态推理框架无明显优势
  3. 单请求极致延迟

    • 如果同时只处理 1 个请求,vLLM 的批处理和调度优势无法体现
    • 此时可能不如 TensorRT-LLM 等专注单请求优化的框架