2、vLLM基础:vLLM简介、安装配置、基本推理流程
2.1 vLLM是什么:从推理痛点说起
在大模型服务化部署中,吞吐量低与显存浪费是两大核心痛点。传统推理框架(如简单的Transformers pipeline)在服务多用户并发请求时,每个请求独立申请KV Cache(键值缓存),导致显存碎片化严重,且无法有效复用已计算的历史状态。vLLM(Very Large Language Model Serving Engine)正是为解决这些问题而生的高性能推理引擎,由加州大学伯克利分校开发,核心创新在于PagedAttention技术。
PagedAttention借鉴操作系统虚拟内存的分页思想,将KV Cache划分为固定大小的块(Block),每个块可以存储若干个Token的键值向量。这些块在显存中不必连续存放,而是通过块表(Block Table)进行映射。这一设计带来了三个直接收益:
- 显存利用率接近100%:不再需要预留连续大块显存,碎片化空间可被任意请求的任意块使用。
- 高效共享:多个序列(如并行采样、Beam Search)可以共享相同的KV块,减少重复计算。
- 动态增长:KV Cache按需分配,请求结束后立即释放,支持更长上下文。
2.2 安装配置:三步完成环境准备
vLLM的安装非常直接,推荐使用Python 3.9~3.12环境,并优先使用CUDA 11.8或12.1+的GPU驱动。以下以最常用的pip方式为例:
# 1. 创建虚拟环境(推荐)
conda create -n vllm_env python=3.11 -y
conda activate vllm_env
# 2. 安装vLLM(自动拉取匹配的torch、transformers等依赖)
pip install vllm
# 3. 验证安装
python -c "from vllm import LLM; print('vLLM版本:', LLM.__module__)"
tensor_parallel_size参数指定并行度。CPU推理目前支持有限,不建议生产环境使用。
安装完成后,建议先运行一个最小化冒烟测试,确认框架能正确加载模型并完成一次推理:
from vllm import LLM, SamplingParams
# 初始化引擎(首次运行会下载模型权重)
llm = LLM(model="facebook/opt-125m", dtype="float16")
# 构造采样参数
sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=64)
# 执行推理
outputs = llm.generate(["Hello, my name is"], sampling_params)
print(outputs[0].outputs[0].text)
如果上述代码能正常输出一段文本,说明vLLM已正确安装并能够调用GPU资源。常见的安装问题包括:CUDA版本不匹配、显存不足(OOM)、模型下载超时等,可参考下表排查:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| ImportError: libcuda.so.1 | CUDA驱动未安装或路径未配置 | 安装NVIDIA驱动,并设置LD_LIBRARY_PATH |
| RuntimeError: CUDA out of memory | 模型过大或批量参数过高 | 降低max_num_seqs或使用dtype="float16" |
| ValueError: The model's max seq len | 模型上下文长度限制 | 设置max_model_len参数(如4096) |
2.3 基本推理流程:从单请求到批量
vLLM的推理流程可以抽象为“初始化 → 调度 → 执行 → 输出”四个阶段。理解这一流程,是后续调优max_num_seqs、max_num_batched_tokens等参数的基础。
- 初始化阶段:创建
LLM实例时,vLLM会加载模型权重、构建KV Cache池(根据gpu_memory_utilization参数预留显存)、初始化调度器。 - 调度阶段:当调用
generate()传入多个prompts时,调度器将请求拆分为序列(Sequence),并决定每步迭代(Iteration)执行哪些序列。默认采用连续批处理(Continuous Batching)策略——某个序列生成结束后,立即插入新序列,而非等待整个批次完成。 - 执行阶段:每步迭代中,vLLM对当前批次的所有序列执行一次前向传播(Forward Pass),生成下一个Token的概率分布,并通过采样参数(temperature、top_p等)选择具体Token。PagedAttention在此阶段负责读写KV块。
- 输出阶段:当所有序列达到
max_tokens或生成结束符(EOS)时,vLLM将Token序列解码为文本,返回RequestOutput对象列表。
generate()是同步阻塞的,即调用后等待所有结果返回。若需异步流式输出,应使用AsyncLLMEngine(后续章节详述)。批处理大小并非由用户直接指定,而是由调度器根据当前显存余量、max_num_seqs上限和max_num_batched_tokens上限动态决定。
2.4 案例:对比单请求与批量请求的吞吐差异
为了直观感受vLLM的批处理优势,我们用一个简单实验对比串行处理与批量处理的耗时。假设有8条不同的提示词,每条期望生成128个Token:
import time
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-2-7b-chat-hf", dtype="float16")
prompts = [
"Explain quantum computing in simple terms.",
"Write a Python function to check if a number is prime.",
# ... 共8条不同提示词
] * 8 # 共64条
sampling_params = SamplingParams(temperature=0.7, max_tokens=128)
# 方式1:串行(每次只发1条)
start = time.time()
for p in prompts[:8]:
llm.generate([p], sampling_params)
serial_time = time.time() - start
# 方式2:批量(一次发64条)
start = time.time()
llm.generate(prompts, sampling_params)
batch_time = time.time() - start
print(f"串行8条耗时: {serial_time:.2f}s")
print(f"批量64条耗时: {batch_time:.2f}s")
print(f"吞吐提升: {64 / batch_time / (8 / serial_time):.1f}x")
实际运行中,批量64条的耗时通常仅比串行8条多30%~60%,但处理的请求数却是8倍,因此整体吞吐量提升显著。这正是max_num_seqs参数存在的意义——允许调度器一次性容纳更多序列,充分利用GPU并行计算能力。
2.5 常见误区与复盘
ValueError: Requested max_num_seqs ... exceeds available)。正确做法是结合gpu_memory_utilization(默认0.9)预留的KV Cache总量,估算可容纳的最大序列数。
max_model_len为每个序列预留最大KV空间。若该值设置过大(如32K),即使实际请求很短,也会导致KV Cache池被“虚占”,降低有效并发数。建议根据业务实际最长上下文设置,而非直接使用模型支持的最大值。
max_num_seqs;若追求高吞吐,则应适当增大。