DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving
这篇论文是北大Xin Jin组和USD合作的,已经被OSDI‘24接收。主要思路是将一个推理请求的prefill和decoding分解进行batch,从而最优化goodput。
摘要
DistServe 通过分解预填充和解码计算来提高大型语言模型 (LLM) 服务的性能。 现有的 LLM 服务系统将两个阶段colocate,并批量计算所有用户和请求的预填充和解码。 我们发现这种策略不仅会导致强烈的预填充解码干扰,而且还会耦合两个阶段的资源分配和并行计划。 LLM 应用程序通常强调每个阶段的单独延迟:预填充阶段的第一个token时间 (TTFT) 和解码阶段每个请求的每个输出token时间 (TPOT)。 在存在严格的延迟要求的情况下,现有系统必须优先考虑一种延迟而不是另一种,或者过度配置计算资源来满足这两种延迟。
DistServe 将预填充和解码计算分配给不同的 GPU,从而消除预填充和解码干扰。 考虑到应用程序的 TTFT 和 TPOT 要求,DistServe 共同优化为每个阶段量身定制的资源分配和并行策略。 DistServe 还根据服务集群的带宽来colocate两个阶段,以最大程度地减少因分解而导致的通信。 因此,DistServe 在每个 GPU 上的 TTFT 和 TPOT 限制内可提供的最大速率方面显着提高了 LLM 服务性能。 我们的评估表明,在各种流行的 LLM、应用程序和延迟要求上,与最先进的系统相比,DistServe 可以服务 4.48 倍以上的请求或 10.2 倍更严格的 SLO,同时将超过 90% 的请求保持在延迟限制内。
Contributions:
- 识别现有 LLM 服务系统中的预填充-解码干扰和资源耦合问题,并建议分解预填充和解码阶段。
- 设计一种新的placement算法来自动选择预填充和解码instances的goodput最佳模式。
- 使用真实的workloads对DistServe做了一个全面的实验验证。
2.Background and Motivation
LLM 服务遵循客户端-服务器架构:客户端将文本序列作为请求提交给服务器; 服务器在 GPU 上托管 LLM,对请求运行推理,并将生成结果响应(或流式传输)回客户端。 如第 1 节中所述,由于独特的预填充解码过程,LLM 服务可能会对 TTFT 和 TPOT 施加激进的服务级别目标 (SLO),具体取决于应用程序的需求。 服务系统必须满足两个 SLO,同时最大限度地降低与昂贵 GPU 相关的成本。 换句话说,我们希望服务系统能够最大限度地提高每秒服务的请求数,并遵循每个配置的 GPU 的 SLO 实现目标——最大限度地提高每个 GPU 的吞吐量。 接下来,我们详细介绍 LLM 推理计算(第 2.1 节)并讨论 LLM 服务的现有优化(第 2.2 节)。
2.1 LLM inference
现代LLMs [32, 43] 在给定输入序列的情况下预测下一个token。 该预测涉及计算序列中每个token的隐藏表示。 LLM 可以采用可变数量的输入token并并行计算其隐藏表示,并且其计算工作量随着并行处理的token数量而超线性增加。 无论输入token数量如何,计算都需要大量 I/O 将 LLM 权重和中间状态从 GPU 的 HBM 移动到 SRAM。 此过程对于不同的输入大小是一致的。
预填充步骤处理新序列,通常包含许多token,并同时处理这些token。 与预填充不同,每个解码步骤仅处理上一步生成的一个新令牌。 这导致两个阶段之间存在显着的计算差异。 当处理不简短的用户prompt时,预填充步骤往往受计算限制。 例如,对于 13B LLM,计算 512 个token序列的预填充会导致 A100 计算受限。 模型越大,将预填充步骤转为计算限制所需的序列就越短(请参阅第 3.1 节)。 相比之下,解码阶段尽管每步仅处理一个新令牌,但会产生与预填充阶段类似水平的 I/O,从而使其受到 GPU 内存带宽的限制。
在这两个阶段中,每个token位置都会生成中间状态,称为 KV cache [28],在后续解码步骤中再次需要这些状态。 为了避免重新计算它们,它们被保存在 GPU memory中。 由于在memory中共享 LLM 权重和 KV 缓存,大多数 LLM 推理引擎选择colocate预填充和解码阶段到GPU,尽管它们具有独特的计算特性。
2.2 LLM Serving Optimization
在实时在线服务中,会出现多个请求,并且必须在 SLO 内提供服务。 批处理和并行化计算是实现 GPU 低延迟、高吞吐量和高利用率的关键。
**Batching.**当前的服务系统 [8,28,45] 使用称为连续批处理的批处理技术。 此方法通过对正在进行的请求进行解码来批量预填充新请求。 它提高了 GPU 利用率并最大限度地提高了整体系统吞吐量——所有用户和请求每秒生成的tokens。 然而,正如第 1 节中提到的以及稍后第 2.3 节中详细阐述的,这种方法会导致 TTFT 和 TPOT 之间的权衡。 连续批处理的高级变体 [8] 试图通过分段预填充和附加解码作业来平衡 TTFT 和 TPOT,以避免超出 GPU 性能限制 - 但本质上,它是用 TTFT 换取 TPOT。 总之,批量预填充和解码总是会导致 TTFT 或 TPOT 的妥协。
Model parallelism. 在LLM服务中,模型并行性通常分为算子内并行性和算子间并行性[29,39,50]。 两者都可以用于支持更大的模型,但可能会以不同的方式影响服务性能。 算子内并行性将计算密集型算子(例如矩阵乘法)划分到多个 GPU 上,从而加速计算,但会导致大量通信。 它减少了执行时间,从而减少了延迟,特别是对于预填充阶段的 TTFT,但需要 GPU 之间的高带宽连接(例如 NVLink)。 运算符间并行性将 LLM 层组织为多个阶段,每个阶段在 GPU 上运行以形成管道。 由于级间通信,它会适度增加执行时间,但会随着每个添加的 GPU 线性扩展系统的速率容量。 在本文中,我们揭示了模型并行性的另一个好处:通过缩短执行时间来减少预填充和解码阶段的排队延迟。 我们将在第 3 节中进一步探讨这一点。 除了模型并行性之外,复制模型实例(无论其模型并行性配置如何)都会线性扩展系统的速率容量。
这些并行策略创建了一个复杂的优化空间,需要根据应用程序的延迟要求进行仔细权衡。
2.3 Problems and Opportunities
与现有系统一样,对预填充和解码计算进行colocate和批处理以最大化整体系统吞吐量,这对于服务提供商来说是具有成本效益的。 然而,在存在 SLO 的情况下,由于下面讨论的问题,当前的方法很难保持高服务质量和低成本。
Prefill-decoding interference. 如图 2 所示,向一批解码请求添加单个预填充作业会显着减慢这两个过程,从而导致 TTFT 和 TPOT 显着增加。 具体来说,批次中的解码任务必须等待更长的预填充作业完成,从而延长了TPOT; 随着预填充时间的延长,减速会加剧,如图 2(b) 所示。 将解码作业添加到预填充还会增加完成预填充任务的时间,特别是当 GPU 已满负荷时(图 2 蓝色曲线)。
Ineffective scheduling. 取消批处理预填充和解码作业并按顺序调度它们并不能减轻干扰。 由于等待 GPU 上正在进行的预填充作业,解码作业可能会遇到较长的排队延迟。 此外,专用于解码的批次通常会导致 GPU 利用率不足。 在任一阶段对任务进行优先级排序都会对另一阶段的延迟产生不利影响,从而导致优先级调度无效。
Resource and Parallelism coupling. 在同一 GPU 上并置预填充和解码阶段不可避免地会共享其资源和并行设置。 然而,每个阶段都有其独特的计算特性和延迟要求,需要更异构的资源分配。 例如,预填充阶段受益于更多 GPU 和操作内并行性,可减少执行时间,以满足 TTFT 上严格的 SLO。 与预填充相比,解码阶段可以使用更少的 GPU 来处理更高的速率,并且其最佳并行配置取决于运行批量大小。 在现有系统中,由于耦合,资源分配和并行计划是为了满足TTFT和TPOT的更高要求而定制的,这对于另一个系统可能并不理想。 这通常会导致资源过度配置以满足两个 SLO。
Opportunities. 为了解决这些问题,我们建议分解预填充和解码阶段。 我们使用术语“实例”来表示精确管理模型权重的一个完整副本的资源单元。 应用模型并行性时,一个实例可以对应多个GPU。 请注意,当我们将两个阶段分解到不同的 GPU 时,每个阶段都会管理其模型权重的副本,从而产生预填充实例和解码实例。 预填充实例在接收到请求后,仅对该请求执行预填充计算以生成第一输出token。 然后将中间结果(主要是KV缓存)发送到解码实例,解码实例负责后续的解码步骤。 由于解码计算的 GPU 利用率通常较低,因此我们可以为每个解码实例分配多个预填充实例。 这允许批处理更多解码作业以实现更高的 GPU 利用率。
分解预填充和解码自然地解决了两个阶段之间的干扰,并使每个阶段能够专注于其优化目标——TTFT 或 TPOT。 每种类型的实例可以采用不同的资源和并行策略来满足各种延迟要求。 通过调整为两种类型的实例提供的 GPU 数量和并行度,我们可以最大限度地提高整个系统的每设备吞吐量,避免过度配置,最终降低每次查询的成本,同时保证服务质量。 接下来,我们制定方法来找出每个阶段的最佳资源分配和并行计划。
3. Tradeoff Analysis
分解将两个阶段分开,并允许对每个阶段的特征进行不同的分析,从而为算法设计提供有价值的见解。 它还扩展了设计空间:现在每个阶段都需要根据其延迟要求独立扩展和调度。 在本节中,我们分析预填充(§3.1)和解码实例(§3.2)分解后的计算模式。 我们的目标是确定关键参数并得出每个阶段的批处理和并行性指南。 然后,我们重点介绍几个实际部署注意事项(第 3.3 节)。 本节为每个 GPU 的吞吐量优化奠定了基础。
3.1 Analysis for Prefill Instance
分解后,预填充阶段通过并行处理用户prompt的所有tokens来生成第一个token。 假设给定的到达率,我们的目标是使用最少的资源满足 TTFT 服务的延迟要求。
Batching strategy. 预填充步骤通常是计算密集型的。 图 3(a) 显示了预填充阶段的吞吐量如何随输入长度和批量大小变化。 对于 13B 参数的 LLM,处理单个 512 个 token 的序列可以充分利用 A100 GPU; 较大的模型需要较短的序列才能达到 GPU 饱和。 一旦 GPU 受到计算限制,向批次添加更多请求将不再提高 GPU 效率。 相反,它会按比例延长批处理的总处理时间,从而无意中延迟了所有包含的请求。 因此,对于预填充实例,有必要提前分析特定的 LLM 和 GPU,以确定关键输入长度阈值(表示为 Lm),超过该阈值,预填充阶段将受到计算限制。仅当计划请求的输入长度低于 Lm 时才应考虑批量处理更多请求。 在实践中,用户prompt通常平均超过数百个tokens [7]。 预填充实例的批量大小通常保持较小。
Parallelism Plan. 为了研究仅预填充实例的并行性偏好,我们在两个 A100 GPU 上使用操作间或操作内并行策略to serve 66B LLM。 为了简化问题,我们假设统一请求输入长度为 512 个tokens,并且采用泊松到达过程。 我们在图 4(a) 中比较了不同到达率下得到的平均 TTFT:操作内并行性在较低到达率时更有效,而操作间并行性随着到达率的增加而获得优势。 分解使预填充阶段的功能类似于 M/D/1 队列,因此我们可以使用排队论来验证观察结果。
我们首先使用没有并行性的单设备情况来开发符号:每个请求的执行时间(表示为 D)由于统一的预填充长度而保持不变。 由于一个请求会使 GPU 饱和,因此我们通过先来先服务 (FCFS) 来调度请求,而不进行批处理。 假设泊松到达率为R,使用条件RD < 1,平均TTFT (Avg_T T FT )可以由M/D/1队列[40]以封闭形式建模:
其中第一项代表执行时间,第二项对应排队延迟。 基于等式1,我们引入并行性。
此外,作者还对操作间并行和操作内并行的TTFT进行了建模,不做赘述。
3.2 Analysis for Decoding Instance
与预填充实例不同,解码实例遵循独特的计算模式:它接收中间状态(KV 缓存)和来自预填充实例的第一个token,并一次生成一个后续token。 对于解码实例,我们的优化目标是使用最少的计算资源满足应用程序的 TPOT 要求。
Batching strategy. 由于单个解码作业严重限制带宽,因此批处理是避免 GPU 利用率低(因此每 GPU 有效吞吐量高)的关键。 在预填充和解码阶段位于同一位置的现有系统中,增加解码批量大小很困难,因为这与满足延迟目标相冲突,特别是在请求率较高的情况下。 这是因为共享 GPU 会导致预填充和解码作业之间的竞争,从而导致 TTFT 和 TPOT 之间的权衡。 例如,较高的到达率会产生更多的预填充作业,如果优先考虑预填充作业,则需要更多的 GPU 时间来满足 TTFT 要求,这反过来会对 TPOT 产生不利影响。
相反,分解提供了一种解决方案,可以将多个预填充实例分配给单个解码实例。 这种方法允许在解码阶段在专用 GPU 上积累更大的批量大小,而无需牺牲 TPOT。
Parallelism Plan. 分解后,解码的批量大小可能会受到 GPU 内存容量的限制,因为有必要为所有活动请求维护 KV 缓存。 通过模型并行性扩展解码实例或利用 LLM KV 缓存的高级内存管理技术(例如 Paged-Attention [28] 和 GQA [9]),可以进一步将解码批量大小扩展到接近计算极限。 随着解码批量大小继续增加以接近计算界限,解码计算开始类似于预填充阶段。 通过这一观察,我们研究了在大批量条件下不同并行度下的延迟和吞吐量如何变化,如图 5 所示:操作内并行性减少了延迟,而收益递减,这是由通信和分区后利用率降低引起的。 操作间并行性几乎可以线性扩展吞吐量。 因此,当 TPOT SLO 很严格时,操作内并行性对于减少 TPOT 以满足延迟目标至关重要。 除此之外,操作间并行性更适合线性提高吞吐量。
值得注意的是,当模型可以装入单个 GPU 的内存时,除了预填充和解码实例的模型并行性之外,复制也是一种有竞争力的选择,以线性扩展系统的速率容量。 它还可以减少排队延迟——如方程式所示。 1 – 用 R/N 替换 R,假设请求被平均分派到 N 个副本,代价是在 GPU 内存中维护模型权重的额外副本。
3.3 Practical Problems
我们制定了为每个阶段选择批处理和并行性的基本原则。 在本节中,我们讨论并解决在分解预填充和解码阶段的实际部署过程中遇到的几个挑战。
Variable prefill length. §3 假定跨请求的prompt长度是统一的。 在实际部署中,根据LLM应用程序的不同,请求的长度是不统一的。 对于应用操作间并行性的预填充实例,非均匀性可能会导致pipeline bubbles [25, 31],因为跨不同长度的请求的pipeline stages的执行时间会有所不同。 这与使用M/D/1队列模型得出的结论略有偏差。 为了解决这个问题,第 4 节开发了根据工作负载搜索并行性的算法,并通过调度来最大限度地减少bubbles(第 4.3 节)。
Communication overhead. 将 KV 缓存从预填充转移到解码实例会产生显着的开销。 例如,OPT-66B 上单个 512 个令牌请求的 KV 缓存大小约为 1.13GB。 假设平均到达率为每秒 10 个请求,我们需要传输 1.13 × 10 = 11.3 GB 数据,或相当于 90Gbps 带宽,以使开销不可见。 KV 缓存的大小随着平均输入长度和到达率的增加而增加。 虽然许多用于LLMs的现代 GPU 集群都配备了 Infiniband(例如 800 Gbps),但在跨节点带宽有限的情况下,分解依赖于常用的节点内 NVLINK,其中 A100 GPU 之间的峰值带宽为 600 GB/ s,再次使传输开销可以忽略不计(参见第 6.3 节)。 然而,这一要求对预填充和解码实例的放置施加了额外的限制,我们将在下一节中考虑这些限制。
通过本节的分析,我们将工作负载模式、placement约束、SLO 要求、并行策略和资源分配确定为关键参数,这些参数在设计分类服务系统时创建了一个考虑因素网络。 如何自动导航搜索空间以找到实现最佳每 GPU 吞吐量的配置是一项挑战,接下来要解决。
4. Method
我们构建 DistServe 来解决上述挑战。 给定模型、工作负载特征、延迟要求和 SLO 达到目标,DistServe 将确定 (a) 预填充和解码实例的并行策略,(b) 要部署的每种实例类型的数量,以及 © 如何部署 将它们放置到物理集群上。 我们将该解决方案称为展示位置。 我们的目标是找到一个能够最大化每 GPU 吞吐量的布局。
如第 3.3 节中所述,一个关键的设计考虑因素是在给定不同的集群设置的情况下管理分解的预填充和解码阶段之间的通信。 在本节中,我们首先介绍两种放置算法:一种适用于具有高速跨节点网络的集群(第 4.1 节),另一种适用于缺乏此类基础设施的环境(第 4.2 节); 后者引入了额外的限制。 然后,我们开发适应现实工作负载细微差别的在线调度优化(第 4.3 节)。
4.1 placement for High Node-affinity Cluster
在配备Infiniband的高节点亲和性集群上,KV缓存跨节点的传输开销可以忽略不计,DistServe可以不受约束地跨任意两个节点高效地部署预填充和解码实例。 我们针对此类场景提出了一种两级放置算法:首先分别优化预填充和解码实例的并行配置,以获得阶段级最佳每 GPU 吞吐量; 然后,我们使用复制来匹配总体流量速率。
然而,由于缺乏简单的分析公式来计算 SLO 达到情况(即满足 TTFT 要求的请求的百分比),为单个实例类型(例如预填充实例)找到最佳并行配置仍然具有挑战性, 考虑到工作负载具有不同的输入、输出长度和不规则的到达模式。 通过真实测试台分析来衡量 SLO 非常耗时。 因此,假设事先了解工作负载的到达过程以及输入和输出长度分布,我们求助于构建模拟器来估计 SLO 达到情况。 尽管短期间隔无法预测,但较长时间尺度(例如,小时或天)的工作负载模式通常是可预测的 [29, 46]。 DistServe 根据历史请求跟踪拟合分布,并从分布中重新采样新跟踪,作为模拟器的输入工作负载来计算 SLO 达到情况。 接下来,DistServe 只需通过二分搜索枚举展示位置,并通过模拟试验找到满足 SLO 达到目标的最大速率。
算法 1 概述了该过程。 我们针对预填充和解码实例枚举了所有可行的并行配置,但受到集群容量限制。 例如,对于特定的预填充阶段配置,我们使用 simu_prefill 来模拟并找到其最大goodput(与使用 simu_decode 进行解码类似)。 在确定预填充和解码实例的最佳并行配置后,我们根据goodput复制它们以实现用户所需的总体流量速率。
算法 1 的复杂度为
O
(
N
M
2
)
O(NM^2)
O(NM2),其中 N 为每个实例的节点限制,M 代表现代集群中每个节点的 GPU 典型数量(例如 8)。 搜索空间是可管理的,并且在我们最大的设置中求解时间不到 1.3 分钟,如第 6.5 节所示。
模拟器构建。 算法 1 依靠模拟器来估计各种 SLO 下的吞吐量以及给定工作负载和并行计划的 SLO 实现目标。 为了构建准确的模拟器,我们分别分析预填充和解码阶段的 FLOP 和内存访问次数,并使用延迟模型来近似推理执行时间。 请参阅附录 A 中的详细信息。由于 DNN 工作负载 [20, 29] 的高可预测性(在第 6.4 节中进行了验证),模拟器与实际分析结果非常吻合。
到目前为止,我们已经开发了算法1,假设我们可以将预填充和解码放在集群的任意两个节点之间,并且KV缓存传输利用高带宽。 在许多实际集群中,节点内的 GPU 访问高带宽 NVLINK,而分布在节点之间的 GPU 的带宽有限。 接下来我们开发一种算法来解决这个约束。
4.2 Placement for Low Node-affinity Cluster
一个简单的解决方案是始终将预填充和解码实例并置在同一节点上,利用 NVLINK(GPU 节点内通常可用)。 对于大型模型,例如 使用 175B 参数 (350GB),我们甚至可能无法在 8-GPU 节点 (80G × 8 = 640G < 350 × 2GB) 中托管一对预填充和解码实例。 我们将其合并为额外的放置约束,并通过算法 2 中介绍的模型并行性对其进行共同优化。
关键的见解是中间状态传输仅发生在预填充和解码实例的相应层之间。 利用互操作并行性,我们将层分组为阶段,并将每个实例划分为段,称为实例段,每个段维护一个特定的互操作阶段。 通过在单个节点内并置同一阶段的预填充和解码段,我们强制中间状态的传输仅通过 NVLINK 发生。 在节点内部,我们为同一实例的段设置相同的并行性和资源分配。 鉴于每个节点 GPU 的典型限制(通常为 8 个),我们可以枚举一个节点内可能的配置,并使用模拟器来识别可产生最佳吞吐量的配置。 如算法 2 中所述,我们首先枚举互操作并行度来获取所有可能的实例段。 对于每个段,我们通过调用 get_intra_node_configs 获取所有可能的节点内配置。 然后我们使用模拟来找到最佳的并复制它以满足目标流量速率。
4.3 Online Scheduling
DistServe 的运行时架构如图 6 所示。DistServe 采用简单的 FCFS 调度策略运行。 所有传入请求到达集中控制器,然后分派到具有最短队列的预填充实例进行预填充处理,然后分派到负载最少的解码实例进行解码步骤。 此设置虽然简单,但却针对实际工作负载的细微差别进行了多项关键增强功能的优化。
Reduce pipeline bubbles。 为了减轻由不一致的prompt长度(第 3.3 节)引起的pipeline bubbles,我们以平衡pipeline中所有批次的执行时间的方式安排请求。 这是通过注意到对于预填充和解码实例而言,批次中新token的数量是批次实际执行时间的可靠指标来实现的。 对于预填充实例,我们分析目标模型和 GPU,以计算出使 GPU 饱和所需的最短提示长度
L
m
L_m
Lm。 我们通过批处理多个短于
L
m
L_m
Lm 的请求或单独调度长于
L
m
L_m
Lm 的请求来调度总序列长度接近
L
m
L_m
Lm 的预填充批次。 对于解码实例,我们将
L
m
L_m
Lm 设置为最大批量大小。
Combat busrtiness。 工作负载的突发可能会导致大量 KV 缓存从预填充转移到解码实例,从而导致解码实例存在内存过载的风险。 为了解决这个问题,DistServe 采用“拉”的方式进行 KV 缓存传输,而不是“推”的方式——解码实例根据需要从预填充实例中获取 KV 缓存,使用预填充实例的 GPU memory作为排队缓冲区。 因此,每种类型的实例都按照自己的节奏运行,无需复杂的协调。
Replacing. DistServe 中的资源和并行计划针对特定工作负载模式进行了优化,如果工作负载模式随时间变化,则可能会变得次优。 DistServe 实施定期重新规划。 工作负载分析器监视关键参数,例如请求的平均输入和输出长度、平均到达率等。如果检测到显着的模式转变,DistServe 将根据最近的历史数据触发重新运行放置算法。 这个过程很方便——所提出的算法在几秒钟内运行(第 6.5 节),并且重新加载 LLM 权重可以在几分钟内完成——远远短于现实世界工作负载变化往往发生的每小时规模。
DistServe 没有实现抢占 [23] 和容错 [49] 等高级运行时策略,这些策略是对分解的补充。 尽管如此,我们还是讨论了它们如何融入 DistServe。 在 DistServe 中,FCFS 策略可能会导致“护航效应”,即较长的请求会在预填充阶段阻塞较短的请求。 正如现有文献[44]中所建议的那样,结合抢占的策略可以提高效率,并且在我们的系统架构中是可行的。 虽然容错不是当前 DistServe 的主要关注点,但容错是需要考虑的一个关键方面。 在传统的基于主机托管和复制的系统中,一个实例中的故障通常不会中断其他副本实例。 然而,在DistServe中,预填充和解码实例之间的依赖关系引入了故障传播的风险。 例如,映射到多个预填充实例的单个解码实例中的故障可能会破坏整个服务和集群。 我们将两者留作未来的工作。