QuanZhou's Wiki
更新于

LLM 推理优化的系统视角:从 KV Cache 到调度

~/ AI Infra#LLM#推理优化#KV Cache#调度#vLLM

1. 从请求链路到性能因果链

阶段 1 的《LLM 推理系统从请求到返回的完整链路》回答了一条请求怎样经过 Tokenizer、Scheduler、Prefill、KV Cache 与 Decode。

本文继续追问另一件事:请求在这条链路上消耗了哪些资源,又为什么会等待?知道组件名称只是起点;要解释性能,还需要把组件地图改写成一条因果链:工作负载改变计算与状态,Scheduler 安排这些工作,资源竞争最终表现为吞吐与延迟。

同一个模型,为什么会出现不同的性能?

Prompt 变长

  • Prefill 需要处理更多输入 token
  • 初始 KV Cache 变大
  • Decode 每一步读取的历史状态也变多

并发增加

  • 更多 Sequence 可以共同分摊权重读取
  • Scheduler 每轮要在更多请求之间分配计算机会
  • Queue、KV Cache 与尾延迟开始相互影响

调度策略改变

  • 吞吐可能提高,但首 token 或 token 间延迟可能恶化
  • 长 Prefill 可能阻塞正在 Decode 的请求
  • Cache 容量不足时,请求可能等待、抢占或重算

只问“哪个优化更快”还不够

常见答案

  • 开启 FlashAttention
  • 使用 PagedAttention
  • 增大 Batch
  • 启用 Prefix Cache
  • 使用量化或 Speculative Decoding

真正需要先回答的问题

  • 时间花在 Prefill、Decode、排队还是数据传输?
  • 当前受限于计算、带宽、容量还是调度?
  • 优化消除了哪一种浪费,又引入了什么成本?
  • 瓶颈被消除以后,会转移到哪里?

本文与阶段 2 其他材料的边界

本文只建立相对稳定的机制模型,不承担当前任务说明、进度追踪或原始实验记录。想跟随练习时,从阶段 2 共学页进入;那里会区分当前任务、执行路线、填写工作表与实验结果,不需要在本文中来回寻找不同手册。

2. 先定义性能问题:阶段、指标与因果链

2.1 Prefill、Decode 与 Online Serving

同一份模型,三种不同的性能问题

Prefill

  • 完整 Prompt 已知,可以同时处理多个输入位置
  • 主要计算更接近 Matrix-Matrix Multiplication
  • 输入长度与 Prefill Batch 共同影响计算效率
  • 结果是首个输出 token,以及每一层的初始 K/V

Decode

  • 单个 Sequence 每轮只能确定一个新 token
  • 每轮读取模型权重和不断增长的历史 K/V
  • 小 Batch 下通常更难充分利用计算单元
  • 结果是新 token,以及追加到 Cache 的新 K/V

Online Serving

  • 请求到达时间、Prompt 长度和生成长度都不固定
  • Prefill 与 Decode 竞争同一设备和调度预算
  • 吞吐、TTFT、TPOT、公平性与 KV 容量相互制约

“Prefill 计算密集、Decode 带宽受限”是一种常见工作区间下的性能倾向,而不是脱离 Batch、长度、模型和硬件的定律。短 Prompt 的 Prefill 也可能无法打满计算单元;Decode Batch 增大后,权重读取可以被更多 Sequence 分摊,瓶颈也可能移动。

性能结论必须带条件

看到“Prefill 是 compute-bound”或“Decode 是 memory-bound”以后,还要继续追问:在哪个模型、什么 Batch、什么长度、哪种硬件和哪个推理引擎上? 最终结论应由工作负载与 Profiling 证明,而不是由一句经验判断代替。

2.2 用户所说的“快”是什么?

Prefill 与 Decode 的资源倾向不同,用户感受到的等待也不止一个数字。讨论优化以前,需要先明确它最终影响的是哪一项指标。

Latency is not one number

TTFT

TTFT=tfirst−tarrival\mathrm{TTFT}=t_{\mathrm{first}}-t_{\mathrm{arrival}}

包含客户端可见的排队、前端处理、Prefill 和传输时间。

ITL 与 TPOT

ITLi=ti−ti−1\mathrm{ITL}_i=t_i-t_{i-1}TPOT=tlast−tfirstNoutput−1\mathrm{TPOT}=\frac{t_{\mathrm{last}}-t_{\mathrm{first}}}{N_{\mathrm{output}}-1}

描述首 token 以后,生成过程是否稳定流畅。

E2E 与 Throughput

E2E=tfinish−tarrival\mathrm{E2E}=t_{\mathrm{finish}}-t_{\mathrm{arrival}}

吞吐需要同时说明是 Request Throughput 还是 Token Throughput。

因此,“吞吐提高”不能自动推出“单个用户等待更短”。一个策略可能让设备在单位时间内处理更多 token,同时让部分请求排队更久。后文讨论每项优化时,都需要重新落回这些用户指标。

2.3 四类资源约束怎样连接?

推理性能的四种约束

Compute

  • Q/K/V、MLP 与输出层需要多少运算
  • Batch 和 Tensor Shape 能否提高设备利用率

Bandwidth

  • 权重、KV Cache 与中间结果需要搬运多少字节
  • 每轮 Decode 的有效算术强度有多高

Capacity

  • 权重、KV Cache、Runtime Buffer 与 Workspace 能否同时放下
  • 活跃 Sequence 增长后还剩多少可分配 Block

Scheduling

  • 哪些请求在本轮运行
  • Prefill 与 Decode 怎样共享 Token Budget
  • 等待、抢占、取消与释放怎样改变后续请求

这四类约束不是互斥标签。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 它复用了什么?

在第 ll 层,位置 ii 的隐藏状态经过线性投影得到:

Ki(l)=hi(l)WK(l),Vi(l)=hi(l)WV(l).K_i^{(l)} = h_i^{(l)}W_K^{(l)}, \qquad V_i^{(l)} = h_i^{(l)}W_V^{(l)}.

计算当前位置 tt 时,Dense Causal Attention 仍需要让新的 Query 与全部历史 Key 计算相关性,再聚合全部 Value:

Ot(l)=softmax⁡(Qt(l)(K1:t(l))Tdh)V1:t(l).O_t^{(l)} = \operatorname{softmax}\left( \frac{Q_t^{(l)}(K_{1:t}^{(l)})^T}{\sqrt{d_h}} \right)V_{1:t}^{(l)}.

历史 token 已经确定,所以同一层中的历史 K/V 在后续 Decode 中不会改变。KV Cache 利用的就是这个不变量。

一次 Decode Step 中,状态怎样变化?

Prefill finishes
  cache = K/V of prompt tokens [1, P]

Decode step t
  input:   latest accepted token
  read:    cached K/V [1, P + t - 1]
  compute: Q/K/V for the new position
  write:   append new K/V to cache
  output:  logits -> sample next token

Next engine step
  scheduler decides when this sequence runs again

没有 KV Cache 时,第 tt 轮 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 大小可以近似写成:

SKV/token=2×L×HKV×Dh×B.S_{\mathrm{KV/token}} = 2 \times L \times H_{\mathrm{KV}} \times D_h \times B.
  • 22:Key 与 Value 两份状态;
  • LL:Transformer Layer 数;
  • HKVH_{\mathrm{KV}}:Key/Value Head 数;
  • DhD_h:每个 Head 的维度;
  • BB:每个缓存元素的字节数。

真实 Serving 中的活跃请求长度不同,因此总量更接近:

SKV,total≈SKV/token×∑r∈activeTr.S_{\mathrm{KV,total}} \approx S_{\mathrm{KV/token}} \times \sum_{r \in \mathrm{active}} T_r.

容量公式告诉了我们什么?

模型结构决定每 token 成本

  • MHA 通常让每个 Query Head 拥有对应的 K/V Head
  • MQA 让所有 Query Head 共享一组 K/V
  • GQA 让一组 Query Head 共享一组 K/V

工作负载决定状态总量

  • Prompt 越长,Prefill 后的初始 Cache 越大
  • 输出越长,Decode 期间追加的 Cache 越多
  • 活跃请求越多,同时驻留的 token 越多

Runtime 决定物理可用量

  • Block 对齐和最后一个未填满的 Block 会带来内部浪费
  • Allocator、CUDA Graph、Workspace 与激活也需要空间
  • “总显存减权重”不能直接当作可用 KV 容量

逻辑容量不等于运行时占用

公式适合建立容量直觉,但 vLLM 真正能分配多少 KV Block,必须从固定版本和启动配置的运行时证据中确认。逻辑 Tensor、Block 管理开销与设备整体内存占用是三个不同层次。

3.3 显存不只装模型权重和 KV Cache

推理进程的设备侧工作集可以先写成一张账:

Mruntime≈Mweights+MKV+Mactivations+Mtemporary+Moverhead.M_{\mathrm{runtime}} \approx M_{\mathrm{weights}} + M_{\mathrm{KV}} + M_{\mathrm{activations}} + M_{\mathrm{temporary}} + M_{\mathrm{overhead}}.

五项不能重复计数的内存

模型权重

由参数量、dtype、量化格式和 Backend 的实际加载方式决定,通常在服务启动后长期驻留。

KV Cache

随活跃请求数、上下文长度、Layer、KV Head、Head Dimension 和 Cache dtype 增长。

激活

推理不保存反向传播状态,但 Prefill、Batch 和算子执行仍需要中间 Tensor;峰值不一定出现在空闲服务上。

临时 Buffer / Workspace

Kernel、排序、通信、编译图或 Backend 算法可能申请临时空间,生命周期与 KV Cache 不同。

Runtime 开销

Allocator 保留、Block 对齐、碎片、Graph 和框架状态会让进程占用大于上述逻辑 Tensor 之和。

因此,显存实验不能只记录一个峰值数字。应分别保存服务启动前、模型加载后、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:改变必须保存的状态

MHA、MQA 与 GQA 改变了什么?

MHA

  • Query Head 与 KV Head 数通常相同
  • 表达能力直接,但每 token 的 K/V 状态更多

MQA / GQA

  • 多个 Query Head 共享一组或一组组 K/V
  • 直接减少 HKVH_{\mathrm{KV}},因此降低 Cache 容量和相关读取字节数

边界

  • 这是模型架构属性,不是任意模型都能无损打开的 Runtime 开关
  • 容量计算应读取模型 config.json,不能只凭参数量猜测

权重量化、激活量化与 KV Cache 量化也作用于不同对象。它们都可能减少字节数,但精度代价、Kernel 支持与实际瓶颈并不相同。

4.2 Kernel / Runtime:改变相同语义怎样执行

FlashAttention 与 PagedAttention 不是一件事

FlashAttention

  • 通过 Tiling 与算子融合改变 Attention 的执行顺序
  • 减少中间结果在较慢内存层级之间的往返
  • 优化的是 I/O 路径,不改变 Attention 的逻辑语义

PagedAttention

  • 把逻辑连续的 KV Cache 切成固定大小 Block
  • 通过 Block Table 映射逻辑 token 位置与物理存储
  • 减少过度预留、外部碎片和连续分配约束

两者都没有做到

  • 不会自动减少每个有效 token 必须保存的逻辑 K/V
  • 不保证所有模型、形状与硬件上得到相同收益

PagedAttention 与操作系统页表很像:上层看到连续空间,底层物理空间不必连续。但它首先解决的是设备侧 KV Block 的分配和寻址,并不意味着必须依靠 Page Fault,或一定把冷 Block 交换到磁盘。

模型架构和 Runtime 决定单个请求需要多少状态、状态怎样存放;在线 Serving 还需要决定多个请求怎样共享这些资源。在讨论调度以前,必须先拆开几个经常被混称为 “Batch Size” 的量。

4.3 “Batch Size”为什么不是一个数字?

四个不能混写成 batch size 的量

Client Concurrency

客户端同时尚未完成的请求数。

Active Sequences

已经进入引擎、正在等待或运行,并保留状态的 Sequence 数。

Sequence Batch

某一次 Engine Step 实际被 Scheduler 选中执行的 Sequence 集合。

Token Budget

某次调度迭代允许处理的 token 总量;Prefill 请求可能一次消耗多个 token,Decode 请求通常只推进一个 token。

因此,改变 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 内等待最长 Sequence 的空槽
  • 动态请求无法及时加入执行的问题
  • 小 Batch 下权重读取难以摊薄的问题

新的竞争

  • 更多请求竞争 Scheduler 的执行机会
  • 活跃 Sequence 共同占用 KV Cache
  • 到达率超过服务能力后,Queueing Delay 持续累积

最终权衡

  • Token Throughput 可以提高
  • TTFT、TPOT、公平性与 P99 不一定同时改善

Continuous Batching 减少的是空槽与小 Batch 的执行浪费,引入的是更多请求对调度机会和 KV 容量的竞争。要证明它生效,既要观察实际 Scheduled Sequences 和 Token 数,也要同时检查吞吐、TTFT、TPOT 与尾延迟。

4.5 Prefix Caching:复用重复的 Prefill

Prefix Caching 复用的是多个请求已经计算过的相同前缀 KV 状态。它可能减少重复 Prefill 工作并降低 TTFT,但不会让后续新 token 的 Decode 自动消失。

判断 Prefix Caching 是否生效

必要条件

  • 请求共享可命中的 token 前缀,而不只是字符看起来相似
  • Runtime 确实启用并保留了对应 Cache
  • 服务端能提供命中、查询或复用证据

新成本与边界

  • Cache 占用和生命周期更复杂
  • 随机前缀或低命中率负载可能得不到收益
  • 只有 TTFT 下降而没有命中证据时,结论仍然只是相关性

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 与尾延迟?

实验开始前先留下预测

Input Length

  • 未命中 Prefix Cache 时,Prompt 变长应提高 TTFT 与初始 KV 占用
  • 历史上下文增长还可能提高 Decode 的单步读取成本

Client Concurrency

  • Token Throughput 预计先提高,再接近饱和
  • 越过容量点后,TTFT、TPOT 或 P99 可能继续恶化

Scheduler Batch Budget

  • 增大预算可能让单轮处理更多 token 或 Sequence
  • 收益取决于工作负载是否真的能填满预算,以及 KV 容量是否允许

Mixed Workload

  • 长 Prefill 可能干扰已有 Decode 的 ITL
  • 若 Chunked Prefill 或其他策略生效,这个干扰可能减弱或不可见

这些预测不是本文的结论。实验必须预先固定环境、模型、负载生成方式、Warm-up、重复次数和聚合方法,并把结果标记为 Supported、Refuted 或 Inconclusive。

学习文章与实验文章的边界

本文负责解释为什么这些预测值得检验;阶段 2 中间机制实验只总结已经完成的 Toy Attention 测量及其证据边界。当前做到哪一步、下一步执行什么,以阶段 2 共学页为入口;未来的真实 vLLM 性能文章只使用实际产生的数据,不能用这里的理论预期代替结果。

6. 重新回答最初的问题

一个请求为什么会变慢?

它可能在等待

  • 到达率超过服务能力
  • Scheduler 优先处理其他 Sequence
  • KV Cache 暂时没有可用 Block

它可能在执行更多工作

  • Prompt 更长,Prefill 计算增加
  • 输出更长,Decode 轮数增加
  • Cache 未命中导致重复计算

它可能在搬运更多状态

  • 历史上下文增长,每一步读取更多 K/V
  • Batch、Head 数或数据类型改变了字节量
  • Runtime 的内存层级与 Kernel 没有高效承载这些访问

这套系统视角应该留下什么理解?

KV Cache

用容量换掉历史重算,同时带来持续读取和生命周期管理问题。

PagedAttention

改善物理 KV 空间的分配与复用,不改变每个有效 token 的逻辑状态量。

Continuous Batching

让动态请求更有效地共享设备,但需要在吞吐、延迟、公平性与容量之间取舍。

Prefix Caching / Chunked Prefill

一个复用重复前缀,一个重排长 Prefill 的执行粒度;两者都需要命中或时间线证据,不能只看开关名称。

Speculative Decoding

用便宜候选和 Target Model 并行验证减少串行 Decode 步骤,收益受接受率与验证成本约束。

显存

权重、KV Cache、激活、临时 Buffer 与 Runtime 开销共同组成工作集,单一峰值不能说明容量花在哪里。

Benchmark

不是寻找一个最大数字,而是用受控工作负载判断瓶颈位于哪里,以及优化后它转移到了哪里。

全文的核心问题

判断一项推理优化是否有效,最终要回答三件事:它消除了哪一种浪费,引入了什么新成本,以及原来的瓶颈被转移到了哪里。