编程 vLLM 深度拆解:当 KV Cache 学会「分页」——用操作系统智慧重写 LLM 推理的心智模型

2026-07-15 10:13:07 +0800 CST views 7

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 CachePagedAttention
显存分配按 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_size1GPU 数量TP 并行度,需能被 num_heads 整除
gpu_memory_utilization0.90.92~0.95显存用于 KV Cache 的比例
max_num_batched_tokens25604096~8192单次 forward 最大 token 数
block_size1616小值→粒度细但块表大;大值→浪费
enable_prefix_cachingFalseTrue有共享前缀时必开
quantizationNoneawq/squeezellm4-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 推理引擎格局已经清晰:

场景推荐原因
高并发在线服务vLLMPagedAttention + Continuous Batching 吞吐最优
极致单次延迟TensorRT-LLMC++ kernel 深度优化,延迟最低
快速原型验证Hugging Face TGI部署最简单,特性最全
多模态/视频生成SGLangRadixAttention 前缀缓存专为多模态设计
边缘/低资源llama.cppCPU/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 已经证明了:好系统是改出来的,不是设计出来的

推荐文章

php微信文章推广管理系统
2024-11-19 00:50:36 +0800 CST
CSS 中的 `scrollbar-width` 属性
2024-11-19 01:32:55 +0800 CST
ElasticSearch 结构
2024-11-18 10:05:24 +0800 CST
一个数字时钟的HTML
2024-11-19 07:46:53 +0800 CST
WebSocket在消息推送中的应用代码
2024-11-18 21:46:05 +0800 CST
PHP 微信红包算法
2024-11-17 22:45:34 +0800 CST
程序员茄子在线接单