router智能调度和负载优化

router智能调度和负载优化
gogongxtsgl-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的主要用途
- 请求分发中心: 作为所有推理请求的统一入口,负责将请求分发到后端计算节点。
- 智能负载均衡: 通过多种策略实现请求的智能分配,避免部分节点过载。
- 缓存感知路由: 根据请求内容与 KV Cache 的匹配情况,优先将请求分发到已有相关缓存的节点,提高命中率。
- 健康检查: 监控 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还在处理的请求数
负载值是通过原子操作维护的计数器实现的:
- 初始化: 每个worker创建时,负载计数器初始化为0
- 增加负载:
当请求被路由到worker时,调用
increment_load()方法将计数器加1 - 减少负载:
当请求处理完成时,调用
decrement_load()方法将计数器减1 - 获取负载: 通过
load()方法返回当前负载值
max_load:所有workers的请求数量的最大值
min_load:所有workers的请求数量的最小值
CacheAware负载均衡可配置参数:
balance_abs_threshold:64 (负载不平衡的绝对阈值)balance_rel_threshold:1.5(负载不平衡的相对阈值)cache_threshold:0.3(缓存命中的最小匹配率阈值)
不均衡条件(两个条件必须同时满足):
- 绝对差值阈值:
max_load - min_load > balance_abs_threshold(默认值为64) - 相对比例阈值:
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
- P和D的dp
worker都是
- 非PD分离模式下: 采用
round_robin轮流调度策略 假如总共有4个DP,例如第一次是DP0,第二次是DP1,后面就是DP2,DP3,DP0,DP1……
所以在多dp情况下,本质上调度又回到了随机模式和round_robin模式,这和我们想要实现负载均衡和提高kvcache命中率想法是违背的
我们实现的DP-aware优化
router改动:
router在初始化workers时会去查询
/get_server_info解析得到每个P或者D节点的dp个数,然后创建对应个数的workers负载均衡判断时就是以dp为worker单位去选择要计算的p和d,然后将请求发送给p和d
参数含义:
data_parallel_rank: 指定的prefill的dpdata_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%)。
不足点和优化点
针对CacheAware有一些参数要调整,事实上这里的参数设置不对会极大的造成负载不均衡。 例如对于长system prompt短user prompt场景,每条请求的kvcache命中率都非常高,那么如果参数设置不对会导致每次请求都会router到相同的[dp]workers,反向导致系统负载不均衡,这时候甚至不如round_robin调度
当前的CacheAware的命中率是按照字符来计算的,并不是实际的token长度,导致计算命中率和真实命中率会有一定偏差,可以考虑引入tokenizer消除这部分偏差
实际系统的kvcache是在动态变化的,可能考虑在通信和cpu计算负担不重的情况下预跑一遍radix tree匹配比较实际的命中率










