AI 工程基础体系 · 第 81/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

LLM 连续批处理:调度、Prefill、Decode、公平性与吞吐延迟

LLM 服务中的“批处理”不是一次把一组请求拼成矩阵、等待整组请求全部结束,再处理下一组。对自回归生成模型而言,不同请求的输入长度、输出长度、到达时间和取消时间都不同。如果仍然采用固定批次,短请求必须等待长请求结束,GPU 也会在部分序列完成后出现空槽。

连续批处理(continuous batching)把调度粒度从“一整个请求批次”降低到“生成迭代”或“解码步”:每轮只要有请求完成、取消或新请求进入,就重新决定下一轮 GPU 应处理哪些序列。因此,已完成的请求可以立即退出,新请求可以补入正在运行的批次。

但连续批处理并不只是一个队列技巧。它同时涉及:

  • Transformer 的 Prefill 和 Decode 两种计算阶段;
  • KV Cache 的显存占用和生命周期;
  • GPU kernel 的批量执行方式;
  • 新请求与已有请求之间的调度竞争;
  • TTFT、TPOT、端到端延迟和吞吐之间的取舍;
  • 多租户配额、优先级、公平性以及取消和故障恢复。

下面从模型计算开始,逐步建立这些概念之间的因果关系。

1. 自回归 Transformer 为什么天然分成 Prefill 和 Decode

1.1 因果自注意力的计算对象

Transformer 的自注意力通常写为:

Attention(Q,K,V)=softmax(QKTdk+M)V\operatorname{Attention}(Q,K,V) = \operatorname{softmax} \left( \frac{QK^\mathsf{T}}{\sqrt{d_k}} + M \right)V

其中:

  • QQ 是查询矩阵;
  • KK 是键矩阵;
  • VV 是值矩阵;
  • dkd_k 是每个注意力头的维度;
  • MM 是因果掩码,禁止位置 tt 读取未来位置 >t>t 的信息。

对于长度为 LL 的输入,因果掩码允许第 tt 个位置看到从第 1 个到第 tt 个位置的内容。模型因此可以一次并行计算整个输入序列的隐藏状态,同时保证每个位置只依赖过去信息。

生成阶段则不同。假设模型已经处理了提示词和此前生成的 token,现在只需要为下一个 token 计算一个新位置。这个新位置的查询向量 qtq_t 需要访问此前所有位置的 KKVV,但不需要重新计算旧位置的 KKVV

这就形成了两个阶段。

1.2 Prefill:处理输入提示词

Prefill 是把用户输入的 prompt 送入模型,计算所有输入 token 的隐藏状态,并产生它们对应的 K,VK,V

例如输入:

请解释 TCP 拥塞控制

经过 tokenizer 后得到 pp 个 token。Prefill 会并行处理这些 token,建立该请求的初始 KV Cache。Prefill 结束后,模型通常可以得到第一个待生成 token 的 logits。

Prefill 的特点是:

  1. 输入 token 数量通常较多;
  2. 同一请求内存在较强的矩阵并行性;
  3. 需要建立一段长度为 pp 的 KV Cache;
  4. 计算和显存带宽压力通常较高;
  5. Prefill 时间直接影响首 token 延迟,也就是 TTFT。

Prefill 并不是“模型理解完输入但没有输出”。从计算角度看,它已经完成了最后一个输入位置的前向计算,并可以据此采样第一个输出 token;服务系统是否把这个 token 在同一轮立即发出,取决于具体执行器。

1.3 Decode:逐 token 生成

Decode 是在已有 KV Cache 的基础上,每次输入一个新 token,生成下一个 token。

设某个请求当前已有 LL 个 token 的 KV Cache。下一轮 Decode 只需:

  1. 读取上一个 token;
  2. 计算这个新位置的 qt,kt,vtq_t,k_t,v_t
  3. 使用 qtq_t 对长度为 LL 的历史 K,VK,V 做注意力;
  4. 产生下一个 token;
  5. 把当前新位置的 kt,vtk_t,v_t 追加到 KV Cache。

Decode 每次通常只处理一个新 token,因此单个请求的矩阵规模较小,计算往往受显存读写和 KV Cache 访问限制。多个请求的 Decode 可以拼成 batch,以提高 GPU 利用率。

1.4 为什么 Decode 可以复用 KV Cache

如果每轮 Decode 都重新对完整序列做注意力,那么生成长度为 oo 的输出时,需要重复处理历史 token,计算量会显著增加。

KV Cache 保存每层、每个历史位置的 KKVV。下一轮只需要新增一个位置的 K,VK,V,并让新查询向量访问缓存中的历史键和值。

对于使用 LlayerL_{\text{layer}} 层、HKVH_{\text{KV}} 个 KV 头、每头维度 dd、数据类型每元素占 bb 字节的模型,单个 token 的 KV Cache 近似占用:

mtoken=2×Llayer×HKV×d×bm_{\text{token}} = 2 \times L_{\text{layer}} \times H_{\text{KV}} \times d \times b

其中前面的 2 表示 KKVV

一个请求的缓存占用近似为:

mi=2×Llayer×HKV×d×b×(pi+gi)m_i = 2 \times L_{\text{layer}} \times H_{\text{KV}} \times d \times b \times (p_i + g_i)

其中:

  • pip_i 是输入 token 数;
  • gig_i 是已经生成的 token 数;
  • 如果按最大输出长度预留,还需要把 gig_i 换成预计的最大输出长度。

使用 Grouped-Query Attention 或 Multi-Query Attention 时,HKVH_{\text{KV}} 小于查询头数,因此 KV Cache 会显著小于标准 Multi-Head Attention。这是模型结构和服务系统内存规划之间的直接联系。

2. 静态批处理与连续批处理

2.1 静态批处理的同步边界

传统静态批处理通常执行如下流程:

  1. 收集一批请求;
  2. 对整批请求做 Prefill;
  3. 对整批请求做一次 Decode;
  4. 继续 Decode,直到整批请求全部完成;
  5. 批次结束后才接收下一批请求。

假设一个 batch 中有三个请求,其输出长度分别为 2、20、5。Decode 第 2 步之后,第一个请求已经完成,但静态批处理仍可能保留它对应的 padding 槽位,或者等待整个 batch 中最长请求完成。

这种方式的主要问题是批次内同步:最慢的请求决定批次的生命周期。

2.2 连续批处理的迭代级调度

连续批处理把每一轮 Decode 视为一个重新调度点:

  1. 移除已经生成 EOS、达到长度上限或被取消的请求;
  2. 更新仍在运行请求的状态;
  3. 从等待队列选择可以进入的请求;
  4. 决定本轮执行哪些 Prefill chunk 和 Decode;
  5. 执行 GPU 前向;
  6. 采样 token、更新 KV Cache 和流式输出;
  7. 进入下一轮。

因此,批次不是一个固定集合,而是一个随时间变化的集合:

BtBt+1B_t \neq B_{t+1}

例如:

flowchart LR
    Q[等待队列] --> S[调度器]
    S --> P[Prefill 队列]
    S --> D[Decode 活跃集合]
    P --> X[GPU 执行器]
    D --> X
    X --> K[KV Cache]
    X --> O[采样与流式输出]
    O --> F{完成/取消/继续}
    F -->|继续| D
    F -->|完成或取消| R[释放槽位与 KV Cache]
    R --> S

关键路径是:等待队列中的请求不会直接访问 GPU,而是必须先通过调度器;调度器不仅选择请求,还必须保证显存预算、批次 token 预算和租户配额都满足。

“连续”不意味着请求可以在任意 GPU 指令执行到一半时插入。常见实现仍然在 kernel 或一次执行迭代之间切换。请求通常只能在调度边界加入,已经发射的 kernel 不能被普通新请求安全地强行打断。

3. 调度器需要维护什么状态

一个生产级请求至少需要以下状态:

WAITING
  └─> PREFILLING
        └─> DECODING
              ├─> FINISHED
              ├─> CANCELLED
              ├─> FAILED
              └─> PREEMPTED / RECOMPUTE_REQUIRED

每个请求还需要记录:

  • 请求到达时间;
  • tokenizer 后的 prompt token 数;
  • 已完成的 Prefill token 数;
  • 已生成 token 数;
  • 最大新 token 数;
  • 停止词、EOS 和采样参数;
  • KV Cache 的物理块或页;
  • 所属租户、优先级和配额;
  • 客户端连接是否仍然有效;
  • 累计输入 token、输出 token 和推理成本。

请求完成的判定不能只依赖 EOS。以下情况也可能结束请求:

  • 达到 max_new_tokens
  • 生成了用户指定的 stop sequence;
  • 客户端取消;
  • 租户超时或预算耗尽;
  • 模型执行失败;
  • worker 进程退出。

如果只处理正常 EOS,取消请求留下的 KV Cache 会造成显存泄漏,最终表现为服务吞吐逐渐下降,随后发生 OOM。

4. Prefill 与 Decode 如何共同占用 GPU

4.1 统一的 token budget

调度器通常需要限制每轮处理的 token 数,例如:

Tprefill+TdecodeTmaxT_{\text{prefill}} + T_{\text{decode}} \leq T_{\max}

这里的 token 是执行预算,不是简单等价的计算量。一个 Prefill token 通常比一个 Decode token 具有更高的计算和内存代价,因此真实执行器可能对两者使用不同权重:

wpTprefill+wdTdecodeCw_p T_{\text{prefill}} + w_d T_{\text{decode}} \leq C

其中 wpw_pwdw_d 是经验或基准测试得到的成本权重,CC 是每轮预算。

仅用“请求数”限制 batch 大小通常不够。例如两个请求:

  • 请求 A:prompt 8,000 token;
  • 请求 B:prompt 20 token。

它们都是两个请求,但 Prefill 的计算、显存写入和 TTFT 影响完全不同。生产调度一般同时考虑:

  • 活跃请求数;
  • 本轮 Prefill token 数;
  • 本轮 Decode token 数;
  • KV Cache 总 token 数;
  • 模型权重和临时激活占用;
  • GPU 显存安全余量。

4.2 Chunked Prefill

如果一个 prompt 很长,一次性 Prefill 会占满整个执行轮次,正在 Decode 的请求就可能长时间得不到下一个 token。Chunked Prefill 将长 prompt 拆成多个片段,例如每次处理 512 或 1,024 个 token。

设请求 A 的 prompt 长度为 2,048,chunk 大小为 512,则 Prefill 可能经历:

第 1 轮:处理 [0, 512)
第 2 轮:处理 [512, 1024)
第 3 轮:处理 [1024, 1536)
第 4 轮:处理 [1536, 2048)

每个 chunk 处理后,KV Cache 增加对应长度。请求在 Prefill 未完成前通常不能进入普通 Decode 阶段,但调度器可以在两个 chunk 之间插入已有请求的 Decode。

需要注意,Prefill chunk 的边界不能破坏因果关系。后一个 chunk 的 token 必须能够访问前面 chunk 已经写入的 KV Cache;因此,chunked Prefill 不是把 prompt 拆成相互独立的批次,而是把同一序列的计算分阶段完成。

5. 一个完整的调度算例

为了说明调度过程,采用一个简化模型:

  • 每轮预算为 4 个“执行 token 单位”;
  • 一个 Decode token 占 1 单位;
  • Prefill 每个 chunk 最多 2 个 token;
  • Prefill 与 Decode 可以在同一轮混合;
  • 新请求只有在它的 Prefill 完成后才能 Decode;
  • 这是用于理解的离散模型,真实 GPU 上 Prefill 和 Decode 的代价并不相同。

三个请求如下:

请求 到达时间 Prompt 长度 最大输出
A 0 4 4
B 1 2 2
C 2 2 1

采用“先推进等待中的 Prefill,再填充 Decode”的简单策略:

轮次 到达请求 本轮动作 结果
0 A A Prefill 2 A 剩余 Prefill 2
1 B A Prefill 2、B 尚未处理 A Prefill 完成
2 C B Prefill 2、A Decode 1 B Prefill 完成,A 输出第 1 个 token
3 C Prefill 2、A Decode 1、B Decode 1 C Prefill 完成,A 输出第 2 个,B 输出第 1 个
4 A Decode 1、B Decode 1、C Decode 1 C 完成,A 输出第 3 个,B 输出第 2 个并完成
5 A Decode 1 A 输出第 4 个并完成

这个例子展示了连续批处理的两个性质:

  1. A 不需要等待 B 和 C 的完整生命周期结束;
  2. B 和 C 加入后,活跃 Decode 集合从 {A} 变为 {A,B},再变为 {A,B,C},最后逐步缩小。

如果采用静态批处理,A、B、C 可能先被放入同一个 batch,并且 Decode 轮次需要按照最长输出长度执行;B 和 C 完成后留下的槽位不能立即服务新请求。连续批处理通过每轮移除已完成请求,减少了这种浪费。

但该策略也有问题:在第 0、1 轮,A 的 Prefill 占用了调度机会;如果不断有长 prompt 到达,并且调度器始终优先 Prefill,已经进入 Decode 的旧请求可能长时间无法获得下一个 token。

6. 调度策略与公平性

6.1 常见调度策略

FCFS(First Come, First Served) 按到达时间服务。它容易解释,也能避免新请求无限插队,但会出现队头阻塞:队头长 prompt 可能阻塞后面的短请求。

优先级调度 给请求或租户设置优先级。它适合区分交互式请求、离线任务和内部维护任务,但如果没有老化机制,低优先级请求可能饥饿。

Shortest Job First 或 SRPT 优先处理预计剩余工作量较小的请求。若工作量估计准确,它通常能降低平均响应时间,但会牺牲长请求的尾延迟。

Token-level round robin 不按请求完成一次服务,而是给每个活跃请求分配有限 token 预算。例如每轮最多为每个请求服务一个 Decode token,再将剩余预算给 Prefill。这能改善 Decode 公平性,但可能降低 GPU 大矩阵操作的效率。

实际系统往往采用混合策略:

  1. 按租户做配额和权重;
  2. 租户内部按优先级或到达时间排序;
  3. 对长 Prefill 做 chunk;
  4. 对 Decode 设置最低服务份额;
  5. 对等待过久的请求增加老化权重;
  6. 预留一部分预算给交互式请求。

6.2 公平性的对象是什么

“公平”不是简单地让每个请求占用相同的 GPU 时间,因为请求的工作量不同。至少需要区分三种公平目标:

请求公平

每个请求都应在合理时间内获得服务。可以监控:

  • 排队时间;
  • 从进入活跃集合到首 token 的等待时间;
  • 最大等待时间;
  • p95、p99 TTFT。

token 公平

让不同请求或租户获得相近的 Decode token 服务份额。对租户 jj,可以定义:

xj=租户 j 获得的 Decode token 数调度窗口内总 Decode token 数x_j = \frac{\text{租户 }j\text{ 获得的 Decode token 数}} {\text{调度窗口内总 Decode token 数}}

再与配置权重 wjw_j 比较。若目标是加权公平,则希望:

xjwjx_j \propto w_j

成本公平

长 prompt 和长输出消耗更多 GPU 资源。仅按请求数限流会让发送长上下文的租户获得更高资源消耗。成本计量至少应记录:

costi=cppi+cdgi+cmKV cache durationi\text{cost}_i = c_p p_i + c_d g_i + c_m \cdot \text{KV cache duration}_i

其中 cpc_pcdc_dcmc_m 是由基准测试或计费策略确定的成本系数。

一个常用的离散公平性指标是 Jain 指数:

J(x1,,xn)=(ixi)2nixi2J(x_1,\ldots,x_n) = \frac{(\sum_i x_i)^2} {n\sum_i x_i^2}

它的值介于 1/n1/n 和 1 之间,越接近 1 表示服务份额越均匀。但它不能单独证明系统公平:如果所有请求都在等待,份额可能很均匀,延迟仍然很差;如果请求需求不同,均匀 token 份额也未必合理。

6.3 公平性与吞吐量不能同时无限优化

假设有两个请求:

  • 请求 A:每次 Decode 的 KV Cache 很长,生成 100 token;
  • 请求 B:上下文很短,生成 2 token。

如果每轮都严格给两者各一个 Decode token,公平性较好,但短请求 B 可能需要等待 A 的大量调度轮次才能完成。另一方面,如果总是优先 B,平均响应时间可能下降,但 A 的等待时间和尾延迟可能无限增加。

更极端的反例是:

  • 每轮都有一个新的短请求到达;
  • 调度器使用严格的短作业优先;
  • 一个长请求已经等待很久。

由于每个新短请求都比长请求“更短”,长请求永远得不到服务。这是饥饿,不是公平调度。

因此,生产策略通常需要把公平性定义成约束,而不是单一目标,例如:

max waiting timeiWmax\text{max waiting time}_i \leq W_{\max}

或:

received servicejαwjeligible service\text{received service}_j \geq \alpha w_j \cdot \text{eligible service}

其中 α\alpha 是最低服务比例。满足最低服务比例后,剩余容量再用于提高吞吐。

7. 吞吐与延迟的形式化关系

7.1 主要指标

TTFT(Time to First Token) 是从请求进入服务端到客户端收到第一个输出 token 的时间:

TTFT=Tqueue+Tprefill+Tfirst decode+Tstream overhead\text{TTFT} = T_{\text{queue}} + T_{\text{prefill}} + T_{\text{first decode}} + T_{\text{stream overhead}}

其中:

  • TqueueT_{\text{queue}}:等待调度的时间;
  • TprefillT_{\text{prefill}}:处理 prompt 的时间;
  • Tfirst decodeT_{\text{first decode}}:得到首个生成 token 的执行时间;
  • 流式开销包括采样、序列化和网络发送。

TPOT(Time Per Output Token) 常指后续输出 token 的平均耗时。也有系统使用 ITL(Inter-Token Latency)表示相邻 token 到达之间的间隔。两者口径可能不同,监控时必须明确是否包含网络和排队。

若输出长度为 NN,近似有:

TE2ETTFT+(N1)×TPOTT_{\text{E2E}} \approx \text{TTFT} + (N-1)\times \text{TPOT}

这个公式不是严格恒等式,因为不同 Decode 轮次的 batch 大小、抢占、网络阻塞和采样时间可能变化,但它能说明:TTFT 主要受 Prefill 和排队影响,长回答的总延迟则对 TPOT 更敏感。

吞吐量 至少有两种:

  • request/s:每秒完成多少请求;
  • token/s:每秒处理或生成多少 token。

LLM 服务更常关注 output token/s,因为一个长请求和一个短请求的资源消耗差异很大。还需要分别监控:

  • Prefill token/s;
  • Decode token/s;
  • 总 token/s;
  • 活跃序列数;
  • GPU 利用率;
  • KV Cache 使用率。

7.2 Little 定律的使用边界

在稳定系统中,Little 定律为:

L=λWL = \lambda W

其中:

  • LL:系统中的平均请求数;
  • λ\lambda:平均到达率;
  • WW:平均响应时间。

因此,当到达率固定时,平均延迟下降通常意味着系统中平均排队和执行的请求数下降;当服务能力提升时,系统可以在相同延迟下承载更高到达率。

但不能把 GPU 利用率直接当作吞吐量。GPU 利用率 99% 可能意味着:

  • GPU 正在高效处理大量 Decode;
  • 一个超长 Prefill 阻塞了所有交互请求;
  • 内存带宽已饱和但有效 token/s 很低;
  • kernel 在处理 padding 或无效工作。

必须将利用率和 token/s、TTFT、TPOT、队列长度一起解释。

8. Prefill 优先还是 Decode 优先

8.1 Prefill 优先的效果

Prefill 优先可以快速处理新请求,降低 TTFT,尤其适合对首 token 敏感的交互式场景。

但长 Prefill 会抢占 GPU 执行预算。已有请求的 Decode 被推迟后,客户端会观察到 token 间隔突然变大,表现为:

首 token 很快
后续 token 不稳定
某些时刻长时间没有新 token

这会使 TPOT 的平均值看起来尚可,但 p95/p99 ITL 很差。

8.2 Decode 优先的效果

Decode 优先可以稳定已开始生成的请求,降低流式输出抖动,提高正在运行请求的服务连续性。

代价是新请求可能长时间拿不到 Prefill,TTFT 增大,等待队列不断增长。若输入请求持续到达,Decode 优先也可能造成 Prefill 饥饿。

8.3 更可行的混合策略

一种常见的抽象策略是:

每轮:
1. 清理完成、取消和失败请求;
2. 为已有 Decode 请求分配最低服务预算;
3. 在剩余预算中处理 Prefill chunk;
4. 如果等待时间超过阈值,提高请求的调度权重;
5. 如果 KV Cache 或显存接近上限,停止接纳新请求;
6. 执行本轮 GPU 计算;
7. 更新 token、KV Cache、指标和客户端流。

这不是某个框架的标准 API,而是调度器应满足的逻辑约束。不同推理引擎对 token budget、分页 KV Cache、chunked Prefill 和抢占的具体实现不同,不能仅凭参数名称推断其语义。

9. KV Cache 是连续批处理的资源边界

9.1 为什么请求完成后必须释放 Cache

Decode 每生成一个 token,就需要追加一组新的 K,VK,V。因此,KV Cache 会随输出长度增长。

如果一个服务有 nn 个活跃请求,单 token KV Cache 占用为 mtokenm_{\text{token}},则仅缓存部分近似为:

MKVmtokeni=1n(pi+gi)M_{\text{KV}} \approx m_{\text{token}} \sum_{i=1}^{n} (p_i + g_i)

当请求完成、取消或失败时,如果不释放对应的缓存块,新的请求即使计算能力充足,也会因为显存不足无法接纳。

9.2 连续批处理通常需要动态内存管理

固定连续内存分配容易受到不同长度请求的影响,产生内部碎片或外部碎片。分页 KV Cache 的常见思路是:

  • 将 KV Cache 切成固定大小的 block;
  • 每个请求维护逻辑 token 到物理 block 的映射;
  • 新 token 到来时按需分配 block;
  • 请求结束后归还 block。

它的好处是不同长度请求可以共享一个物理池,而不是为每个请求分配一段必须连续的显存。

但分页不等于无限内存。调度器仍必须在接纳请求前验证:

Mweights+MKV+Mactivations+Mworkspace<MGPUMmarginM_{\text{weights}} + M_{\text{KV}} + M_{\text{activations}} + M_{\text{workspace}} < M_{\text{GPU}} - M_{\text{margin}}

其中 margin 用于应对临时 kernel 缓冲区、通信和碎片。只按当前已生成长度估算,而不考虑后续最大输出,可能导致请求执行到中途才 OOM。

9.3 OOM 与抢占

当新请求需要更多 KV Cache,而显存不足时,系统可以:

  1. 拒绝新请求;
  2. 延迟接纳;
  3. 暂停某个请求并释放其 KV Cache;
  4. 将状态转移到 CPU 或其他存储;
  5. 之后重新 Prefill 或恢复 Decode。

如果释放缓存后无法保留完整 KV 状态,就必须重新计算被抢占请求的上下文。这样可以提高整体接纳率,但会增加延迟和重复计算。

因此,“支持抢占”不能只看接口是否存在,还需要明确:

  • 抢占粒度是请求、序列还是 KV block;
  • 状态保存在哪里;
  • 恢复时是否重新 Prefill;
  • 抢占成本是否计入租户;
  • 高频抢占是否会造成吞吐下降。

10. 采样、停止和流式输出也属于调度生命周期

GPU 前向通常产生 logits,采样器再根据 temperature、top-k、top-p、重复惩罚等规则选择 token。采样结果会决定请求是继续 Decode 还是结束。

请求结束条件的优先级需要明确。例如:

  1. 若生成 EOS,结束;
  2. 若匹配 stop sequence,通常需要丢弃或不发送 stop sequence 本身;
  3. 若达到最大输出长度,结束;
  4. 若客户端取消,停止继续计算并释放资源。

流式输出还存在一个系统级边界:模型已经生成 token,但网络发送可能阻塞。如果执行器继续为一个慢客户端生成 token,服务会积累未发送的 token 和额外缓存。常见处理是:

  • 对每个连接设置发送缓冲上限;
  • 缓冲超过阈值时暂停该请求;
  • 记录客户端背压时间;
  • 超时后取消请求并释放 KV Cache。

因此,TTFT 和 TPOT 应明确是“模型生成时间”还是“客户端实际收到时间”。对用户体验而言,后者更有意义;对模型性能诊断而言,前者更有意义。

11. 常见错误理解与失败表现

11.1 “连续批处理就是动态增大 batch”

不准确。核心不是 batch 数量变大,而是每个调度迭代都允许请求集合变化。若执行器仍然等待整组请求结束,或者新请求只能等完整 batch 结束,那么它仍然接近静态批处理。

11.2 “Prefill 一定比 Decode 快”

不准确。Prefill 的并行性通常更好,单位 token 吞吐可能更高;但长 prompt 会产生更大的总计算量和 KV 写入。Decode 单轮 token 少,却需要反复读取长 KV Cache,可能受内存带宽限制。

应分别测量:

  • 短 prompt 和长 prompt 的 Prefill 延迟;
  • 不同上下文长度下的 Decode TPOT;
  • 不同并发下的 output token/s;
  • 混合 Prefill/Decode 时的 ITL 尾延迟。

11.3 “GPU 利用率高,所以服务性能好”

错误。GPU 可能在执行无效 padding、超长 Prefill 或频繁小 kernel。若 GPU 利用率升高同时 TTFT、p99 ITL 和拒绝率恶化,通常说明调度或显存已成为瓶颈,而不是系统变得更高效。

11.4 “请求数限制可以防止 OOM”

不够。两个请求可能分别携带 20 和 20,000 个 token。安全准入至少应估算 prompt token、最大输出 token、KV Cache 数据类型和并发增长。

11.5 “优先级高就一定延迟低”

不一定。高优先级请求也可能被一个已经开始的长 Prefill、GPU 排队、KV Cache 不足、网络背压或 worker 故障影响。优先级只能影响调度决策,不能消除已经发射的 GPU kernel 和物理资源限制。

12. 生产诊断:从指标判断瓶颈

12.1 TTFT 高,TPOT 正常

常见原因:

  • 等待队列过长;
  • Prefill chunk 太大;
  • Prefill 优先级过低;
  • 输入 prompt 过长;
  • tokenizer 或请求校验耗时;
  • KV Cache 准入不足;
  • 首轮 GPU kernel 启动或模型加载延迟。

验证方法是分解时间戳:

request_received
tokenization_done
queued
prefill_started
prefill_finished
first_decode_started
first_token_sent

如果 queued -> prefill_started 很长,问题在准入或调度;如果 prefill_started -> prefill_finished 很长,问题在输入长度、模型计算或 chunk 配置;如果模型已生成但 first_token_sent 很晚,则应检查采样、序列化和网络。

12.2 TTFT 正常,TPOT 或 ITL 尾延迟高

优先检查:

  • 长 Prefill 是否频繁插入 Decode;
  • Decode batch 是否过大;
  • KV Cache 访问是否达到带宽瓶颈;
  • 是否存在频繁抢占和重算;
  • 慢客户端是否造成背压;
  • 某些租户是否长期占用调度预算。

平均 TPOT 不能掩盖尾延迟。流式服务应重点观察 p95/p99 inter-token latency,以及单请求在连续两次 token 之间的最大间隔。

12.3 吞吐高但公平性差

典型表现:

  • 总 output token/s 上升;
  • 低优先级租户的最大等待时间持续上升;
  • 某些长请求几乎没有 Decode 服务;
  • 队列中请求年龄不断增大。

这通常说明调度器优化了吞吐,却没有限制饥饿。可以增加最大等待约束、租户最小服务份额或优先级老化,并重新测量吞吐损失,而不是只观察 GPU 利用率。

12.4 逐渐 OOM 而不是立即 OOM

这种情况常由资源释放路径缺失造成:

  • 客户端断开没有触发取消;
  • stop sequence 结束后未归还最后一组 KV block;
  • 异常请求仍留在活跃集合;
  • worker 重试时重复保留旧 Cache;
  • 统计系统与执行器状态不一致。

恢复流程应包括:

  1. 暂停新请求接纳;
  2. 列出执行器认为活跃的请求;
  3. 对照连接、队列和 KV block 分配表;
  4. 释放已结束请求;
  5. 若状态无法可靠修复,重启 worker;
  6. 重启后验证模型权重、KV Cache 池和健康检查;
  7. 逐步恢复流量,而不是立即全量放开。

13. 模型、数据、权限与成本的统一边界

连续批处理位于模型服务层,但它不能脱离生产系统的其他约束。

输入 prompt 可能包含敏感数据,日志不应为了诊断而无条件保存完整 prompt 和生成内容。至少需要区分:

  • 可记录的请求元数据;
  • 脱敏后的 token 长度;
  • 受权限保护的原始输入;
  • 不应持久化的秘密和个人数据。

多租户调度不能只使用一个全局优先级。租户还应有:

  • 最大并发请求数;
  • 最大输入 token 速率;
  • 最大输出 token 速率;
  • KV Cache 或 GPU 时间配额;
  • 单请求最大上下文和输出长度;
  • 超预算后的拒绝、排队或降级策略。

模型评测也应包含服务调度因素。相同模型在不同 Prefill/Decode 策略下,可能出现不同的截断率、超时率和用户可见质量。离线只测 tokens/s,无法发现长 prompt 下 TTFT 爆炸或流式输出抖动。

成本计量应同时记录输入 token、输出 token、GPU 时间和重算成本。发生抢占重算时,如果只按最终输出 token 计费,会低估系统真实资源消耗,也会掩盖导致重算的调度问题。

14. 实现时应明确区分的三类结论

规范保证来自模型或算法定义。例如因果注意力不能读取未来 token;Decode 可以使用已经正确计算的历史 KV Cache;请求达到停止条件后不应继续生成。

常见实现来自推理引擎设计。例如迭代级连续批处理、分页 KV Cache、chunked Prefill、Prefill 与 Decode 混合调度。这些机制广泛存在,但具体参数、抢占方式、缓存布局和 kernel 行为取决于实现与版本。

经验建议来自基准测试和业务目标。例如 Prefill chunk 大小、Decode 最低服务预算、租户权重和显存安全余量都不能脱离硬件、模型和流量分布直接照搬。应使用真实 prompt 长度分布、输出长度分布和连接行为压测验证。

Transformer 的注意力基础可参考论文 Attention Is All You Need;具体模型接口、tokenizer、生成配置和版本行为应以对应的 Hugging Face Transformers 文档 及实际推理引擎文档为准。

连续批处理的本质,是在 GPU 计算迭代边界上动态管理一组具有不同生命周期的自回归序列。Prefill 决定新请求多久看到首 token,Decode 决定已有请求的流式速度,KV Cache 决定系统能同时容纳多少上下文,调度策略决定这些请求如何竞争资源,而公平性约束决定高吞吐是否会以牺牲部分请求为代价。只有把这几个部分放在同一个资源和状态模型中,吞吐、延迟与稳定性之间的取舍才可以被测量和解释。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。