请求路由#

Ray Serve LLM 提供可定制的请求路由,以针对不同的工作负载模式优化副本之间的请求分发。请求路由在副本选择级别上运行,这与入口级别的模型路由不同。

路由与入口#

您需要区分两个路由级别

入口路由(模型级别)

  • model_id 映射到部署

  • 示例: OpenAiIngress 接收到 model="gptoss"/v1/chat/completions 请求,并将其映射到 gptoss 部署。

请求路由(副本级别)

  • 选择将请求发送到哪个副本

  • 示例: OpenAiIngress 副本内的 gptoss 部署句柄决定将请求发送到该部署的哪个副本(1、2 或 3)。

本文档重点介绍请求路由(副本选择)。

HTTP Request → Ingress (model routing) → Request Router (replica selection) → Server Replica

请求路由架构#

Ray Serve LLM 请求路由在部署句柄级别运行

┌──────────────┐
│   Ingress    │
│  (Replica 1) │
└──────┬───────┘
       │
       │ handle.remote(request)
       ↓
┌──────────────────┐
│ Deployment Handle│
│   + Router       │  ← Request routing happens here
└──────┬───────────┘
       │
       │ Chooses replica based on policy
       ↓
   ┌───┴────┬────────┬────────┐
   │        │        │        │
┌──▼──┐  ┌──▼──┐  ┌──▼──┐  ┌──▼──┐
│ LLM │  │ LLM │  │ LLM │  │ LLM │
│  1  │  │  2  │  │  3  │  │  4  │
└─────┘  └─────┘  └─────┘  └─────┘

可用的路由策略#

Ray Serve LLM 提供多种请求路由策略,以优化不同工作负载模式

默认路由:二选一算法#

默认路由器使用二选一算法来

  1. 随机采样两个副本。

  2. 路由到正在处理的请求较少的副本。

这在最小的协调开销下提供了良好的负载均衡。

感知前缀的路由#

PrefixCacheAffinityRouter 通过将具有相似前缀的请求路由到同一副本,从而优化具有共享前缀的工作负载。这提高了 vLLM 的自动前缀缓存 (APC) 中的 KV 缓存命中率。

路由策略

  1. 检查负载均衡:如果副本平衡(队列差异 < 阈值),则使用前缀匹配。

  2. 高匹配率 (≥10%):路由到前缀匹配度最高的副本。

  3. 低匹配率 (<10%):路由到缓存利用率最低的副本。

  4. 回退:当负载不平衡时,使用二选一算法。

有关更多详细信息,请参阅 感知前缀的路由

自定义路由策略的设计模式#

自定义请求路由器是 Ray Serve 原生 API 中的一项功能,您可以按部署进行定义。对于每个部署,您可以自定义每次从调用方调用部署句柄上的 .remote() 时执行的路由逻辑。由于部署句柄是集群中全局可用的对象,因此您可以从 Ray 集群中的任何 actor 或任务调用它们。有关此 API 的更多详细信息,请参阅 使用自定义算法进行请求路由

这允许您运行相同的路由逻辑,即使您有多个句柄。Ray Serve 中的默认请求路由器是二选一算法,它平衡了负载均衡并优先考虑局部性路由。但是,您可以自定义此设置以使用特定于 LLM 的指标。

Ray Serve LLM 在框架中包含感知前缀的路由。自定义请求路由器的两种常见架构模式。它们之间存在明显的权衡,因此请选择合适的模式并平衡简洁性与性能。

模式 1:集中式单例指标存储#

在此方法中,您维护一个集中式指标存储(例如,一个单例 actor)来跟踪与路由相关的信息。请求路由器逻辑实际运行在拥有部署句柄的进程上,因此可以有许多这样的进程。每个进程都可以查询单例 actor,创建一个多租户 actor,为请求路由器提供集群状态的一致视图。

单个 actor 可以提供原子线程安全的​​操作,例如用于查询全局状态的 get() 和用于更新全局状态的 set(),路由器可以在 choose_replicas()on_request_routed() 期间使用它们。

┌─────────┐     ┌─────────┐     ┌─────────┐
│ Ingress │────►│ Metric  │◄────│ Ingress │
│    1    │     │  Store  │     │    2    │
└────┬────┘     └─────────┘     └────┬────┘
     │                               │
     └────────────────┬──────────────┘
                      │
           ┌──────────┴──────────┐
           │                     │
      ┌────▼────┐           ┌────▼────┐
      │  LLM    │           │  LLM    │
      │ Server  │           │ Server  │
      └─────────┘           └─────────┘
../../../_images/routing_centralized_store.png

自定义路由的集中式指标存储模式#

优点

  • 实现简单 - 无需修改部署逻辑即可记录副本统计信息。

  • 请求指标立即可用。

  • 强大的一致性保证。

缺点

  • 在吞吐量很高的应用程序中,单个 actor 可能会成为瓶颈,此时 TTFT 会受到 RPC 调用(~1000 次请求/秒)的影响。

  • 每次路由决策都需要额外的网络跳数。

模式 2:从 Serve 控制器广播的指标#

在此方法中,Serve 控制器轮询每个副本以获取本地统计信息,然后将它们广播到其部署句柄上的所有请求路由器。然后,请求路由器可以使用此全局广播的信息来选择正确的副本。请求到达副本后,副本会更新其本地统计信息,以便在控制器下次轮询时将其发送回 Serve 控制器。

          ┌──────────────┐
          │    Serve     │
          │  Controller  │
          └──────┬───────┘
                 │ (broadcast)
       ┌─────────┴─────────┐
       │                   │
  ┌────▼────┐         ┌────▼────┐
  │ Ingress │         │ Ingress │
  │  +Cache │         │  +Cache │
  └────┬────┘         └────┬────┘
       │                   │
       └────────┬──────────┘
                │
         ┌──────┴──────┐
         │             │
    ┌────▼────┐   ┌────▼────┐
    │  LLM    │   │  LLM    │
    │ Server  │   │ Server  │
    └─────────┘   └─────────┘
../../../_images/routing_broadcast_metrics.png

自定义路由的指标广播模式#

优点

  • 可扩展到更高的吞吐量。

  • 每次路由决策无需额外的 RPC 开销。

  • 分布式路由决策。

缺点

  • 请求路由器对统计信息的视图与副本的实际状态之间存在时间延迟。

  • 最终一致性 - 路由器可能会根据略微过时的数据做出决策。

  • 需要与 Serve 控制器协调,实现更复杂。

  • 使用模式 1(集中式存储):当您需要强一致性、中等吞吐量要求或希望简化实现时。

  • 使用模式 2(广播指标):当您需要极高的吞吐量、可以容忍最终一致性或希望最小化每次请求开销时。

自定义路由策略#

您可以通过扩展 Ray Serve 的 RequestRouter 基类来实施自定义路由策略。有关实施自定义路由器的详细示例和分步指南,请参阅 使用自定义算法进行请求路由

要实现的键方法

实用工具混合类#

Ray Serve 提供混合类,为路由器添加通用功能。有关示例,请参阅 使用自定义算法进行请求路由

  • LocalityMixin:优先选择同一节点上的副本以减少网络延迟。

  • MultiplexMixin:跟踪每个副本上加载了哪些模型,用于 LoRA 部署。

  • FIFOMixin:确保请求的 FIFO 顺序。

路由器生命周期#

请求路由器的典型生命周期包括以下阶段

  1. 初始化:使用副本列表创建路由器。

  2. 请求路由:为每个请求调用 choose_replicas()

  3. 回调:成功路由后调用 on_request_routed()

  4. 副本故障:副本死亡时调用 on_replica_actor_died()

  5. 清理:部署删除时清理路由器。

异步操作#

为了获得最佳性能,路由器应使用异步操作。以下示例演示了推荐的模式

# Recommended pattern: Async operation
async def choose_replicas(self, ...):
    state = await self.state_actor.get.remote()
    return self._select(state)

# Not recommended pattern: Blocking operation
async def choose_replicas(self, ...):
    state = ray.get(self.state_actor.get.remote())  # Blocks!
    return self._select(state)

状态管理#

对于具有状态的路由器,请使用适当的同步。以下示例显示了推荐的模式

class StatefulRouter(RequestRouter):
    def __init__(self):
        self.lock = asyncio.Lock()  # For async code
        self.state = {}
    
    async def choose_replicas(self, ...):
        async with self.lock:  # Protect shared state
            # Update state
            self.state[...] = ...
            return [...]

另请参阅#