现代推理引擎的四大核心技术:从显存管理到调度的全链路优化

现代推理引擎的四大核心技术:从显存管理到调度的全链路优化 一、问题定义推理引擎需要解决的根本矛盾大语言模型的推理服务在表面上看是一个简单的接收 prompt → 返回 completion流程但在生产环境中这个流程需要同时处理数百个不同到达时间、不同序列长度、不同生成长度的请求并将它们高效地映射到 GPU 的物理约束之上。这中间存在三组根本性的矛盾显存的动态性与硬件的固定性每个请求的 KV Cache 大小在请求到达时未知随着生成过程动态增长且不同请求的生命周期参差不齐。但 GPU 显存是一块固定大小的连续空间。将动态的、异构的存储需求高效地映射到静态的物理存储上且避免碎片化和预分配浪费是第一个核心矛盾。延迟敏感性与计算效率的冲突单个 token 的 decoding 阶段每次只处理一个 token算术强度极低GPU 大量计算单元闲置——这是 memory-bound 工作负载。如果每次只处理一个请求的一个 tokenGPU 利用率会低到不可接受。但如果等待凑齐一批请求再做批处理请求的延迟就会被排队时间绑架。在延迟和吞吐之间找到动态平衡是第二个核心矛盾。Prefill 与 Decode 的计算模式冲突prefill 阶段一次处理整个输入序列属于 compute-bound 的密集矩阵乘法GPU 计算单元可以跑满。decode 阶段每次只处理一个 token属于 memory-bound 的稀疏计算。当这两种负载混合在同一时刻执行时——一个请求正在进行 prefill另一个正在进行 decode——它们对 GPU 资源的需求完全不同prefill 的长耗时会导致同批次 decode 请求的延迟抖动TTFT 和 TPOT 的不稳定。如何调度这两种工作负载使其互不阻塞是第三个核心矛盾。现代推理引擎的四大核心技术——PagedAttention、Continuous Batching、Prefix Cache / RadixAttention、Chunked Prefill——分别对应这三个矛盾的解决方案。更准确地说它们是同一套系统设计理念在不同维度的展开将动态性从预分配和静态编排中解放出来用分治和按需分配的思想重建整个推理栈。二、PagedAttention将 KV Cache 管理映射为虚拟内存问题2.1 核心思想PagedAttention 由 vLLM 团队在 2023 年提出其核心思想简洁而深刻把操作系统管理进程内存的方法搬到 GPU 显存管理上来。在 PagedAttention 出现之前KV Cache 的管理方式是为一每个请求预分配一块连续的显存区域大小 max_seq_len × 2(num_layers) × hidden_dim × dtype_size。问题显而易见一个最大支持 4096 token 的请求如果实际只生成 200 token剩余 3896 token 的预留空间全部浪费。此外不同长度的请求反复分配和释放不规则的连续空间必然导致显存碎片。PagedAttention 的解决方案将 KV Cache 切分为固定大小的block页典型大小为 16 或 32 个 token通过page table将逻辑位置某个 token 在序列中的第几个位置映射到物理位置显存中某个 block 的第几个槽位分配策略改为按需分配——只有当 token 实际生成时才分配新的 block释放策略为整页回收——请求完成后所有 block 整页归还到空闲池2.2 为什么有效PagedAttention 从根本上消除了 KV Cache 的三个痛点内部碎片归零每个 block 要么完全使用要么完全不分配不存在部分使用的情况外部碎片归零所有分配/回收操作的对象是固定大小的 block不会产生无法利用的间隙预分配浪费归零显存仅在 token 实际生成时才被占用不存在预留但未使用的空间在典型的生产负载中对话机器人、代码补全PagedAttention 能将 KV Cache 的显存利用率从静态预分配的 30-40% 提升至接近 95%这意味着同等显存可以服务 2-3 倍的并发请求数。2.3 设计与局限PagedAttention 的设计有一个隐性的工程判断选择了固定大小的页而非可变大小以接受少量内部碎片为代价换取零外部碎片。当 block 大小选择不当时如 block size 16 但大多数请求只生成 8 个 token仍有少量浪费。此外page table 本身占用一定的显存但其开销在模型中占比通常小于 0.5%可以忽略。自 2023 年 vLLM 开源以来PagedAttention 已被几乎所有主流推理引擎TensorRT-LLM、SGLang、LMDeploy、HuggingFace TGI采用或借鉴成为 LLM 推理的 KV Cache 管理的事实标准。三、Continuous Batching当批次不再是静态的3.1 核心思想Continuous Batching也称 In-flight Batching、Dynamic Batching解决的是传统静态 batching 的一个结构性缺陷批次一旦开始执行就必须等到批次中所有请求都完成生成后才能释放与新请求拼接新的批次。这意味着一个只生成 10 token 的短回复必须等待同批次中另一个生成 500 token 的长回复完成才能从批次中退出。GPU 在等待批次填充和清空的过程中计算单元周期性空闲。Continuous Batching 的核心思想是允许请求动态地加入和离开正在执行的批次。每当一个请求完成生成遇到 EOS token 或达到 max_new_tokens立即将其从批次中移除释放它的 KV Cache pages每当有新请求到达且批次有空位立即将其插入批次开始 prefill 并分配初始 KV Cache pages下一轮 decoding step 的输入序列 上一轮的输入序列 1已完成的请求被移除新请求被插入3.2 为什么有效Continuous Batching 将 GPU 的计算饱和度从周期性尖峰变成持续高原——只要请求队列非空引擎就能一直维持接近满额的批处理规模。它的收益对于 GPU 吞吐是结构性的而非常数因子的改进。在工程实践中Continuous Batching 通常与 PagedAttention 耦合——前者负责谁在批次中后者负责批次中每个请求的 KV Cache 如何存储。因为请求的加入和离开需要频繁的 KV Cache 分配和回收PagedAttention 的整页分配/回收机制使这一操作的延迟可控仅涉及页表更新无数据移动。3.3 实际上做了什么与 PagedAttention 一样Continuous Batching 已在主流推理引擎中成为标准配置。TensorRT-LLM 的 GptManager、vLLM 的 Scheduler、SGLang 的 Token Scheduler 都是这一机制的实现。各引擎在调度策略上有差异先到先服务 vs. 优先级队列 vs. 前缀感知调度但核心机制一致。四、Prefix Cache / RadixAttention当共享不再是偶然这是四大技术中最容易被低估的一项。PagedAttention 和 Continuous Batching 解决了如何高效存储和调度的问题但尚未触及一个更本质的优化空间如果多个请求有重叠的前缀如何避免重复计算4.1 问题的根源哈希表的全有或全无匹配在 Prefix Cache 被系统化设计之前KV Cache 复用是一个随机的、不可控的启发式过程。引擎维护一个全局哈希表将(prefix_tokens_hash, block_index)映射到 KV 块。当新请求到达时在哈希表中查找是否有完全匹配的前缀——如果前缀的 token 序列一字不差地完全匹配就能复用 KV Cache跳过 prefill。这个方案的致命缺陷在于哈希表只回答是或否不能回答有多少。考虑两个请求请求 Asystem_prompt800 tokens What is machine learning?5 tokens user_context200 tokens请求 Bsystem_prompt800 tokens What is deep learning?5 tokens user_context200 tokens哈希表看到的A 的前缀 key hash(1005 tokens) → 某个值B 的前缀 key hash(1005 tokens) → 与 A 不同差一个字母结论没有可复用的缓存。但实际上前 800 token 的 system prompt KV 完全可以复用前 5 token 中至少 What is 的 KV 也可以复用。哈希表的精确匹配思维在存在部分重叠的场景下造成了系统性的缓存浪费。4.2 Radix Tree为最长公共前缀而生SGLang 的RadixAttention通过一种根本性的数据结构选择解决了这个问题用 Radix Tree压缩前缀树也称 Patricia Trie替代哈希表。Radix Tree 的每个节点代表一个 token 序列片段压缩前缀从根到任意节点的路径代表一个完整的前缀。当新请求到达时引擎沿着树向下遍历沿着与请求前缀匹配的边行走直到无法匹配为止——所到达的最深节点就是最长可复用前缀。root │ You are a helpful assistant.\n ← 深度 1: system prompt (最高频共享) │ ┌────┴────┐ │ │ User: What User: Write is AI? a poem about │ │ Assistant: Assistant: Here │ │ gen_1A ┌──┴──┐ │ │ gen_2A gen_2B在这个结构中所有请求自动共享 system prompt 节点的 KV只要 system prompt 相同部分重叠的 user message如 What is也会在前几个 token 层级共享每个节点的 KV 只存储一份由引用计数管理生命周期Radix Tree 解决的核心问题是将前缀复用从布尔匹配提升为度量匹配——不是能不能复用而是最多能复用多少。对于 KV Cache 复用而言这个区别是质变而非量变。4.3 引用计数淘汰与活跃请求深度绑定的生命周期哈希表方案的另一个痛点是淘汰策略。典型的 LRU/LFU 淘汰决策完全基于访问历史——最近最少使用的前缀被踢出。问题是一个前缀即使最近被使用了 1000 次如果当前没有活跃请求正在使用它它也应当可以被淘汰反之一个即使只被访问过 1 次的前缀如果当前有一个慢速请求正在生成过程中它也不应该被淘汰。Radix Tree 通过引用计数解决了这个问题每个节点维护一个计数器记录当前有多少活跃请求正在使用该节点的 KV Cache。当一个请求完成或被取消它路径上所有节点的引用计数减一。当一个节点的引用计数归零它的 KV 块可以被回收——但这个决策不是由LRU 时钟驱动而是由所有使用者都已离开这一语义事件驱动。从系统设计的视角看这是在 KV Cache 的生命周期中引入了所有权语义ownership semantics——不是系统帮你缓存了系统决定什么时候释放而是每个活跃请求都对它使用的前缀持有引用最后一个离开的请求触发释放。这与 C 的shared_ptr、Rust 的Arc有异曲同工的逻辑。4.4 编译时前缀分析当缓存成为程序语义的一部分RadixAttention 的讨论中还有一个容易被忽略的层次编译时前缀分析。SGLang 的 DSLfunctiongenselect在执行前会进入一个 trace 阶段——以无实际 LLM 调用的方式运行函数体记录所有的gen/select调用点和它们之间的 token 追加关系。编译器随后分析这个操作日志自动识别出哪些gen调用共享同一个前缀。以这段 SGLang 代码为例function def agent(s, task): s system_prompt # ← 所有调用共享 s Task: task s Analysis: s gen(analysis, max_tokens200) s Action: s gen(action, max_tokens100) s Result: s gen(result, max_tokens500)编译器自动发现system_prompt的 token 序列在gen(analysis)、gen(action)、gen(result)之间完全共享当多个用户同时调用agent()时他们的system_prompt前缀在 Radix Tree 中指向同一节点这种共享不需要开发者手动管理由编译器自动保证编译时前缀分析的本质是将跨请求缓存共享这个看似纯运行时的优化问题部分地提升到了编译时来解决。这背后的洞察是前缀共享的结构信息在程序的源代码中就已被明确编码——编译器完全可以看到它而不需要等到运行时去发现它。4.5 vLLM 的 Automatic Prefix CachingvLLM 从 0.4.x 版本开始引入了Automatic Prefix Caching (APC)原理是在 PagedAttention 的基础上增加一个全局的哈希表将(prefix_tokens_hash, block_index)映射到物理 KV block。当新请求的某个 block 的 token 序列与某个已缓存 block 完全一致时直接在 page table 中映射到该物理 block避免重复 prefill。APC 与 Radix Tree 的核心差异在于APC 是 block 级精确匹配每个 KV block 的 token 序列必须完全一致才能复用。两个请求的 system prompt 如果只差一个 token它们复用的 block 数取决于差异出现在哪个 block。Radix Tree 是 token 级最长公共前缀即使两个请求在某个位置开始分叉分叉点之前的所有 token 的 KV 都可以被复用它不按 block 边界切割。在大部分生产场景中Radix Tree 的前缀复用率高于 APC5-15%但 APC 的实现复杂度显著低于 Radix Tree不需要维护树结构不需要处理引用计数和并发遍历。两者在普通场景下的加速差距通常不大Radix Tree 的真正优势体现在高并发 多层次共享前缀的复杂场景中如 Agent workflow、多工具编排、长 few-shot 示例。4.6 前缀感知调度让局部性最大化前缀缓存有效的前提是共享前缀的请求能在时间上聚拢。如果一个请求的 system prompt 被缓存了但下一个到达的请求用了完全不同的 system prompt缓存中的 KV 就在那里闲置而新请求仍需要从零开始 prefill。前缀感知调度Prefix-aware Scheduling是 SGLang 的 SRT 引入的一项调度优化调度器在决定哪些请求进入下一个批次时主动将共享相同前缀的请求编组到同一批次中。这样批次内的所有请求可以共享一次 prefill 的结果——共享前缀只被计算一次而非每个请求各算一次。这是一种用调度优化缓存命中率的思想——将计算机体系结构设计中经典的**数据局部性data locality**原则应用到了 LLM serving 的请求调度层面。五、Chunked Prefill将长序列拆解为调度友好的碎片5.1 Prefill 与 Decode 的结构性冲突这是四大技术中最后一个被系统化解决的难题。在 Chunked Prefill 出现之前推理引擎处理一个请求的 prefill 时采用全量一次性 prefill的方式——将整个输入序列一次性注入模型做一次完整的前向传播生成响应序列的第一个 token。这种方式在单请求场景下没有问题。但当同一个批次中混合了 prefill 请求和 decoding 请求时问题发生了批次中有: - 请求 A: 正在 decoding 阶段每次只需处理 1 个 tokenmemory-bound, 约 2-5ms/step - 请求 B: 刚到达有 8000 token 的输入需要 prefillcompute-bound, 约 80-200ms 传统批次执行: 时间线: [B prefill: 80-200ms] → [AB decode: 2-5ms] → [AB decode: 2-5ms] → ... 请求 A 的用户在这一步: 我明明只需要生成下一个 token, 为什么等了 200ms? → 这就是 prefill 导致的 decode 延迟抖动 (TTFT/POT latency spike)这个问题的根本原因在于prefill 和 decode 的 computation time 差异可达 50-100 倍但传统的批次执行要求所有请求在每步计算中同步——等待最慢的那个操作完成。一个 8000 token prefill 的执行时间足以容纳 40-100 次 decode step但在这段时间内decoding 请求被完全阻塞。5.2 Chunked Prefill 的核心思想Chunked Prefill 的解决方案简洁而有效不要把 prefill 做成一个不可中断的长耗时操作而是将它切分成多个小块chunk每次只处理固定 token 数的 prefill中间穿插 decoding step。Chunked Prefill 的执行模式 (假设 chunk_size 512 tokens): 时间线: [B prefill chunk 0 (512 tokens)] → [AB decode] → [B prefill chunk 1 (512 tokens)] → [AB decode] → ... 每个 chunk 的处理时间 ≈ 5-10ms (与 decode step 在同一数量级) 请求 A 的 decode 延迟不受长 prefill 的影响 → 延迟稳定关键配置参数是max_num_batched_tokensvLLM或等价的chunk size。这个参数定义了一次 prefill chunk 最多能包含多少个 token——引擎在处理每个推理 step 时首先为活跃的 decoding 请求各生成一个 token然后用剩余的算力处理 pending 请求的 prefill chunk如果仍有余力。下一轮 step 中相同的过程重复直到所有请求的 prefill 完成。5.3 为什么有效调度颗粒度的统一化Chunked Prefill 的核心贡献在于统一了 prefill 和 decode 的调度颗粒度。在无分块的传统方案中调度器面对的是两类截然不同的工作负载一类耗时 ~2-5msdecode另一类耗时 ~50-200msprefill。调度器没有能力将一个 200ms 的 prefill 拆解为 10 个 20ms 的 chunk——因此它只能在让 decoding 请求等待牺牲延迟或让 prefill 请求排队牺牲 TTFT之间二选一。Chunked Prefill 将 prefill 从一个大的原子操作分解为一系列小的、可控的操作。每个 chunk 的计算时间与 decode step 在同一数量级两者可以公平轮流调度。这是一种**时间片轮转time-slicing**式的公平性设计——用操作系统的术语说就是把一个长 CPU burst 拆短避免饥饿短 burst 的进程。5.4 与 Continuous Batching 的耦合Chunked Prefill 与 Continuous Batching 的结合是自然且必要的没有 Continuous Batching请求在 prefill 完成后自动进入 decode 阶段decode 请求在批次中混存。Chunked Prefill 正是在这个混存环境中的时间片调度。没有 Chunked Prefilllong prefill 会导致同批次 decode 请求的延迟尖刺Continuous Batching 虽然能动态调整批次组成但无法解决一个请求赖在 prefill 阶段不释放的问题——因为 prefill 在传统实现中是原子的。两者的组合产生了可抢占式的批次调度引擎可以在任何时候暂停某个请求的 prefill服务一轮 decode再恢复 prefill——批次的计算负载从尖峰-低谷变为持续平稳。5.5 Chunked Prefill 的性能权衡Chunked Prefill 并非零成本优化。将一次长 prefill 拆成多个 chunk 会引入额外的调度开销多次 kernel launch、多次 attention mask 计算以及prefill 本身的延迟略有增加因为中间穿插了 decode step总 wall-clock 时间变长。但这些开销通常被控制在实际 prefill 时间的 5-10% 以内——相比于它对 decode 延迟稳定性的改善这是完全可以接受的代价。因此 Chunked Prefill 通常作为默认开启的选项而非需要手动启用的优化开关。在 vLLM 中自 0.4.x 起 Chunked Prefill 已成为默认行为通过--max-num-batched-tokens参数控制 chunk 大小。在 SGLang 中它被内建在Scheduler的running_batch调度循环中。5.6 一个微妙的设计细节Chunk Size 的选择chunk size 的选择不是一个简单的越大越好或越小越好的问题。它涉及一个隐晦但真实的权衡chunk size 过小如 128 tokens每个 prefill chunk 非常短decode 延迟的保证最好几乎没有延迟尖刺但 prefill 被切得太碎导致总 prefill 时间显著增加调度开销占比升高TTFT 恶化。chunk size 过大如 2048 tokens每个 chunk 仍然可能造成明显的 decode 延迟尖刺因为一个 2048 token 的 prefill 仍需要 15-30mschunking 的效果打折。最优的 chunk size 通常在256-512 tokens之间——这个数值使得每个 chunk 的执行时间对于 7B-70B 模型落在 3-8ms 左右与 decode step 的时间在同一数量级既不造成显著的 delay spike也不会过度拖长 prefill 总时间。六、四者的协同一个统一的调度模型四大技术不是独立存在的——它们在引擎内核中以复杂的耦合方式协同工作。一个完整的推理 step 调度循环如下┌──────────────────────────────────────────┐ │ Scheduler: 下一轮 step 的批次组成 │ │ - Continuous Batching: 谁加入/退出 │ │ - Prefix-aware: 同等条件下优先选择 │ │ 共享前缀的请求进入同一批次 │ │ - Chunked Prefill: pending 请求的 │ │ prefill 切分为 chunk_size 的片段 │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ 批次执行 │ │ - PagedAttention: 每个请求的 KV Cache │ │ 通过 page table 寻址到物理 block │ │ - Prefix Cache: 共享前缀的 KV block │ │ 直接映射到已缓存物理 block跳过计算 │ │ - 为 decoding 请求各生成 1 个 token │ │ - 为 prefill 请求处理 chunk 个 token │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ 后处理 │ │ - 更新 page table: 新 token → 新 block │ │ - 更新 prefix cache tree/hash │ │ 新前缀节点 → 引用计数 1 │ │ - 完成/取消的请求 → 释放所有 block │ │ 引用计数 -1归零节点可能被回收 │ │ - Continuous Batching: 从批次移除 │ │ 已完成请求插入新到达请求 │ └──────────────────────────────────────────┘ │ ▼ 下一轮 step (循环)这个协同模型的精妙之处在于四者之间没有冗余——每一项解决一个不同维度的瓶颈技术解决维度核心手段PagedAttention显存管理分页 页表 按需分配Continuous Batching请求调度批次成员动态增删Prefix Cache / RadixAttention计算复用最长公共前缀匹配 引用计数淘汰Chunked Prefill工作负载混合prefill 拆分 时间片轮转PagedAttention 解决怎么存Continuous Batching 解决谁先跑Prefix Cache 解决哪些不需要重新算Chunked Prefill 解决长跑和短跑怎么公平使用跑道。七、总结将四大技术放在一起审视一条贯穿的设计主线浮现出来在 2022 年及以前LLM 推理是一个批处理问题——把一批请求打包扔给 GPU等全部完成后再处理下一批。每一项资源显存、计算、缓存都是静态预分配的由开发者提前设定上限。2023-2024 年这四大技术将 LLM 推理重新定义为一个操作系统级别的问题PagedAttention把显存管理变成了虚拟内存系统Continuous Batching把请求调度变成了多任务分时系统Prefix Cache把计算复用变成了带所有权和引用计数的缓存系统Chunked Prefill把长任务的执行变成了可抢占的时间片轮转这不是巧合。当一个系统的多个资源存在动态需求 vs. 固定供给的矛盾时操作系统的设计原则——分页、分时、缓存、抢占——在不同场景下被反复证明是最优的通用解。现代推理引擎的设计者所做的本质上是在 GPU serving 的约束下重新发现并适配了这些原则。理解这四项技术不仅是在理解LLM 推理引擎怎么工作更是在理解当一个计算系统面对高度动态、异构、不可预测的工作负载时系统设计应当如何回应。本文基于 vLLM、SGLang、TensorRT-LLM 等主流推理引擎的架构设计撰写具体实现细节可能因版本而异。