Fused MoE Kernel Notes#

This note records implementation invariants for VeOmni’s GPU fused MoE kernels. The DeepSeek-V4 hash-MoE CI failure exposed one of these invariants, but the invariant itself is generic and applies to the non-EP fused_triton MoE path.

Scatter/gather layout#

The non-EP fused MoE path expands tokens by their top-k routes before running per-expert grouped GEMMs:

original hidden states: [tokens, hidden]
scattered activations: [tokens * topk, hidden]

scatter_index maps each (token, topk_slot) pair into the expert-grouped scattered tensor. Later, moe_gather() sums the routed expert outputs back to [tokens, hidden].

Backward grouped-GEMM launch bound#

For group_gemm_same_nk, max_M is a per-expert launch bound. It must cover the largest expert group in the scattered tensor. The total scattered row count is a conservative safe bound:

grad_fc2_output = moe_scatter(grad_output, scatter_index)
num_scattered_tokens = grad_fc2_output.shape[0]

Use num_scattered_tokens for the downstream group_gemm_same_nk(..., max_M=...) calls in non-EP backward. The matching group_gemm_same_mn(..., max_K=...) value is kept consistent with the scattered input row count, but the stale-row correctness issue is about group_gemm_same_nk under-covering its output rows.

Do not use the original token count as the backward grouped-GEMM bound. It is only safe when every expert group has at most tokens scattered rows.

Why ordinary top-k routing may hide the bug#

Consider tokens = 128 and topk = 2.

If each token routes to two different experts in a balanced pattern:

token0 -> expert0, expert1
token1 -> expert0, expert1
...
token127 -> expert0, expert1

After grouping by expert:

expert0: 128 rows
expert1: 128 rows

Each expert group has at most tokens rows, so a stale bound like max_M = tokens may still cover every group. The implementation would still be relying on the wrong invariant, but this routing pattern may not expose it.

Duplicate top-k routes#

Some routers can assign multiple top-k slots of the same token to the same expert. DeepSeek-V4 hash-MoE can do this when the token-to-expert lookup maps both slots to the same expert.

For tokens = 128 and topk = 2:

token0 -> expert0, expert0
token1 -> expert0, expert0
...
token127 -> expert0, expert0

After grouping by expert:

expert0: 256 rows

With BLOCK_M = 128, a stale max_M = 128 launches enough blocks for only the first 128 rows of that expert group. Rows 128-255 can remain uncomputed or stale, and later gather/gradient operations may consume arbitrary values, including inf or nan. A visible symptom is a non-finite gradient norm such as gnorm = nan.

Regression test pattern#

A focused regression test should explicitly construct duplicate top-k routes:

selected_experts = zeros([tokens, topk])
routing_weights = full([tokens, topk], weight)

Then compare split-fc1 fused, merged-fc1 fused, and eager MoE backward results, and assert all produced gradients are finite.