3、vLLM核心概念:PagedAttention、Continuous Batching、KV Cache、吞吐量与延迟优化原理
在大模型推理服务化落地过程中,vLLM 之所以能成为生产环境的首选框架之一,核心在于它对 Transformer 解码阶段的内存管理与调度机制做了系统性重构。本章将深入拆解 vLLM 的四大支柱:KV Cache 的内存本质、PagedAttention 的分页管理、Continuous Batching 的动态调度,以及吞吐量与延迟之间的权衡艺术。理解这些原理,是后续章节中配置引擎参数、诊断性能瓶颈、设计服务容量的基础。
在开始深入之前,请先建立一个整体认知:vLLM 的优化并非单一技术突破,而是“内存管理(PagedAttention)+ 调度策略(Continuous Batching)+ 硬件利用(吞吐/延迟权衡)”三位一体的系统工程。上图展示了这三者如何协同工作——左侧解决显存碎片化问题,中间解决 GPU 空闲问题,右侧则定义了优化的量化指标。
3.1 KV Cache 的本质与显存瓶颈
核心原理:Transformer 解码器在生成每个 token 时,都需要计算注意力分数,而注意力计算依赖当前 token 的 Query 与所有历史 token 的 Key 和 Value。为了避免重复计算,推理框架会将历史 token 的 K 和 V 矩阵缓存在显存中,这就是 KV Cache。以 Llama-2-7B 为例,每个 token 的 KV Cache 大小为:2(K和V)× 32(层数)× 4096(隐藏维度)× 2(字节,FP16)= 1MB。当生成长度为 2048 的序列时,仅 KV Cache 就需要 2GB 显存。
显存碎片化问题:传统推理框架(如 HuggingFace Transformers 原生实现)会为每个请求预先分配最大长度的 KV Cache 空间。假设设置最大生成长度为 2048,但实际请求平均只生成 200 token,那么 90% 的显存被浪费。更严重的是,不同请求的生成长度差异巨大,导致显存出现大量碎片,无法被其他请求利用。
| 对比维度 | 传统预留式分配 | vLLM 分页式分配 |
|---|---|---|
| 分配单位 | 整个请求的连续显存 | 固定大小的 Block(默认 16KB) |
| 内部碎片 | 严重(预留长度远大于实际) | 仅最后一块可能不满 |
| 外部碎片 | 无法利用其他请求的空闲区 | 通过块表映射,物理上可不连续 |
| 显存利用率 | 通常 20%~40% | 可达 90% 以上 |
| 内存共享 | 不支持 | 支持(如并行采样时共享前缀) |
案例拆解:假设我们有一张 24GB 显存的 A10G 显卡,部署 Llama-2-7B(权重占 14GB)。传统方式下,剩余 10GB 显存按最大长度 2048 预留,每个请求需要 2GB,最多只能并发 5 个请求。而实际平均生成长度只有 300 token(0.3GB/请求),理论上可以支持 33 个并发,但传统方式只能利用 5 个并发,GPU 利用率极低。vLLM 通过按需分配 Block,可以将并发数提升至 20~30 个,吞吐量提升 4~6 倍。
--max-model-len 时,不要盲目设置过大。该参数直接影响 KV Cache 的预留策略。建议根据业务实际分布设置,例如 P99 生成长度 + 20% 余量。过大的 max-model-len 会导致显存预留过多,降低有效并发数。3.2 PagedAttention:操作系统的内存管理思想
核心原理:PagedAttention 借鉴了操作系统虚拟内存的分页机制。它将 KV Cache 划分为固定大小的 Block(默认 16KB,可容纳 16 个 token 的 K/V),每个请求维护一张“块表”(Block Table),记录逻辑 Block 到物理 Block 的映射。当请求需要新的 KV 空间时,vLLM 从全局 Block 池中分配一个空闲物理 Block,并在块表中添加映射项。
关键数据结构:
# 逻辑视图(请求视角)
Request A 的 KV Cache: [Block 0] → [Block 1] → [Block 2] → ...
# 物理视图(显存视角)
Block Table: {0: 物理块7, 1: 物理块3, 2: 物理块9, ...}
# 全局 Block 池
free_blocks = [1, 2, 4, 5, 6, 8, ...] # 空闲块列表
核心优势:一是按需分配,请求生成多少 token 就分配多少 Block,彻底消除内部碎片;二是物理不连续,通过块表映射,显存碎片可以被任意请求利用;三是支持内存共享,当多个请求共享相同前缀(如系统提示词)时,可以共享相同的物理 Block,通过引用计数管理生命周期。
操作流程:当请求完成解码后,vLLM 会释放其占用的物理 Block 并归还全局池。如果显存不足,vLLM 会触发抢占(Preemption):将某些请求的 Block 交换到 CPU 内存,腾出 GPU 显存给新请求。被抢占的请求在后续调度时,需要从 CPU 加载回 GPU,这会增加延迟,因此生产环境应尽量避免频繁抢占。
3.3 Continuous Batching:迭代级动态调度
核心原理:传统推理框架采用“静态批处理”(Static Batching):一批请求同时开始,必须等最慢的请求完成后,整批才能释放,新请求只能排队等待。这导致 GPU 在批处理尾部出现大量空闲。vLLM 采用 Continuous Batching(又称 Iteration-level Scheduling),在每个解码步骤(Step)结束后,立即将已完成的请求移出批次,并加入新的请求。
调度流程:
- 初始化:从等待队列中选取请求,填充至 GPU 可容纳的最大批次。
- 迭代解码:每个 Step 对所有活跃请求执行一次前向传播,生成下一个 token。
- 完成检测:检查是否有请求生成 EOS token 或达到最大长度,将其移出批次并释放 KV Cache。
- 补充新请求:从等待队列中选取新请求,加入下一轮迭代。
对比分析:
| 特性 | Static Batching | Continuous Batching |
|---|---|---|
| 调度粒度 | 请求级(整个生命周期) | 迭代级(每个解码步骤) |
| GPU 空闲率 | 批次尾部明显空闲 | 几乎为零 |
| 新请求响应 | 需等待当前批次完成 | 下一个 Step 即可加入 |
| 实现复杂度 | 简单 | 需要管理动态批次和显存分配 |
| 吞吐量提升 | 基线 | 2~3 倍(混合长短请求场景) |
实战场景:某客服机器人服务,请求长度分布极不均匀:约 30% 的请求只生成 50 token(简单问答),50% 生成 200~500 token(多轮对话),20% 生成 1000+ token(长文本总结)。使用静态批处理时,短请求必须等待长请求完成,平均响应时间被拉长 3 倍。切换到 vLLM 后,短请求在 2~3 个 Step 内即可完成并释放显存,新请求立即补入,整体吞吐量提升 2.8 倍,P95 延迟降低 60%。
--max-num-seqs 控制最大并发批次大小。增大该值可提升吞吐量,但会增加显存压力和单请求延迟。建议从 256 开始,根据显存余量和延迟要求逐步调整。同时,--max-paddings 参数控制批次中允许的 padding token 数量,过大的 padding 会浪费计算资源。3.4 吞吐量与延迟的权衡艺术
核心指标定义:
- 吞吐量(Throughput):单位时间内生成的 token 总数,通常以 tokens/s 衡量。计算公式:
吞吐量 = 并发请求数 × 平均生成长度 / 平均完成时间。 - TTFT(Time To First Token):从请求发出到收到第一个 token 的时间。受 prefill 阶段影响,与输入长度和批次大小相关。
- TPOT(Time Per Output Token):生成每个输出 token 的平均时间。受 decode 阶段计算量和批次大小影响。
- 端到端延迟:TTFT + TPOT × 输出长度。
优化杠杆分析:增大批次大小可以摊薄权重加载和计算开销,提升吞吐量,但会线性增加每个请求的 TPOT(因为 GPU 计算资源被更多请求共享)。反之,减小批次可降低延迟,但 GPU 利用率下降。vLLM 提供了 --max-num-seqs 和 --gpu-memory-utilization 两个核心旋钮来调节这一平衡。
案例拆解:某 AI 写作助手服务,目标要求:P95 TTFT < 1.5s,P95 TPOT < 80ms,吞吐量最大化。初始配置 --max-num-seqs 128,实测吞吐量 850 tokens/s,但 P95 TPOT 达到 120ms,超出目标。逐步调低至 96,TPOT 降至 85ms,吞吐量降至 780 tokens/s。继续调低至 64,TPOT 降至 65ms,但吞吐量仅 620 tokens/s。最终选择 96 作为平衡点,并配合 --gpu-memory-utilization 0.9 确保显存充足。
--priority 参数(如果版本支持)或通过应用层做请求分类,将短请求优先调度。另外,--max-model-len 设置过大会导致 KV Cache 预留过多,降低有效并发数,反而降低吞吐量。3.5 生产环境调优实践与复盘
调优流程:
- 基准测试:使用
vllm benchmark工具或自定义脚本,测量不同并发下的吞吐量和延迟分布。 - 显存监控:通过
nvidia-smi或 vLLM 的--verbose日志观察 KV Cache 使用率,目标利用率在 85%~95% 之间。 - 参数扫描:对
--max-num-seqs、--gpu-memory-utilization、--block-size做网格搜索,记录每组参数下的吞吐量和延迟指标。 - 长尾优化:分析 P99 延迟,定位是否因抢占(Preemption)导致。如果抢占频繁,考虑降低
--max-num-seqs或增加 CPU offload 缓冲。
复盘总结:在一次实际部署中,我们发现某模型在 --block-size 16 下吞吐量比 --block-size 32 高 12%,但显存碎片率也更高。原因是该模型的平均生成长度较短(约 200 token),16 的 block-size 能更精细地分配显存。对于长文本场景(平均 2000+ token),32 的 block-size 反而更优,因为减少了块表查询次数。这提醒我们:没有绝对最优的参数,只有针对特定负载的最优配置。