Skip to content

Fused MoE V2 优化速览

这篇文档把 Ling-2.6-1T MoE serving 优化拆成三层: 先看普通 MoE / EPMoE 的数据排布,再看 Fused MoE V1 为什么要把 scatter、expert FFN、gather 放进同一个 Pallas kernel,最后看 Fused MoE V2 怎样把 token、weight、output accumulator 的生命周期改成一条 TPU 上更容易调度的流水线。

核心结论可以先记住一句:V2 的收益不来自更少的 MoE FLOPs,而来自把通信、HBM 读写、VMEM 常驻、VPU scale / activation、MXU dot 放到同一个 kernel 时间线里, 让能被隐藏的成本尽量离开 critical path。

1. MoE 层在做什么

MoE 层可以看成 router + routed expert FFN + weighted sum

Ling-2.6-1T 的典型形状:

数值
routed experts256
top-k8
hidden8192
intermediate2048
EP size32
每个 device 持有的 local experts8
16384-token prefill 下 local tokens512

EP=32 时,一个 token 选中的 8 个 expert 很可能分布在不同 device 上。 所以每层 MoE 不只是 FFN 计算,还天然包含一次 token scatter 和一次 output return / gather。优化的重点会落到两个问题:

  • token 怎么按 expert 重排、跨 chip 发送、再放回原 token 顺序。
  • expert FFN 怎么在不等通信、不等 HBM 的情况下持续喂给 MXU。

2. EPMoE baseline:permute、GMM、unpermute

普通 EPMoE 的代码结构贴近 MoE 的数学定义:

text
_permute -> _dispatch -> _gmm_compute -> _unpermute -> psum / combine

它先根据 router 结果把 token/top-k slots 按 expert 聚到一起,让 GMM 可以按 expert group 连续计算;GMM 完成后,再把 expert-sorted output 放回原来的 token/top-k slots,最后按 routing weight 做加权求和。

permute / unpermute 到底在排什么

用一个 top_k=2 的小例子说明。原始 flatten 后是 token-major:

原 slottokentop-k slotselected expert
0T0k0E2
1T0k1E0
2T1k0E1
3T1k1E2
4T2k0E0
5T2k1E1
6T3k0E2
7T3k1E1

_permute 之后变成 expert-major:

text
E0 group: slot1, slot4
E1 group: slot2, slot5, slot7
E2 group: slot0, slot3, slot6

GMM 在这个顺序里连续处理每个 expert group。_unpermute 再用 inverse permutation 把输出放回 slot0..slot7,然后 reshape 成 [tokens, top_k, hidden],按 top-k routing weight 做 weighted sum。

实现上,_permute 不一定物化完整的 sorted token tensor。它主要生成 token_indicessorted_selected_expertsgroup_sizes,真正读取 token 可以放在 GMM 里做,例如 inputs_2d[token_indices]。这样可以减少一段大的 [T * top_k, hidden] 中间物化。

EPMoE 的优点是表达清楚、通用性强;瓶颈是通信、GMM、unpermute、psum 被多个 较粗粒度 op 串起来,kernel 很难同时看到 ICI、HBM、VMEM、VPU 和 MXU 的完整 时间线。

3. TPU v7 MXU mental model

TPU 的数据流可以简化成:

text
HBM -> VMEM -> MXU / VPU -> VMEM -> HBM

MXU 是 systolic array。矩阵乘 C[M,N] = A[M,K] x B[K,N] 进入 MXU 时, 可以把 A 看成从一侧流入的 activation / token tile,把 B 看成从另一侧流入的 weight tile,C 是持续累加的 output accumulator。

Google Cloud TPU 架构文档里的硬件事实可以用来建立直觉: TPU v6e 和 TPU7x 预览版的 MXU 是 256 x 256 systolic array,v6e 之前是 128 x 128;MXU 提供 TensorCore 里的主要计算能力;乘法输入是 bfloat16, 累加是 FP32。

让 MXU 更忙的四个条件

条件含义
tile 要够大BT/BK/BN 要摊薄 systolic fill/drain、vmatmul setup、scale 和 reorder 的固定成本。
复用侧要常驻常驻的不一定永远是 A;谁复用次数高、放得下、能减少关键路径等待,就优先留在 VMEM。
C 要常驻output accumulator 留在 VMEM,K / bf 循环里只累加,最后一次性写回 HBM。
流动侧要预取当前 tile 在 MXU 上计算时,下一块 streamed tile 用 DMA 进另一个 VMEM slot。

对 Fused MoE V2 来说,常驻侧通常是 token tile 和 output accumulator; weight tile 更大、随 expert/projection 变化更快,所以用 double buffer 连续流入。

好的 tile 不只是 K 对齐

对 256 x 256 MXU 来说,K 最好是 256 的整数倍,但这只是必要条件之一。 M/N 决定阵列有多少行列可以同时工作,K 决定这段工作可以持续多久,总 FLOPs 决定固定成本能不能被摊薄。

tile 形状利用率直觉原因
[256,1024] x [1024,256]M/N 覆盖 256 x 256 阵列,K 有 4 个 256-wide wave。
[512,1024] x [1024,512]可以分成多个 256 x 256 output tiles,每个 tile 都有足够 K 流水。
[32,8192] x [8192,256]K 很长,但 M 只有 32,阵列行方向吃不满;MoE decode 常见。
[256,129] x [129,256]M/N 看起来够大,但 K 太短且不对齐,padding/setup 占比高。

TPU v7 microbenchmark 的规律也支持这个判断:独立 GEMM 在几十 GFLOPs 以下很容易被 fixed cost 主导;超过约 0.1T FLOPs 后利用率才明显健康;同时要有足够的 K_tiles_256 和足够大的 M/N output face。Fused MoE 的单 expert tile 通常不是 天然接近 dense GEMM peak 的形状,因此 V2 的重点是 pipeline scheduling 和 overlap, 不是宣称每个小 expert GEMM 都能打满峰值。

4. Fused MoE V1:先拿到调度权

V1 的第一步不是改变 MoE 数学结构,而是改变执行边界:把 scatter、expert FFN、 gather 收进同一个 Pallas kernel。这样 kernel 才能同时安排 ICI 通信、HBM 读写、 VMEM staging、MXU dot 和 VPU activation。

V1 gather 是什么

这里的 gather 不是单纯的本地数组 gather。scatter 把每个 token/top-k slot 送到 持有目标 expert 的 device;expert 计算完成后,gather 是这条路径的回程:

  • 把每个 routed expert 的输出从 expert-local scratch 送回原 token 所在的位置。
  • 按原始 token/top-k slot 顺序做本地重排。
  • 根据 routing weight 做 top-k 加权聚合,形成 T x hidden 输出。

因此它既包含跨 device 的 return / all-to-all-like 数据移动,也包含本地 gather 和 weighted sum。名字叫 gather,是因为从 token 视角看,它在收回自己的 top-k expert outputs;从网络视角看,它是 scatter 的反向数据搬运。

V1 block config 控制什么

V1 的 config 同时给外层 token 生命周期和内层 FFN 生命周期定颗粒度:

旋钮控制对象影响
btlocal token tile一次让多少 local tokens 进入 fused kernel。太小会让 scatter/gather 和 metadata 过碎,太大推高 scratch 上界。
bts / btcexpert 内 token staging / compute M 维bts 是 HBM -> VMEM token staging 粒度;btc 是实际 dot 的 token subtile。
bf / bfcFFN intermediate tilebf 越大,intermediate wave 越厚、wave 数越少;代价是 W1/W3/W2 buffer 和 accumulator 变大。
bd1FFN1 hidden input sliceV1 为了 VMEM 放得下而切 hidden=8192 的第一刀。
bd2FFN2 hidden output slice第二刀;每个 bd2 slice 都可能触发 FFN2 partial output 的 load/add/store。

Ling V1 tuned config 在 16384-token prefill、EP=32 下是:

text
bt=128, bf=1024, bd1=2048, bd2=2048,
bts=128, btc=32, bfc=1024, bd1c=2048, bd2c=2048, bse=1024

把它展开成循环次数:

  • local tokens = 512,bt=128,所以每个 device 有 4 个外层 bt tiles。
  • intermediate = 2048,bf=1024,所以每个有 token 的 expert 有 2 个 bf waves。
  • hidden = 8192,bd1=bd2=2048,所以 hidden 被切成 4 片。
  • 每个 bf wave 里,FFN1 对 4 个 bd1 slices 做 gate/up 两组 dot; FFN2 对 4 个 bd2 slices 做 down projection。

FFN1 和 FFN2 的理论 dot 量并不一样:同一个 bf wave 里,FFN1 要分别算 W1 gate 和 W3 up,所以是 4 个 hidden slices x 2 组 dot;FFN2 是 4 个 hidden output slices x 1 组 dot。实际耗时还会受 activation、scale、DMA、HBM staging 影响。

V1 的 b_acc_vmem

b_acc_vmem 保存一个 bf wave 内的 gate/up intermediate accumulator。V1 会按 bd1 扫 hidden input:每扫一片 hidden,就做一次 partial dot,并把结果累加到 b_acc_vmem。这和 tensor parallel 切 weight 后需要对 partial result 做累加的 直觉类似,只是这里的切分发生在 expert kernel 内部的 hidden 维。

在 Ling V1 tuned config 下,b_acc_vmem 的主尺寸约为:

text
2 x a2a_max_tokens x bf x fp32
= 2 x (128 x 32) x 1024 x 4B
= 32 MiB

这里的 2 是 gate/up 两个 accumulator,a2a_max_tokens=bt*EP=4096 是一个 bt tile 在最坏路由下可能落到任意 expert/device 的 routed slots 上界。 V1 的 scatter 和 expert FFN 是两阶段组织:scatter 先把这个 bt tile 可能产生的 routed tokens 放进 A2A scratch,expert_ffn 再从 scratch 里按 expert 处理。 因此 V1 的 accumulator / scratch layout 需要覆盖整个 bt tile 可能产生的 routed token 空间,而不是像 V2 那样只拿一个较小的 bts resident tile 周转。

V1 的限制

V1 已经拿到了 kernel 内调度权,但 expert 内部仍然围绕 bf -> bd1/bd2 周转:

bd1/bd2 控制住了 VMEM 占用,却把 FFN 拆成多轮小 GEMM、小 DMA 和小 VPU work。 更关键的是,FFN2 output 需要跨多个 bf waves 累加,V1 的 partial output accumulator 仍然在 HBM,后续 wave 要 read-add-store。

5. 为什么 V1 不能直接 full-hidden

直觉上,把 bd1/bd2 放大到 full hidden 可以减少 slice 数。但在 V1 的 buffer layout 里,这不是一个独立旋钮:hidden slice 变大时,token staging、W1/W3/W2 double buffer、FFN2 staging buffer 会一起变大。即使某个形状勉强放得下,V1 的 loop order 仍然是 bf -> bd1/bd2,FFN2 accumulator 仍然在 HBM;这只是更大的 V1, 不是 V2。

以 Ling prefill 形状估算主要 VMEM scratch:

方案主要 buffer估算
V1 tunedW1/W3/W2 double buffer、token stage、FFN2 stage、b_acc_vmem、gather/top-k scratch约 55 MiB
V1 假设 full hiddenbd1/bd2 从 2048 放大到 8192,多个 hidden-sliced buffer 同时放大约 99 MiB
V2 tunedfull-hidden weight streaming、b_x_vmem、gate/up intermediate、b_y_acc_vmem、writeback/gather scratch约 46 MiB

V2 的关键不是简单把 V1 的 hidden slice 调大,而是重新定义哪些东西常驻、哪些东西 流过来。

6. Fused MoE V2:token/output 常驻,weight 流水

V2 的 loop order 可以概括为:

text
for each bts token tile:
  load tokens once into b_x_vmem
  initialize / keep b_y_acc_vmem
  for each bf intermediate tile:
    prefetch W1/W3/W2 into double-buffered VMEM slots
    W1/W3 dot -> gate/up accumulator
    activation / scale
    W2 dot -> accumulate into b_y_acc_vmem
  write output once

也就是源码注释里的 bts loop is OUTER, bf loop is INNER

V2 的 VMEM 角色更清晰:

VMEM buffer作用
b_x_vmem当前 bts token tile,跨多个 bf weight tiles 复用。
b_y_acc_vmemf32 final output accumulator,跨所有 bf waves 累加。
b_gate_acc_vmem / b_up_acc_vmem当前 bf wave 的 gate/up intermediate,仍然存在但只按 bts x bf 周转。
b_w1_x2_vmem / b_w3_x2_vmem / b_w2_x2_vmem权重双缓冲;当前 tile 计算时,下一 tile 通过 DMA 进入另一个 slot。
b_x_scale_vmemactivation quant 打开时的 per-token scale。

V2 会不会多读 weight

会。V2 选择 token/output 常驻,weight 连续流入;当一个 expert 的 routed tokens 超过 bts 时,同一组 expert weights 可能被不同 bts tiles 反复读取。以 local routed tokens 约 512、bts=160 为例,一个 expert 可能需要约 4 个 bts tiles。

这个 trade-off 的目标不是让 weight bytes 的理论值最小,而是让 weight DMA 更大块、 更规则、更容易被 double buffering 藏在 MXU compute 后面。V1 可能读更少的 weight slice,但它的关键路径被小 slice staging、b_acc_vmem 大上界和 FFN2 HBM read-add-store 切碎。V2 接受更多可预取的 weight streaming,换来更长的 resident compute window。

V2 不切 hidden 的好处

V2 没有 bd1/bd2,所以每个 bf tile 直接读取 full hidden 方向的权重。 这带来三个效果:

  • FFN1/FFN2 不再被 hidden slices 切成很多小 dot,MXU 的 K 维流水更完整。
  • final output accumulator 在 VMEM 中跨 bf 累加,避免 FFN2 partial output 的 HBM read-add-store。
  • token tile 进入 VMEM 后跨多个 bf tiles 复用,DMA/MXU/VPU 的 overlap 窗口更长。

V2 block config 主旋钮

旋钮控制对象说明
bt外层 token tilerouting、scatter、gather 的单位。
bts / btcresident token tile / compute subtilebts 控制 token 常驻工作集,btc 控制 MXU token M 维 subtile。
bfintermediate / weight waveW1/W3 的 N,W2 的 K;也是 weight streaming 的主节奏。
bseshared expert intermediate tileV2 把 shared expert 放进 kernel 时使用。

Ling full shared-expert、act-quant ON 的 V2 tuned config 使用:

text
bt=128, bf=512, btc=80, bse=512, bts=160

bts=160 让 token/output 常驻工作集保持在 VMEM ceiling 内;bf=512 降低单个 intermediate wave 的 accumulator / weight buffer 压力,为 double buffering、 scatter/gather 和 shared expert 留出空间。

7. Activation quant、direct scaled dot、shared expert

Activation quant

V2 可以先把 bf16 token quantize 成 fp8 token,并把每个 token 的 scale 带进 VMEM。scatter 阶段如果打开 activation quant,就搬 fp8 token 而不是 bf16 token, ICI bytes 直接降低。代价是 VPU 需要做 quantize、scale、pack/reorder;如果 e2e 瓶颈已经转移到别处,act-quant 的端到端收益可能接近噪声。

Per-channel FP8 和 direct scaled dot

如果 scale 依赖 K 维,例如 per-block scale,大 GEMM 会被拆成多个 K-block dot, 每段 dot 后都要乘 scale 再累加。Ling 的 MoE 权重是 per-channel FP8,scale 只依赖 输出 channel,所以可以先做完整 fp8 dot,再在输出上乘 scale。这样 MXU 看到的是 更完整的 K 维流水。

scale 类型MXU 看到的形态
per-blockK 维被切成多段 dot,VPU scale 反复插入。
per-channelscale 不依赖 K,可以 hoist 到 dot 之后,dot 更完整。

In-kernel shared expert

Ling 每层还有一个 shared expert。如果它在 kernel 外单独跑,就会在 routed MoE 之外新增一段 dense MLP critical path。V2 把 shared expert 放到 routed expert loop 之前,利用 scatter recv / metadata 的时间窗,并复用 routed path 的 token、 weight 和 output VMEM buffers。

V1 在 Ling linear path 里 shared expert 是 external dense MLP;V2 full path 则把 shared expert 放进 kernel,因此 V1 的 tuned config 不能直接套给 V2。

8. 性能数字和剩余瓶颈

原始分享材料里的关键数字:

场景V1V2解释
MoE layer prefill,16384 tokens约 5.16 ms约 2.44 ms约 53% latency 降低,主要来自 VMEM 常驻、大 tile、通信 overlap、act quant 和 shared expert overlap。
MoE layer decode,512 tokens约 0.249 ms约 0.211 ms约 15% 提升;decode 的 per-expert M 太小,更接近 weight HBM 读 lower bound。
端到端 prefill throughputbaseline约 25% 提升MoE 不再是唯一瓶颈,GLA / Lightning Linear Attention prefill kernel 成为主要剩余成本。

V2 不是无限优化,剩余约束仍然明确:

  • 通信侧:32-way all-to-all 受 topology、multi-hop forwarding、link contention 限制。
  • 计算侧:fp8 pack/unpack、lane reorder、scale broadcast 会占用 VPU。
  • VMEM 侧:约 64 MiB/core 的 VMEM ceiling 限制 bts 继续变大,也限制更多 prefetch buffer 同时常驻。

9. 代码入口

以下路径用于把上面的数据流对应回 sglang-jax 代码:

主题代码位置关注点
EPMoE baselinepython/sgl_jax/srt/layers/moe.py_permute -> _gmm_compute -> _unpermute -> psum
Fused V1 kernelpython/sgl_jax/srt/kernels/fused_moe/v1/kernel.pybt/bf/bd1/bd2 的 hidden-sliced 生命周期。
Fused V2 kernelpython/sgl_jax/srt/kernels/fused_moe/v2/kernel.pybts outer、bf inner、b_x_vmemb_y_acc_vmem、weight double buffer。
Fused MoE wrapperpython/sgl_jax/srt/layers/fused_moe.pyV1/V2 backend 选择、direct scaled dot、activation quant 参数。
V2 tuned configpython/sgl_jax/srt/kernels/fused_moe/v2/tuned_block_configs.pyLing shape 下的 bt/bf/bts/btc/bse

10. 总结

EPMoE 的优点是数学表达清晰;V1 的关键进步是把 MoE 的执行边界收进一个 Pallas kernel,拿到通信和 expert compute 的调度权;V2 的关键进步是改变数据 生命周期,让 token tile 和 output accumulator 常驻 VMEM,让权重以更大、更规则的 tile 连续流入,并用 double buffering 把 HBM DMA 藏在 MXU compute 后面。

因此,Fused MoE V2 的本质不是“少几个 op”,而是把 MoE 从多个 op 的组合改造成 一条可调度的数据流生产线。通信、HBM、VPU、MXU 都在同一个 kernel 时间线上排队, 优化目标是让能隐藏的成本尽量不出现在 critical path 上。