3. vLLM核心概念:PagedAttention、Continuous Batching、KV Cache管理与吞吐量指标
3.1 为什么传统推理框架难以支撑高吞吐?
在深入vLLM之前,我们先要理解一个关键矛盾:大语言模型推理是“显存饥饿型”任务。以LLaMA-13B为例,模型权重约占26GB显存(FP16),但处理一个长度为2048的序列时,其KV Cache(键值缓存)就可能消耗超过16GB显存。传统推理框架(如FasterTransformer的静态批处理模式)在请求到达时,会为每个序列预分配固定长度的KV Cache空间,这导致两个严重问题:
- 显存碎片化:不同请求的实际生成长度差异极大,预分配的空间往往浪费50%以上。
- 等待阻塞:必须等当前批次中最长的序列生成完毕,才能释放显存并接收新请求,造成GPU利用率波动。
vLLM的核心创新,正是通过操作系统虚拟内存的分页思想来解决上述问题,从而将吞吐量提升数倍甚至一个数量级。
核心原理速览:vLLM将KV Cache划分为固定大小的“块”(Block),每个块存储固定数量token的键值向量。请求不再占用连续显存,而是通过块表(Block Table)映射到非连续的物理块上。这就像操作系统的虚拟内存分页——逻辑上连续,物理上离散。
3.2 PagedAttention:让KV Cache像内存分页一样灵活
PagedAttention是vLLM的底层注意力实现,它改变了传统注意力机制中KV Cache的存储与访问方式。其核心数据结构包括:
- KV块(KV Block):固定大小(默认16个token的键值向量),是显存分配的最小单位。
- 块表(Block Table):每个序列维护一张映射表,记录其逻辑块到物理块的对应关系。
- 块管理器(Block Manager):全局统筹物理块的分配、释放与复制,支持写时复制(Copy-on-Write)优化。
在计算注意力分数时,PagedAttention并非一次性读取整个序列的KV,而是按块遍历,逐块计算部分注意力分数并累加。这种设计带来了三个直接收益:
- 显存零浪费:最后一个块可以只分配实际需要的部分,不再预留整个序列长度。
- 共享前缀:当多个请求共享相同的系统提示词(如ChatML模板)时,这些前缀的KV块可以被多个序列同时引用,极大节省显存。
- 并行采样支持:同一个Prompt生成多个候选回复时,只需复制块表而非复制实际KV数据。
实操验证:在vLLM中,你可以通过环境变量 VLLM_LOG_LEVEL=DEBUG 启动服务,观察日志中的块分配信息。例如:
# 启动时设置日志级别
VLLM_LOG_LEVEL=DEBUG python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--gpu-memory-utilization 0.9
# 日志中会显示类似信息:
# Block manager: block_size=16, num_gpu_blocks=18432, num_cpu_blocks=0
# 这说明每个块16个token,GPU上共有18432个可用块
3.3 Continuous Batching:动态调度,消灭气泡
传统静态批处理(Static Batching)的流程是:收集请求 → 组成批次 → 推理直到所有序列结束 → 释放批次。这会导致严重的GPU气泡——当批次中大部分序列已生成完毕(如生成了EOS),但仍有少数长序列在运行时,GPU计算单元大量空闲。
vLLM采用Continuous Batching(连续批处理),也称为迭代级调度(Iteration-level Scheduling)。其核心思想是:在每个解码步骤(Decode Step)结束后,立即判断哪些序列可以结束,并立刻将新到达的请求加入当前批次。调度粒度从“整个序列”细化到“单个解码步骤”。
| 对比维度 | 静态批处理 | Continuous Batching |
|---|---|---|
| 调度粒度 | 整个序列生命周期 | 单个解码迭代 |
| GPU利用率 | 存在明显气泡(等待长尾序列) | 持续饱和,气泡极小 |
| 新请求延迟 | 必须等待当前批次完全结束 | 下一个迭代即可加入 |
| 显存需求 | 按最大可能长度预分配 | 按实际生成进度动态分配 |
| 典型吞吐量(8×A100) | 约500-800 tokens/s | 约2000-4000 tokens/s |
vLLM的调度器(Scheduler)在每个迭代执行以下逻辑:
1. 检查当前批次中所有序列的状态
2. 若某序列生成EOS或达到max_tokens → 标记为完成,释放其KV块
3. 从等待队列中取出新请求,检查是否有足够空闲块
4. 为新请求分配KV块,加入当前批次
5. 执行一次前向传播(Prefill + Decode 混合)
6. 重复步骤1-5直到所有请求完成
案例:混合Prefill与Decode。假设当前批次有3个序列正在Decode阶段,此时新到达一个长Prompt请求(需要Prefill)。vLLM不会让新请求等待,而是将Prefill计算与Decode计算合并到同一个迭代中执行。GPU的Tensor Core同时处理矩阵乘(Prefill)和向量操作(Decode),实现计算资源的最大化利用。
3.4 KV Cache管理:显存优化的艺术
KV Cache管理直接决定了服务能同时处理多少并发请求。vLLM通过以下机制实现精细化管理:
3.4.1 显存预留与水位控制
vLLM启动时会根据 --gpu-memory-utilization 参数(默认0.9)预留显存。预留的显存中,一部分用于模型权重,剩余部分全部用于KV Cache块池。你可以通过 --max-num-seqs 限制最大并发序列数,防止显存溢出。
3.4.2 Prefix Caching(前缀缓存)
当多个请求共享相同的前缀(如系统提示词、Few-shot示例)时,vLLM会自动检测并复用这些前缀的KV块。实测中,对于包含长系统提示词的对话场景,Prefix Caching可以减少30%-50%的Prefill计算量。
# 启用Prefix Caching(vLLM >= 0.4.0)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--enable-prefix-caching \
--max-model-len 8192
3.4.3 块大小调优
默认块大小为16个token。增大块大小(如32)可减少块表开销,但会增加显存碎片;减小块大小(如8)则相反。对于长序列生成场景,建议保持默认;对于短对话场景,可尝试 --block-size 8 观察效果。
常见误区:不要盲目设置 --gpu-memory-utilization 0.98。过高的利用率会导致CUDA OOM(Out of Memory)风险,尤其在处理突发长请求时。建议生产环境设置为0.85-0.92,预留部分显存给CUDA上下文和临时张量。
3.5 吞吐量指标:如何正确衡量服务性能
吞吐量是衡量LLM服务性能的核心指标,但错误的测量方式会得出完全误导性的结论。vLLM官方与业界通常关注以下指标:
3.5.1 核心指标定义
- 输出吞吐量(Output Tokens/s):每秒生成的输出token数。这是最直接反映生成能力的指标。
- 总吞吐量(Total Tokens/s):每秒处理的输入+输出token总数,反映系统整体处理能力。
- TTFT(Time to First Token):从发送请求到收到第一个输出token的延迟。对交互式应用至关重要。
- ITL(Inter-Token Latency):相邻两个输出token之间的时间间隔,反映生成流畅度。
3.5.2 测量方法
vLLM内置了Benchmark脚本,推荐使用官方工具进行标准化测试:
# 安装benchmark依赖
pip install vllm[benchmark]
# 测试吞吐量(模拟真实并发场景)
python benchmarks/benchmark_serving.py \
--backend vllm \
--model meta-llama/Llama-3.1-8B-Instruct \
--tokenizer meta-llama/Llama-3.1-8B-Instruct \
--dataset sharegpt \
--num-prompts 200 \
--request-rate 20 \
--max-concurrency 50 \
--host 127.0.0.1 \
--port 8000
# 输出示例:
# Successful requests: 200
# Output tokens: 45230
# Total tokens: 67890
# Request throughput: 4.2 req/s
# Output token throughput: 950.3 tokens/s
# Total token throughput: 1425.7 tokens/s
# Mean TTFT: 245.3 ms
# Mean ITL: 18.5 ms
3.5.3 指标解读与调优方向
| 现象 | 可能原因 | 调优建议 |
|---|---|---|
| TTFT高但ITL正常 | Prefill阶段计算量大,或排队请求多 | 启用Prefix Caching;增加实例数;限制max-num-seqs |
| ITL波动大 | 批次中序列长度差异过大 | 启用Continuous Batching;调整块大小 |
| 总吞吐量低 | GPU利用率不足或显存受限 | 增大并发数;检查gpu-memory-utilization;使用更小的模型 |
| OOM频繁 | 并发过高或单序列过长 | 降低max-num-seqs;减小max-model-len;增加块大小 |
3.6 案例复盘:从500到3000 tokens/s的优化之路
某团队部署Llama-3.1-8B服务,初始使用TGI(Text Generation Inference)框架,测得输出吞吐量仅520 tokens/s。迁移到vLLM并完成以下优化后,吞吐量提升至3100 tokens/s:
- 启用PagedAttention:显存利用率从45%提升至92%,并发数从8提升至32。
- 开启Continuous Batching:GPU利用率从60%提升至95%,消除了长尾请求的气泡。
- 配置Prefix Caching:由于所有请求共享相同的系统提示词(约800 tokens),Prefill计算量减少40%。
- 调整实例参数:设置
--gpu-memory-utilization 0.92 --max-num-seqs 64 --block-size 16。
复盘要点:优化过程中,团队最初尝试增大 --max-num-seqs 到128,结果出现OOM。回退到64后,通过观察 nvidia-smi 发现KV Cache利用率已达95%,说明瓶颈在显存而非计算。最终通过启用Prefix Caching,在相同显存下支持了更多并发,才安全提升到64并发。
理解这四大核心概念是掌握vLLM性能调优的基础。PagedAttention解决了显存碎片化问题,Continuous Batching消除了调度气泡,KV Cache管理决定了并发上限,而吞吐量指标则提供了客观的度量标准。在实际部署中,这四个方面相互影响,需要综合考量才能获得最优性能。