LLM 推理优化的系统视角:从 KV Cache 到调度
1. 从请求链路到性能因果链
阶段 1 的《LLM 推理系统从请求到返回的完整链路》回答了一条请求怎样经过 Tokenizer、Scheduler、Prefill、KV Cache 与 Decode。
本文继续追问另一件事:请求在这条链路上消耗了哪些资源,又为什么会等待?知道组件名称只是起点;要解释性能,还需要把组件地图改写成一条因果链:工作负载改变计算与状态,Scheduler 安排这些工作,资源竞争最终表现为吞吐与延迟。
本文与阶段 2 其他材料的边界
本文只建立相对稳定的机制模型,不承担当前任务说明、进度追踪或原始实验记录。想跟随练习时,从阶段 2 共学页进入;那里会区分当前任务、执行路线、填写工作表与实验结果,不需要在本文中来回寻找不同手册。
2. 先定义性能问题:阶段、指标与因果链
2.1 Prefill、Decode 与 Online Serving
“Prefill 计算密集、Decode 带宽受限”是一种常见工作区间下的性能倾向,而不是脱离 Batch、长度、模型和硬件的定律。短 Prompt 的 Prefill 也可能无法打满计算单元;Decode Batch 增大后,权重读取可以被更多 Sequence 分摊,瓶颈也可能移动。
性能结论必须带条件
看到“Prefill 是 compute-bound”或“Decode 是 memory-bound”以后,还要继续追问:在哪个模型、什么 Batch、什么长度、哪种硬件和哪个推理引擎上? 最终结论应由工作负载与 Profiling 证明,而不是由一句经验判断代替。
2.2 用户所说的“快”是什么?
Prefill 与 Decode 的资源倾向不同,用户感受到的等待也不止一个数字。讨论优化以前,需要先明确它最终影响的是哪一项指标。
因此,“吞吐提高”不能自动推出“单个用户等待更短”。一个策略可能让设备在单位时间内处理更多 token,同时让部分请求排队更久。后文讨论每项优化时,都需要重新落回这些用户指标。
2.3 四类资源约束怎样连接?
这四类约束不是互斥标签。KV Cache 用容量换掉历史重算,却让 Decode 持续读取更多状态;Batching 提高设备利用率,却会增加请求竞争;PagedAttention 降低物理空间浪费,却不会让每个有效 token 的逻辑 K/V 消失。
客户端时间只能告诉我们“用户等待了多久”,不能独立证明时间花在 Scheduler、KV Cache 还是 Model Runner。解释因果关系还需要服务端证据:waiting / running requests、Queue Time、每轮调度的 Sequence 与 Token 数、KV Block 的分配与释放,以及模型实际执行时间。
request rate / client concurrency
-> waiting and active sequences
-> scheduler selects sequences and token budget
-> KV blocks lookup / allocate / grow / release
-> prefill and decode execute
-> stream chunks return to client
-> TTFT / ITL / TPOT / E2E / throughput / P99
这条链是后文的主线:工作负载从左侧进入,用户指标从右侧出现,中间每一个箭头都需要服务端状态才能排除替代解释。四类约束仍然有些抽象,KV Cache 恰好同时牵动计算、带宽、容量和调度,适合用来建立第一条完整的因果模型。
3. KV Cache:从计算优化变成状态管理
3.1 它复用了什么?
在第 层,位置 的隐藏状态经过线性投影得到:
计算当前位置 时,Dense Causal Attention 仍需要让新的 Query 与全部历史 Key 计算相关性,再聚合全部 Value:
历史 token 已经确定,所以同一层中的历史 K/V 在后续 Decode 中不会改变。KV Cache 利用的就是这个不变量。
没有 KV Cache 时,第 轮 Decode 必须重新处理 Prompt 与此前生成的 token,历史位置的 Q/K/V、Attention 和 MLP 都会重复执行。有了 Cache,每轮只为最新位置计算新的 Transformer 状态。
但“复用”不等于“不再访问”。新的 Query 仍然需要读取全部历史 K/V。因此 KV Cache 消除了历史状态的重复计算,同时把问题转化为容量、带宽和生命周期管理。
3.2 每个 token 占多少 Cache?
对常见 Decoder-only Transformer,一个 token 的逻辑 KV Cache 大小可以近似写成:
- :Key 与 Value 两份状态;
- :Transformer Layer 数;
- :Key/Value Head 数;
- :每个 Head 的维度;
- :每个缓存元素的字节数。
真实 Serving 中的活跃请求长度不同,因此总量更接近:
逻辑容量不等于运行时占用
公式适合建立容量直觉,但 vLLM 真正能分配多少 KV Block,必须从固定版本和启动配置的运行时证据中确认。逻辑 Tensor、Block 管理开销与设备整体内存占用是三个不同层次。
3.3 显存不只装模型权重和 KV Cache
推理进程的设备侧工作集可以先写成一张账:
因此,显存实验不能只记录一个峰值数字。应分别保存服务启动前、模型加载后、Warm-up 后与稳态压力下的内存,并把每一项标成“实测”“由配置估算”或 Unobservable。在统一内存 Backend 上,也必须说明观测的是设备专用显存、进程驻留还是系统统一内存,不能混用口径。
3.4 最小实验能够验证到哪里?
配套的 NumPy 实验使用 No Cache、动态扩展 Cache 与预分配 Cache 三条路径验证逐 token 数值等价,再把多个独立 Sequence 组织成固定形状的 Batched Decode。实验支持两个机制判断:KV Cache 避免了历史位置的重复计算;Batched 执行能够摊薄调用与分配开销,却不会消除每个 Sequence 自身的 Attention 工作。
这些结果只属于 CPU、NumPy、单层和受控 Shape,不能外推成真实 GPU Serving 的固定加速倍数。实现、原始样本、异常结果与复现边界统一放在《LLM 推理机制实验:KV Cache、预分配与 Batch Size》中;本文由此只保留机制结论,继续追踪瓶颈怎样从计算转移到状态管理。
4. 按作用层次理解推理优化
KV Cache 表明,推理优化通常不会让所有成本同时下降,而是改变工作的形态或位置。沿着这条思路,可以依次观察模型架构、Kernel 与 Runtime、存储布局、请求调度和解码算法。无论位于哪一层,都用同样三个问题检查它:消除了什么浪费,引入了什么成本,应该用什么证据证明它生效?
4.1 Model Architecture:改变必须保存的状态
权重量化、激活量化与 KV Cache 量化也作用于不同对象。它们都可能减少字节数,但精度代价、Kernel 支持与实际瓶颈并不相同。
4.2 Kernel / Runtime:改变相同语义怎样执行
PagedAttention 与操作系统页表很像:上层看到连续空间,底层物理空间不必连续。但它首先解决的是设备侧 KV Block 的分配和寻址,并不意味着必须依靠 Page Fault,或一定把冷 Block 交换到磁盘。
模型架构和 Runtime 决定单个请求需要多少状态、状态怎样存放;在线 Serving 还需要决定多个请求怎样共享这些资源。在讨论调度以前,必须先拆开几个经常被混称为 “Batch Size” 的量。
4.3 “Batch Size”为什么不是一个数字?
因此,改变 Client Concurrency 不等于直接设置模型执行 Batch;修改 max_num_seqs 或 max_num_batched_tokens 也不等于每一轮都会用满这些上限。真实 Sequence Batch 是请求状态、长度、KV 容量与调度策略共同形成的结果。文章标题可以使用较直观的 “batch size”,实验变量却必须精确到上述某一个量。
4.4 Serving / Scheduler:改变请求怎样共享设备
Static Batching 中,已经完成的 Sequence 可能继续等待最长 Sequence,空出的执行槽不能及时接纳新请求。Continuous Batching 在迭代边界移除完成请求,并把新请求加入后续执行轮次,更适合长度动态的生成任务。
Continuous Batching 减少的是空槽与小 Batch 的执行浪费,引入的是更多请求对调度机会和 KV 容量的竞争。要证明它生效,既要观察实际 Scheduled Sequences 和 Token 数,也要同时检查吞吐、TTFT、TPOT 与尾延迟。
4.5 Prefix Caching:复用重复的 Prefill
Prefix Caching 复用的是多个请求已经计算过的相同前缀 KV 状态。它可能减少重复 Prefill 工作并降低 TTFT,但不会让后续新 token 的 Decode 自动消失。
4.6 Chunked Prefill:把长 Prompt 拆进多轮预算
Chunked Prefill 不减少一个 Prompt 必须处理的 token 总数,而是把长 Prefill 拆成较小块,让 Scheduler 有机会在块之间安排已有 Decode。它试图降低长 Prefill 对 Decode ITL 的阻塞,却可能增加调度轮次、Kernel Launch 或中间状态管理成本。
因此它的基本问题不是“开关打开后是否更快”,而是:在同一 Mixed Workload 下,Decode 的 ITL 尖峰是否减弱,整体吞吐和长请求 TTFT 又付出了什么代价?没有对齐的流式时间线和服务端预算记录时,不能把波动归因于 Chunked Prefill。
4.7 Speculative Decoding:减少串行的大模型 Decode 步骤
Speculative Decoding 让更便宜的 Draft Model 先提出多个候选 token,再由 Target Model 并行验证这些位置。正确的接受与拒绝规则保持 Target Model 的输出分布;它不是用更差质量直接换速度。
收益取决于候选接受率、Draft 成本、一次验证的宽度以及 Backend 是否高效执行验证。候选经常被拒绝时,Draft 和验证都可能成为额外工作;它也不能替代 KV Cache、Batching 或 Scheduler。更完整的候选—验证过程见《从猜想到验证》中的投机解码说明。本文只解释它的机制与收益条件,不把尚未进行的 vLLM Benchmark 写成结论。
这些技术分属不同层次,却都可以放回同一条因果链:先确认工作量或状态怎样改变,再确认 Runtime 和 Scheduler 实际采用了新路径,最后观察用户指标及其代价。没有中间证据时,“打开开关以后更快”仍然不足以说明原因。
5. 把机制模型变成可证伪实验
把上述机制落到真实 Serving 时,需要回答:
在固定模型、推理引擎和硬件后,Input 长度、Client Concurrency 与 Scheduler Batch Budget 如何改变请求排队、KV Cache 占用和实际执行 Batch,并最终影响 TTFT、TPOT、Token Throughput 与尾延迟?
这些预测不是本文的结论。实验必须预先固定环境、模型、负载生成方式、Warm-up、重复次数和聚合方法,并把结果标记为 Supported、Refuted 或 Inconclusive。
学习文章与实验文章的边界
本文负责解释为什么这些预测值得检验;阶段 2 中间机制实验只总结已经完成的 Toy Attention 测量及其证据边界。当前做到哪一步、下一步执行什么,以阶段 2 共学页为入口;未来的真实 vLLM 性能文章只使用实际产生的数据,不能用这里的理论预期代替结果。
6. 重新回答最初的问题
全文的核心问题
判断一项推理优化是否有效,最终要回答三件事:它消除了哪一种浪费,引入了什么新成本,以及原来的瓶颈被转移到了哪里。
阅读材料
- Ashish Vaswani et al.,Attention Is All You Need
- Gyeong-In Yu et al.,Orca: A Distributed Serving System for Transformer-Based Generative Models
- Woosuk Kwon et al.,Efficient Memory Management for Large Language Model Serving with PagedAttention
- Tri Dao et al.,FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness
- Charlie Chen et al.,Accelerating Large Language Model Decoding with Speculative Sampling
- vLLM,Architecture Overview