高效请求排队——优化LLM性能

社区文章 发布于2025年4月2日

同时为多个应用程序和用户提供LLM服务具有挑战性,因为它们会争夺有限的GPU资源。本文是LLM性能系列的第一篇,基于我们TNG Technology Consulting GmbH在自托管LLM服务方面的经验。在第一部分中,我们重点关注排队的影响,并讨论不同的调度策略。

起点:一个纯粹的推理引擎

一个推理引擎,如vLLMHuggingFace TGI,由以下部分组成:

  • 一个工作器,负责实际计算请求中的下一个token
  • 一个队列,用于存储首次到达的请求
  • 一个调度器,从队列中取出请求并将其移动到工作器

为什么这里需要一个队列?因为在GPU上进行计算时,批量处理比单独处理单个请求更高效、更节省资源。这个后端队列允许调度器选择多个请求并将它们放在同一个批次中进行处理。

请注意,通常每个推理引擎只服务一个模型,我们有多个并行运行的部署用于不同的模型。

问题:“高级用户”可以阻塞其他用户

当单个用户“A”发送大量请求时,他们会迅速占满队列。其他稍后发送请求的用户(“B”和“C”)将被阻塞,无法使用相同的模型(直到“A”的所有请求都已处理)。请注意,图示侧重于vLLM作为推理引擎,但该问题更具普遍性,也适用于其他后端。

image/png

解决方案:公平调度

在TNG,用户不直接向vLLM后端发送请求,而是向API服务器(我们称之为“LLM-Server”)发送请求。在这里,我们可以为每个用户(和模型)设置独立的队列,并且调度器不是FIFO(先进先出),而是轮询所有用户队列。这实现了某种**“公平调度”**:例如,在图中,用户B和C稍晚发送请求,此时用户A的第一个请求已被调度。那时,用户A的三个请求已经等待了一段时间,但用户C只需等待用户A的一个请求完成。

核心思想是:在我们自己的组件中,而不是在推理后端中,优先处理来自不同用户的请求!通常,一旦请求被发送到推理引擎,就无法更改请求的顺序,因此您必须在LLM-Server中,也就是我们完全控制的地方,以正确的顺序处理它们。

image/png

可能的扩展

您可以考虑不同的方面来决定什么是“公平”调度。在上面的例子中,任何带有单个请求的新用户都应该在任何用户连续被服务两次或更多次之前被服务。这在请求数量层面上是“公平的”。但是您也可以考虑**处理时间**:一个请求会占用后端多长时间?长的提示和长的生成会更长时间地“阻塞”LLM供其他用户使用,所以也许短的请求应该优先?不幸的是,很难估计生成长度。虽然有些请求有“max_tokens”限制,但来自交互式AI助手的典型聊天消息没有token限制,可以从非常短的生成(“总结这段文本”)到非常长的生成(“给我讲个故事”/“为xyz编写所有代码”)不等。

根据提示,根据相似性安排请求可能会有一些好处,这样 vLLM 可以**最大化缓存命中率**,从而提高性能。这种KV缓存感知路由最近受到了 NVIDIA DynamoAIBrix 等框架的关注。

在商业环境中,对于托管式LLM,单个请求的**成本**可能是另一个需要考虑的指标,但它也伴随着类似的挑战。

该解决方案还可以通过不仅为每个用户(和模型)设置一个队列,而是设置**多个具有不同优先级的队列**来扩展。例如,像TNG的AI助手这样带有聊天界面的交互式应用程序应该具有更高的优先级,因为用户如果在五秒内看不到任何进展,就会认为应用程序出现故障。然而,为数十个文件和数千行代码生成代码审查的用户,则会预期LLM请求需要一些时间。而某些用例(如通过批处理API调度的基准测试运行)应该具有如此低的优先级,以至于它们不会干扰其他用例,并且只在没有其他运行的情况下才会被调度。

问题:后端队列没有反压

再次考虑这样的场景:用户A一次性发送大量请求,过了一段时间,新用户C加入并希望调度单个请求。如果LLM-Server上的调度器立即将每个请求(根据公平优先级)发送到后端,那么A的所有请求将再次在FIFO队列中累积。新用户C将不得不再次等待,直到所有先前接收到的请求都被处理。这几乎不会比没有LLM-Server的初始场景有任何改进。

理想情况下,您可以限制后端队列中元素的最大数量,但vLLM没有提供该选项。因此,我们必须动态调整LLM-Server上的调度器向后端发送新请求的速度。在这里,我们的目标是:**保持后端队列长度短**,以最大程度地减少新用户遇到的延迟。

(最简单的方法是静态速率限制,但这可能会在大多数请求都很短时导致资源利用不足,并且难以针对不同模型和负载模式进行校准)。

解决方案:获取指标

为了在LLM-Server中获取后端队列长度,我们需要从vLLM的/metrics端点获取相应的Prometheus指标。我们的公平调度器只有在**后端队列长度指标**小于三(例如)时才允许向后端发送请求。为了进一步缩短新用户的延迟,可以降低后端队列的目标长度,直到导致批处理利用不足和效率降低——这是一个权衡。但是,请记住,目标队列长度并不反映最大并发性;仍然可以有三个以上的请求并行处理;vLLM会在有足够空间时将排队请求添加到当前批次中(“连续批处理”)。

image/png

可能的扩展

一旦您在LLM-Server中建立了后端队列与调度器之间的反馈循环,您就可以轻松扩展用于制定调度决策的vLLM指标集。例如,为了在TNG的交互式AI助手中获得良好的用户体验,我们追求高token生成速度(例如,>7 tokens/s,即每个token约150毫秒),如果报告的**每个输出token时间指标**超过150毫秒,则不会调度新的请求。

您还可以为**不同请求优先级**编程**不同的度量阈值**。例如,对于来自批量API的低优先级请求,我们只在后端队列完全为空时才调度请求:我们宁愿冒短期GPU利用不足的风险,也不愿为任何后续高优先级请求造成延迟增加。

优化的潜力巨大:例如,如果您允许后端长度短于三,并且当前指标为零时进行调度,您可以立即向后端发送三个请求,而无需再次获取指标。在最坏的情况下,它们都不适合当前批次,并且都位于后端队列中(在这种情况下,新长度为三,这很好)。

替代方案:后端优先级调度

最近,vLLM增加了基于优先级的调度选项,而不是坚持FIFO。对于这个新功能,请求在发送到后端之前会被标记一个优先级。vLLM会定期检查是否有更高优先级的请求正在排队,然后按优先级对所有请求进行排序(包括正在运行的批次和等待队列)。这不仅会让高优先级请求字面上跳过队列,甚至会直接将其移动到已处理的批次。代价是(除了排序带来的一些开销)较低优先级的请求将被从正在运行的批次中逐出,并重新放回等待队列。

image/png

后端优先级调度能否取代LLM服务器端的所有队列?

vLLM 将请求优先级理解为一个连续的数字。这不仅允许您区分默认优先级、高优先级(交互式AI助手)和低优先级(批处理API),甚至可以为用户已发送到后端且响应待定的每个请求进行更小的递减。例如,用户B和C只发送单个请求,它们的优先级为零。用户A一次发送四个请求;第一个请求的优先级也为零,但接下来的请求的优先级将为一、二和三,并且会稍后处理(数字越高=优先级越低)。

仍然存在一些注意事项:

  • 后端优先级功能仅适用于vLLM,不适用于HuggingFace TGI。
  • 在LLM服务器端设置调度器允许您根据每输出token时间调整调度速率。后端优先级调度不控制调度速率。
  • 在实际负载场景下,频繁重新排序队列和批处理对延迟的影响仍需衡量。

总的来说,后端优先级调度对于基于vLLM的系统来说是一个不错的策略,因为它简化了上游LLM-Server上的队列逻辑。不幸的是,在实际环境中,您无法摆脱LLM-Server作为额外的调度层,因为您需要一个客观的实例来为单个请求分配优先级。

总结与展望

排队和调度对于具有不同优先级的多用户和客户端应用程序至关重要,因为它们显著影响用户体验。在多个客户端并行提交请求的场景中尤其如此,受益于专门的“公平调度”策略。尽管后端功能(如优先级调度)可以简化优化,但上游网关服务器仍然是完全管理这些复杂性所必需的。

在本系列博客文章的下一部分中,我们将转换重点,关注推理引擎在预填充和解码阶段的token生成。特别是,我们将讨论资源利用和并发处理多个请求的策略。

社区

0.9.1版本中,分块预填充(Disaggregated Prefill)是否得到了改进?现在许多人都在使用1P和ND模式。

注册登录 以发表评论