跳到主要内容

MoE 混合专家模型推理参数与前向传播详解

· 阅读需 13 分钟
Zhiyuan Pan
Blog Author

以 DeepSeek-V2-Lite 为例,详细拆解 MoE 的每个参数含义、Router 路由决策、Token Permutation、Grouped GEMM、共享专家计算、MLA 注意力压缩与多 GPU 并行策略。

DeepSeek-V2-Lite 为参考模型,详细拆解 MoE 推理过程中每个参数的含义和前向传播流程。

1. DeepSeek-V2-Lite 架构总览

1.1 模型基本参数

hidden_size:           2048        # 隐藏层维度
num_hidden_layers: 27 # Decoder 层数
num_attention_heads: 16 # 注意力头数
vocab_size: 102400 # 词表大小
dtype: bfloat16

1.2 层结构:Dense 层 vs MoE 层

DeepSeek-V2-Lite 的 27 层不全是 MoE

first_k_dense_replace = 1     # 前 1 层使用 Dense FFN
moe_layer_freq = 1 # 之后每层都是 MoE

Layer 0: Attention + Dense FFN (intermediate_size = 10944)
Layer 1: Attention + MoE (64 routed experts + 2 shared experts)
Layer 2: Attention + MoE
...
Layer 26: Attention + MoE

共计: 1 层 Dense + 26 层 MoE

为什么第一层用 Dense? 第一层直接接 Embedding 输出,表征还不够丰富,路由决策质量低。用 Dense 层先做一次充分变换,后续层再做稀疏路由效果更好。

1.3 MoE 配置

n_routed_experts       = 64       # 路由专家总数
num_experts_per_tok = 6 # 每个 token 激活的专家数 (top-k)
n_shared_experts = 2 # 共享专家数(始终激活)
moe_intermediate_size = 1408 # 每个专家的中间维度
scoring_func = softmax # 路由评分函数
topk_method = greedy # top-k 选择方法
n_group = 1 # 专家分组数(Lite 不分组)
topk_group = 1 # 每组选择数
norm_topk_prob = false # 不重归一化路由权重
routed_scaling_factor = 1.0 # 路由输出缩放因子

1.4 MLA(Multi-head Latent Attention)配置

kv_lora_rank       = 512         # KV 压缩的 LoRA 秩
q_lora_rank = null # Lite 不压缩 Q(完整版 V2 = 1536)
qk_nope_head_dim = 128 # Q/K 非旋转部分维度
qk_rope_head_dim = 64 # Q/K 旋转位置编码部分维度
v_head_dim = 128 # V 头维度

1.5 DeepSeek-V2-Lite 完整 Decoder Layer 结构图

输入 hidden_states: (num_tokens, 2048)

├── input_layernorm (RMSNorm)

├── MLA Attention ─────────────────────────────────────────────┐
│ ├── kv_a_proj: (2048 → 512+64) KV 压缩 + rope 部分 │
│ ├── kv_a_layernorm (RMSNorm) │
│ ├── kv_b_proj: (512 → 16*(128+128)) 解压为 K_nope + V │
│ ├── q_proj: (2048 → 16*(128+64)) Q 直接投影 │
│ ├── RoPE (对 rope 部分) │
│ ├── Attention 计算 │
│ └── o_proj: (16*128 → 2048) │
│ │
├── + residual ←───────────────────────────────────────────────┘

├── post_attention_layernorm (RMSNorm)

├── MoE / Dense FFN ──────────────────────────────────────────┐
│ ├── [Layer 0] Dense FFN: │
│ │ gate_up_proj: (2048 → 2*10944) │
│ │ SiLU + mul │
│ │ down_proj: (10944 → 2048) │
│ │ │
│ ├── [Layer 1-26] MoE: │
│ │ ├── gate: (2048 → 64) Router │
│ │ ├── 64 Experts: 每个 (2048↔1408) SwiGLU FFN │
│ │ └── 2 Shared Experts: (2048↔2816) SwiGLU FFN │
│ └─────────────────────────────────────────────────────────┘

└── + residual

输出 hidden_states: (num_tokens, 2048)

2. MoE 层的全部参数(以 DeepSeek-V2-Lite 为例)

2.1 Router(门控网络)

gate.weight: (64, 2048)
属性
参数量64 × 2048 = 131K (0.13M)
有无 bias
作用将 token 映射为 64 个专家的分数

计算过程

router_logits = hidden_states @ gate.weight.T    # (num_tokens, 64)

Router 参数量极小(仅 0.13M),但决定了所有 token 的路由去向,是 MoE 中最关键的组件。

2.2 路由专家(Routed Experts)

每个路由专家是一个 SwiGLU FFN,包含 3 个权重矩阵:

单个 Expert 的参数:
├── w1 (gate_proj): (2048, 1408) # 门控投影
├── w3 (up_proj): (2048, 1408) # 上投影
└── w2 (down_proj): (1408, 2048) # 下投影

单个 Expert 参数量: 3 × 2048 × 1408 = 8.65M

vLLM 中所有专家堆叠存储以支持 Grouped GEMM:

实际存储 (FusedMoE):
├── w13: (64, 2*1408, 2048) # w1 和 w3 合并,64 个专家堆叠
└── w2: (64, 2048, 1408) # 64 个专家堆叠

64 个路由专家总参数量: 64 × 8.65M = 553.6M

2.3 共享专家(Shared Experts)

DeepSeek-V2-Lite 有 2 个共享专家,它们对所有 token 始终激活,不经过 Router。

2 个共享专家被合并为一个更大的 FFN:

shared_experts 参数:
├── gate_up_proj: (2048, 2 × 2 × 1408) = (2048, 5632)
└── down_proj: (2816, 2048)

shared_intermediate_size = n_shared_experts × moe_intermediate_size
= 2 × 1408 = 2816

共享专家参数量: 2 × 2048 × 2816 + 2816 × 2048 = 17.3M

为什么合并? 2 个小专家可以合并为 1 个 intermediate_size=2816 的 FFN,减少 kernel launch 次数。

2.4 单层 MoE 参数汇总

组件                    Shape                         参数量
──────────────────────────────────────────────────────────────
gate (Router) (64, 2048) 0.13M
64 路由专家 w13 (64, 2816, 2048) 368.9M
64 路由专家 w2 (64, 2048, 1408) 184.5M
共享专家 gate_up_proj (2048, 5632) 11.5M
共享专家 down_proj (2816, 2048) 5.8M
──────────────────────────────────────────────────────────────
单层 MoE 总参数: ~570.8M

但每个 token 实际计算量:
6 个路由专家: 6 × 8.65M = 51.9M
2 个共享专家: = 17.3M
Router: = 0.13M
─────────────────────────────────────────
每 token 计算量: ~69.3M (仅总量的 12.1%)

2.5 全模型参数构成

Embedding:            102400 × 2048                 = 209.7M
Layer 0 (Dense):
Attention: ~MLA 参数 ≈ 25M
Dense FFN: 2048×10944×3 = 67.2M
Layer 1-26 (MoE × 26):
Attention × 26: ~MLA 参数 ≈ 650M
MoE × 26: 570.8M × 26 = 14.84B
LM Head: 2048 × 102400 = 209.7M
──────────────────────────────────────────────────────────────
总参数量: ~15.7B
每 token 激活参数: ~2.4B(共享专家 + 6/64 路由专家 + Attention)

3. 完整前向传播流程

以处理 32 个 token 为例,详细跟踪每一步的张量变化。

3.1 Phase 1: Routing(路由决策)

# Step 1: 计算 router logits
router_logits = hidden_states @ gate.weight.T
# (32, 2048) × (2048, 64) → (32, 64)

# Step 2: Softmax 评分
# scoring_func = "softmax"
routing_weights = softmax(router_logits, dim=-1) # (32, 64) 概率分布

# Step 3: Greedy Top-6 选择
# topk_method = "greedy", num_experts_per_tok = 6
topk_weights, topk_ids = topk(routing_weights, k=6)
# topk_weights: (32, 6) — 6 个被选中专家的权重
# topk_ids: (32, 6) — 6 个被选中专家的编号 (0~63)

# Step 4: 不重归一化
# norm_topk_prob = false → 保持原始 softmax 概率,不除以 sum

具体示例(Token "深"):

router_logits: [0.3, -0.1, 0.8, ..., 1.2, 0.5, ...]  (64 个值)
softmax 后: [0.02, 0.01, 0.03, ..., 0.05, 0.02, ...] (和为 1.0)

Greedy top-6 选择:
Expert 47 (权重 0.053)
Expert 12 (权重 0.048)
Expert 3 (权重 0.041)
Expert 58 (权重 0.039)
Expert 21 (权重 0.036)
Expert 7 (权重 0.033)

注意: 6 个权重之和 = 0.25 (不是 1.0,因为 norm_topk_prob=false)

DeepSeek-V2-Lite 不使用分组 Top-k

n_group = 1, topk_group = 1 → 直接 greedy 选 top-6
(完整版 V2 使用 n_group=8, topk_group=3 的分组路由)

3.2 Phase 2: Token Permutation(Token 重排)

每个 token 被复制 6 份(top_k=6),按专家 ID 重新排列:

路由结果(简化为 4 token, 3 expert 示例):
Token 0 → Expert [2, 5, 11]
Token 1 → Expert [5, 2, 8]
Token 2 → Expert [11, 5, 2]
Token 3 → Expert [8, 2, 11]

Permute 后(按专家分组,连续排列):
Expert 2: [Token 0, Token 1, Token 2, Token 3] offset: 0→4
Expert 5: [Token 0, Token 1, Token 2] offset: 4→7
Expert 8: [Token 1, Token 3] offset: 7→9
Expert 11: [Token 0, Token 2, Token 3] offset: 9→12

expert_first_token_offset = [0, 4, 7, 9, 12] ← Grouped GEMM 的边界
inv_permuted_idx = [...] ← 恢复原始顺序的映射

张量变化(实际 32 token 场景):

输入:    hidden_states       (32, 2048)
复制 ×6: expanded (192, 2048) = 32 × 6
重排: permuted_hidden (192, 2048) 同大小,但按专家排序
expert_offsets (65,) 64 个专家 + 1 个终止位

3.3 Phase 3: Expert Computation(专家计算)

3.3.1 路由专家的 SwiGLU FFN

每个专家对分配给它的 token 执行:

# Expert i 处理 M 个 token
x = permuted_hidden[start:end] # (M, 2048)

# Step 1: gate_proj + up_proj(合并为一次矩阵乘法)
gate_up = x @ w13[i].T # (M, 2048) × (2048, 2816) → (M, 2816)
gate_out, up_out = gate_up.chunk(2) # 各 (M, 1408)

# Step 2: SwiGLU 激活
x = SiLU(gate_out) * up_out # (M, 1408)

# Step 3: down_proj
expert_out = x @ w2[i].T # (M, 1408) × (1408, 2048) → (M, 2048)

SwiGLU 激活函数详解

SwiGLU(gate, up) = SiLU(gate) × up
= (gate × σ(gate)) × up
= gate × sigmoid(gate) × up

其中 σ(x) = 1/(1+e^(-x)) 是 sigmoid 函数
SiLU(x) = x × σ(x),也称 Swish 激活

Grouped GEMM:实际不会逐专家循环,而是一次 kernel 处理所有专家:

输入:  permuted_hidden (192, 2048)   + expert_offsets (65,)
w13 (64, 2816, 2048)
w2 (64, 2048, 1408)

kernel 内部按 expert_offsets 分段:
Expert 0: offset[0]:offset[1] 的 token → w13[0], w2[0]
Expert 1: offset[1]:offset[2] 的 token → w13[1], w2[1]
...
Expert 63: offset[63]:offset[64] 的 token → w13[63], w2[63]

输出: permuted_output (192, 2048)

3.3.2 共享专家的计算

共享专家独立于路由,对所有 32 个 token 都计算:

# 共享专家 = 合并后的单个 SwiGLU FFN
# shared_intermediate_size = 2 × 1408 = 2816
x = hidden_states # (32, 2048)

gate_up = x @ shared_gate_up_proj.T # (32, 2048) × (2048, 5632) → (32, 5632)
gate_out, up_out = gate_up.chunk(2) # 各 (32, 2816)

x = SiLU(gate_out) * up_out # (32, 2816)

shared_out = x @ shared_down_proj.T # (32, 2816) × (2816, 2048) → (32, 2048)

3.4 Phase 4: Unpermute & Combine(加权合并)

# Step 1: 恢复原始 token 顺序 + 按路由权重加权
# 伪代码:
for token_id in range(32):
routed_output[token_id] = 0
for j in range(6): # top_k = 6
expert_id = topk_ids[token_id, j]
weight = topk_weights[token_id, j]
routed_output[token_id] += weight * expert_output_of(token_id, expert_id)

# routed_output: (32, 2048)

# Step 2: 合并路由输出 + 共享专家输出
# routed_scaling_factor = 1.0 → 无额外缩放
final_output = routed_output + shared_out # (32, 2048)

3.5 完整张量形状变化一览

阶段                         张量                              Shape
─────────────────────────────────────────────────────────────────────────
输入 hidden_states (32, 2048)

Phase 1: Routing
gate 线性层 router_logits (32, 64)
softmax + greedy top-6 topk_weights (32, 6)
topk_ids (32, 6)

Phase 2: Permute
token 复制 × top_k=6 permuted_hidden (192, 2048)
expert_offsets (65,)
inv_permuted_idx (192,)

Phase 3a: Routed Expert Compute (Grouped GEMM)
w13 (gate+up_proj) intermediate (192, 2816)
chunk → SiLU × up activated (192, 1408)
w2 (down_proj) expert_output (192, 2048)

Phase 3b: Shared Expert Compute (并行)
shared gate_up_proj intermediate (32, 5632)
chunk → SiLU × up activated (32, 2816)
shared down_proj shared_out (32, 2048)

Phase 4: Unpermute + Combine
加权合并路由结果 routed_output (32, 2048)
+ 共享专家 final_output (32, 2048)

4. MLA:DeepSeek 的注意力压缩

DeepSeek-V2-Lite 不仅在 FFN 用了 MoE,Attention 也有独特设计——MLA(Multi-head Latent Attention),大幅压缩 KV Cache。

4.1 标准 MHA vs MLA

标准 MHA (如 LLaMA):
KV Cache 大小 = num_heads × head_dim × 2 (K+V)
= 16 × 128 × 2 = 4096 per token

MLA (DeepSeek-V2-Lite):
KV Cache 大小 = kv_lora_rank + qk_rope_head_dim
= 512 + 64 = 576 per token

压缩比: 576 / 4096 = 14.1% → KV Cache 缩小到 1/7!

4.2 MLA 前向过程

# --- KV 路径: 压缩 → 解压 ---
# 1. 压缩: hidden_states → 低秩表示 + rope 部分
kv_a = kv_a_proj(hidden_states) # (N, 2048) → (N, 512+64)
kv_compressed, k_rope = split(kv_a, [512, 64])

# 2. LayerNorm
kv_compressed = kv_a_layernorm(kv_compressed) # (N, 512)

# 3. 解压: 低秩 → 全尺寸 K_nope 和 V
kv = kv_b_proj(kv_compressed) # (N, 512) → (N, 16*(128+128))
k_nope, v = split(kv, [16*128, 16*128])

# KV Cache 只存 kv_compressed (512) + k_rope (64) = 576 维!

# --- Q 路径: 直接投影 ---
q = q_proj(hidden_states) # (N, 2048) → (N, 16*(128+64))
q_nope, q_rope = split(q, [16*128, 16*64])

# --- RoPE ---
q_rope = apply_rope(q_rope)
k_rope = apply_rope(k_rope)

# --- 拼接 Q 和 K ---
q = cat([q_nope, q_rope]) # (N, 16, 192)
k = cat([k_nope, k_rope]) # (N, 16, 192)

# --- Attention ---
attn_output = scaled_dot_product_attention(q, k, v) # (N, 16, 128)
output = o_proj(attn_output) # (N, 2048)

5. 多 GPU 下的参数分布

5.1 Tensor Parallelism(TP)

切分每个专家的 intermediate 维度:

TP=2 时 (DeepSeek-V2-Lite):
GPU 0: w13[:, :1408, :] w2[:, :, :704] # 前半 intermediate
GPU 1: w13[:, 1408:, :] w2[:, :, 704:] # 后半 intermediate

每个 GPU 都有全部 64 个专家,但每个专家只有一半参数
最后 all-reduce 合并结果

5.2 Expert Parallelism(EP)

将不同专家分配到不同 GPU:

EP=2 时 (64 experts):
GPU 0: Expert 0~31 (完整参数)
GPU 1: Expert 32~63 (完整参数)

Token 路由到不在本地的专家 → All-to-All 通信

5.3 EP + TP 混合

EP=2, TP=2 时 (4 GPUs):
GPU 0: Expert 0-31 的前半 intermediate
GPU 1: Expert 0-31 的后半 intermediate
GPU 2: Expert 32-63 的前半 intermediate
GPU 3: Expert 32-63 的后半 intermediate

5.4 All-to-All 通信(EP 核心)

                 ┌─ GPU 0 (Expert 0-31)  ─┐
Token 分发 ──→ │ │ ──→ 结果收集
(All-to-All) └─ GPU 1 (Expert 32-63) ─┘ (All-to-All)

Step 1 (Dispatch): 每个 GPU 把路由到远程专家的 token 发出去
Step 2 (Compute): 每个 GPU 用本地专家处理收到的 token
Step 3 (Combine): 每个 GPU 把结果发回给原始 token 所在的 GPU

共享专家与 All-to-All 重叠

时间线:
├─ All-to-All dispatch ─┤─ Expert compute ─┤─ All-to-All combine ─┤
├─ Shared expert compute ───────────────────┤
(共享专家计算与通信并行,隐藏延迟)

6. Router 的负载均衡

6.1 问题:专家负载不均

64 个专家,如果所有 token 都涌向少数几个 → 热门专家过载,其余空闲。

6.2 辅助损失(训练时)

DeepSeek-V2-Lite 使用 sequence-level 辅助损失(seq_aux=true, aux_loss_alpha=0.001):

# 计算每个专家收到的 token 比例(在序列级别)
expert_load = (topk_ids == expert_id).float().mean(dim=0)

# 计算每个专家的平均路由权重
expert_importance = routing_weights[:, expert_id].mean()

# 辅助损失: 鼓励均匀分配
aux_loss = n_routed_experts * (expert_load * expert_importance).sum()
total_loss = main_loss + 0.001 * aux_loss

6.3 EPLB(推理时,vLLM 特有)

Expert Parallel Load Balancing:动态复制热门专家:

原始: 64 experts + N redundant slots
如果 Expert 12 过载:
Physical 0-63: 原始 64 个 expert
Physical 64: Expert 12 的副本 ← 冗余副本分担负载
Physical 65: Expert 47 的副本
...

7. DeepSeek-V2-Lite vs 完整版 V2 vs V3

特性V2-LiteV2 (236B)V3 (671B)
hidden_size204851207168
层数276061
n_routed_experts64160256
top_k668
n_shared_experts221
moe_intermediate_size140815362048
n_group (分组路由)1 (不分组)88
topk_group134
q_lora_ranknull (不压缩)15361536
kv_lora_rank512512512
routed_scaling_factor1.01.016.0
总参数~15.7B236B671B
激活参数~2.4B~21B~37B

V2-Lite 的简化

  • 不分组路由(n_group=1)→ 直接 greedy top-6
  • 不压缩 Q(q_lora_rank=null)→ 直接投影
  • 无路由缩放(routed_scaling_factor=1.0

8. MoE 的量化

MoE 参数量大部分在 Expert FFN,量化收益显著:

量化方式Expert 权重精度V2-Lite 估计大小
BF1616-bit~31.4 GB
FP8 (W8A8)8-bit~15.7 GB
INT4 (W4A16)4-bit~7.8 GB

Router (gate) 通常不量化——仅 0.13M 参数,但决定路由质量。

9. 源码索引

组件vLLM 文件位置
DeepSeek-V2 模型定义vllm/.../models/deepseek_v2.py
DeepseekV2MoE 类deepseek_v2.py:235-398
DeepseekV2MLP (Dense FFN)deepseek_v2.py:188-232
MLA Attentiondeepseek_v2.py:537-581
FusedMoE 主模块vllm/.../layers/fused_moe/layer.py
SharedFusedMoE (共享专家)vllm/.../fused_moe/shared_fused_moe.py
Router / GateLinearvllm/.../fused_moe/router/gate_linear.py
Token Permute/Unpermutevllm/.../fused_moe/moe_permute_unpermute.py
Expert 并行分配layer.pydetermine_expert_map()
CUDA MoE kernelsvllm/csrc/moe/
HuggingFace 配置transformers/.../deepseek_v2/configuration_deepseek_v2.py

10. 一句话总结

DeepSeek-V2-Lite 的 MoE 推理 = Router 从 64 个专家中 greedy 选 6 个 → Token 按专家分组 Grouped GEMM → 加权合并 + 2 个共享专家输出。15.7B 总参数中每个 token 只激活 ~2.4B,配合 MLA 的 KV Cache 压缩(7 倍),实现了高效推理。