(论文阅读)Sarathi-Serve:LLM 推理服务吞吐量和延迟的平衡

Posted on Jul 23, 2025

背景

在提供LLM推理服务时,优化吞吐量(以降低服务成本)和延迟(以保证用户体验)是两个至关重要的目标 。 一个LLM推理请求通常包含两个阶段 :

  • 预填充(Prefill)阶段:处理用户的完整输入提示(prompt),并生成第一个输出令牌(token)。此阶段并行处理大量输入令牌,计算密集度高,能够有效利用GPU的计算能力 。
  • 解码(Decode)阶段:自回归地逐个生成剩余的输出令牌。每个解码步骤只处理一个令牌,计算量小,主要瓶颈在于内存带宽,导致 GPU 计算单元利用率低 。 为了提高GPU利用率,尤其是在内存密集型的解码阶段,推理系统普遍采用批处理(batching)技术,将多个请求合并处理 。

动机

Prefill vs. Decode

本文首先通过实验发现,Prefill 和 Decode 在计算资源需求上有根本的差别。 批处理(Batching)可以极大地 Decode 阶段的吞吐量,但对本已高效的预填充 Prefill 阶段几乎没有增益 。这里 Prefill 的单条数据包含大量 Token。 一次 Prefill 迭代的执行时间远长于一次 Decode 迭代,并且主要耗时均在线性层计算上 。这种巨大的耗时差异是导致现有系统调度预填充任务时会阻塞解码任务,从而产生高延迟的根本原因。下图通过实验验证了上述结论。

Prefill and Decode Performance

现有系统的缺陷

现有系统在处理 Prefill-Decode 混合负载时,往往面临着吞吐量和延迟之间的艰难权衡 。目前的调度策略可分为两类 :

  • 解码优先(Decode-prioritizing): 以 Faster Transformer 为代表的系统,会等待一批请求中所有任务都完成解码后,才开始处理下一批新的预填充请求 。这种策略能保证解码过程不受干扰,从而获得较低的“令牌间时间”(Time-Between-Tokens, TBT)延迟 。但其吞吐量很低,因为当批次中部分请求提前完成时,系统会以缩小的批次规模继续运行,造成GPU资源浪费 。
  • 预填充优先(Prefill-prioritizing): 以 vLLM 和 Orca 为代表的系统,采用迭代级批处理(Iteration-Level Batching,也被称为 continuous batching),会积极地调度新的预填充请求以尽快加入批次 。这样做可以使后续的解码批次更大,从而提高整体吞吐量 。然而,由于预填充任务的执行时间可能非常长(取决于输入长度),它会中断或延迟正在进行的解码任务,导致一种被称为“生成停滞”(generation stalls)的现象,造成服务的高尾延迟 。

Current LLM Systems

此外,在需要多GPU进行流水线并行(Pipeline Parallelism)部署时,预填充和解码迭代的计算时间差异巨大,会导致严重的“流水线气泡”(Pipeline Bubbles),即GPU空闲等待,进一步降低了系统效率 。尽管一些老方法认为微批处理(micro-batching)可以消除推理中的流水线气泡,但这篇论文通过实验指出,在LLM推理的混合负载下,每个微批次的计算时间差异依然巨大。这是由于批次中混合了两种完全不同的任务:

  • 长预填充(Prefill)任务:处理成百上千个token,耗时很长。
  • 短解码(Decode)任务:一次只处理一个token,耗时很短。 当一个执行时间很长的批次(如包含长预填充)后面跟着一个执行时间很短的批次(如纯解码)时,流水线中的后级 GPU 会很快完成短任务,然后不得不空闲等待前级 GPU 完成那个长任务,这就形成了“气泡”,即 GPU 的无效工作时间。

Pipelines

Sarathi-Serve

本文罗列了三个技术点。

Chunked Prefill

这项技术的核心目标是:在不显著增加延迟的前提下,利用 Decode 批次中闲置的GPU计算能力来处理新请求的 Prefill 任务。其核心思想是将一个长预填充请求分解成多个固定大小的“块”(chunks),并在多个计算迭代中分步完成 。

这项技术的可行性基于两个关键发现:

  • 预填充计算易饱和。即使是一个中等长度的 Prefill 序列(例如 512 个 token),也足以让 GPU 的计算能力达到饱和 。这意味着我们不需要一次性处理数千个 token 才能实现高效计算。
  • 长输入很常见。许多现实世界的工作负载包含非常长的输入提示 。这为我们将长输入分解成多个仍然足够高效的计算块提供了机会 。 因此,通过将一个例如包含2048个令牌的预填充任务,拆分成4个512令牌的块,并在4个连续的迭代中分别执行,每次迭代的计算负载就变得可控且短暂了。

第三个图就展示了 Chunked Prefill 的效果,可以明显看到,混合填充后等待的空闲时间显著变少了。

Stall-free Batching

与 vLLM 和 Orca 等系统会暂停(Stall)正在进行的解码任务,来优先执行新的预填充任务不同 ,Sarathi-Serve 的核心思想是避免停滞。它利用解码迭代中的计算“余量”(Arithmetic Intensity Slack),将新请求的 chunk 搭载在解码任务上一起执行,而不是粗暴地中断解码 。

Sarathi-Serve 的调度过程由一个基于延迟服务等级目标(SLO)计算好的“token 预算”(Token Budget)来控制,这个预算决定了每个批次能处理的最大令牌数 。在每个调度周期中,它都遵循一套优先级机制来构建批次:

  • 优先保障解码:调度器首先将所有正在运行的解码任务打包到下一个批次中 。
  • 继续未完的预填充:其次,它会检查是否有已开始但未完成的预填充任务,并将其下一个块加入批次 。
  • 最后接纳新请求:只有在所有正在进行的任务都被安排好之后,调度器才会考虑接纳新的请求 。它会计算在剩余的 token budget 内,可以容纳新请求多大的一个预填充块,然后将其加入批次

下述算法展示了整个调度的流程:

Scheduling

Token Budget

自然而然的,我们引出一个问题,怎么才能确定合适的 Token Budget?选择 Token Budget 的核心是在 延迟(Latency)和效率(Efficiency)之间找到最佳平衡点。

  • 追求低延迟 -> 倾向小预算:从满足服务等级目标(SLO)的角度看,一个更小的 Token Budget 是更好的。因为这意味着每个批次中包含的 Prefil Token 更少,迭代的执行时间更短,从而能实现更低的 TBT 延迟。
  • 追求高效率 -> 倾向大预算:从系统运行效率的角度看,一个更大的令牌预算是更好的。因为过小的预算会导致长输入被切分成过多的“块”,而太小的计算块可能不足以完全利用 GPU 的计算能力。另外,在处理一个被切分的预填充任务时,后面块的注意力计算需要反复从显存中读取前面块的 KV-Cache,这增加了内存访问开销。

除此之外,GPU 的矩阵乘法(matmul)是以 tile 为单位并行执行的,如果 token 数量不是 tile size 的整数倍,会导致线程块资源浪费。例如,chunk size 为 257 可能比 256 慢 32%。

最后,作者使用一个名为 Vidur 的 LLM 推理 profiler 和模拟器,来一次性离线 profiling 不同 token budget 下的系统表现,最终选择一个在满足 TBT SLO 的前提下,最大化吞吐量的 token budget 值。

实验

本文在 A100 和 A40 GPU 上,对 Mistral-7B、Yi-34B、LLaMA2-70B 和 Falcon-180B 四个模型,分别采用单卡、TP、PP 等配置,使用 openchat_sharegpt4 和 arxiv_summarization 两个真实数据集进行实验

Capacity

上图展示了 Capacity 的比较,其实就是在满足 SLO 要求的延迟情况下的端到端吞吐量。