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。但传统方案仍存在两个问题:
- KV Cache 预分配浪费:无法精确预测最终长度,系统必须按 max_seq_len 预分配连续显存。
- 抢占代价高:一旦调度器决定让出某个请求,需要序列化其 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 定义了三个抢占优先级:
- 无抢占(best-effort):让所有序列跑完,最长序列拖垮整体吞吐——只适合延迟不敏感场景。
- 重新计算(Recomputation):释放 GPU 上的 KV Cache,后续重新前向计算恢复。适合显存极度紧张但计算充分的场景。
- 交换(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
发表评论 取消回复