预计阅读 7 分钟

Data Parallelism:多副本扩 QPS

谁该读这一篇? 模型已经装下、要扩 QPS、得选 DP 还是 TP 的工程师。 前置阅读: 01-tensor-parallelism.md耗时: 25 分钟 学完能: 1. 区分"同进程 DP"和"多 server 多副本"两种 DP 形态; 2. 解释 DP 副本之间是否共享 KV(不共享); 3. 知道 SGLang router 在 DP 里扮演的角色; 4. 给定流量目标算需要多少副本; 5. 知道 DP 与 TP 怎么混合(典型 4 TP × 2 DP)。


1. DP 是什么

Data Parallel:多份完整模型副本,每副本独立处理请求。 共享:词表、tokenizer。 不共享:模型权重(各副本一份)、KV cache、attention state。

副本 A (4 卡 TP=4)  ──────┐
副本 B (4 卡 TP=4)  ──── Router ───── Client
副本 C (4 卡 TP=4)  ──────┘

QPS 线性扩展(理想情况),代价是显存 N 倍(N 个副本各装一份模型)。


2. SGLang 里 DP 的两种形态

2.1 多 server 多副本(推荐)

每副本是独立的 SGLang server 进程:

# 节点 1: 副本 A
CUDA_VISIBLE_DEVICES=0,1,2,3 python -m sglang.launch_server \
    --model-path Qwen/Qwen2.5-72B-Instruct --tp 4 --port 30001

# 节点 1: 副本 B
CUDA_VISIBLE_DEVICES=4,5,6,7 python -m sglang.launch_server \
    --model-path Qwen/Qwen2.5-72B-Instruct --tp 4 --port 30002

# Router 前置
sglang-router --worker-urls http://localhost:30001 http://localhost:30002 --port 30000

特点:

  • 副本完全独立,故障隔离最好。
  • 显存 = 副本数 × 单副本显存。
  • Router 做负载均衡 + cache-aware 路由。

2.2 同进程 DP(--dp N

python -m sglang.launch_server --model-path ... --dp 2 --tp 4 --port 30000

特点:

  • 一个 main process 内部起 2 个 Scheduler 组(每组 TP=4)。
  • 共享 HTTP server / TokenizerManager。
  • 内部有 data_parallel_controller.py 做路由。

适合:单节点想跑多副本省个 router 进程。


3. Router 的工作

源码:sglang/rust/sgl_router/sgl-router/

Router 不是简单 HTTP 反向代理,它有几个关键策略:

策略 说明
Round-robin 最简单的轮询
Shortest-queue 选当前 in-flight 请求最少的副本
Cache-aware 选对当前请求前缀命中最长的副本
Sticky-session 同 session(多轮 chat)固定到同副本

Cache-aware 路由实现:每副本定期上报 RadixCache 摘要(前缀树的哈希摘要),Router 用 Bloom filter / consistent hash 做近似匹配。


4. DP 副本之间不共享 KV

很重要:每个 DP 副本的 RadixCache 完全独立。 直接后果:

  • 同 system prompt 在 N 副本上各算一遍 prefill(除非 router 做 sticky)。
  • 长共享 prefix 场景下 DP 比 TP "不划算"——TP=8 单副本能完整共享 cache;DP=2 × TP=4 需要 router 帮忙。

何时选 DP > TP

  • 业务流量主要是独立单轮请求,前缀复用低。
  • 显存非常宽裕。
  • 需要副本级故障隔离(一个副本 crash 不影响别人)。

何时选 TP > DP

  • 业务有大量共享 prefix。
  • 显存吃紧(DP 副本占多)。
  • Latency 比 throughput 重要(TP 减时延,DP 增吞吐但不减时延)。

5. 容量公式

设:

  • $Q$ = 目标 QPS
  • $T_e$ = 平均请求处理时间(秒)
  • $C$ = 单副本最大并发(受 KV pool / max_running_requests 限制)

需要的副本数:

$$N = \left\lceil \frac{Q \cdot T_e}{C} \right\rceil$$

例:

  • $Q = 100$ QPS
  • $T_e = 2$ s
  • $C = 64$
  • $N = \lceil 200 / 64 \rceil = 4$ 副本

再留 30% 余量(突发 / 灰度 / 故障)→ 5-6 副本。


6. 数据并行 + 张量并行混合

主流配置:每节点 TP,节点间 DP。

节点 1 (4 卡, TP=4)  ┐
节点 2 (4 卡, TP=4)  ├── Router
节点 3 (4 卡, TP=4)  ┘

或者单节点 8 卡:

副本 A (GPU 0-3, TP=4)
副本 B (GPU 4-5, TP=2)    # 不对称配置

SGLang 允许这种异构(不同 server 不同 TP),Router 不在意 TP 内部。


7. K8s 部署典型架构

Service: sglang-router (LB)
  ↓
Pod: sglang-router (Rust)
  ↓ (envoy / direct)
Pod: sglang-worker-0 (4 GPU)
Pod: sglang-worker-1 (4 GPU)
Pod: sglang-worker-2 (4 GPU)

每个 worker pod 是独立 server;router pod 做负载均衡。 HPA / KEDA 按 QPS 或 GPU 利用率扩缩 worker。

详见 08-production-deployment/02-kubernetes.md


8. 排障 cheatsheet

现象 排查
副本之间负载不均 Router 策略;副本健康检查
部分副本响应慢 单副本性能差异;GPU 不一致
cache_hit_rate 副本之间差异大 Cache-aware 路由没生效;考虑 sticky session
副本启动慢 模型同步 / 拉取慢;用本地 model cache
扩容慢 模型加载 + CUDA Graph 捕获各 1-2 分钟;预热池

9. 小结

  • DP = 多副本,每副本独立模型 + KV cache。
  • 两种形态:多 server(生产推荐)+ 同进程 --dp N(单节点便利)。
  • Router 做 cache-aware / round-robin / sticky 路由。
  • DP vs TP:前者扩吞吐、显存翻倍;后者省显存、有通信代价。
  • 容量公式:$N = Q \cdot T_e / C$

10. 自检

  1. DP 副本之间共享 KV cache 吗?为什么?
答案 **不共享**。每个 DP 副本是独立 SGLang server,自己 GPU 上有独立的 KV pool 和 RadixCache。 不共享的原因:(a) GPU 显存物理隔离,跨副本读 KV 要走 RDMA / IPC,开销大;(b) 多副本独立故障域,共享反而成单点。 后果:同 system prompt 在 N 副本各算一次 prefill(除非 router sticky 或 cache-aware 路由)。这是 DP 的固有代价;想真正共享要走 disagg KV transfer,目前不便宜。
  1. 同 system prompt 在 4 副本上 cache 命中率会比单副本高还是低?
答案 **副本内命中率低于单副本聚合 cache 的命中率**(每副本只见 1/4 流量,cache 沉淀慢);但 router cache-aware 把同 system prompt 请求路由到同一副本后,**有效命中率可以接近单副本水平**。 关键看 router 策略: - 纯 round-robin:4 副本各自见到不相关请求,命中率约 1/4。 - cache-aware:router 用 Bloom filter 估每副本对当前 prefix 的命中长度,把同 prefix 请求送同副本 → 命中率接近单副本水平。 - sticky session by user:多轮 chat 同用户固定副本,命中率最高。 实测开 cache-aware 后命中率比 round-robin 高 30-50%。
  1. Cache-aware router 的实现思路?
答案 核心是 **近似 prefix 匹配**:router 不持有 RadixCache 副本,只持有每个 worker 的 cache 摘要(Bloom filter 或 radix tree 摘要)。 流程: (1) 每 worker 定期上报 cache 摘要(如每秒)。 (2) Router 收到请求,把 prompt 按 chunk hash(如每 64 token 一个 hash)。 (3) 对每个 worker 查 hash 命中数:`hits[w] = sum(1 for h in chunks if h in worker_w.bloom)`。 (4) 选 hits 最多的 worker;多 worker 命中相同 → 按 shortest_queue 兜底。 Bloom filter 有 false positive,所以是"近似"——但实际效果接近精确。Rust 实现见 [`sglang/rust/sgl_router/`](../sglang/rust/)。
  1. 我需要 200 QPS、单副本 max_running_requests=64、平均请求 1.5s,副本数?
答案 Little's Law: 平均 in-flight = QPS × T_e = 200 × 1.5 = 300。 单副本上限 64 → 副本数 = ⌈300 / 64⌉ = **5**。 加 30% safety margin(峰值突发 / 滚动重启)→ **6-7 副本**。 再加 KEDA 自动扩缩 + minReplicas=6 + maxReplicas=12,能扛 2× 峰值。 注意:T_e 是真实"在 server 端"的时长,含等待 + prefill + decode;用 `histogram_quantile(0.5, e2e_request_latency)` 估,而不是用 SLO 目标 TTFT 估。
  1. 单节点 8 卡,70B 模型,DP=2 TP=4 还是 TP=8 更划算?依据是什么?
答案 **取决于业务前缀复用度**: - **复用度高(chat / RAG / agent)**:TP=8 单副本,全 8 卡共享一份 RadixCache,命中率最高;AllReduce 在 NVLink 上仅 5-15% 时延占比可接受。 - **复用度低(独立单轮翻译 / 短问答)**:DP=2 TP=4,两个独立副本各扛一半 QPS,cache 命中率本就低、共享与否无差,副本故障隔离。 - **TTFT 卡死,TPOT 宽松**:TP=8 单副本时 TTFT 由整 8 卡承担 prefill,更快。 - **吞吐重要**:DP=2 TP=4,2 副本独立处理峰值流量,max_running_requests 翻倍。 实战 chat 业务 TP=8,独立 API 业务 DP=2 TP=4。

11. 下一步

上游源码:sglang/python/sglang/srt/managers/data_parallel_controller.py