跳到主要内容
推理优化

6.4 数据并行与专家并行

从 Dense 模型的 DP 副本讲到 MoE 的注意力 DP + 专家 EP,分析 Dispatch/Combine 的 All-to-All、空跑同步、专家负载不均与 EPLB 冗余专家,结合 DeepEP 与 DeepSeek-V3 的部署方案给出 vLLM 配置

数据并行 专家并行 MoE All-to-All EPLB

TP 和 PP 都是在切一个副本。吞吐不够、单副本已经满足延迟时,最简单的办法是多开几个副本,这就是 DP。对 Dense 模型,DP 几乎没什么可讲的:副本之间互不通信,难点在负载均衡。

MoE 模型的情况不同。DeepSeek-V3 的 671B 参数里,约 97% 是路由专家,而每个 Token 只用到其中 8 个。专家适合按个分到不同的卡上(EP),注意力则更适合每张卡一份、各算各的请求(DP)。两者拼在一起,就成了现在大 MoE 推理的主流布局。本节先讲 DP,再讲 EP 的通信、同步和负载均衡,最后看 DeepSeek-V3 的实际部署。

⏰ 信息时效:vLLM 相关描述基于 v0.32.0(2026-10-18 发布)文档与源码;DeepEP 性能数据取自其 v1.2.1 README;DeepSeek-V3 部署方案取自其技术报告(arXiv 2412.19437)。

📑 目录


1. Dense 模型的 DP:多开几家分店

1.1 副本之间不通信

DP 就是 6.1 节比喻里的”开分店”:每家店有完整的一套厨具,各接各的客人。每个副本存完整的模型(或者一个完整的 TP/PP 组),独立做调度、独立管理 KV Cache。

Dense 模型的前向中,副本之间没有任何同步,吞吐随副本数近似线性增长。它不省显存,前提是一个副本已经装得下。

1.2 难点在请求怎么分

副本之间不通信,请求怎么分就决定了效果:

  • 负载:某个副本积压了一堆长 Prompt,它的 TTFT 会变差,其他副本却闲着。按”正在运行 + 排队”的请求数分配比轮询好。
  • 前缀缓存:每个副本的 KV Cache 是独立的。同一个 System Prompt 的请求如果被打散到不同副本,每个副本都要各算一遍前缀。把共享前缀的请求尽量路由到同一个副本,能提高第 2 章 2.3 节讲过的前缀缓存命中率。

1.3 vLLM 里的两种做法

对 Dense 模型,最直接的做法是启动多个独立的 vllm serve,前面放一个负载均衡器。vLLM 文档对 Dense 模型也是这样建议的:外部负载均衡模式只支持 MoE,Dense 模型应该启动不带 --data-parallel-* 参数的独立实例。

也可以让 vLLM 自己管:

# 一台 8 卡机器:4 个副本,每个副本 TP=2,对外一个端口
vllm serve $MODEL --data-parallel-size 4 --tensor-parallel-size 2

这时对外只有一个 HTTP 端口。API Server 进程数默认等于 DP 大小(这里是 4 个),它们根据各副本的运行队列和等待队列分配请求。注意 --max-num-seqs 是每个 DP rank 的上限,而 --max-num-queued-reqs 是整个服务的上限,设置时要按副本数换算。


2. MoE 为什么要按专家切

2.1 参数几乎都在专家里

以 DeepSeek-V3 为例(h=7168h=7168,61 层,前 3 层 Dense、后 58 层 MoE,每层 256 个路由专家加 1 个共享专家,专家中间维 2048)。一个专家是 gate、up、down 三个矩阵:

3×7168×2048≈44M 参数3\times7168\times2048\approx44\text{M 参数}

58 层 × 256 个路由专家,合计约 654B,占 671B 总参数的 97%。而每个 Token 在每层只激活其中 8 个,激活参数只有 37B。

所以 MoE 推理的显存几乎全花在一大堆”大部分时间闲着”的专家上。怎么摆放这些专家,是部署要解决的主要问题。

2.2 用 TP 切专家的问题

最直接的办法是把每个专家都按 TP 切开,和 Dense 的 FFN 一样。这有两个问题:

  • 矩阵切得太碎:专家中间维本来就只有 2048,TP=8 时每张卡只剩 256 列。这么小的 GEMM 很难用满 Tensor Core。
  • 每张卡都要处理所有 Token:每个专家都分布在所有卡上,所以不管 Token 选了哪个专家,每张卡都要参与计算和 AllReduce。

2.3 EP:每个专家整块放在一张卡上

EP 换了个思路:专家不切,整块分到不同的卡上。EP=8 时每张卡放 32 个完整的专家,EP=32 时每张卡 8 个。

按 FP8 权重算,DeepSeek-V3 的路由专家在 EP=8 时每卡约 82 GB,H100 80GB 放不下,所以 vLLM 文档的单机 EP 示例用的是 H200 或 H20;EP=32 时降到每卡约 20 GB。

代价是 Token 要”出差”:每个 Token 必须被送到它选中的专家所在的卡上,算完再送回来。这就是 6.1 节比喻里的”按菜系分档口”。


3. Dispatch 和 Combine:两次 All-to-All

3.1 一个 MoE 层里的数据流

每个 MoE 层分四步:

  1. 路由:每张卡对自己的 Token 算 Router,得到每个 Token 的 top-kk 专家和权重。
  2. Dispatch:按路由结果,把每个 Token 的隐藏状态复制 kk 份,发给对应专家所在的卡。每张卡既发也收,这是一次 All-to-All。
  3. 专家计算:每张卡用自己的专家处理收到的 Token。
  4. Combine:把结果按原路送回 Token 所在的卡,按路由权重加权求和,又是一次 All-to-All。

下图是 EP=4、8 个专家、每个 Token 选 2 个的示意,颜色表示 Token 来自哪张卡。左边是注意力之后各卡自己的 Token,右边是 Dispatch 之后每张卡收到的 Token:

EP=4 下的 Dispatch:4 张 GPU 各有 4 个 Token,每个 Token 按路由结果复制发往 2 个专家所在的卡;GPU0 上的 E0、E1 收到 12 份,GPU3 上的 E6、E7 只收到 4 份,这一层要等最忙的 GPU0 算完

GPU0 收到 12 份,GPU3 只有 4 份。Combine 要等所有卡都算完,这一层的耗时由 GPU0 决定,下文第 5 部分专门讨论这个问题。

3.2 通信量

每个 Token 在每个 MoE 层的往返数据量约为 2×k×h×b2\times k\times h\times b。DeepSeek-V3 的 k=8k=8,h=7168h=7168:

📊 方向精度每 Token 每 MoE 层
DispatchFP88×7168×1 B≈57 KB8\times7168\times1\ \text{B}\approx57\ \text{KB}
CombineBF168×7168×2 B≈115 KB8\times7168\times2\ \text{B}\approx115\ \text{KB}

Dispatch 用 FP8 传,是 DeepSeek-V3 和 DeepEP 的做法,比全程 BF16 省了四分之一。目标专家恰好在本卡上的那部分不需要过网络。

和 TP 比,EP 的通信只发生在 MoE 层,每层 2 次;但每次的数据量和 kk 成正比,并且是多对多的不规则流量,跨节点时对网络的压力比 PP 大得多。


4. 注意力 DP + 专家 EP

4.1 注意力为什么走 DP

专家用 EP 切开以后,注意力层还要选一种布局。对 DeepSeek-V3 这类 MLA 模型,TP 不划算:6.2 节说过,MLA 只缓存一份压缩后的潜向量,TP 下每张卡都要存完整的潜向量 KV,KV 显存随 TP 成倍浪费。

DP 正好避开这个问题:每张卡放一份完整的注意力权重,只处理分给自己的那批请求,也只存这些请求的 KV。注意力权重在 MoE 模型里只占很小一部分,复制几份的代价不大。

vLLM 文档也是这样建议的:MoE 模型,尤其是使用 MLA 的 DeepSeek 系列,注意力层用 DP,专家层用 EP 或 TP。

4.2 EP 的大小由 TP × DP 决定

vLLM 里没有单独的 EP 大小参数。打开 --enable-expert-parallel 后:

EP_SIZE=TP_SIZE×DP_SIZE\text{EP\_SIZE}=\text{TP\_SIZE}\times\text{DP\_SIZE}
📊 层TP=1、DP=8(8 卡)TP=2、DP=4(8 卡)
注意力每卡一份完整权重,8 个 DP 组每个 DP 组内 TP=2,共 4 组
MoE 专家EP=8,专家分到 8 张卡EP=8,专家分到 8 张卡

不加 --enable-expert-parallel 时,MoE 层默认组成一个大小为 TP × DP 的 TP 组,每个专家都被切开,就回到了上文 2.2 小节的问题。

4.3 DP rank 不再独立:空跑也得来

Dense 模型的 DP 副本互不相干,MoE 的 DP rank 却必须步调一致。All-to-All 需要所有 rank 一起参与,如果某张卡这一步没有请求,其他卡发给它的 Token 没人收,整组都会卡住。

所以 vLLM 规定:只要任何一个 rank 还有请求在跑,没有请求的 rank 也要执行一次空的(dummy)前向,陪其他卡把每个 MoE 层的通信走完。下图中 GPU3 暂时没有请求,但仍要参与 Dispatch 和 Combine:

DP=4 注意力 + EP=4 专家的一层:GPU0 到 GPU2 各自处理自己的请求,GPU3 没有请求但必须空跑,所有卡一起进入 Dispatch 和 Combine 两次 All-to-All,然后各自进入下一层注意力

协调这件事的是单独的 DP Coordinator 进程。它和所有 rank 通信,并每隔若干步做一次集合操作,确认是否所有 rank 都空闲了,全部空闲时才一起暂停。源码中这个间隔由 dp_sync_interval 控制,默认 16 步。

📌 关键点:这带来两个后果。第一,各 DP rank 之间的请求要尽量分均匀,否则请求少的卡大部分时间在空跑。第二,整组的每一步都要等最慢的那个 rank,某个 rank 上塞了长 Prefill,所有 rank 的 Decode 都会被拖慢。

4.4 启动命令

单节点 8 卡(vLLM 文档的示例,需要 H200 或 H20 这类显存更大的卡):

vllm serve deepseek-ai/DeepSeek-V3-0324 \
    --tensor-parallel-size 1 \
    --data-parallel-size 8 \
    --enable-expert-parallel

每个 DP rank 是一个独立的 EngineCore 进程,有自己的调度器和 KV Cache,通过 ZMQ 和前端的 API Server 通信。多节点的写法见 6.5 节。


5. 负载不均:最忙的专家决定速度

5.1 为什么会不均

训练时通常会加负载均衡的辅助损失,或者像 DeepSeek-V3 那样用无辅助损失的偏置项,让各专家收到的 Token 数大致相当。但这是在训练数据的整体分布上平衡的。推理时,一段时间内的请求可能集中在某个领域(比如都是代码),热门专家收到的 Token 会远多于平均。

EP 下每层要等所有卡做完专家计算才能 Combine,所以一层的耗时取决于收到 Token 最多的那张卡。衡量的指标是”最忙的卡 ÷ 平均”,等于 1 表示完全均衡。

5.2 动手模拟:EP 越大越不均

下面的脚本假设专家热度服从幂律分布(指数 0.4 是假设值,不对应任何真实模型的统计),按 DeepSeek-V3 的 256 选 8 路由一步 4096 个 Token,比较三种放置方式:

# 模拟 MoE 的负载不均:专家热度服从幂律(假设分布),比较三种专家放置方式下
# 最忙的卡比平均多出多少。只统计 Token 数,不考虑通信和计算效率。
import random

random.seed(0)
EXPERTS, TOP_K, TOKENS = 256, 8, 4096      # DeepSeek-V3 式路由:256 选 8;一步 4096 个 Token
SKEW = 0.4                                  # 幂律指数,0 为完全均匀,假设值

weights = [1 / (i + 1) ** SKEW for i in range(EXPERTS)]
random.shuffle(weights)                     # 热门专家随机散落在各个编号上
counts = [0] * EXPERTS
for _ in range(TOKENS):
    chosen = set()
    while len(chosen) < TOP_K:              # 每个 Token 选 8 个不同的专家
        chosen.add(random.choices(range(EXPERTS), weights=weights)[0])
    for e in chosen:
        counts[e] += 1


def contiguous(ep):
    """默认放置:专家按编号连续分到各卡"""
    per = EXPERTS // ep
    load = [sum(counts[g * per:(g + 1) * per]) for g in range(ep)]
    return max(load) / (sum(load) / ep)


def balanced(ep, redundant):
    """按统计到的负载重排:先把冗余名额给"每份副本负载"最高的专家,
    再把所有副本从重到轻依次放到还有空位、当前最闲的卡上"""
    replicas = [1] * EXPERTS
    for _ in range(redundant):
        e = max(range(EXPERTS), key=lambda i: counts[i] / replicas[i])
        replicas[e] += 1
    copies = sorted((counts[e] / replicas[e] for e in range(EXPERTS) for _ in range(replicas[e])), reverse=True)
    slots = (EXPERTS + redundant) // ep
    load, used = [0.0] * ep, [0] * ep
    for c in copies:
        g = min((g for g in range(ep) if used[g] < slots), key=lambda g: load[g])
        load[g] += c
        used[g] += 1
    return max(load) / (sum(load) / ep)


avg = TOKENS * TOP_K / EXPERTS
print(f"最热专家 {max(counts)} 份 Token,最冷 {min(counts)} 份,平均 {avg:.0f} 份")
print(f"{'EP':>4} {'每卡专家':>6} {'连续放置':>7} {'按负载重排':>7} {'每卡+1冗余':>8}")
for ep in [8, 32, 64, 256]:
    print(f"{ep:>4} {EXPERTS // ep:>8} {contiguous(ep):>10.2f}x {balanced(ep, 0):>11.2f}x {balanced(ep, ep):>11.2f}x")

输出:

最热专家 640 份 Token,最冷 67 份,平均 128 份
  EP   每卡专家    连续放置   按负载重排   每卡+1冗余
   8       32       1.14x        1.01x        1.00x
  32        8       1.41x        1.17x        1.00x
  64        4       1.93x        1.64x        1.00x
 256        1       5.00x        5.00x        1.01x

在这组假设下:

  • EP 越大越不均。EP=8 时每卡有 32 个专家,冷热互相抵消,最忙的卡只比平均多 14%;EP=256 时每卡 1 个专家,最热专家的 640 份 Token 全压在一张卡上,是平均的 5 倍。
  • 只重排、不加副本,EP 大时没用。每卡只有 1 个专家时没有可挪的余地,重排前后都是 5.00 倍。
  • 每卡多放 1 个冗余专家,几乎完全均衡。热门专家有了多个副本,Token 分给各副本,最忙的卡降到平均的 1.01 倍以内。

⚠️ 注意:这个模拟用同一步的统计来重排,相当于提前知道了这一步的路由,是理想情况。真实系统只能根据过去一段时间的统计去调整,流量变化时会有滞后。

5.3 EPLB:冗余专家加定期重排

vLLM 的 Expert Parallel Load Balancer(EPLB)就是上面”冗余 + 重排”的思路:每次前向都记录各专家的负载,定期按统计结果重新分配专家到各卡,热门专家可以多放几份。

vllm serve deepseek-ai/DeepSeek-V3-0324 \
    --tensor-parallel-size 1 \
    --data-parallel-size 8 \
    --enable-expert-parallel \
    --enable-eplb \
    --eplb-config '{"window_size":1000,"step_interval":3000,"num_redundant_experts":8,"log_balancedness":true}'
📊 参数默认值含义
window_size1000统计最近多少步的负载
step_interval3000每隔多少步重排一次
num_redundant_experts0全局额外放多少个冗余专家
log_balancednessfalse打印均衡度(平均 ÷ 最大,越接近 1 越好)
use_asynctrue非阻塞地搬运专家权重

几个要点:

  • --enable-eplb 必须和 --enable-expert-parallel 一起用,否则启动时报错。
  • 加冗余后,每张卡的专家数是 (总专家数+冗余数)/EP(\text{总专家数}+\text{冗余数})/\text{EP}。上面的命令是 (256+8)/8=33(256+8)/8=33 个。
  • 冗余专家要占显存。按 vLLM 文档的估算,DeepSeek-V3 每张卡多放 1 个冗余专家约需 2.4 GB,与上文 2.1 小节的算法一致:58 层 × 44M 参数,FP8 下约 2.55 GB(2.4 GiB)。KV 空间紧张时要权衡。
  • 大规模多节点部署时,vLLM 文档建议设置 num_redundant_experts 为 32,保证热门专家总有副本可用。

💡 提示:先开 log_balancedness 看一段时间的均衡度,再决定要不要开冗余、开多少。均衡度已经接近 1 时,冗余专家只是白占显存。


6. 通信 Kernel:DeepEP 与 vLLM 的后端

6.1 DeepEP 的两套 Kernel

DeepSeek 开源的 DeepEP 是专为 MoE Dispatch/Combine 写的通信库,提供两类 Kernel,对应 Prefill 和 Decode 的不同需求:

  • Normal Kernel:吞吐优先。跨节点时先走 RDMA 到目标节点,再在节点内经 NVLink 转发,把跨机流量合并到最少。适合 Prefill 这种一次几千个 Token 的大消息。
  • Low-latency Kernel:延迟优先。纯 RDMA,支持 CUDA Graph;通信和计算的重叠通过 Hook 实现,不占用 SM。适合 Decode 这种每步几十到几百个 Token 的小消息。

DeepEP v1.2.1 README 给出的数据(H800,每卡一块 400 Gb/s CX7 网卡):

📊 Normal Kernel(4096 Token/批)Dispatch 带宽Combine 带宽
节点内 EP=8153 GB/s(NVLink)158 GB/s(NVLink)
跨节点 EP=1643 GB/s(RDMA)43 GB/s(RDMA)
跨节点 EP=3258 GB/s(RDMA)57 GB/s(RDMA)
跨节点 EP=6451 GB/s(RDMA)50 GB/s(RDMA)
📊 Low-latency Kernel(128 Token/批)Dispatch 延迟Combine 延迟
EP=877 µs114 µs
EP=32155 µs273 µs
EP=64173 µs314 µs
EP=256194 µs360 µs

测试设置是 hidden 7168、top-8,Dispatch 用 FP8、Combine 用 BF16。Low-latency Kernel 的延迟从 EP=64 到 EP=256 只多了约 20 µs(Dispatch)和 50 µs(Combine),这是 Decode 能用大 EP 的前提。

6.2 vLLM 的 All-to-All 后端

vLLM 用 --all2all-backend 选择 EP 的通信实现,文档列出的常用选项:

📊 后端适用场景
allgather_reducescatter(默认)通用,任何 EP + DP 配置都能用,用 AllGather / ReduceScatter 实现
deepep_high_throughput多节点、Prefill 为主,对应 DeepEP Normal Kernel
deepep_low_latency多节点、Decode 为主,支持 CUDA Graph,对应 DeepEP Low-latency Kernel
flashinfer_nvlink_one_sided / flashinfer_nvlink_two_sided机柜级 NVLink(MNNVL)系统,如 GB200 NVL72

DeepEP 后端需要先安装 DeepEP 和 DeepGEMM。文档还提醒,DeepEP 的两类 Kernel 是为 PD 分离设计的,Prefill 和 Decode 混跑时性能可能不理想。PD 分离的部署里,Prefill 实例用 deepep_high_throughput,Decode 实例用 deepep_low_latency(第 7 章)。

另一个相关选项是 --enable-dbo(Dual Batch Overlap):把一个 Batch 拆成两个,一个在做 All-to-All 时另一个在算,用计算掩盖通信,思路和下文 DeepSeek-V3 案例中的双 Micro-batch 重叠相同。

⚠️ 注意:较早版本里的 pplx、naive 后端在 v0.32.0 中已移除,配置成这两个值会打印警告并回退到 allgather_reducescatter。网上的旧教程里还能看到它们。


7. 案例:DeepSeek-V3 的推理部署

DeepSeek-V3 技术报告公开了它的线上推理布局,是本节各项技术的一个完整组合。Prefill 和 Decode 分开部署(PD 分离),两者的并行方式不同:

📊PrefillDecode
最小部署单元4 节点、32 张 H80040 节点、320 张 H800
注意力TP4 + SP,DP8TP4 + SP,DP80
MoE 专家EP32EP320
每卡专家8 个路由专家 + 1 个冗余专家1 个专家
冗余与共享专家32 个冗余专家64 张卡专门放冗余专家和共享专家
通信跨节点 IB、节点内 NVLink 转发点对点 IB 直传,使用 IBGDA 降低延迟
重叠两个 Micro-batch 交替,一个的注意力/MoE 计算掩盖另一个的 Dispatch/Combine同样在研究双 Micro-batch 重叠

几点和前文对应:

  • Prefill 的 EP 小、Decode 的 EP 大。Prefill 一次处理大量 Token,每个专家都有足够的工作量,EP32 够用;Decode 每步 Token 少,EP320 让每张卡只管 1 个专家,每卡读的专家权重最少,降低每步延迟。这和上文 DeepEP Low-latency Kernel 的数据一致:EP 从 64 加到 256,Dispatch 延迟增加得很少。
  • 冗余专家来自负载统计。Prefill 阶段按线上统计的负载挑出高负载专家做冗余,并大约每 10 分钟根据统计调整一次。这就是上文 5.3 小节 EPLB 做的事。
  • 共享专家当路由专家用。Decode 时共享专家被视为一个必选的路由专家,所以每个 Token 实际选 9 个专家。
  • 注意力是 TP4 + DP,而不是纯 DP。技术报告只说 TP 取 4 这样小的值可以限制 TP 通信开销,没有解释为什么不用纯 DP。一个合理的理解是:TP4 的组很小,能留在节点内,同时减少每张卡上的注意力权重和计算。

⚠️ 注意:这是 DeepSeek 自研推理系统在 H800 上的方案,不是 vLLM 的默认配置。用 vLLM 部署时,可以照着这个思路组合 --data-parallel-size、--enable-expert-parallel、--all2all-backend 和 EPLB,但具体数字要结合自己的卡数和负载实测。


📝 总结

  • DP 复制整个副本,Dense 模型的副本之间零通信,难点在请求分配;--max-num-seqs 按每个 DP rank 计。
  • MoE 的参数几乎都在路由专家里(DeepSeek-V3 约 97%),用 TP 切专家会把矩阵切得太碎,EP 把每个专家整块放在一张卡上。
  • EP 每个 MoE 层两次 All-to-All:Dispatch 把 Token 送到专家所在的卡,Combine 送回并加权求和,每 Token 每层的往返量约 2khb2kh b。
  • MoE 的常见布局是注意力 DP + 专家 EP,vLLM 中 EP 大小等于 TP × DP。所有 DP rank 必须步调一致,没有请求的 rank 也要空跑。
  • 一层的耗时由收到 Token 最多的卡决定,EP 越大越不均;EPLB 用冗余专家和定期重排缓解,代价是显存。
  • DeepEP 的 Normal Kernel 适合 Prefill、Low-latency Kernel 适合 Decode,对应 vLLM 的 deepep_high_throughput 和 deepep_low_latency。

🎯 自我检验清单

  • 能说明 Dense 模型 DP 和 MoE 模型 DP 的区别,以及 MoE 下为什么需要空跑
  • 能算出 DeepSeek-V3 一个专家的参数量、路由专家的总占比,以及给定 EP 下每卡的专家权重
  • 能写出 Dispatch 和 Combine 每 Token 每层的数据量,并说明 Dispatch 用 FP8 的好处
  • 能解释 MLA 模型的注意力为什么更适合 DP 而不是 TP
  • 能根据 TP 和 DP 算出 vLLM 中的 EP 大小,以及开启冗余后每卡的专家数
  • 能解释 EP 越大负载越不均的原因,以及冗余专家为什么比单纯重排更有效
  • 能为多节点 Prefill 和 Decode 分别选出 vLLM 的 --all2all-backend

📚 参考资料