router智能调度和负载优化

sgl-router的smart router策略

为什么要有sgl-router

flowchart LR
    subgraph Client["💻 Clients"]
        C1["Client 3"]
        C2["Client 1"]
        C3["Client 2"]
    end

    C1 --> RT
    C2 --> RT
    C3 --> RT

    subgraph Router["sglang-router"]
        RT["🔀 Router"]
    end

    subgraph PCluster["Prefill Workers (P-nodes)"]
        P1["P1"]
        P2["P2"]
        P3["P3"]
    end

    subgraph DCluster["Decode Workers (D-nodes)"]
        D1["D1"]
        D2["D2"]
        D3["D3"]
    end

    RT --> P1
    RT --> P2
    RT --> P3
    RT --> D1
    RT --> D2
    RT --> D3

    %% 请求示例路径
    C1 -. 发送请求 .-> RT
    RT -. 分配prefill任务 .-> P2
    RT -. 分配decode任务 .-> D3
    P2 -->|KV Cache| D3
    D3 -->|decode结果| RT
    RT -->|返回响应| C1

    %% 样式定义
    classDef client fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef router fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    classDef prefill fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
    classDef decode fill:#fff3e0,stroke:#e65100,stroke-width:2px

    class Client client
    class Router router
    class PCluster prefill
    class DCluster decode
  • PD 聚合模式 下,请求可以直接发送到节点上;

  • 而在 PD 分离模式 下,请求需要通过一个 Router 来统一管理 Prefill(P)和 Decode(D)节点,并负责请求分发与协调。

sgl-router是SGLang框架中的智能路由组件,主要作用是在普通worker和PD分离的worker之间进行智能请求分发和负载均衡。

sgl-router的主要用途

  1. 请求分发中心: 作为所有推理请求的统一入口,负责将请求分发到后端计算节点。
  2. 智能负载均衡: 通过多种策略实现请求的智能分配,避免部分节点过载。
  3. 缓存感知路由: 根据请求内容与 KV Cache 的匹配情况,优先将请求分发到已有相关缓存的节点,提高命中率。
  4. 健康检查: 监控 Worker 节点状态,自动剔除不健康节点。

worker调度策略

SGLang Router 提供四种策略:

策略 特点
CacheAware 智能缓存匹配+负载均衡
Random 纯随机选择
RoundRobin 雨露均沾(依次选择过去)
PowerOfTwo 随机选出两个,然后选择其中负载轻的那个

CacheAware策略

flowchart TD
    Start([来了个请求]) --> GetHealthy[拉取健康worker名单]
    GetHealthy --> CheckHealthy{有活着的worker吗?}
    CheckHealthy -->|没啦| ReturnNone[返回空值]

    CheckHealthy -->|有| GetLoads[检查各worker负载]
    GetLoads --> CheckImbalance{系统负载均衡吗?}

    CheckImbalance -->|不平衡| LoadBalancingMode[负载均衡模式]
    CheckImbalance -->|均衡| CacheAwareMode[缓存感知模式]

    LoadBalancingMode --> FindMinLoad[找最闲的worker]
    FindMinLoad --> UpdateCache[更新缓存树]
    UpdateCache --> ReturnWorker1[返回选中worker]

    CacheAwareMode --> PrefixMatch[前缀匹配查询缓存树]
    PrefixMatch --> CalculateRate[计算匹配率]
    CalculateRate --> CheckThreshold{匹配率 > 30%?}

    CheckThreshold -->|是| CacheHit[缓存命中!]
    CheckThreshold -->|否| CacheMiss[缓存miss]

    CacheHit --> ReturnCached[返回缓存对应的worker]
    CacheMiss --> FindSmallestTree[找树最小的worker]

    ReturnCached --> UpdateTree[更新缓存树]
    FindSmallestTree --> UpdateTree
    UpdateTree --> ReturnWorker2[返回选中worker]

    ReturnWorker1 --> End([结束])
    ReturnWorker2 --> End
    ReturnNone --> End

CacheAware 策略使用 Radix Tree 维护一个全局缓存,存储请求前缀与处理节点的映射关系:

  • 树中每个节点对应一个租户(在 DP Aware 模式下对应一个 DP 进程)
  • 存储原始文本字符,避免 tokenization 开销
  • 使用 LRU 策略进行缓存清理,控制最大节点数(默认 2^26 = 67,108,864)

每个worker的load负载值是当前worker还在处理的请求数

负载值是通过原子操作维护的计数器实现的:

  1. 初始化: 每个worker创建时,负载计数器初始化为0
  2. 增加负载: 当请求被路由到worker时,调用increment_load()方法将计数器加1
  3. 减少负载: 当请求处理完成时,调用decrement_load()方法将计数器减1
  4. 获取负载: 通过load()方法返回当前负载值

max_load:所有workers的请求数量的最大值

min_load:所有workers的请求数量的最小值

CacheAware负载均衡可配置参数:

  • balance_abs_threshold:64 (负载不平衡的绝对阈值)
  • balance_rel_threshold:1.5(负载不平衡的相对阈值)
  • cache_threshold:0.3(缓存命中的最小匹配率阈值)

不均衡条件(两个条件必须同时满足):

  1. 绝对差值阈值:max_load - min_load > balance_abs_threshold(默认值为64)
  2. 相对比例阈值:max_load > min_load * balance_rel_threshold(默认值为1.5)
1
2
3
4
5
6
7
8
// Get current load statistics
let loads: Vec<usize> = workers.iter().map(|w| w.load()).collect();
let max_load = *loads.iter().max().unwrap_or(&0);
let min_load = *loads.iter().min().unwrap_or(&0);

// Check if load is imbalanced
let is_imbalanced = max_load.saturating_sub(min_load) > self.config.balance_abs_threshold
    && (max_load as f32) > (min_load as f32 * self.config.balance_rel_threshold);

DP-aware优化

为什么需要引入DP-aware

在开启 --enable-dp-attention 且设置 --dp-size n 时,会出现以下问题:

  • KV Cache 存储分散: 每个 DP 进程独立计算 attention,KV Cache 也是以 DP 为单位独立存储。
  • 调度粒度过粗: Router 只能以 Worker 级别调度,无法感知 Worker 内部各 DP 的负载与缓存情况。

结果是:在多 DP 场景下,Router 的调度仍然退化为随机或 round-robin 模式,无法保证负载均衡和缓存命中率。

原来的DP调度

普通请求下,router发送给PD节点的请求内容:

1
2
3
4
5
6
7
8
9
curl -X POST "http://127.0.0.1:58888/v1/completions" \
  -H "Content-Type: application/json" \
  -d '{"model":"Qwen/Qwen2-1.5B-Instruct",
    "prompt":"What is the capital of France?",
    "max_tokens":10,
    "stream":true,
    "stream_options": {"include_usage" : true},
    "logprobs":3
  }'

Prefill和decode接收到的请求是一模一样的,为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
 'bootstrap_host': '127.0.0.1',
 'bootstrap_port': 19001,
 'bootstrap_room': 5691568803136743161, # router会随机生成一个bootstrap_room发送给prefill和decode
 'logprobs': 3,
 'max_tokens': 10,
 'model': 'Qwen/Qwen2-1.5B-Instruct',
 'prompt': 'What is the capital of France?',
 'rid': 'cmpl-VNOx9p4VyB9iMfI12dq82Jry',
 'stream': True,
 'stream_options': {'include_usage': True},
 'suffix': None,
 'temperature': 1.0,
 'top_k': -1,
 'top_p': 1.0,
  ......
}

原来的调度:

分为DP分离和非DP分离模式:

  • PD分离模式下:
    • P和D的dp worker都是req.bootstrap_room % len(self.workers) 这里的workers是PD节点自己的dp-size
  • 非PD分离模式下: 采用round_robin轮流调度策略 假如总共有4个DP,例如第一次是DP0,第二次是DP1,后面就是DP2,DP3,DP0,DP1……

所以在多dp情况下,本质上调度又回到了随机模式和round_robin模式,这和我们想要实现负载均衡和提高kvcache命中率想法是违背的

我们实现的DP-aware优化

router改动:

  1. router在初始化workers时会去查询/get_server_info解析得到每个P或者D节点的dp个数,然后创建对应个数的workers

  2. 负载均衡判断时就是以dp为worker单位去选择要计算的p和d,然后将请求发送给p和d

    参数含义:

    • data_parallel_rank: 指定的prefill的dp
    • data_parallel_rank_decode: 指定的decode的dp

    decode接收到的请求结构体会多两个参数:

    1
    2
    'data_parallel_rank': 1,
    'data_parallel_rank_decode': 2,

    prefill接收到的请求结构体会多一个参数:

    1
    'data_parallel_rank': 1,

sglang python改动:

精简代码:

1
2
3
4
5
6
7
8
if ( # prefill
    self.server_args.disaggregation_mode == "prefill"
):
    self.workers[req.data_parallel_rank].send_pyobj(req)
elif ( # decode
    self.server_args.disaggregation_mode == "decode"
):
    self.workers[req.data_parallel_rank_decode].send_pyobj(req)

由此就实现了让router以dp为worker单位,能够更加精准的实现负载均衡调度和提升显存命中率

性能对比

我们在 3 轮多轮对话数据集(multi-turn shared prefix) 上实测了下。

--enable-dp-attention --dp-size 8

并发数 (Concurrency) TTFT P95 关闭 DP-Aware (ms) TTFT P95 开启 DP-Aware (ms) TTFT 改善幅度 TPOT P95 关闭 DP-Aware (ms) TPOT P95 开启 DP-Aware (ms) TPOT 改善幅度
1 522.54 239.77 54% 4.80 4.81 ≈ 0%
2 464.71 229.49 51% 5.34 4.85 9%
4 383.35 260.86 32% 5.78 5.38 7%
8 394.94 272.95 31% 6.45 6.01 7%
16 451.09 309.73 31% 7.33 6.94 5%
32 502.24 374.03 26% 8.96 8.54 5%
64 658.78 486.28 26% 14.95 13.51 10%
128 1226.66 1050.63 14% 21.97 21.01 4%

表格里可以看到,开启 DP-Aware 后,在1并发下,TTFT 从 ~500+ms 下降到 ~200+ms,随着并发增加,差距依然保持。

  • TTFT(P95)整体下降 14%~55%,多轮对话场景下收益尤其明显。

  • TPOT(P95)下降约 7%,主要得益于更细粒度的 DP 级别负载均衡。

DP-Aware 使得 Router 能以更细粒度调度,从而显著降低首 token 延迟,并保持整体吞吐稳定。

总结

需要说明的一点是,CacheAware和DP-Aware是不冲突的,两者是独立的关系,可以选择开启其中的一个,也可以都开启

  • CacheAware是一种负载均衡策略
  • DP-aware能够以dp为单位更加贴合attention计算和kvcache缓存的角度来进行调度

DP-Aware 提升了调度精度,使 CacheAware 的缓存命中策略更有效。 联合使用可显著降低 TTFT(14%~55%)与 TPOT(约 7%)。

不足点和优化点

  1. 针对CacheAware有一些参数要调整,事实上这里的参数设置不对会极大的造成负载不均衡。 例如对于长system prompt短user prompt场景,每条请求的kvcache命中率都非常高,那么如果参数设置不对会导致每次请求都会router到相同的[dp]workers,反向导致系统负载不均衡,这时候甚至不如round_robin调度

  2. 当前的CacheAware的命中率是按照字符来计算的,并不是实际的token长度,导致计算命中率和真实命中率会有一定偏差,可以考虑引入tokenizer消除这部分偏差

  3. 实际系统的kvcache是在动态变化的,可能考虑在通信和cpu计算负担不重的情况下预跑一遍radix tree匹配比较实际的命中率