AI 基建
0%
第五部分 · 推断与服务 · 第 31 章

服务问题

作者Changkun Ou
阅读时长约 12 分钟

模型检查点不会决定请求何时运行、可以占用多少内存,也不会决定下一个该处理哪个请求。服务系统必须在提示词长度、输出长度和截止时间各不相同的请求陆续到达时,作出这些决定。它既要创建模型状态,并在生成词元的间隙保留这些状态,又要让多个用户共享有限的加速器,还要判断何时继续接纳任务会让已有请求逾期。

本章仅讨论解码器式自回归 Transformer。为这类模型提供服务,本质上是一个在线资源分配问题。预填充(prefill) 阶段处理提示词并创建注意力状态,解码(decode) 阶段则反复读取这些状态,每次生成一个新词元。这两个阶段通常以不同方式使用硬件,但任何一个阶段都不存在普遍适用的瓶颈。真正有用的目标是有效吞吐量(goodput),也就是满足明确延迟约定的已完成请求数;衡量它时,还必须同时报告接纳率和成本。

测量请求的完整生命周期

端点确定之后,延迟数值才有意义。在下面的公式中,aia_i 表示请求 ii 到达服务器的时刻,ti,jt_{i,j} 表示该请求的第 jj 个输出词元离开服务器的时刻,OiO_i 表示输出词元数。对于至少输出两个词元的请求,有

TTFTi=ti,1ai,TPOTi=ti,Oiti,1Oi1,E2Ei=ti,Oiai.\begin{gathered} \operatorname{TTFT}_i=t_{i,1}-a_i, \\ \operatorname{TPOT}_i= \frac{t_{i,O_i}-t_{i,1}}{O_i-1}, \\ \operatorname{E2E}_i=t_{i,O_i}-a_i. \end{gathered}

第一个词元之后,每两个相邻词元之间都有一个间隔,可以用下面这个式子表示:

ITLi,j=ti,jti,j1,E2Ei=TTFTi+j=2OiITLi,j.\begin{gathered} \operatorname{ITL}_{i,j}=t_{i,j}-t_{i,j-1}, \\ \operatorname{E2E}_i = \operatorname{TTFT}_i +\sum_{j=2}^{O_i}\operatorname{ITL}_{i,j}. \end{gathered}

首词元时间(TTFT)包括服务器在发出第一个词元之前产生的全部延迟。词元间延迟(ITL)是某两个相邻词元之间实际观测到的间隔;每输出词元时间(TPOT)则是一个请求中这些间隔的平均值。端到端延迟(E2E)到最后一个词元发出为止。只输出一个词元时,TPOT 没有定义。ITL 的分布能暴露平均值掩盖的停顿。上述定义以服务器为边界,客户端测得的延迟还包括网络传输和客户端缓冲。

TTFT 并不等于“预填充时间”。它可能包含接纳延迟、排队、分词、前缀缓存查询、预填充、调度间隙和第一次采样。TPOT 除了模型执行,还可能包含调度器延迟和其他请求带来的干扰。应按请求类别分别报告百分位数,因为短提示词的交互请求与长时间运行的离线生成,需要遵守的截止时间并不相同。

请求路径把这些延迟逐一呈现出来:

flowchart TD
    A[请求到达服务器] --> B[接纳与排队]
    B --> C[预留状态并预填充提示词]
    C --> D[采样并流式发送第一个词元]
    D --> E{是否结束?}
    E -->|否| F[调度器选择一次解码步骤]
    F --> G[追加词元并扩展 KV 状态]
    G --> E
    E -->|是| H[KV 管理器释放状态]
    H --> I[请求完成]
图 31.1. 一个服务请求依次经过接纳、预填充和多轮解码。调度器与 KV 内存管理器既会影响预填充之前的排队,也会影响此后每个词元的间隔。

吞吐量也必须注明单位。请求吞吐量统计每秒完成的请求数,词元吞吐量统计每秒处理的提示词词元或输出词元数。当请求长度不同时,系统可能改善其中一个指标,却损害另一个指标。对于时长为 Δ\Delta 的观测区间,一种可用于运维的请求有效吞吐量定义是

GΔ=1ΔiCΔ1[TTFTiSifirst]1[TPOTiSitoken].G_{\Delta} = \frac{1}{\Delta} \sum_{i\in\mathcal C_{\Delta}} \mathbf 1[\operatorname{TTFT}_i\le S_i^{\mathrm{first}}] \mathbf 1[\operatorname{TPOT}_i\le S_i^{\mathrm{token}}].

这里,CΔ\mathcal C_{\Delta} 是该区间内完成的请求集合,SifirstS_i^{\mathrm{first}}SitokenS_i^{\mathrm{token}} 分别是请求的 TTFT 与 TPOT 服务等级目标(SLO),条件成立时 1[]\mathbf 1[\cdot] 取 1,否则取 0。DistServe 采用另一种同样有用的定义:在选定比例的请求(例如 90%)同时满足两个阶段 SLO 的前提下,系统能够承受的最大到达率 (Zhong et al. 2024)。只输出一个词元的请求可以只采用 TTFT 或 E2E 约定,不设 TPOT 条件。无论采用哪种定义,有效吞吐量都必须与输入负载、接纳率和拒绝率一起报告。系统完全可以通过拒绝难处理的请求,制造出看似出色的延迟。

预填充与解码处于不同运行区间

预填充会处理提示词中的所有位置,并生成后续注意力所需的键和值。它的矩阵运算能在提示词的不同位置之间展开并行,通常具有更高的算术强度。解码则为每条活跃序列计算一个新位置。批较小时,每一步相对于必须读取的模型权重和缓存字节,只完成很少的计算,因此常常受到内存带宽限制。把多个解码请求组成一个批,可以让已经读入的权重承担更多计算,使这一步逐渐接近算力饱和 (Pope et al. 2023; Yuan et al. 2024)。

roofline 模型描述的是判断条件,而不是一句口号。若一项操作需要 FF 次浮点运算并搬运 DD 字节数据,那么它的执行时间下界为

τmax ⁣(FPmax,DBmax),I=FD.\begin{gathered} \tau \ge \max\!\left( \frac{F}{P_{\max}}, \frac{D}{B_{\max}} \right), \\ I=\frac{F}{D}. \end{gathered}

其中,τ\tau 是执行时间,PmaxP_{\max} 是峰值算术吞吐量,BmaxB_{\max} 是峰值内存带宽,II 是算术强度。在这个简化模型中,I<Pmax/BmaxI<P_{\max}/B_{\max} 时操作受带宽限制,反向不等式成立时则受算力限制。模型形状、序列长度、批的组成、量化、内核、并行方式和硬件,都会改变 FFDD,或改变系统实际能够达到的两个峰值比例。长提示词的预填充与小批量解码只是常见运行区间,而不是自然定律。

这种差异解释了批处理最基本的取舍。增加解码序列数,可以摊薄权重读取成本并提高词元吞吐量;但更大的批也需要更长的执行时间、占用更多状态内存,还可能让每条序列等待更久才能轮到下一步。因此,最佳批并不是原始吞吐量最高的批,而是测得的负载下,仍能维持目标延迟分布的最大可行批。

KV 状态让内存成为接纳约束

在第 tt 个解码步骤,注意力需要读取所有保留位置对应的键和值。保存这些状态,可以避免每生成一个词元都重新计算整段前缀。对于未经分片的标准缓存,请求 ii 的逻辑 KV 内存为

mi=2LnkvdheadbkvTi,MKV=iRmi.\begin{gathered} m_i = 2L\,n_{\mathrm{kv}}\,d_{\mathrm{head}}\,b_{\mathrm{kv}}\,T_i, \\ M_{\mathrm{KV}}=\sum_{i\in\mathcal R}m_i. \end{gathered}

这里,LL 是 Transformer 层数,nkvn_{\mathrm{kv}} 是每层的键值头数,dheadd_{\mathrm{head}} 是每个头的维度,bkvb_{\mathrm{kv}} 是每个缓存元素的字节数,TiT_i 是请求 ii 中需要保留的提示词和已生成词元数,R\mathcal R 是常驻请求的集合,因子二代表键和值两类状态。总内存还必须为其他分配留出空间:

Mused=Mweights+MKV+Mworkspace+Mreserve,MusedMdevice.\begin{aligned} M_{\mathrm{used}} &=M_{\mathrm{weights}}+M_{\mathrm{KV}}\\ &\quad+M_{\mathrm{workspace}}+M_{\mathrm{reserve}},\\ M_{\mathrm{used}}&\le M_{\mathrm{device}}. \end{aligned}

等式左侧四项依次是模型权重、常驻 KV 状态、内核与通信的临时工作区,以及安全预留空间。它们的总和为 MusedM_{\mathrm{used}},不能超过加速器的可用内存 MdeviceM_{\mathrm{device}}。这个公式描述的是前缀共享之前的逻辑字节数。共享物理前缀块时,每个唯一块只计算一次。张量并行可能切分或复制 KV 头,因此每台设备上的内存占用还取决于并行布局,而不只取决于模型架构。

图 31.2. 合成的 KV 容量计算器。计算假设每个头有 128 个维度、每个缓存元素占两个字节、批内各序列的上下文长度相同,并且没有采用张量并行切分。图中的数值来自上面的公式,并非基准测试结果。

在这些符号中,架构决定 LLnkvn_{\mathrm{kv}}dheadd_{\mathrm{head}},服务层仍能控制缓存精度、接纳哪些序列、如何分块、是否共享前缀、何时逐出状态,以及怎样安排并行。因此,KV 缓存通常是接纳约束,但不一定总是最先触顶的约束。序列较短时,模型权重可能占据大部分内存;KV 容量耗尽之前,也可能先受到算力、内存带宽、网络传输或排队的限制。

与普通多头注意力相比,多查询注意力和分组查询注意力会减少 nkvn_{\mathrm{kv}},缓存量化则会减少 bkvb_{\mathrm{kv}}。两者都能改变容量公式,但也可能改变准确率或内核行为。第 8 章 定义模型会产生哪些状态,第 32 章 则会详细说明这些状态的分配和逐出机制。

迭代调度消除静态批处理的空闲时间

自回归请求会在不同数量的解码步骤后结束。静态批始终受其中最长的请求牵制:短请求结束后留下的槽位处于空闲状态,新到的任务也只能等待。Orca 引入了迭代级调度。每轮模型迭代结束后,服务器都可以移除已完成的请求,再从仍在运行和正在等待的任务中组成下一批 (Yu et al. 2022)。现代系统通常把这种做法称为连续批处理(continuous batching)

下面的可运行示例只呈现这种调度方式带来的差异。四个请求都在零时刻就绪,系统有两个槽位,输出长度分别为 [2, 8, 3, 7] 个解码步骤。示例忽略预填充、内存限制、新请求到达和内核成本,因此只是一个记账示例,而不是性能模型。

lengths = [2, 8, 3, 7]
slots = 2

static_steps = sum(
    max(lengths[start:start + slots])
    for start in range(0, len(lengths), slots)
)

waiting = list(lengths)
running = []
continuous_steps = 0
active_slot_steps = 0

while waiting or running:
    while waiting and len(running) < slots:
        running.append(waiting.pop(0))
    active_slot_steps += len(running)
    running = [remaining - 1 for remaining in running if remaining > 1]
    continuous_steps += 1

tokens = sum(lengths)
print("static steps:", static_steps)
print("continuous steps:", continuous_steps)
print("static slot utilization:", f"{tokens / (static_steps * slots):.1%}")
print("continuous slot utilization:", f"{active_slot_steps / (continuous_steps * slots):.1%}")

连续批处理并不会减少生成这些词元所需的模型计算,它改变的是空闲槽位何时可以重新使用。真实的调度器还必须决定接纳哪个等待中的请求、为未来增长预留多少 KV 内存、优先处理预填充还是解码,以及何时抢占正在运行的任务。调度策略属于服务约定的一部分,不是藏在约定背后的实现细节。

调度器必须保证什么

生产环境中的每轮调度都需要明确的执行顺序:

  1. 清理已完成或已取消的请求,并释放其状态。
  2. 拒绝或推迟不符合当前策略或资源限制的请求。
  3. 在词元数、内存、延迟和公平性预算内,选择要运行的预填充分块与解码步骤。
  4. 启动模型计算之前预留全部所需的 KV 块。
  5. 执行选定的批,然后提交生成的词元、时间戳和缓存状态。

这个顺序保护三项不变量:已分配的内存绝不超过内存池容量;正在运行的请求所引用的块绝不会被释放或重新分配;尚未预留下一状态的请求不得执行。如果当前没有任何批可以运行,调度器应明确说明它是在有意等待、拒绝任务,还是被某项资源阻塞。无声重试会把接纳失败变成没有上限的排队。

五种机制分别消除不同的浪费

多种服务技术常被笼统地称为加速手段,但它们作用于不同资源,也会引入不同成本。

机制 消除的浪费或干扰 新增成本或限制 主要作用
连续批处理 已完成请求继续占用槽位 逐轮调度与形状不规整的批 更早复用执行容量
PagedAttention 分页注意力(PagedAttention) 连续内存预留与缓存碎片 块表与未填满的最后一个块 接纳更多常驻序列
前缀复用 重复计算相同的词元前缀 查询、缓存容量、身份识别和失效规则 减少重复预填充计算
分块预填充 一段长预填充独占整轮迭代 更多调度决策,以及可能更小的内核 接纳提示词时限制解码停顿
预填充与解码分离 两阶段相互干扰,资源规模彼此绑定 KV 传输、额外队列与网络容量 让两个阶段使用独立资源

PagedAttention 把类似虚拟内存的分块机制用于 KV 存储。一个请求的逻辑序列可以映射到不连续的物理块,并在序列增长时继续分配。这种方法大幅减少预留和碎片造成的浪费,也支持块共享,但并不会减少每个独有词元所需的逻辑字节数。在 vLLM 论文评估的负载中,这种内存管理在延迟相近时,吞吐量达到对比系统的二至四倍 (Kwon et al. 2023)。这个结果只适用于当时测试的模型、负载和基线,不能归因于分页机制本身。

只有服务器能在兼容的模型与缓存配置下复用完全相同的词元前缀,前缀复用才会省去计算。SGLang 的 RadixAttention 用基数树保存可复用前缀,并采用缓存感知的逐出策略 (Zheng et al. 2024)。复用需要明确的身份识别和隔离规则。模型版本、适配器、缓存格式、位置处理方式和租户策略,都可能使词元序列虽然相同,已有状态却不能复用。

分块预填充与阶段分离以不同方式处理两个阶段之间的干扰。Sarathi-Serve 把长预填充分成多个块,与正在进行的解码合并执行,从而限制解码任务被一个提示词阻塞的时间 (Agrawal et al. 2024)。DistServe 把预填充与解码分配到不同的 GPU 池,并为每个阶段分别选择资源和并行方式 (Zhong et al. 2024)。阶段分离消除了同机内核之间的干扰,却不会让两个阶段真正独立:解码池必须等到 KV 状态传到之后才能开始,而且两个池仍然都可能排队。

争议所在

采用分块预填充的同机部署,与预填充和解码分离之间,不存在普遍适用的优劣排序。两者的交叉点取决于请求到达率、提示词和输出长度分布、SLO、模型并行方式、缓存传输量及互连带宽。阶段分离可以隔离两个阶段的延迟,并让两个资源池独立扩展;同机部署让 KV 状态留在本地,在负载较低或提示词较短时,也能更灵活地使用同一个资源池。某个生产框架同时提供两种方案,并不能证明其中一种适合特定负载。必须在实际承载服务的拓扑与流量上测出交叉点。

在负载下评估服务策略

一次没有负载的延迟测量,几乎无法说明调度器的表现。有效的评估应保持请求流特征不变,并持续观察系统直至饱和:

  1. 说明负载。 记录到达时序、提示词长度、请求的输出上限、实际输出长度、采样设置、请求类别、可复用前缀和取消请求。
  2. 说明约定。 调优之前定义测量边界,并为各类请求确定 TTFT、TPOT、E2E、接纳率和可用性目标。
  3. 扫描输入负载。 逐步提高请求到达率,直到排队、拒绝或尾延迟违反约定。报告整条曲线,而不是某个有利的运行点。
  4. 匹配资源。 比较策略时,使用相同的模型、缓存精度、加速器型号与数量、互连、并行布局和输出分布。
  5. 核算全部工作。 报告请求与词元吞吐量、有效吞吐量、接纳率和拒绝率、队列深度、KV 利用率、前缀命中率、抢占次数、传输的 KV 字节数、加速器时间和成本。
  6. 按原因检查长尾。 分开统计排队延迟、预填充执行、解码间隔、缓存未命中、抢占、传输和客户端背压。平均值无法指出究竟是哪种资源失效。

接纳策略尤其需要仔细检查。按每个请求声明的最大长度预留内存,能够保护内存安全,却可能浪费内存池的大部分容量;只按当前长度预留可以接纳更多任务,但必须为后续增长、抢占、逐出或拒绝准备可信的方案。优先级调度可以保护交互流量,也可能让批处理任务一直得不到执行。前缀缓存可以改善 TTFT,但如果隔离规则含糊,也可能带来跨租户的数据泄露或时序侧信道风险。每项优化都会移动一条边界,运维系统必须把它显式暴露出来。

下层约束

本章定义了服务目标,以及调度器必须分配的资源。第 32 章 会说明块分配、抢占和前缀缓存;第 33 章 会减少或重叠生成词元所需的计算;第 34 章 则会改变权重、缓存和算术运算的成本。只有端到端调度器能在负载下把这些改进转化为按约定完成的请求,它们才真正有用。

收益与边界

服务性能不能靠“计算量”到“速度”的简单换算来解释。它必须协调完整的请求生命周期,面对两种执行区间、不断增长的状态、无法预知的输出长度、有限内存和截止时间。连续批处理、分页式 KV 分配、前缀复用、分块预填充和阶段分离,每种机制消除的浪费来源不同,但都不能取代接纳控制和针对具体负载的测量。

一项可信的服务结果应说明模型与硬件、输入负载与接纳负载、请求长度分布、调度策略、缓存策略和延迟约定,然后同时报告尾延迟、有效吞吐量、拒绝情况和成本。这正是快速内核演示与并发用户到来时仍然有用的服务之间的分界线。

延伸阅读

  • Yu et al., “Orca: A Distributed Serving System for Transformer-Based Generative Models” (用于自回归模型服务的迭代级调度), 2022. usenix.org
    Orca 引入迭代级调度,让生成模型服务器在每个解码步骤后重组批次,而不必等待静态批次全部结束。
  • Kwon et al., “Efficient Memory Management for Large Language Model Serving with PagedAttention” (vLLM 中基于分块的 KV 缓存分配), 2023. arXiv:2309.06180
    vLLM 通过分块分配与共享 KV 缓存来减少碎片,并提高服务系统可同时驻留的序列数量。
  • Agrawal et al., “Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve” (分块预填充与面向停顿的批处理), 2024. arXiv:2403.02310
    Sarathi-Serve 将长预填充拆成多个块并与解码共同调度,以限制生成停顿并保留批处理机会。
  • Zhong et al., “DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving” (在联合延迟目标下分离部署预填充与解码), 2024. arXiv:2401.09670
    DistServe 将预填充与解码放到独立 GPU 池,并用在指定 TTFT 与 TPOT 达标率下可持续的到达率定义 goodput。
  • Pope et al., “Efficiently Scaling Transformer Inference,” 2023. arXiv:2211.05102
    本文提出用于 TPU v4 的张量并行分区框架与底层优化,使 PaLM 540B 在 int8 量化下实现 29ms/词元延迟与 76% 模型 FLOPs 利用率(MFU)。

评论

登录后评论