vLLM 深度拆解:当 KV Cache 学会「分页」——用操作系统智慧重写 LLM 推理的心智模型
2026年,大模型推理仍是AI Infra最卷的战场。当所有人都在讨论「模型参数多少」的时候,真正决定大模型能不能跑起来、跑得起的,是推理引擎。vLLM,就是那个改变了游戏规则的选手。
一、大模型推理的「最后一公里」困境
在 LLM 从实验室走向工业落地的进程中,推理阶段是卡脖子的那环。训练技术已经成熟,但推理环节面临三大结构性挑战:
高延迟:自回归解码的串行特性导致端到端延迟难以压缩——生成第 N 个 token 必须等第 N-1 个 token 完成,每个 token 生成都依赖完整的注意力计算。
低吞吐:传统推理框架(如 Hugging Face Transformers + PyTorch)在 batch 处理、显存管理上效率极低。在典型的在线推理场景下,GPU 利用率常常低于 30%——这意味着你花大价钱买的 A100,有 70% 的时间在空转。
高成本:以 70B 参数模型为例,FP16 推理需要约 140GB 显存,这意味着至少需要 2 张 A100(80GB/卡)才能跑起来。单个 token 的 KV Cache 随并发数线性增长,显存碎片化导致实际可用的模型并发极低。
这就是 vLLM 试图解决的核心问题。2023 年 6 月,UC Berkeley LMSYS Org 开源 vLLM(Very Large Language Model inference),凭借 PagedAttention 机制和高度工程化的推理流水线,迅速成为工业界事实上的 LLM 推理标准。
二、传统 KV Cache 的三大原罪
理解 vLLM 的创新,先要搞清楚传统推理框架为什么效率低下。
2.1 显存预分配的灾难
传统框架将每个请求的 Key-Value Cache(KV Cache)存储为独立的连续张量:
# 传统方式:PyTorch 预分配
for request in batch:
# 假设 max_seq_len = 4096,head_dim = 128,num_heads = 32
k_cache[request.id] = torch.zeros(
max_seq_len, # 一次性分配最大长度
num_heads,
head_dim
) # 每个请求占 4096 * 32 * 128 * 2(FP16) ≈ 32MB
问题在于:各请求生成长度差异巨大(10 tokens vs 1000 tokens),但 torch.zeros 需要预先分配最大可能长度。这导致:
- 7B 模型、batch_size=32、平均生成长度 256 的场景下,KV Cache 占用显存超 12GB,其中有效数据不足 40%
- 大量「看起来在用、实际没在用」的显存被白白浪费
2.2 注意力计算的 I/O 瓶颈
标准的 Multi-Head Attention 计算为:
Attention(Q, K, V) = softmax(Q · K^T / √d) · V
在解码阶段,Query 是当前 token 的单向量(shape: [1, num_heads, head_dim]),而 Key、Value 是历史所有 token 的缓存(shape: [seq_len, num_heads, head_dim])。
当序列长度达到 1024 tokens 时,K/V 张量总大小约 4MB(FP16),而 Q 仅 8KB。这意味着:99.8% 的 GPU 带宽用于读取历史缓存,GPU 计算单元长期处于饥饿状态。
2.3 静态批处理的调度僵化
传统服务采用 static batching(静态批处理):固定 batch size,等待所有请求就绪后统一处理。
# 传统 static batching
def serve_static_batch(requests: list[Request]):
# 必须等所有请求到齐
while len(pending) < BATCH_SIZE:
time.sleep(0.1) # 短请求在这里空等
batch = [pop_batch_from_queue(BATCH_SIZE)]
return model(batch) # 慢请求拖住整个 batch
缺陷:
- 长尾阻塞:生成慢的请求阻塞整个 batch,哪怕其他请求只需生成 3 个 token
- 资源浪费:短请求需要等待凑满 batch,GPU 利用率波动剧烈
- 无法应对突发流量:突发请求只能排队或丢弃
三、PagedAttention:用操作系统思想破解 KV Cache 困局
vLLM 的核心创新是 PagedAttention,其灵感直接来自操作系统中的虚拟内存分页机制。
3.1 从虚拟内存到 GPU 显存
操作系统处理进程内存的核心设计:
- 进程虚拟地址空间被划分为固定大小页(通常 4KB)
- 物理内存以页帧为单位管理
- 页表记录虚拟页到物理页帧的映射
- 按需调页(Demand Paging):仅当访问时才加载数据
vLLM 将这一思想移植到 GPU KV Cache 管理:
逻辑块(Logical Block):每个请求的 KV Cache 在逻辑上被划分为固定大小块
↓
物理块(Physical Block):GPU 显存中预分配的物理页,大小固定
↓
块表(Block Table):每个请求维护一个索引数组,记录逻辑块到物理块的映射
3.2 物理块池:O(1) 的分配与释放
vLLM 启动时预分配一大块显存作为块分配器(Block Allocator):
# vLLM 物理块分配器核心逻辑
class BlockAllocator:
def __init__(self, num_blocks: int, block_size_bytes: int):
# 一次性分配 num_blocks 个物理块
self.gpu_memory = cudaMalloc(num_blocks * block_size_bytes)
# 维护空闲块 ID 链表——O(1) 分配
self.free_list = list(range(num_blocks))
def allocate(self) -> int:
"""分配一个物理块,O(1) 复杂度"""
if not self.free_list:
raise OutOfMemoryError()
block_id = self.free_list.pop() # 尾部弹出,无搜索开销
return block_id
def free(self, block_id: int):
"""释放一个物理块,O(1) 复杂度"""
self.free_list.append(block_id) # 尾部插入
关键优势:零拷贝迁移——块内容不移动,仅更新 block table。这意味着即使用户在生成过程中动态扩展序列长度,也不需要重新分配整个缓存,只需追加新的物理块并更新映射关系。
3.3 Block Table:逻辑与物理的解耦
每个请求维护自己的 Block Table:
# 示例:请求 A 生成了 3 个逻辑块,映射到物理块 15, 88, 3
request_a.block_table = [15, 88, 3, ...]
# 逻辑块 0 → 物理块 15
# 逻辑块 1 → 物理块 88
# 逻辑块 2 → 物理块 3
当需要读取逻辑块 1 的 KV 数据时,GPU kernel 通过 block_tables[0 * stride + 1] = 88 查到物理块 ID,然后从物理块 88 读取:
# PagedAttention kernel 核心循环
for block_idx in range(num_blocks_for_sequence):
physical_block_id = block_tables[seq_id * stride + block_idx]
# 从物理块中加载当前 block 内的有效 token
for token_in_block in range(BLOCK_SIZE):
if global_token_id >= context_len:
break # 跳过 padding token
# 从非连续物理块中提取 KV 向量
K = k_cache[physical_block_id, head_id, :, token_in_block]
V = v_cache[physical_block_id, head_id, :, token_in_block]
# 计算注意力
score = dot(Q, K) / sqrt(head_dim)
scores.append(score)
values.append(V)
这与传统方式的关键区别:
| 特性 | 传统 KV Cache | PagedAttention |
|---|---|---|
| 显存分配 | 按 max_len 预分配,大量浪费 | 按需分配物理块,利用率 >95% |
| 内存碎片 | 变长张量导致严重碎片 | 固定大小物理块,类似 slab allocator |
| OOM 处理 | 直接崩溃 | 可驱逐低优先级块到 CPU(实验性) |
| 共享能力 | 无 | 支持 prefix sharing |
四、连续批处理:从「等菜上齐」到「随到随炒」
PagedAttention 解决了 KV Cache 的存储问题,但 vLLM 的端到端优化还包括调度层面的革命——Continuous Batching(又称 Iteration-level Batching)。
4.1 为什么静态批处理注定低效
假设一个场景:4 个请求同时到达
- 请求 A:prompt=512 tokens,预计生成 10 tokens
- 请求 B:prompt=64 tokens,预计生成 500 tokens
- 请求 C:prompt=128 tokens,预计生成 50 tokens
- 请求 D:prompt=256 tokens,预计生成 200 tokens
静态批处理必须等待所有请求生成完毕才能释放 GPU。请求 A 只用了 10 个 token 就完成了,却要和请求 B 一起等到它生成 500 个 token。GPU 在 A 完成后、B 还在跑的时候,处于完全浪费状态。
4.2 Continuous Batching 的调度逻辑
vLLM 弃用 static batching,采用 continuous batching:
# vLLM Scheduler 核心逻辑
class Scheduler:
def schedule(self) -> Batch:
# 1. 检查 running queue 中哪些请求可以继续 decode
running_requests = self._get_runnable_requests()
# 2. 计算剩余 token budget(显存限制)
used_tokens = sum(r.num_tokens for r in running_requests)
remaining_budget = self.max_num_tokens - used_tokens
# 3. 从 waiting queue 选取新请求进行 prefill
new_requests = []
for req in self.waiting: # 按 FCFS 顺序
if req.prompt_len <= remaining_budget:
new_requests.append(req)
remaining_budget -= req.prompt_len
else:
break # 不跳过长请求,避免饿死
# 4. 组合成 batch 返回
return Batch(
prefill=new_requests, # 新请求做 prefill
decode=running_requests # 老请求继续 decode
)
核心思想:每个 iteration(GPU forward step)动态组合可执行的请求。当请求 A 完成生成后,立即释放其占用的 GPU 资源,下一个 iteration 的 batch 会自动用新请求补位。
4.3 Chunked Prefill:超长上下文的救星
当 prompt 长度超过 max_model_len 时,传统框架直接 OOM。vLLM 的解决方案是 chunked prefill:
# 示例:prompt_len=8000,chunk_size=512
def chunked_prefill(prompt, model, kv_cache):
chunks = split(prompt, chunk_size=512) # [chunk_0, chunk_1, ..., chunk_15]
for chunk in chunks[:-1]:
# 中间 chunks:只更新 KV Cache,不生成 token
model(chunk, kv_cache=kv_cache) # no output consumed
# 最后一个 chunk:生成首个 token
output = model(chunks[-1], kv_cache=kv_cache)
return output # 返回首个生成的 token
这使得 vLLM 在 32K 上下文场景下成功加载 70B 模型,而 Hugging Face Transformers 直接崩溃。
五、高级特性:Speculative Decoding 与 Prefix Caching
5.1 Speculative Decoding:用小模型「预跑」大模型
自回归解码的串行性是延迟的天花板——必须等第 N 个 token 生成后才能开始第 N+1 个。Speculative Decoding 通过「投机」打破这个限制:
def speculative_decoding(prompt, draft_model, target_model, num_draft=5):
# Step 1: Draft model 快速生成 N 个候选 token
draft_tokens = draft_model.generate(prompt, max_new_tokens=num_draft)
# draft_tokens = ["Hello", "world", "!", "This", "is"]
# Step 2: Target model 一次前向传播验证所有候选
logits = target_model(prompt + draft_tokens)
# 单次 forward,计算所有候选位置的 logits
# Step 3: 验证每个候选 token
accepted = []
for i, draft_token in enumerate(draft_tokens):
prob = softmax(logits[i])[draft_token]
# 接受概率越高,说明 draft model 猜得越准
if random.random() < prob:
accepted.append(draft_token)
else:
# 首次拒绝处停止,后面的候选全部作废
break
# 如果全部接受:一步生成 N tokens!
# 即使部分拒绝:也从首个 mismatch 位置继续,至少节省了部分时间
return accepted
vLLM 的关键优化:
- 共享 KV Cache:draft/target 模型共享 prompt 的 KV Cache,避免重复计算
- Pipelined 验证:将 logits 计算与 sampling 重叠
在 LLaMA-7B + TinyLlama-1.1B 组合下,token/s 提升 2.1x,且生成质量无损(理论保证)。
5.2 Prefix Caching:共享公共上下文
当多个请求共享相同 prompt 前缀时,vLLM 可以共享其 KV Cache:
请求 1: [系统指令 512 tokens] + "什么是人工智能?"
请求 2: [系统指令 512 tokens] + "解释一下大语言模型"
→ [系统指令 512 tokens] 的 KV Cache 只需计算一次,两个请求共享
实现机制:
class PrefixCacher:
def add_prompt(self, prompt_tokens):
# 在 Trie 树中查找最长匹配前缀
node = self.trie.root
for token in prompt_tokens:
if token not in node.children:
break
node = node.children[token]
# 只计算未缓存的后缀部分
suffix_tokens = prompt_tokens[node.depth:]
suffix_blocks = self._compute_kv_cache(suffix_tokens)
# 链接已有前缀块 + 新后缀块
request.block_table = node.block_ids + suffix_blocks
node.ref_count += 1 # 引用计数,用于释放
在 Chatbot 场景(共享 512-token system prompt),Prefix Caching 使吞吐提升 2.4x,P99 延迟降低 52%。
六、CUDA Kernel 融合:从 100 个 kernel 到 1 个
vLLM 的极致性能还来自 CUDA kernel 层面的深度优化。
6.1 算子融合:减少 kernel launch 开销
传统推理每个 Transformer 层执行数十个独立 CUDA kernel:
LayerNorm → QKV 投影 → Attention → Softmax → Linear →
Residual Add → LayerNorm → MLP up → SiLU → MLP down → Residual Add
vLLM 将这些算子融合为单个 fused kernel:
def fused_decoder_layer(x, attn_weights, mlp_weights):
# Attention block + residual + norm → 单一 fused kernel
attn_out = paged_attention(x, kv_cache, block_table)
x = x + attn_out
x = rms_norm(x) # fused 进同一个 kernel,无额外 global memory 写回
# MLP block + residual → 单一 fused kernel
mlp_out = silu(x @ up_proj) * (x @ gate_proj) @ down_proj
x = x + mlp_out
return x
优势:
- 减少 kernel launch 开销(每次 ~5μs,在 24 层 × N tokens 的循环中累积巨大)
- 避免中间激活写回显存(HBM → SRAM → HBM 的代价)
6.2 FlashAttention 集成与 PagedAttention 融合
vLLM 深度集成 FlashAttention-2,并在此基础上实现 PagedAttention 的定制 kernel:
// PagedAttention CUDA kernel(简化版)
__global__ void paged_attention_kernel(
float* output, // [num_seqs, num_heads, head_size]
const float* query, // [num_seqs, num_heads, head_size]
const float* k_cache, // [num_blocks, num_heads, head_size, block_size]
const float* v_cache, // [num_blocks, num_heads, head_size, block_size]
const int64_t* block_tables, // [num_seqs, max_num_blocks_per_seq]
const int32_t* context_lens // [num_seqs]
) {
int seq_id = blockIdx.x;
int head_id = threadIdx.y;
// 1. 将 Q 加载到 shared memory(减少 global memory 访问)
__shared__ float Q_SHARED[HEAD_SIZE];
load_q_to_shared(Q_SHARED, query, seq_id, head_id);
// 2. 按逻辑块遍历(通过 block_table 获取物理块 ID)
for (int block_idx = 0; block_idx < num_blocks[seq_id]; ++block_idx) {
int physical_block_id = block_tables[seq_id * stride + block_idx];
// 3. Coalesced 访问:block内所有 thread 协同加载 K/V
float K_local[HEAD_SIZE], V_local[HEAD_SIZE];
load_kv_coalesced(K_local, V_local,
k_cache, v_cache,
physical_block_id, head_id, BLOCK_SIZE);
// 4. 计算注意力分数
float score = dot_product(Q_SHARED, K_local) * scale;
scores[token_id] = score;
values[token_id] = V_local;
token_id++;
}
// 5. Softmax + weighted sum
softmax_normalize(scores, output, head_id);
}
性能收益:在 A100 上,PagedAttention 相较于 naive attention,带宽利用率提升 3.2x,计算延迟降低 45%。
七、分布式推理:从单机到多机扩展
7.1 Tensor Parallelism:Ring Attention
vLLM 支持 tensor parallelism(TP),但设计哲学与训练框架不同:
传统 TP(如 Megatron-LM)依赖 All-to-All 通信同步 attention scores,带宽开销随 GPU 数 N 呈 O(N²) 增长。
vLLM 采用 ring-based attention(灵感来自 RingAttention):
GPU 0: 持有 head 0-7 → 计算局部 attention
GPU 1: 持有 head 8-15 → 计算局部 attention
→ 每步:Q/K/V 沿 ring 传递 partial 结果
→ 通信量从 O(N²) 降至 O(N)
7.2 实战:8×A100 部署 LLaMA-70B
from vllm import LLM, SamplingParams
# 多卡并行:TP=4, PP=2
llm = LLM(
model="meta-llama/Llama-2-70b-hf",
tensor_parallel_size=4, # 4 张 GPU tensor 并行
pipeline_parallel_size=2, # 2 组 pipeline
gpu_memory_utilization=0.95, # 95% 显存用于 KV Cache
max_num_batched_tokens=8192,
max_model_len=8192,
)
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.95,
max_tokens=512,
)
outputs = llm.generate(["解释量子计算原理", "分析俄乌战争影响"], sampling_params)
实测:在 8×A100(TP=4, PP=2)上部署 LLaMA-70B,vLLM 吞吐达 1850 token/s,较 DeepSpeed-Inference 高 37%。
八、生产部署:架构、监控与调优
8.1 推荐架构
[ Nginx / 云负载均衡 ]
↓
[ vLLM Ray Cluster ]
├── Head Node: Scheduler + API Gateway
├── Worker Node × N: GPU + Model Shard
└── Prometheus + Grafana (vLLM 导出 >50 个指标)
生产推荐使用 Ray Serve 集成,支持自动扩缩容和多模型共存:
# 一键启动生产级服务
vllm serve meta-llama/Llama-2-70b-hf \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.92 \
--max-num-batched-tokens 8192 \
--enforce-eager \
--port 8000
8.2 关键调参指南
| 参数 | 默认值 | 生产推荐 | 说明 |
|---|---|---|---|
| tensor_parallel_size | 1 | GPU 数量 | TP 并行度,需能被 num_heads 整除 |
| gpu_memory_utilization | 0.9 | 0.92~0.95 | 显存用于 KV Cache 的比例 |
| max_num_batched_tokens | 2560 | 4096~8192 | 单次 forward 最大 token 数 |
| block_size | 16 | 16 | 小值→粒度细但块表大;大值→浪费 |
| enable_prefix_caching | False | True | 有共享前缀时必开 |
| quantization | None | awq/squeezellm | 4-bit 量化,显存节省 70% |
8.3 必须监控的指标
vLLM 暴露 Prometheus 指标,这些是生产环境的关键告警指标:
# vLLM 关键指标(Prometheus)
# - vllm:num_requests_running: 当前运行请求数
# - vllm:gpu_cache_usage_perc: KV Cache 显存占用率
# - vllm:time_per_output_token_seconds: 每 token 延迟 P50/P99
# - vllm:num_preemptions_total: 被抢占请求数(调度压力大时飙升)
# 告警策略
ALERT_RULES = """
- alert: HighCacheUsage
expr: vllm:gpu_cache_usage_perc > 95
for: 1m
labels: severity=critical
annotations:
summary: "vLLM KV Cache 使用率超过 95%,建议扩容或降低并发"
- alert: HighPreemptions
expr: rate(vllm:num_preemptions_total[5m]) > 10
labels: severity=warning
annotations:
summary: "请求抢占率异常,可能需要调高 max_num_batched_tokens"
"""
九、vLLM vs 竞品:选型决策树
2026年的 LLM 推理引擎格局已经清晰:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 高并发在线服务 | vLLM | PagedAttention + Continuous Batching 吞吐最优 |
| 极致单次延迟 | TensorRT-LLM | C++ kernel 深度优化,延迟最低 |
| 快速原型验证 | Hugging Face TGI | 部署最简单,特性最全 |
| 多模态/视频生成 | SGLang | RadixAttention 前缀缓存专为多模态设计 |
| 边缘/低资源 | llama.cpp | CPU/GPU 均支持,无 CUDA 依赖 |
| 个人本地部署 | Ollama | 零配置,一条命令跑起来 |
vLLM 的核心优势:在保持通用性的前提下,实现接近专用引擎的吞吐,且 API 与 OpenAI 兼容,迁移成本极低。
十、未来演进:2026年的 vLLM 走向何方
10.1 正在路上的新特性
MoE 模型支持优化:当前 vLLM 对 Mixtral 等 MoE 模型的 expert routing 效率有限,团队正在开发 sparse attention 的原生支持,预计可将 MoE 模型的推理吞吐提升 2-3 倍。
持久化 KV Cache:将高频前缀的 KV Cache 持久化到 SSD,冷启动时间可从分钟级降至秒级。这对于 Chatbot 类应用(大量共享系统指令)意义重大。
FP8 量化原生支持:H100 的 FP8 Tensor Core 理论上可将推理速度再提升 2x,但需要解决激活值量化误差问题。vLLM 正在集成 calibrated FP8 方案。
10.2 与 SGLang 的融合趋势
vLLM 和 SGLang 在 2026 年出现了明显的功能交叉:
- vLLM 引入 RadixAttention 的 prefix caching 机制
- SGLang 基于 vLLM 后端重构了自己的推理引擎
两者最终可能走向更深的整合——vLLM 提供底层计算优化,SGLang 提供上层的调度灵活性。
结语:系统工程思维才是真正的护城河
vLLM 的成功证明了一个朴素的道理:大模型落地的核心竞争力,不在 model scaling law,而在 system co-design。
它将操作系统的智慧(分页内存管理)、数据库的优化(连续批处理)、HPC 的技巧(kernel fusion、ring attention)熔于一炉,为 LLM 推理树立了新标杆。
作为工程师,我们应该从 vLLM 中汲取两点核心启示:
第一,问题驱动创新,而非技术堆砌。 PagedAttention 不是凭空发明的,它是 UC Berkeley 团队对 KV Cache 痛点深刻洞察后的必然产物。理解问题是解决问题的一半。
第二,端到端思维胜于单点优化。 vLLM 的 24 倍吞吐提升不是来自某一个「黑科技」,而是内存管理、计算优化、调度策略、分布式扩展四个层面协同优化的结果。在系统工程领域,没有银弹,只有组合拳。
大模型推理的战争才刚刚开始,而 vLLM 已经证明了:好系统是改出来的,不是设计出来的。