MoE Marlin 计算流程示例
本文用一个小例子说明 Int4MoePlugin 中 MoE Marlin 路径的完整计算过程,包括:router topK、expert 重排、padding、Marlin Gate/Up GEMM、SwiGLU、Marlin Down GEMM、slot 聚合。
1. 示例参数
假设:
| Text Only |
|---|
| numTokens = 3
topK = 2
totalSlots = numTokens * topK = 6
numExperts = 3
moeBlockSize = 4 (代码中实现,decode是8, prefill是32)
hiddenSize = H
moeInterSize = I
|
每个 token 会选出 topK=2 个 expert,因此一共有 6 个 routed slot:
| Text Only |
|---|
| token0: slot0 = token0,k0 slot1 = token0,k1
token1: slot2 = token1,k0 slot3 = token1,k1
token2: slot4 = token2,k0 slot5 = token2,k1
|
slot 和 token 的关系是:
| Text Only |
|---|
| slot = tokenId * topK + k
tokenId = slot / topK
k = slot % topK
|
2. Router TopK 输出
假设 router softmax + topK 后得到如下路由结果:
| Text Only |
|---|
| slot0 = token0,k0 -> expert1, weight = w0
slot1 = token0,k1 -> expert0, weight = w1
slot2 = token1,k0 -> expert1, weight = w2
slot3 = token1,k1 -> expert1, weight = w3
slot4 = token2,k0 -> expert0, weight = w4
slot5 = token2,k1 -> expert2, weight = w5
|
对应数组:
| Text Only |
|---|
| topkIndices = [1, 0, 1, 1, 0, 2]
topkWeights = [w0, w1, w2, w3, w4, w5]
|
这里的 topkIndices[slot] 表示该 slot 被路由到哪个 expert,topkWeights[slot] 表示该 slot 的 routing 权重。
3. 统计每个 expert 的 slot 数
launchCountSlotsPerExpertKernel 会统计每个 expert 被分到多少 slot:
| Text Only |
|---|
| expert0: slot1, slot4 -> count = 2
expert1: slot0, slot2, slot3 -> count = 3
expert2: slot5 -> count = 1
|
得到:
| Text Only |
|---|
| slotsPerExpertWorkspace = [2, 3, 1]
|
4. 计算 padded count 和 offset
Marlin MoE GEMM 要求每个 expert 的 slot 数按照 moeBlockSize 对齐。
这里:
所以:
| Text Only |
|---|
| expert0: count=2 -> paddedCount=4
expert1: count=3 -> paddedCount=4
expert2: count=1 -> paddedCount=4
|
得到:
然后对 paddedCounts 做 exclusive prefix sum,得到每个 expert 在 sortedTokenIds 中的起始 offset:
| Text Only |
|---|
| paddedOffsets[0] = 0
paddedOffsets[1] = 4
paddedOffsets[2] = 8
|
因此:
| Text Only |
|---|
| paddedOffsets = [0, 4, 8]
numTokensPostPadded = 12
|
最终 padded 后的 Marlin row 总数是 12,而真实 slot 数只有 6。多出来的是 padding row。
5. 构建按 expert 分桶的 slot list
launchBuildSlotListsKernel 会遍历所有 slot,根据 topkIndices[slot] 把 slot 放入对应 expert 的 list:
| Text Only |
|---|
| expert0: [1, 4]
expert1: [0, 2, 3]
expert2: [5]
|
这一步输出到 slotsByExpertWorkspace,逻辑上是:
| Text Only |
|---|
| slotsByExpertWorkspace[expertId][offset] = slot
|
实际内存是一维数组:
| Text Only |
|---|
| slotsByExpertWorkspace[expertId * totalSlots + offset] = slot
|
每个 expert 预留 totalSlots 个位置,因为最坏情况下所有 slot 都可能路由到同一个 expert。
6. 构建 Marlin 使用的 sortedTokenIds / expertIds
launchBuildMarlinIndicesKernel 会根据 slotsByExpertWorkspace、paddedCounts、paddedOffsets 生成 Marlin GEMM 需要的索引。
真实 slot 写在前面,padding 位置写 sentinel:
| Text Only |
|---|
| sentinel = totalSlots = 6
|
重排后:
| Text Only |
|---|
| sortedTokenIds:
expert0: [1, 4, 6, 6]
expert1: [0, 2, 3, 6]
expert2: [5, 6, 6, 6]
|
展开成一维:
| Text Only |
|---|
| sortedTokenIds = [1, 4, 6, 6, 0, 2, 3, 6, 5, 6, 6, 6]
|
其中 6 == totalSlots,表示 padding row,不对应真实 slot。
同时每 moeBlockSize=4 个 row 是一个 Marlin block,每个 block 对应一个 expert:
| Text Only |
|---|
| rows 0..3 -> expert0
rows 4..7 -> expert1
rows 8..11 -> expert2
|
所以:
topkWeightsFlat 也会按 sortedTokenIds 同步重排:
| Text Only |
|---|
| topkWeightsFlat = [w1, w4, 0, 0, w0, w2, w3, 0, w5, 0, 0, 0]
|
padding row 的 weight 为 0。
7. 第一次 Marlin GEMM:Gate/Up Projection
第一次调用:
| Text Only |
|---|
| moeAwqW4A16MarlinGemm(
hiddenStates2D,
gateUpOutputTensor,
gateUpQWeightsTensor,
gateUpScalesTensor,
sortedTokenIdsTensor,
expertIdsTensor,
numTokensPostPaddedTensor,
topkWeightsFlatTensor,
marlinWorkspaceTensor,
moeBlockSize,
topK,
mulTopkWeights = false)
|
它计算的是:
| Text Only |
|---|
| gate_up[slot] = hidden[token] @ W_gate_up[expert]
|
其中:
| Text Only |
|---|
| hiddenStates2D shape = [numTokens, hiddenSize]
gateUpOutputTensor shape = [totalSlots, 2 * moeInterSize]
W_gate_up[expert] shape = [hiddenSize, 2 * moeInterSize]
|
Marlin 处理过程可以理解为:
| Text Only |
|---|
| for each paddedRow in [0, numTokensPostPadded):
slot = sortedTokenIds[paddedRow]
if slot == totalSlots:
continue // padding row
token = slot / topK
expert = expertIds[paddedRow / moeBlockSize]
gateUpOutput[slot] = hidden[token] @ W_gate_up[expert]
|
套入本例:
| Text Only |
|---|
| padded row 0: slot1 -> token0 -> expert0
gateUpOutput[1] = hidden[token0] @ W_gate_up[expert0]
padded row 1: slot4 -> token2 -> expert0
gateUpOutput[4] = hidden[token2] @ W_gate_up[expert0]
padded row 4: slot0 -> token0 -> expert1
gateUpOutput[0] = hidden[token0] @ W_gate_up[expert1]
padded row 5: slot2 -> token1 -> expert1
gateUpOutput[2] = hidden[token1] @ W_gate_up[expert1]
padded row 6: slot3 -> token1 -> expert1
gateUpOutput[3] = hidden[token1] @ W_gate_up[expert1]
padded row 8: slot5 -> token2 -> expert2
gateUpOutput[5] = hidden[token2] @ W_gate_up[expert2]
|
padding row 不产生有效 slot output。
最终 gateUpOutputTensor 仍然按原始 slot id 排列:
| Text Only |
|---|
| slot0, slot1, slot2, slot3, slot4, slot5
|
这点很重要:Marlin 内部按 expert 分组计算,但输出写回原始 slot layout,方便后续算子继续按 slot = tokenId * topK + k 使用。
8. SwiGLU 激活
第一次 GEMM 的输出每行包含 gate 和 up 两部分:
| Text Only |
|---|
| gateUpOutput[slot] = [gate, up]
shape = [totalSlots, 2 * moeInterSize]
|
swiGluActivation 计算:
| Text Only |
|---|
| activation[slot, i] = silu(gate[slot, i]) * up[slot, i]
|
其中:
| Text Only |
|---|
| silu(x) = x / (1 + exp(-x))
|
输出:
| Text Only |
|---|
| activationOutputTensor shape = [totalSlots, moeInterSize]
|
继续本例:
| Text Only |
|---|
| activation[0] = silu(gateUpOutput[0].gate) * gateUpOutput[0].up
activation[1] = silu(gateUpOutput[1].gate) * gateUpOutput[1].up
...
activation[5] = silu(gateUpOutput[5].gate) * gateUpOutput[5].up
|
9. 第二次 Marlin GEMM:Down Projection
第二次调用计算 down projection:
| Text Only |
|---|
| downOutput[slot] = W_down[expert](activation[slot])
|
在代码中第二次调用传入:
| Text Only |
|---|
| topK = 1
mulTopkWeights = true
|
所以实际输出是:
| Text Only |
|---|
| downOutput[slot] = topkWeight[slot] * (activation[slot] @ W_down[expert])
|
为什么这里才乘 routing weight?
MoE FFN 的数学形式是:
| Text Only |
|---|
| expert_out = W_down(silu(W_gate x) * W_up x)
final = sum(topk_weight * expert_out)
|
如果在 gate/up GEMM 后就乘 routing weight,会变成:
| Text Only |
|---|
| silu(weight * gate) * (weight * up)
|
这会改变 SwiGLU 的非线性结果,所以第一次 Gate/Up GEMM 必须:
第二次 Down GEMM 才能:
本例中:
| Text Only |
|---|
| downOutput[0] = w0 * (activation[0] @ W_down[expert1])
downOutput[1] = w1 * (activation[1] @ W_down[expert0])
downOutput[2] = w2 * (activation[2] @ W_down[expert1])
downOutput[3] = w3 * (activation[3] @ W_down[expert1])
downOutput[4] = w4 * (activation[4] @ W_down[expert0])
downOutput[5] = w5 * (activation[5] @ W_down[expert2])
|
输出 shape:
| Text Only |
|---|
| downOutputTensor shape = [totalSlots, hiddenSize]
|
10. 聚合 slot 输出回 token
最后调用 launchAggregateSlotOutputsKernel,把同一个 token 的 topK 个 slot 加起来:
| Text Only |
|---|
| output[token, dim] = sum_{k=0}^{topK-1} downOutput[token * topK + k, dim]
|
本例:
| Text Only |
|---|
| output[token0] = downOutput[slot0] + downOutput[slot1]
output[token1] = downOutput[slot2] + downOutput[slot3]
output[token2] = downOutput[slot4] + downOutput[slot5]
|
代入权重和 expert:
| Text Only |
|---|
| output[token0] =
w0 * (activation[0] @ W_down[expert1]) +
w1 * (activation[1] @ W_down[expert0])
output[token1] =
w2 * (activation[2] @ W_down[expert1]) +
w3 * (activation[3] @ W_down[expert1])
output[token2] =
w4 * (activation[4] @ W_down[expert0]) +
w5 * (activation[5] @ W_down[expert2])
|
聚合后输出 shape:
| Text Only |
|---|
| output shape = [numTokens, hiddenSize]
|
在 plugin 中这个 shape 对应原始 hidden states 的 [B, S, hiddenSize]。
11. 和普通 GEMM 的区别
普通 dense GEMM 是:
MoE Marlin GEMM 是:
| Text Only |
|---|
| for each routed slot:
token = slot / topK
expert = expertIds[paddedRow / moeBlockSize]
C[slot] = A[token] @ B[expert]
|
它多了三层机制:
- 按 expert 重排:把同一个 expert 的 slot 放在一起,提高 expert 权重访问局部性。
- 按 moeBlockSize padding:让 Marlin 每个 block 都处理规整数量的 routed slots。
- slot id 映射:内部按 expert 分组算,但输出写回原始 slot layout,方便后续 SwiGLU、Down GEMM 和最终 aggregate。
- INT4 packed weight + scale:Marlin 内部对 AWQ INT4 权重边反量化边做 W4A16 GEMM。
12. 完整流程总览
| Text Only |
|---|
| hiddenStates [numTokens, hiddenSize]
|
| router logits -> topK
v
topkIndices/topkWeights [numTokens, topK]
|
| count slots per expert
v
slotsPerExpertWorkspace
|
| pad counts + exclusive scan offsets
v
paddedCounts / paddedOffsets / numTokensPostPadded
|
| build slot lists by expert
v
slotsByExpertWorkspace
|
| build Marlin indices
v
sortedTokenIds / expertIds / topkWeightsFlat
|
| Marlin W4A16 Gate/Up GEMM, mulTopkWeights=false
v
gateUpOutput [totalSlots, 2 * moeInterSize]
|
| SwiGLU: silu(gate) * up
v
activationOutput [totalSlots, moeInterSize]
|
| Marlin W4A16 Down GEMM, mulTopkWeights=true
v
downOutput [totalSlots, hiddenSize]
|
| aggregate slots by token
v
output [numTokens, hiddenSize]
|
13. 关键结论
topkIndices 决定每个 slot 去哪个 expert。
sortedTokenIds 是按 expert 分组、padding 后的 slot id 列表。
expertIds 是每个 Marlin block 对应的 expert。
totalSlots 作为 sentinel,表示 padding row。
- Gate/Up GEMM 不乘 routing weight,因为后面有 SwiGLU 非线性。
- Down GEMM 融合乘 routing weight,最终 aggregate 只需要求和。
- Marlin 内部按 expert-contiguous layout 计算,但输出保持原始 slot layout。