引言:从"等待"到"实时"的跨越
当你在 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] 开始
时刻 3:B 完成 → 立即将请求 D 加入批次
当前批次:[A, C, D]
时刻 5:A 完成 → 立即将请求 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-7B | HuggingFace | 1,200 | 1× |
| LLaMA-7B | vLLM | 14,400 | 12× |
| LLaMA-13B | HuggingFace | 640 | 1× |
| LLaMA-13B | vLLM | 10,240 | 16× |
| LLaMA-70B | vLLM | 3,500 | 24× |
显存利用率
实验场景:LLaMA-13B 处理 256 个并发请求
传统框架(HuggingFace):
├─ 显存占用:76GB
├─ 实际利用率:42%(大量碎片)
└─ 最大并发:128 个请求
vLLM:
├─ 显存占用:68GB
├─ 实际利用率:86%
└─ 最大并发:256 个请求(2× 提升)
延迟对比
场景:生成 100 tokens 的响应
| 指标 | HuggingFace | vLLM | 改善幅度 |
|---|---|---|---|
| 首 token 延迟 | 450ms | 380ms | 16% ↓ |
| 平均 token 延迟 | 18ms | 12ms | 33% ↓ |
| P99 延迟 | 85ms | 45ms | 47% ↓ |
适用场景与局限性
✅ 最适合的场景
-
高并发服务
- 在线 API 服务(如 OpenAI API 的替代部署)
- 多租户推理平台
- 批量内容生成
-
长上下文应用
- 文档问答(RAG 系统)
- 代码补全(需要整个文件上下文)
- 多轮对话(历史对话越长,优势越明显)
-
资源受限环境
- 显存有限的 GPU(如 RTX 4090 24GB)
- 需要最大化单卡吞吐量的场景
⚠️ 不适合的场景
-
训练任务
- PagedAttention 的间接访问开销在训练的多次迭代中累积
- 训练通常使用固定批量大小,动态批处理无优势
-
极短文本推理
- 如果平均生成长度 < 20 tokens,块管理的开销占比增加
- 首 token 延迟(TTFT)相比优化的静态推理框架无明显优势
-
单请求极致延迟
- 如果同时只处理 1 个请求,vLLM 的批处理和调度优势无法体现
- 此时可能不如 TensorRT-LLM 等专注单请求优化的框架