2026-06-06
vLLM 源码剖析(一):从零搭建推理压测环境
vLLM 是目前最主流的 LLM 推理引擎之一,以 PagedAttention 和 continuous batching 两大创新著称。本文是「vLLM 源码剖析」系列的第一篇,记录从零搭建环境、跑通首次推理、完成基准压测的全过程。后续文章将深入 PagedAttention、Scheduler、Block Manager 等核心模块。
硬件环境:RTX 4070 Super 12GB VRAM,CUDA 13.2,vLLM 0.22.1,PyTorch 2.11
测试模型:Qwen2.5-1.5B-Instruct(2.9GB,bf16,28 层,hidden_size=1536)
一、环境搭建
# 安装 vLLM(清华镜像源,国内更快)
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple vllm
# 下载测试模型
hf download Qwen/Qwen2.5-1.5B-Instruct --local-dir ./models/qwen2.5-1.5b
踩坑记录
- HF_ENDPOINT 镜像冲突:如果设置了
HF_ENDPOINT=https://hf-mirror.com,httpx 会报UnsupportedProtocol错误。需要unset HF_ENDPOINT后再下载。 - numpy 版本冲突:vLLM 安装会升级 numpy 到 2.3.5,和旧版 scikit-learn 二进制不兼容。升级 sklearn 即可解决:
pip install --upgrade scikit-learn。 - Triton JIT 编译:首次推理会触发 attention kernel 的 JIT 编译,造成延迟尖刺(latency spike)。生产环境建议通过 warmup 预热。
选择 Qwen2.5-1.5B-Instruct 作为测试模型:参数量小(~3GB 显存),12GB 显卡可同时跑 batch=32 不会 OOM,便于快速迭代实验。
二、首次推理
vLLM 提供两套 API:离线批处理(vllm.LLM)和 OpenAI-compatible server(vllm serve)。先用离线 API 跑通:
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen2.5-1.5B-Instruct", gpu_memory_utilization=0.85)
outputs = llm.generate(["Explain PagedAttention."], SamplingParams(max_tokens=256))
关键参数说明
| 参数 | 含义 | 推荐值 |
|---|---|---|
gpu_memory_utilization |
GPU 显存使用比例 | 12GB 卡建议 0.85(留余量给 CUDA context) |
max_tokens |
最大生成 token 数 | 不宜设太大,vLLM 会预分配 KV cache |
temperature |
生成随机性 | 压测时设 0 保证确定性 |
三、基准压测
3.1 Batch Size vs Throughput
固定 max_tokens=128, temperature=0,测试不同 batch size 的吞吐量和延迟:
| Batch Size | Tokens/sec | Requests/sec | Avg Latency (ms) |
|---|---|---|---|
| 1 | 49.1 | 0.38 | 2607 |
| 2 | 245.4 | 1.96 | 512 |
| 4 | 504.0 | 3.98 | 252 |
| 8 | 1006.4 | 8.05 | 124 |
| 16 | 1857.3 | 14.62 | 68 |
| 32 | 3692.5 | 29.12 | 34 |
关键发现:
- 吞吐量接近线性扩展:batch=32 达到 3692 tok/s,是 batch=1 的 75 倍
- 延迟大幅下降:2607ms → 34ms,得益于 GPU 计算密度提升
- GPU 利用率是关键:单请求时 GPU 大量时间在等待(Compute M. 低),batch 增大填满计算单元
- batch=1 延迟高达 2.6s 的原因:模型加载 + Triton JIT 编译开销集中体现
3.2 Output Length vs Latency
固定 batch=1, temperature=0,测试不同 max_tokens 下的延迟:
| Max Tokens | Actual Tokens | Time (s) | Tokens/sec |
|---|---|---|---|
| 32 | 32 | 0.45 | 70.9 |
| 64 | 64 | 0.48 | 134.0 |
| 128 | 128 | 0.95 | 135.2 |
| 256 | 132 | 0.99 | 133.7 |
| 512 | 132 | 0.98 | 135.3 |
关键发现:
- 稳态吞吐约 135 tok/s:一旦进入 decode 阶段,速度恒定
- 首 token 延迟约 350ms:包括 tokenization + prefill + 首次 forward
- 模型自然停在第 132 token:遇到 EOS token,max_tokens=256 和 512 均提前停止
- 短文本(32 token)的 tok/s 偏低(70.9),因为首 token 开销占比大
3.3 延迟分解
以 max_tokens=128 为例(总耗时 0.95s,生成 128 token):
首 token 延迟: ~0.35s (tokenization + prefill + 第一次 forward)
Decode 阶段: ~0.60s (127 token × 4.7ms/token)
稳态吞吐: 135 tok/s
四、初步发现与思考
为什么 batch=1 这么慢?
单请求时,GPU 的计算单元大部分时间在空转。Qwen2.5-1.5B 只有 28 层 × 12 heads,单次 forward 的计算量填不满 RTX 4070S 的 CUDA core。等到 batch=8 以上,多个请求的矩阵乘法和 attention 可以并行,GPU 利用率才真正发挥出来。
这正是 continuous batching 的核心价值——不是等到一批请求都到达才一起处理,而是动态地将新到达的请求插入正在执行的 batch 中,最大化 GPU 利用率。
为什么稳态约 135 tok/s?
这个数字由模型架构决定:每生成一个 token 需要跑一次完整的 forward pass。1.5B 参数 × 28 层,在 bf16 下,内存带宽和计算能力达到平衡点。如果换成 7B 模型,预计会降到 20-30 tok/s。
五、下一步
Phase 2 将深入 PagedAttention 源码:
- 阅读
vllm/core/block_manager.py:理解 KV cache 的分页分配和回收 - 阅读
vllm/core/scheduler.py:理解 continuous batching 的调度策略 - 手动 trace 一次请求的 block table 变化
本系列所有代码和压测数据见 vllm-deep-dive