LLM 推理引擎架构演进:PagedAttention、Continuous Batching 与 vLLM/SGLang 深度工程

从 Batch-then-Serve 到 Iteration-Level Scheduling,解析现代 LLM 服务如何通过虚拟内存管理与细粒度调度将 GPU 利用率从 15% 提升至 90%+。

引言:2026 年的推理之困

截至 2026 年 9 月,大语言模型(LLM)的推理服务已成为 AI 基础设施中最肥也最难的战场。GPT-5、Gemini 3、Claude 4 等主流模型的 API 调用成本中,推理侧占比超过 70%。而开发者面临的核心矛盾没有改变:用户体验要求低延迟(P99 < 500ms),而 GPU 显存在 KV Cache 的吞噬下捉襟见肘。

以 Llama 3.1 405B 为例,单序列 8K 上下文的 KV Cache 占用约 1.2 GB 显存。当 1000 个并发请求涌入时,仅 KV Cache 就需要 1.2 TB 显存——这在单节点 8×H100(640 GB)上显然无法承载。更糟的是,传统推理引擎的 KV Cache 内存利用率长期徘徊在 20%-40%,大量显存被预分配却因序列长度参差不齐而形成内部碎片。

本文将深度剖析现代 LLM 推理引擎的两大核心技术——PagedAttention(虚拟内存分页)与 Continuous Batching(连续批调度),并以 vLLM、SGLang 为工程案例,完整拆解其架构设计、源码关键路径与生产部署最佳实践。


一、传统 Batching 为何失败

1.1 Static Batching 的批处理危机

最朴素的推理方式是一个请求一个请求地处理:GPU 执行完一个请求后,再取下一个。这种方式下,H100 的算力利用率不到 10%,因为 GPU 在等数据搬运。

Static Batching 的改进是将请求聚合成一个 batch 一次性推理。但问题在于:LLM 是自回归生成的,序列长度动态变化。最长序列完成前,batch 内的短序列只能空转等待——显存白白占用却无法释放。

Static Batching 时间线:

Request A: ██████████████████████████████ [等待最长序列完成]
Request B: ██████████████████████████████ [已完成,但显存未释放]
Request C: ██████████████████████████████ [GPU 空转]
Request D: █████████████████████████████  [已完成]

总时间 = 最长序列时间
GPU 有效计算时间 ≈ 总时间的 30%-50%

1.2 Dynamic Batching 的折中困境

Orca(OSDI‘22)首次提出 Iteration-Level Scheduling,允许在不同迭代间动态调整 batch。但传统方案仍存在两个问题:

  1. KV Cache 预分配浪费:无法精确预测最终长度,系统必须按 max_seq_len 预分配连续显存。
  2. 抢占代价高:一旦调度器决定让出某个请求,需要序列化其 KV Cache,恢复时反序列化,开销可达总时间的 15%。

这两个问题直接催生了 PagedAttention 的诞生。


二、PagedAttention:将操作系统分页思想注入 KV Cache

2.1 核心类比:虚拟内存 × 推理

PagedAttention 的灵感来自操作系统的虚拟内存管理。回忆一下操作系统如何解决内存碎片:

  • 物理内存被切成固定大小的 Page(通常 4KB)
  • 进程的虚拟地址空间通过 Page Table 映射到物理页
  • 不连续的虚拟页可以映射到离散的物理页
  • 不需要一次性分配连续物理内存

vLLM 的 PagedAttention 将这个思想原封不动地搬到了 GPU 显存管理上:

PagedAttention 核心架构:

┌──────────────────────────────────────────┐
│           GPU Device Memory              │
│  ┌─────┬─────┬─────┬─────┬─────┬─────┐ │
│  │Block│Block│Block│Block│Block│Block│ │
│  │  0  │  1  │  2  │  3  │  4  │  5  │ │
│  └─────┴─────┴─────┴─────┴─────┴─────┘ │
└──────────────────────────────────────────┘
         │         ▲                   ▲
         │ Block   │                   │
         │ Table   │                   │
┌────────▼─────────┴───────────────────┴──┐
│      Process A (seq_len=7, block_size=4) │
│      Logical:  [0] [1]                  │
│      Physical: [2] [0]                  │
│      # 只需最后1个slot在Block1,无需对齐  │
└──────────────────────────────────────────┘

Key insight:KV Cache 不再按 batch_size × max_seq_len × hidden_dim 的连续张量分配,而是固定大小 Block 的动态链表。内存浪费从 ~40% 降至 <4%。

2.2 块级写时复制(Copy-on-Watch)

PagedAttention 的另一个杀手级特性是 CoW。在多轮对话中,System Prompt 的 KV Cache 会被多个不同后续响应共享:

# 理解 PagedAttention 的 CoW 机制

class Block:
    """一个 KV Cache 块,存储固定数量的 token 的 K/V"""
    def __init__(self, block_size: int):
        self.block_size = block_size
        self.keys = torch.empty(block_size, num_heads, head_dim)
        self.values = torch.empty(block_size, num_heads, head_dim)
        self.ref_count = 0  # 引用计数

    def add_ref(self):
        self.ref_count += 1

    def release(self):
        self.ref_count -= 1
        if self.ref_count == 0:
            free_pool.put(self)  # 回收到空闲池

class BlockTable:
    """管理一个序列到物理块的映射"""
    def __init__(self):
        self.table: List[int] = []  # logical_block -> physical_block

    @staticmethod
    def coalesce(seq1_blocks: List[int], seq2_blocks: List[int]):
        """
        两个前缀相同的序列共享物理块:
        序列1: System Prompt → "Hello" → "World"
        序列2: System Prompt → "Hello" → "GPU"

        序列1 和 序列2 的 System Prompt + "Hello" KV Cache
        共享同一个物理块,ref_count=2

        分叉点之后才创建新块(CoW)
        """
        shared = []  # 共享块
        for b1, b2 in zip(seq1_blocks, seq2_blocks):
            if b1 == b2:
                shared.append(b1)
            else:
                break
        return shared

2.3 Prefix Caching 的进化:RadixAttention

SGLang 在 PagedAttention 之上引入了 RadixAttention,将共享前缀组织成 Radix Tree。当 Agent 场景下大量请求具有相同的 System Prompt 或工具定义前缀时,缓存命中率可达 60%-90%。

Radix Tree 示例:

                    [root]
                       │
              System Prompt KV (shared)
                       │
            ┌──────────┴──────────┐
        "你好"                    "Hello"
        │                          │
    ┌───┴───┐                  ┌───┴───┐
  "世界"  "GPU"               "World" "你好"
    │        │                  │       │
  Seq1     Seq2               Seq3    Seq4

命中缓存时只需计算新增分支,推理速度提升 3-8x。

三、Continuous Batching:从 Batch-as-a-whole 到 Iteration-Level

3.1 核心解耦:将 batch 从”一批请求”变成”一组槽位”

传统推理以 batch 为单位:满载后推理一次只能等待最长序列生成完毕才释放资源。Continuous Batching 将这个粒度打细——每次迭代(每个 token 生成 step)都可以调整 batch 组成:

class ContinuousBatchingScheduler:
    """Iteration-Level 调度器"""

    def __init__(self, max_num_seqs: int, max_num_batched_tokens: int):
        self.max_num_seqs = max_num_seqs          # 最大并发序列数
        self.max_num_batched_tokens = max_num_batched_tokens  # 单步最大 token 数
        self.waiting: Deque[SequenceGroup] = deque()   # 等待队列
        self.running: List[SequenceGroup] = []         # 正在运行的序列
        self.swapped: Dict[int, SequenceGroup] = {}    # 交换出去的序列

    def schedule(self) -> SchedulerOutputs:
        """每步迭代调用一次"""
        # 1. 尝试调入等待队列中的请求
        while self.waiting and self._can_admit(self.waiting[0]):
            seq_group = self.waiting.popleft()
            self.running.append(seq_group)

        # 2. 对运行中的序列做抢占判断
        preempt_needed = self._check_running_budget()
        if preempt_needed:
            # 将末尾序列换出到 CPU 内存
            victim = self.running.pop()
            self._preempt(victim)

        # 3. 生成这一步需要推理的所有 token
        return self._make_batch()  # 最多 max_num_batched_tokens

    def _preempt(self, seq_group):
        """
        抢占:释放该序列的全部 GPU KV Cache block
        数据保留在 CPU 侧 KV Cache,恢复时重新拷回 GPU
        vLLM 实际使用三种策略:recomputation > swap > mixed
        """
        for block in seq_group.blocks:
            block.release()
        self.swapped[seq_group.request_id] = seq_group

3.2 抢占策略的三层模型

vLLM 定义了三个抢占优先级:

  1. 无抢占(best-effort):让所有序列跑完,最长序列拖垮整体吞吐——只适合延迟不敏感场景。
  2. 重新计算(Recomputation):释放 GPU 上的 KV Cache,后续重新前向计算恢复。适合显存极度紧张但计算充分的场景。
  3. 交换(Swap):将 KV Cache 从 GPU 内存拷贝到 CPU 内存。需要 PCIe 带宽,适合 CPU 内存充足的情况。

实际生产中,vLLM 默认使用混合策略:优先 Swap,Swap 不够则 Recomputation。


四、vLLM 架构深度剖析

4.1 整体架构

vLLM 请求生命周期:

Client ──HTTP──► API Server ──ZMQ──► LLMEngine ──GPU──► Output
                              │
                     ┌────────┴────────┐
                     │   Scheduler     │
                     │   BlockManager  │
                     │   ModelRunner   │
                     └─────────────────┘
                              │
                     ┌────────┴────────┐
                     │  KV Cache Engine│
                     │  (PagedAttention)│
                     └─────────────────┘
                     ┌────────────────┐
                     │  Worker x N    │
                     │  (Multi-GPU)   │
                     └────────────────┘

vLLM 使用解耦架构: - Frontend:FastAPI HTTP 服务器,遵循 OpenAI 兼容 API - LLMEngine:核心调度层,协调 Scheduler + ModelRunner - Worker:GPU 执行单元,通过 NCCL 通信支持 Tensor Parallelism

4.2 Block Manager:内存管理的神经中枢

class BlockSpaceManagerV1:
    """vLLM v0.1 使用的 Block Manager,纯贪心分配"""

    def __init__(self, block_size: int, num_gpu_blocks: int, num_cpu_blocks: int):
        self.block_size = block_size        # 通常 16 tokens/block
        self.gpu_allocator = BlockAllocator(num_gpu_blocks)
        self.cpu_allocator = BlockAllocator(num_cpu_blocks)

    def allocate(self, seq_group):
        """为等待中的序列分配 GPU block"""
        num_blocks = ceil(seq_group.num_computed_tokens / self.block_size)
        block_ids = [self.gpu_allocator.allocate() for _ in range(num_blocks)]
        seq_group.block_table = block_ids

    def append_slot(self, seq):
        """每生成一个 token 调用"""
        block_idx = len(seq.block_table) - 1
        if self._is_last_block_full(seq):
            # 物理块已满,分配新块
            new_block = self.gpu_allocator.allocate()
            seq.block_table.append(new_block)
        # 否则在同一个 block 内追加 slot

    def swap_out(self, seq_group) -> SwapTable:
        """将序列的所有 block 搬到 CPU"""
        block_mapping = {}
        for gpu_block in seq_group.block_table:
            cpu_block = self.cpu_allocator.allocate()
            # GPU → CPU 拷贝 KV Cache 数据
            copy_kv_cache(gpu_block, cpu_block)
            block_mapping[gpu_block.id] = cpu_block.id
            self.gpu_allocator.free(gpu_block)
        return SwapTable(block_mapping)

4.3 多 GPU 并行策略

vLLM 支持两种并行模式:

策略 用途 KV Cache 分配
Tensor Parallelism (TP) 单模型跨 GPU 拆分 每 GPU 持有每层的部分权重 + 全流程 KV Cache
Pipeline Parallelism (PP) 模型按层切分到不同 GPU 各 GPU 负责对应层的 KV Cache

当使用 TP=4 × Llama 3.1 70B 时,每块 H100 只需存放模型权重的 1/4,剩余显存全部可用于 KV Cache。


五、SGLang:为 Agent 场景优化的结构化推理

5.1 RadixAttention 缓存复用

SGLang 的核心洞察是:在 Agent 驱动的多轮交互中,System Prompt、Function Calling Schema、示例对话都会在请求间高度重复。

# SGLang RadixAttention 核心接口

class RadixCache:
    """将 KV Cache 组织成 Radix Tree 结构"""

    def __init__(self):
        self.root = RadixNode(tokens=[])

    def match_prefix(self, token_ids: List[int]) -> Tuple[List[int], int]:
        """
        查找 token_ids 在 Radix Tree 中的最长匹配前缀。
        返回: (命中前缀对应的 block_id 列表, 命中 token 数)
        """
        node = self.root
        matched_blocks = []
        i = 0

        while i < len(token_ids):
            found = False
            for child_token, child_node in node.children.items():
                if token_ids[i] == child_token:
                    matched_blocks.append(child_node.blocks)
                    node = child_node
                    i += 1
                    found = True
                    break
            if not found:
                break

        return matched_blocks, i

    def insert(self, token_ids: List[int], block_ids: List[int]):
        """将新结果插入 Radix Tree"""
        # 从根节点出发,沿 token_ids 路径找到匹配位置,
        # 分叉点创建新分支节点
        ...

实测数据:在 Agent 场景(同一 System Prompt + 不同用户输入)下,SGLang 的 RadixAttention 相比 vLLM 的首 token 延迟降低 5-8 倍,吞吐量提升 40%-60%。

5.2 Jump-Forward Decoding

SGLang 的另一创新是 Jump-Forward Decoding:当检测到结构化输出(JSON、正则模式)中的”死区”(Skipping Zone,下一批 token 完全由文法决定)时,跳过这些 token 的生成,直接写入确定性 token:

# Jump-Forward 示例:结构化 JSON 输出
# 模板要求输出: {"name": "...", "age": ..., "city": "..."}

# 当模型输出到 '{"name": ' 后,后续的 '"' 和闭合结构可以直接确定
# 跳过 4-6 个确定 token,使有效解码步骤减少 15%-30%

在 JSON Schema 约束输出场景下,SGLang 的跳跃解码使端到端延迟降低 1.3×-1.8×。


六、生产部署实战

6.1 启动 vLLM 服务

# A100 80GB 部署 Llama 3.1 8B
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-8B-Instruct \
    --tensor-parallel-size 1 \
    --max-num-seqs 256 \
    --max-num-batched-tokens 8192 \
    --gpu-memory-utilization 0.90 \
    --enable-prefix-caching \
    --max-model-len 32768 \
    --dtype float16 \
    --port 8000

# 8×H100 TP=8 部署 Llama 3.1 405B
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-405B-Instruct \
    --tensor-parallel-size 8 \
    --pipeline-parallel-size 1 \
    --max-num-seqs 512 \
    --max-num-batched-tokens 16384 \
    --gpu-memory-utilization 0.92 \
    --enable-chunked-prefill \
    --enable-prefix-caching \
    --port 8000

调优参数说明:

参数 推荐范围 影响
max-num-seqs 64-512 越高并发吞吐越优,但显存占用越大
max-num-batched-tokens 4096-16384 控制单迭代总 token 数,超大会增加延迟
gpu-memory-utilization 0.85-0.95 KV Cache 分配比例,0.92 是甜点
enable-chunked-prefill true 将长 Prefill 拆成块,不阻塞 Decode 请求

6.2 vLLM vs SGLang 选型决策

┌─────────────────────────────────────────────────────────────────┐
│                     选型决策树                                    │
├─────────────────────────────────────────────────────────────────┤
│  Q1: 是否多轮对话/Agent场景?                                    │
│       ├─ 是 → SGLang(RadixAttention 缓存复用增益巨大)           │
│       └─ 否 → vLLM(更成熟的社区和兼容性)                       │
│                                                                  │
│  Q2: 是否需要结构化输出?                                         │
│       ├─ 是 → SGLang(Jump-Forward 加速 1.3-1.8x)               │
│       └─ 否 → 两者均可                                          │
│                                                                  │
│  Q3: 是否需要 Prefix/Custom 调度策略?                            │
│       ├─ 是 → vLLM v0.4+(内置 Priority Scheduling)             │
│       └─ 否 → SGLang(默认 FCFS,延迟更公平)                     │
│                                                                  │
│  Q4: 是否需要量化部署(GPTQ/AWQ)?                                │
│       ├─ 是 → vLLM(FP8/INT4 支持更完整)                        │
│       └─ 否 → 两者均可                                          │
└─────────────────────────────────────────────────────────────────┘

6.3 监控指标三件套

生产环境中需要重点关注的指标:

# Prometheus + Grafana 关键指标
vllm:time_to_first_token_seconds:    # TTFT - 首 token 延迟
  p50 < 200ms, p99 < 500ms

vllm:time_per_output_token_seconds:  # TPOT - 生成速率
  目标: tok/s × batch_size > 5000 tok/s per H100

vllm:gpu_cache_usage_perc:           # KV Cache 使用率
  警戒线: > 90%(需扩容或降低 max-num-seqs)

vllm:num_requests_running:           # 并发请求数
  配合 queue 深度监控调度健康度

vllm:avg_prefill_length_seconds:     # Prefill 耗时
  长上下文下可能成为瓶颈,考虑 Chunked Prefill

七、前沿趋势:2026 年推理生态的演进方向

7.1 Disaggregated Prefill-Decode

NVIDIA TensorRT-LLM 和 vLLM v0.4+ 开始支持 Prefill-Decode 分离部署:Prefill 是计算密集但并行性好的阶段,Decode 则是访存密集且需要大 Batch 的阶段。两者对 GPU 资源的需求截然不同:

传统合并部署:
  GPU1: [Prefill A][Decode A][Prefill B][Decode B]  ← 资源互斥耦合

分离部署:
  Prefill Pool (H100 x 4): [Prefill A][Prefill B][Prefill C] ← 高计算
  Decode Pool (H100 x 8):  [Decode A][Decode B...][Decode H] ← 大 Batch + 高频小请求

分离部署可将 Prefill 延迟降低 60%,Decode 吞吐提升 40%(NVIDIA 公开测试数据)。

7.2 Speculative Decoding 的成熟

当草稿模型与目标模型分布接近时(同一模型的量化版),Speculative Decoding 可将吞吐量提升 2-3 倍。GPT-5 级别的大模型通常有对应的”小草稿”(蒸馏版)配合使用。

7.3 KV Cache 的跨节点共享

Dynamo(NVIDIA 新项目)和 LMCache 探索将 KV Cache 从 GPU 内存层扩展到 CPU 内存、NVMe 乃至分布式 Redis。当 Agent 场景的 System Prompt 高达 100K token 时,KV Cache 云化存储使每次请求的前 100K token 前向计算从”必须”变为”可选”。


八、结论

PagedAttention 和 Continuous Batching 不是简单的”性能 trick”,而是 LLM 推理从”学术原型”走向”生产可用”的架构基石。它们回答了同一个核心问题:如何在动态长度、高并发、显存受限的条件下,让 GPU 始终满载运行。

  • PagedAttention 通过移植操作系统分页思想,将 KV Cache 碎片率从 40% 降至 4%。
  • Continuous Batching 通过 Iteroation-Level 调度,将 GPU 利用率从 30% 提升至 80%+。
  • SGLang 的 RadixAttention 进一步利用 Agent 场景的前缀重复特征,使首 token 延迟再降一个数量级。

对于正在构建 LLM 服务的工程团队,建议从 vLLM 起步(生态成熟、文档完善),当 Agent 场景成为主业时切换到 SGLang。无论选择哪条路线,理解 PagedAttention 和 Continuous Batching 的底层原理,都是调优和排查问题的必要前置知识。


参考资源: - vLLM 项目:https://github.com/vllm-project/vllm - SGLang 项目:https://github.com/sgl-project/sglang - PagedAttention 论文:Kwon et al., SOSP 2023 - Orca 论文:Yu et al., OSDI 2022 - NVIDIA TensorRT-LLM:https://github.com/NVIDIA/TensorRT-LLM

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.363159s