乐于分享
好东西不私藏

AI Infra:vLLM 与 SGLang DPLB 实现解析

AI Infra:vLLM 与 SGLang DPLB 实现解析
SGLang: https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/managers/data_parallel_controller.py
vLLM: https://docs.vllm.ai/en/latest/serving/data_parallel_deployment/

背景

MoE 在线部署除 TP 外,还常用 DP + EP。未做 DPLB 时,KV 亲和、超长输入或简单轮询会把流量挤到少数 DP rank,各 rank 的 num_queue_reqs 会严重分叉(下图:无 DPLB 单 rank 冲高,有 DPLB 后更均衡)。 DPLB 在请求入口为每个 HTTP 请求选择负载更轻的 DP rank,缓解 DP 层 straggler。

vLLM

SGLang

Server参数

--data-parallel-size N 

--data-parallel-hybrid-lb

--data-parallel-external-lb

--load-balance-methodtotal_tokens / round_robin(默认) / follow_bootstrap_room (用于PD) / total_requests

调度粒度

queue-aware

queue-aware、token-aware 

vLLM 三种--data-parallel-*模式决定 入口与选 rank 范围;SGLang 用--load-balance-method决定 算法。

vLLM DPLB: 三种部署模式

Internal

调度分数(Internal / Hybrid 内置 LB): score = waiting × 4 + running,取 score 最小的 rank。

图1: Internal

# Node 0  (with ip address 10.99.48.128)vllm serve $MODEL --data-parallel-size 4 --data-parallel-size-local 2 \                  --data-parallel-address 10.99.48.128 --data-parallel-rpc-port 13345# Node 1vllm serve $MODEL --headless --data-parallel-size 4 --data-parallel-size-local 2 \                  --data-parallel-start-rank 2 \                  --data-parallel-address 10.99.48.128 --data-parallel-rpc-port 13345

如图,DP=4(EngineCore 共 4:Node0 两个 + Node1 两个);

API Server / AsyncLLM 也是 4 组(Internal 默认 api_server_count = data_parallel_size

组件
职责
DPLBAsyncMPClient(在 API Server / AsyncLLM 内)
用本地的 lb_engines 算 score(来自coodinator广播),选最轻 rank,经 ZMQ 下发请求
DPCoordinator(独立进程,多在 node0)
汇聚各 Engine 的 waiting/running 并广播;MoE 时另管 wave / dummy forward
Coordinator 负责 周期性推送负载快照;DPLBAsyncMPClient 在 API 内 用本地缓存选 rank。二者有 数据依赖,但 请求不经过 Coordinator 转发。
1. Stats 路径(把各 rank 的 waiting/running 数字汇总、再发给 API,经过coordinator):Engine → Coordinator → API(更新 lb_engines
EngineCore ──上报 [waiting, running](发现有变化时)──→ DPCoordinator(汇聚)          ←──约每 100ms 广播 stats──→ API 侧更新 lb_engines(不选 rank)
  1. Coordinator 与 AsyncLLM 的交互 → stats 下行广播(给 DPLB 用)。

  2. Coordinator 与 Engine Core 的交互 → stats 上行上报(虚线 publish request counts) + 同一条协调链路上的 MoE wave(见下)

2. 请求路径(把这次请求送到某个 EngineCore 去算,不经过coordinator):HTTP → API → ZMQ → Engine
HTTP →(Kernel 分到某一 API 进程)→ API Server / AsyncLLM     → DPLBAsyncMPClient 选 rank(waiting×4 + running)     → ZMQ → EngineCore(DP rank k) (k可在 Node0 或 Node1)
  • ZMQ分发完,KV 绑在该 rank。

3. MoE控制路径

    • WAVE

      在分布式数据并行场景下,多个EngineCore进程需要协同处理一批请求。每一批请求被称为一个"wave"(波次),每个wave代表一组需要同步处理的请求,所有进程必须在同一个wave上协同工作,以保证状态一致。

      https://zhuanlan.zhihu.com/p/1927317160889386326

图2: https://zhuanlan.zhihu.com/p/1927317160889386326

vLLM中每32个stepall-reduce一次状态(看一下全局是否还有 unfinished 请求), if no, 集体 paused,wave_complete,current_wave++
  •     EP Collective
两段 all‑to‑all 是 集体通信:要求参与者都进入同一个通信点。如果某个 DP rank 这一步没请求就不跑,别的 rank 会卡在 collective 上。所以需要 Coordinator 用 wave 把所有 rank 的“运行/暂停”节拍对齐:有请求的跑真实 forward,没请求的跑 dummy forward 占位(保证 EP collective 不缺席)。
EngineCore(每个 DP rank)  ──(collective 判断全局是否空闲)──→  wave / running_state 变化API/AsyncLLM  ── FIRST_REQ (只有在全局 paused 时) ──→  DPCoordinatorDPCoordinator ── START_DP_WAVE(wave++) ──→  所有 EngineCore(一起进入 running

Dummy怎么来的?

vLLLM会拼出一套 能跑 forward 的张量(input_ids/inputs_embedsseq_lens、attention metadata 等),再调用 self.model(...) 走一次(或多次)forward。

Dummy forward会污染KV cache吗?

Dummy forward 不会更新 KV cache:实现上把 slot_mapping 全设为 -1,concat_and_cache 等写 KV 的 kernel 会跳过写入。
# vllm/vllm/v1/worker/gpu_model_runner.py# Dummy runs have no real slot assignments — fill with -1 so# concat_and_cache kernels skip the KV write.... sm.fill_(-1)

Hybrid和External

Hybrid

Internal 模式把所有请求都打到 一个 node 的 API,DP 大了以后这个 node 的入口/调度会变成瓶颈;Hybrid 的思路是:上游 LB 把 HTTP 分到各 node 的 API;每个 node 的 API 只把请求排到“本机的 DP ranks”(只在local rank上比score)。减少跨 node 流量。
如果按照图1 Internal的例子(2个node, 每个node 2张卡), Hybrid需要指定的参数为:
# Node 0  --data-parallel-size 4 \  --data-parallel-hybrid-lb \  --data-parallel-size-local 2 \  --data-parallel-address 10.99.48.128 --data-parallel-rpc-port 13345 # Node 1  --data-parallel-size 4 \  --data-parallel-hybrid-lb \  --data-parallel-size-local 2 \  --data-parallel-start-rank 2 \  --data-parallel-address 10.99.48.128 --data-parallel-rpc-port 13345 \

External

图3: External

到了External模式, 每个 rank (每张卡)就是一个独立的vLLM服务实例
  • 请求路径:
Client → External LB → 某个 rank 的 API endpoint → AsyncLLM → 本实例的 EngineCore关键点:“选哪个 rank”由 External LB 决定(基于你自己的策略/监控)。
  • stats路径:
External LB 下 rank0 仍可能拉起 DPCoordinator(MoE 用于 wave/dummy 同步),但 data_parallel_external_lb 会让 Engine不再向 Coordinator 上报 waiting/running

2种command写法:

CUDA_VISIBLE_DEVICES=0 vllm serve $MODEL --data-parallel-size 4 --data-parallel-rank 0 --port 8000# ... rank 1/2/3 同理
vllm serve $MODEL \  --data-parallel-size 4 \  --data-parallel-rank 0 \  --data-parallel-external-lb \  --port 8000

SGLang DPLB: 算法枚举

不同于vLLM,SGLang用 DataParallelController 负责把请求分发到 DP worker;分发算法由 --load-balance-method 决定。”

--load-balance-method 

含义

round_robin (auto)

轮询 DP worker,跳过 unhealthy

follow_bootstrap_room

bootstrap_room % dp_size 固定映射(PD prefill)

total_requests

选 running+waiting请求数最少(argmin) 的 rank (不像vLLM有做加权)

total_tokens

选 KV 已用 token (=log中的full token) + waiting 里 sum(seqlen)

‼️  “PD disaggregation 场景下,load-balance-method=auto 时:prefill 用 follow_bootstrap_room,decode 用 round_robin;decode 拉 KV 仍按同一 bootstrap_room 定位到 prefill rank。”
  • 这里dp_prefill等同于dp_size
    1. Router/客户端 给这条请求一个 R(bootstrap_room),prefill、decode 共用。2. Prefill DP Controller(follow)    k = R % dp → 请求进 Prefill DP rank k,KV 写在这里。3. Decode DP Controller(默认 round_robin)    请求进 Decode DP rank m(轮询,和 R 无关,只是分 decode 算力)。4. Decode 拉 KV 时    再算一遍 k = R % dp → 去 Prefill rank k 取 KV(图上「见 decode.py」)。
一句话:R 随机 → 各 prefill DP 统计上请求数接近(等同于掷骰子);decode 轮询分活, 不认实时负载,拉 KV 仍用同一 R 找回 prefill 上的 k,所以本质上没有做到LB

建议

多实例生产环境下,应优先让外部 cache-aware 路由(SMG)做“入口级”的实例选择;框架内的--load-balance-method作为“实例内/节点内”的二级分发策略。

References

[1] LLM推理数据并行负载均衡(DPLB)浅析

[2] vLLM DP特性与演进方案分析