vLLM基础:架构、推理流程与吞吐量瓶颈分析
2.1 vLLM核心架构:从PagedAttention到分布式引擎
vLLM之所以能在LLM推理服务中脱颖而出,核心在于其PagedAttention机制与连续批处理(Continuous Batching)引擎的协同设计。要理解vLLM的缓存优化,必须先厘清其整体架构脉络。
vLLM的架构可划分为三大层次:
- 模型执行层:基于PyTorch/XFormers的算子层,负责Transformer的forward计算,但将KV Cache的存储方式从“连续内存”改为“分页内存块”。
- 调度与缓存层:这是vLLM的核心,包含BlockManager(块管理器)、Scheduler(调度器)和CacheEngine(缓存引擎)。BlockManager以固定大小的block(默认16个token)为单位分配KV Cache;Scheduler决定每轮迭代哪些序列参与计算;CacheEngine负责GPU/CPU缓存块的换入换出。
- 服务与API层:提供OpenAI兼容的HTTP接口,支持流式输出、多轮对话等,并通过AsyncLLMEngine处理并发请求。
关键认知:vLLM与传统推理框架的本质区别在于——它把KV Cache视为“虚拟内存”,通过块表(Block Table)映射到物理块,从而实现了接近零碎片的内存利用。
2.2 推理流程:从请求到生成的完整链路
vLLM的推理流程与传统的“静态批处理”有显著差异。其核心是迭代级调度(Iteration-level Scheduling),即每个forward step都重新决定批次的组成。一个典型请求的完整流程如下:
- 请求接入:客户端发送prompt到HTTP服务,AsyncLLMEngine将其封装为序列(Sequence)对象,加入等待队列。
- 预填充阶段(Prefill):调度器从等待队列中选取请求,为其分配初始物理块,执行一次完整的forward计算,生成第一个token,并将所有输入的KV Cache写入分配的物理块。
- 解码阶段(Decode):进入生成循环。每步只输入上一步生成的token,从KV Cache中读取历史信息,计算当前token的logits并采样。
- 块分配与释放:随着生成token数增加,BlockManager动态分配新物理块;当序列结束(遇到EOS或达到max_tokens)时,释放其占用的所有物理块。
- 流式返回:每个token生成后立即通过HTTP流式接口返回给客户端,无需等待整个序列完成。
效率关键:Prefill阶段是计算密集型(矩阵乘法),Decode阶段是访存密集型(读取KV Cache)。vLLM通过连续批处理,让多个处于Decode阶段的序列共享一次forward,大幅提升GPU利用率。
2.3 吞吐量瓶颈:为什么朴素实现会“卡住”
在未使用vLLM的朴素推理系统中,吞吐量主要受以下瓶颈制约:
| 瓶颈类型 | 根因分析 | 对吞吐量的影响 |
|---|---|---|
| 内存碎片化 | KV Cache按最大序列长度预分配连续显存,不同序列长度差异导致内部碎片和外部碎片 | 显存利用率仅40%~60%,能并发处理的序列数大幅减少 |
| 批处理粒度粗 | 静态批处理必须等批次内最慢的序列全部生成完才能释放资源 | GPU在批处理尾部出现大量空闲,有效算力利用率低 |
| KV Cache冗余存储 | 多轮对话中,历史对话的KV Cache重复计算;并行采样时相同前缀重复存储 | 浪费大量显存和计算资源,增加延迟 |
| CPU-GPU同步开销 | 每个step都需要CPU调度GPU kernel,启动开销大 | 小batch时kernel启动时间占比超过50% |
用一个具体场景来说明:假设GPU显存为40GB,模型为7B参数(权重约14GB),剩余约26GB用于KV Cache。若每个序列最大长度2048,每token的KV Cache占用约0.5MB(以7B模型为例),则每个序列最多需要1GB。朴素实现中,由于预分配连续内存,实际可能只能容纳8~10个并发序列。而vLLM通过分页管理,碎片率降低到接近0,可容纳20~25个序列,吞吐量直接提升2~3倍。
2.4 案例复盘:一个真实的生产环境瓶颈诊断
某团队部署Llama-2-13B模型(单卡A100 80GB),使用朴素FastAPI+Transformers实现推理服务。压测发现:吞吐量仅约8 requests/s,GPU利用率波动剧烈(30%~95%),且延迟P99高达4.2秒。
诊断过程:
- 通过
nvidia-smi观察显存分配,发现显存占用率仅55%,但新请求报CUDA out of memory——典型的碎片化问题。 - 使用PyTorch Profiler分析,发现GPU kernel启动间隙(gap)占总时间的47%,说明batch内序列完成时间差异大,GPU频繁空闲。
- 检查多轮对话场景,发现每轮对话都重新计算历史KV Cache,导致有效计算量浪费约60%。
优化方案与效果:
# 迁移到vLLM后的关键配置
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-2-13b-chat-hf",
gpu_memory_utilization=0.90, # 允许vLLM使用90%显存
max_num_seqs=64, # 最大并发序列数
max_num_batched_tokens=8192, # 每批次最大token数
enable_prefix_caching=True, # 启用前缀缓存(多轮对话优化)
)
sampling_params = SamplingParams(
temperature=0.7,
max_tokens=1024,
)
优化后,吞吐量从8 req/s提升至31 req/s(提升约2.9倍),GPU利用率稳定在85%~95%,P99延迟降至1.8秒。显存碎片率从约45%降至接近0%。
常见误区:不要盲目调大
gpu_memory_utilization到0.95以上。需要预留足够的KV Cache交换空间和CUDA context内存,否则会导致GPU显存溢出或频繁换入换出,反而降低吞吐量。建议从0.85开始,逐步调优。
2.5 吞吐量优化核心公式与关键参数
理解吞吐量的决定因素,有助于后续章节深入缓存优化。系统吞吐量(tokens/s)可近似表示为:
吞吐量 ≈ (GPU算力利用率 × 峰值算力) / (每token平均计算量)
其中:
- 每token平均计算量 ≈ 2 × 模型参数量(FLOPs)
- GPU算力利用率 = f(批大小, KV Cache命中率, 内存带宽, kernel效率)
vLLM通过以下手段提升公式中的“GPU算力利用率”:
- 增大有效批大小:通过分页KV Cache容纳更多并发序列,让每次forward处理更多token。
- 减少内存访问开销:PagedAttention减少显存碎片,提高Cache命中率。
- 前缀缓存复用:对相同system prompt或多轮对话前缀,直接复用KV Cache,避免重复计算。
- 动态batch组合:每步将处于Prefill和Decode阶段的序列混合调度,填满GPU计算间隙。