Soumajyoti Sarkar

Better MoE Routing Through Algo-Sys Co-Design, Field Notes for Pretraining

I keep this as a working diary of my own thoughts and experiments that I'd want to run for MoE model design. The constraint is pretraining-only where we desire higher throughput at large global batch and bf16/mxfp8, not single-token decode latency. It alternates between articles and code throughout. A lot of the notes here is me talking to AIs and noting them, so there is a fair bit of slop.

Why I think routing, and not just expert computation in MoEs deserves the kernel attention

  1. The router is three operators, not one. A small GEMM into E logits, a bandwidth-bound top- K , and a permute/unpermute pair that streams the activation through HBM 4 times. These are all bandwidth-bound on B200, and B200's compute scaled faster than its HBM, so the bandwidth side is hurting more, not less.
  2. Dispatch / combine all-to-all is fused with quantization, scale-factor shuffling, and grouped-GEMM layout in the modern stack. "Improving the routing layer" is no longer about the router, it is about the whole pipeline gating → topk → permute → A2A-dispatch → grouped-GEMM → A2A-combine → unpermute, and the wins come from collapsing the boundaries between those stages.

What follows is a tour through the recent literature with one constant question: what changes in megatron/core/transformer/moe/ do these papers and blog posts imply, and what is the smallest experiment that could run on top of the current code to validate one of those changes?

1. The Megatron MoE path these blogs are read against

For grounding, here is the actual call chain in the public NVIDIA/Megatron-LM tree, megatron/core/transformer/moe/moe_layer.py. MoELayer is defined at l. 213, and its forward (l. 598) wraps a nested custom_forward (l. 642) that runs the pipeline route → preprocess → dispatch → routed_experts_compute → combine → postprocess:

# MoELayer.custom_forward, moe_layer.py:642, each stage a method on MoELayer
probs, routing_map     = self.route(hidden_states, padding_mask)          # :438  → apply_module(self.router)(...)
hidden_states, probs   = self.preprocess(hidden_states, probs, routing_map)  # :448
dispatched_input, probs = self.dispatch(hidden_states, probs)             # :489  → token_dispatcher.token_dispatch(...)
output, mlp_bias       = self.routed_experts_compute(dispatched_input, probs)  # :529
output                 = self.combine(output)                             # :557  → token_dispatcher.token_combine(...)
output                 = self.postprocess(output, shared_expert_output)   # :566  → combine_postprocess(...)

(Line numbers are against main and will drift. The older token_permutation / token_unpermutation dispatcher entry points have been split into dispatch_preprocess / token_dispatch / dispatch_postprocess and combine_preprocess / token_combine / combine_postprocess.) Unrolled, that is:

Stage File (NVIDIA/Megatron-LM main) What runs
Gating GEMM moe_utils.py:1294 RouterGatingLinearFunction (wrapper router_gating_linear, l. 1394) TE GEMM in fp32 (router-dtype) or torch.mm
Input jitter (noise) router.py:706 TopKRouter.apply_input_jitter (gated by moe_input_jitter_eps) optional uniform-noise multiply, when enabled
Top-k + score fn + bias moe_utils.py:710 topk_routing_with_score_function (+ group_limited_topk, l. 617) softmax/sigmoid score → topk → optional capacity/group
Permute (forward) moe_utils.py:337 permute argsort of routing-map transpose + index_select
EP all-to-all (dispatch) token_dispatcher.py:359 MoEAlltoAllTokenDispatcher (dispatch_preprocess l. 605) or fused_a2a.py:71 FusedDispatch (DeepEP) NCCL all_to_all or DeepEP fused dispatch
Expert TP all-gather token_dispatcher.py:552 gather_from_sequence_parallel_region TE/Megatron sequence-parallel gather
Second permute moe_utils.py:572 sort_chunks_by_idxs second permutation when num_local_experts > 1
Grouped GEMMs (up + down) experts.py:173 TEGroupedMLP (or SequentialMLP, l. 1165) TE grouped GEMM / grouped_gemm package, bf16
GLU activation experts.py:739 weighted_bias_swiglu_impl bandwidth-bound
Combine path token_dispatcher.py:776 combine_preprocesstoken_combinecombine_postprocess (l. 856), unpermute (moe_utils.py:470) RS(TP) → A2A(EP) → unpermute

A few non-obvious things stand out on a careful read:

That is the code to be changed. Now the literature.

2. The literature, condensed

2a. Nous Research, MoE Scaling Field Notes

The single most useful thing in Nous's notes is what they observed while profiling, not the architectural take.

The scaling number worth remembering: 15,057 tokens/sec/GPU on B200 → "57% of B200 theoretical bandwidth." That is the order-of-magnitude headroom in question, not 2×, but every percentage of bandwidth utilization counted.

2b. Cursor, Kernels for MoE training

This Cursor blog post is the closest thing to a recipe in the public literature. The numbers and where they came from:

Two things stand out here. First, the quantization layout is the bottleneck, not the matmul. Second, B200 ≠ Hopper: Blackwell's TMEM and tcgen05.mma require a different quantization layout than Hopper. Anything ported from H100 needs to be re-checked on B200.

2c. PyTorch ao, MoE mxfp8 next steps (issue #3379)

The PyTorch ao tracking issue is the clearest statement of what an end-to-end mxfp8 MoE pipeline should look like.

2d. SGLang, Large-scale Expert Parallelism (May 2025)

The LMSYS post is decode-leaning, but a few things matter for pretraining:

2e. NVIDIA, Hybrid Expert Parallel

The Hybrid-EP blog is the cleanest description of what topology-aware EP looks like.

2f. Poolside, Laguna M.1 / XS.2 (May 2026)

The Laguna M.1/XS.2 technical report has the cleanest description of MoE comm-compute overlap in a recent foundation-model report, and crucially it's H200-specific (8× H200 over NVLink, EP=8 within a node). Their Compute-communication Overlap for MoE recipe is worth recording in detail because it's directly portable to the Megatron path:

A few things from this writeup that change how to think about the experiments here:

  1. The fusion target is the grouped GEMM tile scheduler, not the permute kernel. Their dispatch is a separate set of SMs running concurrently with the grouped GEMM, gated by a flag-based barrier per expert. That is a more invasive change than a fused-permute Triton kernel, but it eliminates the dispatch from the critical path entirely rather than just compressing it.
  2. 128-bit vectorized loads over NVLink is the right granularity on H200, it matches the LDGSTS / NVLink transaction size and gives you the bandwidth without needing TMA-style descriptors.
  3. The scheme is dtype-agnostic as written (bf16 grouped GEMM in their report), but the same flag-gated tile-scheduler design composes with mxfp8 grouped GEMM as long as the dispatch SMs also write the E8M0 scale factor alongside each FP8 tile. This is not a "mxfp8 all the way" report, it is a "bf16 dispatch + grouped GEMM overlap perfectly" report, and the mxfp8 layer is orthogonal.
  4. EP=8 within a node (no inter-node EP) is what makes this clean: every dispatch is over NVLink, never over IB, so there is no finter correction. For multi-node EP groups (common in real workloads), Laguna's intra-node scheme must be composed with Hybrid-EP's hierarchical inter-node dispatch.

2g. DeepSeek V3 + V4, DualPipe → MegaMoE wave-pipeline

The DeepSeek line is the longest-running public study of EP comm-overlap and is worth treating as two distinct contributions. V3 (Dec 2024) introduced DualPipe and the hierarchical IB+NVLink dispatch kernel. V4 (Jan 2026) replaced both with a fused per-wave EP mega-kernel that the team open-sourced as MegaMoE inside DeepGEMM PR #304.

DeepSeek V3, DualPipe and the IB↔NVLink kernel (H800)

DeepSeek V4, Fine-Grained EP Wave Pipeline (DeepGEMM MegaMoE)

The compact way to read DeepSeek V3→V4 is that V3 made intra-step overlap work (DualPipe + hierarchical kernel), and V4 went one level deeper to make intra-layer overlap work (wave pipeline + fused mega-kernel). Both still apply to bf16/FP8, neither hinges on FP4 (which is post-training only) or B200-specific features.

2h. Microsoft AI, MAI-Thinking-1 / MAI-Base-1 (June 2026)

The MAI-Thinking-1 report is the first detailed public Microsoft frontier-model report. The base model MAI-Base-1 is 35B-active / 1T-total MoE pre-trained on 8K GB200 GPUs in Azure (Microsoft confirms an earlier blog mentioned ~15K H100s for an earlier MAI-1-preview, while this report covers a different, newer model on Blackwell).

2i. UniEP / FlashMoE / megakernel papers

UniEP and FlashMoE are the "single-megakernel for MoE" papers. The pitch is: fuse routing + expert GEMM + all-to-all into one persistent CUDA kernel using NVSHMEM, eliminate CPU-GPU sync entirely, get 2–4× over multi-kernel baselines.

The numbers are believable, but megakernels are set aside here as a last-resort approach for this setting:

The useful idea from these papers, even without writing a megakernel, is the mental model: deterministic token ordering. UniEP guarantees numerical consistency of overlap schedules by making the token order a first-class invariant. That can be replicated with one extra argsort and a stable scheduler. It matters for grad-bit-equivalence in load-balancing-loss training runs.

2j. Comparison table, recent MoE pretraining systems

The reports above span very different points in the design space (HW platform, EP scope, dtype, overlap granularity). It is useful to put them in one place, with only facts that are explicitly stated in the source PDFs. Empty cells are marked N/D (not disclosed in the available public report). This table is updated as new MoE pretraining reports drop.

Model / system Source Total / Active Layers / Hidden Routed experts (top-K) HW & cluster size Train precision EP scope Comm-compute overlap mechanism TP Dispatch dtype Comm kernel impl
Laguna M.1 Poolside, May 2026 225.8B / 23.4B N/D / N/D N/D 6,144 H200 N/D (BF16 path implied) EP=8 (intra-node, NVLink) Flag-gated dispatch fused into CUTLASS grouped-GEMM tile scheduler; modified epilogue does combine via SMEM→NVLink N/D BF16 CUTLASS, ParallelKittens-inspired
Laguna XS.2 Poolside, May 2026 33.4B / 3B N/D / N/D 256 (top-8) + 1 shared 2,048 H200 BF16 (FP8/INT4/NVFP4 only post-training quant for inference) EP=8 (intra-node, NVLink) Same flag-gated scheme as M.1; 8 of 132 SMs to dispatch + 5 SMs to NCCL = ~10% of SMs EGP × ETP = TP, ETP=1 BF16 CUTLASS
DeepSeek V3 DeepSeek-AI, Dec 2024 671B / 37B 61 / 7168 256 (+ shared) 2,048 H800 FP8 mixed precision (E4M3 fwd, BF16 sensitive paths) EP=64 across 8 nodes DualPipe schedule + IB↔NVLink hierarchical dispatch; 20 SMs / 10 channels with warp specialization TP=1 BF16 over IB+NVLink Custom CUDA, IB+NVLink-aware
DeepSeek V4-Flash DeepSeek-AI, Jan 2026 284B / 13B N/D / 4096 256 (top-6) + 1 shared, first 3 layers Hash-routed N/D FP8 fwd, BF16 combine; FP4-QAT for routed-expert weights post-training N/D EP size Wave-pipeline mega-kernel: experts split into waves so dispatch / Linear-1 / Linear-2 / combine of consecutive waves overlap. Theoretical 1.92×, measured 1.50–1.73× N/D FP8 dispatch + BF16 combine Open-sourced as MegaMoE in DeepGEMM PR #304; written in TileLang
DeepSeek V4-Pro DeepSeek-AI, Jan 2026 1.6T / 49B 61 / 7168 384 (top-6) + 1 shared, first 3 layers Hash-routed N/D Same as V4-Flash N/D EP size Same wave-pipeline as V4-Flash; derives C/B6144 FLOPs/Byte sufficiency rule N/D FP8 dispatch + BF16 combine TileLang MegaMoE
MAI-Base-1 / MAI-Thinking-1 Microsoft AI, Jun 2026 1015B (962B effective) / 35B (34.7B effective) 78 / 6656 512 (top-8), LatentMoE compression 2× before A2A 8K GB200 (Azure) BF16 master, FP8 E4M3 fwd, FP8 E5M2 dgrad, BF16 wgrad / FP32 accum, BF16-sensitive paths, delayed scaling EP=64 (intra-NVLink) Per-EP-group dispatch→compute→collect pipeline, all but first/last overlap; static-memory dropless mode with per-round recompute TP=1 FP8 (compressed-latent payload) CuTe DSL symmetric-memory kernels (NVSHMEM-style); CLC on B200
Cursor Blackwell MoE Cursor blog N/D N/D N/D B200 MXFP8 (FP8E4M3 + FP8E8M0, 32-elem block scales) N/D Quantization layout fused with tcgen05.mma; expert-wise L2 supergrouping; 2-CTA collaborative MMA N/D MXFP8 across boundary Custom CUDA + CUTLASS
NVIDIA Hybrid-EP NVIDIA dev blog N/A (kernel system) N/A N/A DGX Hopper / 32-GPU H100 / GB200 FP8/BF16 dispatch, BF16 combine spans nodes (intra-NVLink + inter-IB hierarchical) Block-level pipeline with warp groups for RDMA / G2S / S2G / reduce; staged combine: intra-node reduction → RDMA → inter-node consolidation N/A FP8 or BF16 Megatron-Core integration, exposed as PyTorch operators

A few patterns this table makes obvious:

The table is a living artifact. As Qwen3.6, GLM-5, and the next DeepSeek release publish reports, their rows get added here.

2k. The broader 2025–2026 open-weight MoE landscape

The systems story above only makes sense against the architectures people are actually shipping. The table below is an open-weight MoE configuration sweep (adapted from internal scaling-laws notes), kept to the columns that matter for the comm/compute analysis. Every numeric cell here is taken from the official technical report or the model's published config.json. Cells that could not be verified are N/D.

As a definition reminder, sparsity = 1(active params/total params) , the fraction of parameters not touched per token. MLP ratio = expert intermediate dim / hidden dim (the per-expert FFN expansion, where smaller = more fine-grained). "Width/Depth" = hidden size / number of layers.

Model Total / Active Sparsity % Experts (top-k) Shared Width / Depth MLP ratio MoE / dense layers Routing
DeepSeek V3 671B / 37B 94.5 256 (top-8) 1 7168 / 61 0.28 58 / 3 sigmoid + expert-bias (aux-free)
DeepSeek V4-Pro 1.6T / 49B 96.9 384 (top-6) 1 7168 / 61 0.43 61 / 0 (first 3 Hash-routed) DeepSeekMoE; Hash routing first 3 layers
DeepSeek V4-Flash 284B / 13B 95.4 256 (top-6) 1 4096 / N/D 0.50 all MoE (first 3 Hash) DeepSeekMoE; Hash routing first 3 layers
Qwen3 235B 235B / 22B 90.7 128 (top-8) 0 4096 / 94 0.375 94 / 0 global-batch load-balancing loss
Qwen3-Next 80B 80B / 3B 96.25 512 (top-10) 1 2048 / 48 0.25 48 / 0 N/D
Kimi K2 1.04T / 32.6B 96.7 384 (top-8) 1 7168 / 61 0.286 60 / 1 aux-loss-free
GLM 4.5 355B / 32B 91 160 (top-8) 1 5120 / 92 0.30 89 / 3 N/D
GPT-OSS 120B 117B / 5.1B 96 128 (top-4) 0 2880 / 36 1.0 36 / 0 N/D
GPT-OSS 20B 20B / 3.6B 85 32 (top-4) 0 2880 / 24 1.0 24 / 0 N/D
MiniMax M1 456B / 45B 90.2 32 (top-2, non-fine-grained) 0 N/D 1.5 80 / 0 softmax
MiniMax M2 230B / 10B 95.7 256 (top-8) 0 3072 / 62 0.50 62 / 0 sigmoid + expert-bias
Mistral Large 3 675B / 41B 93.9 128 (top-4) 1 N/D 0.57 58 / 3 sigmoid + expert-bias
Hunyuan 389B / 52B 86.6 16 (top-1, non-fine-grained) 1 N/D 2.86 64 / 0 N/D
ERNIE 424B (300B text) 84.5 64 (top-8) 0 N/D 0.4375 52 / 3 diversity-orthogonalization loss
TeleChat3 1119B / N/D N/D 384 (top-8) 1 N/D 0.6 61 / 0 N/D
Solar Open 100B 102B / 12B 90 128 (top-8) 1 N/D 0.31 48 / 0 N/D
Sigma MoE (MSR) 20B / 0.5B 97.5 96 (top-1) 0 N/D 0.5 56 / 0 N/D
Joy AI 48B / 3B 93.5 256 (top-8) 1 N/D 0.38 40 / 1 N/D
Laguna XS.2 33.4B / 3B 91.0 256 (top-8) 1 N/D N/D N/D / 1 sigmoid, post-top-k norm, aux-loss

Cells filled in this revision come from primary sources. The entire DeepSeek V4-Pro / V4-Flash rows are from the V4 technical report §4.2.2 (1.6T/49B and 284B/13B, 384/256 routed experts top-6, hidden 7168/4096, expert intermediate 3072/2048, first 3 MoE layers Hash-routed). The Width/Depth, MLP-ratio, and routing fields for Qwen3, Qwen3-Next, Kimi K2, GLM 4.5, GPT-OSS, MiniMax M2 are from their published config.json files. MiniMax M2 and Mistral Large experts/top-k normalized to the #experts (top-k) convention. Laguna XS.2 from its tech report (8 of 256 + 1 shared).

What this landscape says, distilled to the parts that drive the kernel work:

For contrast, the 2023–2024 generation

It is worth keeping the previous generation in view, because the direction of every trend above is what makes the kernel work newly urgent. The early open MoEs were coarse-grained (few, large experts) with top-2/top-4 switch/GShard routing, a regime where the dispatch tensor was small and the per-expert GEMM was large enough to be efficient on its own. The routing layer simply was not the bottleneck it is today.

Model Org Total params Routing granularity
Mixtral 8×7B Mistral 46B 8 experts, top-2, switch/GShard
DBRX Databricks 132B 16 experts, top-4, switch/GShard
Qwen1.5-MoE Alibaba 14.3B 64 experts, top-8; 4 shared + 60 routed
OpenMoE Open source 8B 32 experts, top-2, switch/GShard
Arctic Snowflake 408B 128 experts / 17B dense backbone (hybrid dense-MoE)
DeepSeekMoE DeepSeek N/D top-k (k≥4), 1 shared + rest routed (the fine-grained + shared-expert template everyone later adopted)
Grok-1.5 xAI 300B N/D
Sambanova CoE Sambanova ~1T composition / routing across smaller expert models (not a conventional single-network MoE)

The contrast is the whole point. Going from Mixtral's 8 experts / top-2 / MLP-ratio ≈ 3.5 to Qwen3-Next's 512 experts / top-10 / MLP-ratio 0.25 moves roughly two orders of magnitude more routing decisions and dispatch destinations per token, while shrinking each expert GEMM by ~14×. The router, permute, and dispatch kernels that were rounding error in 2023 are the MFU-limiting path in 2026. (Sources for this table are the routing-industry-trends table in the Scaling Conditional Computation with Sparse LLM Pretraining notes and the DeepSeekMoE entry per Dai et al. 2024.)

3. A taxonomy, where the wins come from

Stripping all of the above to its skeleton, MoE-routing-layer wins come from one of seven places. Each is marked with what currently runs in the Megatron path.

# Bucket Current Megatron behavior Latest practice
1 Router GEMM fp32 TE-GEMM, separate kernel bf16/mxfp8 GEMM fused with sigmoid+topk
2 Top-k + bias correction Python orchestration with several sigmoid + topk + gather kernels one Triton kernel from logits → routing-map+probs
3 Permute (forward + backward) argsort + index_select (or TE fused) Triton kernel that emits the contiguous-GEMM layout and permutes scale factors
4 Dispatch all-to-all NCCL all_to_all or DeepEP fused_dispatch (bf16) mxfp8 dispatch (FP8 elements + E8M0 scales over the wire), hierarchical NVLink+IB
5 Grouped GEMM bf16 via grouped_gemm package or TE mxfp8 grouped GEMM with tcgen05.mma-compatible layout, expert-wise L2 supergrouping, 2-CTA
6 GLU activation weighted_bias_swiglu_impl or JIT-fused glu mxfp8 SwiGLU with dequant prologue / requant epilogue
7 Comm/compute overlap TBO not enabled by default; shared-expert-overlap is TBO + persistent dispatch streams + topology-aware SM allocation; or Laguna-style flag-gated tile scheduler that fuses dispatch into the grouped-GEMM kernel itself

Buckets 1–3 are router/permute. Buckets 4–7 are the rest of the MoE block.

Since the routing layer is the focus here, buckets 1–3 carry the weight in the experiment design and 4–7 are treated as context.

4. Hardware: B200 vs H200 and what it means for the kernels

A few hardware facts that change which buckets are leverage points on which platform.

B200 (Blackwell)

H200 (Hopper refresh)

Sequencing experiments by platform:

For both, across an EP group that spans nodes, the inter-node fraction in the all-to-all term dominates. Hybrid-EP-style hierarchical dispatch gets you the biggest single jump, and that part is independent of dtype.

5. Mapping the kernel changes onto the existing code

For each bucket, here is what code to change.

Bucket 1, Router GEMM

Bucket 2, Top-k + bias + capacity

Bucket 3, Permute + scale shuffle

Bucket 4, Dispatch all-to-all

Bucket 5, Grouped GEMM

Bucket 6, GLU activation

Bucket 7, Comm/compute overlap

6. If I were to run a couple experiments at first to internalize the above

The target is 1–2 experiments, in Triton, on top of the existing Megatron code, focused on reducing the MoE layer's impact on pretraining MFU. Here is the cut.

Experiment A, Fused mxfp8-aware permute+dispatch-prologue Triton kernel

This is the highest-leverage routing experiment because it sits exactly at the boundary between the bandwidth-bound router stack and the compute-bound expert grouped-GEMM.

What the kernel does, in one pass:

  1. Read tokens : [N, H] (bf16) from HBM.
  2. Read routing_map : [N, E] (bool) and probs : [N, K] (fp32).
  3. For each token, look up its k destination experts.
  4. Quantize tokens[i] to mxfp8: 32-element groups, FP8E4M3 elements + FP8E8M0 scale.
  5. Scatter the FP8 element + the per-group E8M0 scale into the per-expert contiguous output buffer, padded to grouped-GEMM tile alignment, in the layout tcgen05.mma expects on B200 (or the SMEM-tile layout on H200).
  6. Emit tokens_per_expert and the sorted_indices needed by the backward unpermute.

Why a single kernel. The naive path does 4 HBM round-trips for these steps: bf16 permute, bf16→mxfp8 quant, scale-factor reshape, layout-shuffle into grouped-GEMM format. Fusing them collapses to one read of tokens + one read of routing_map + one write to the dispatch buffer. On the operator cost model that is a 4× reduction in HBM traffic for this stage.

Where it plugs in. Replace permute(...) at token_dispatcher.py:522 with a Triton path that returns the dispatch buffer and indices directly. Skip the standalone TE quantization that today runs immediately after permute. The unpermute path needs a paired kernel that does mxfp8 dequant + scatter-add-with-probs in one pass.

Predicted gain. Cursor's report of "40% of matmul time" being quant overhead is the upper bound on what fusing these stages can save in the matmul-adjacent code. A more conservative estimate from the operator cost model: at mdbw bytes per HBM pass and 4 collapsed passes, on B200 (~8 TB/s) the saving is on the order of 0.4–0.8 ms per MoE layer per microbatch. With 47 MoE layers and 64 microbatches the per-step floor is ~1.2–2.5s, call it 2–4% MFU on the GBS=8192 / PP=16 / mbs=4 trace from the earlier post.

Falsification. If the new kernel does not show ≥1.5× HBM-bandwidth utilization vs the unfused path (measured via Nsight HBM throughput counter), the fusion is wrong. If end-to-end MFU does not move ≥1%, either bf16 grouped-GEMM is fast enough that the saved bytes are not on the critical path, or the fusion has not actually displaced the unfused TE quantization that runs after permute.

Risk. mxfp8 numerics on the routing path. The router itself stays in bf16. Only the tokens are mxfp8-quantized on the way to the experts. The permuted scales must match exactly across forward and backward, UniEP's "deterministic token ordering" idea is an explicit design constraint here. One extra argsort enforces stable order so backward and recompute see identical layouts.

Experiment B, Fused router: gating + noise + sigmoid + topk in one Triton kernel

What the kernel does:

  1. logits = input @ weight.T in bf16 with fp32 accumulation. Output dim is small ( E ), so this is a strip-mined GEMM, fine for Triton.
  2. Apply tanh-cap (moe_router_logit_cap) and z-loss term in registers.
  3. Apply the input jitter (apply_input_jitter, router.py:706) inline when moe_input_jitter_eps is set, from a counter-based RNG rather than a host-side torch.distributions.Uniform round-trip. (If a training run wants Gumbel/Gaussian logit noise instead of the stock uniform jitter, the same in-register RNG slot covers it.)
  4. Compute clean_scores = sigmoid(logits) and noisy_scores = sigmoid(logits + tau * noise) (or the score-space variant) without rematerializing logits.
  5. Add expert_bias to noisy_scores if present.
  6. Top-K reduction across E , emitting top_indices : [N, K] and gathered clean_scores for those indices.
  7. Build routing_map : [N, E] bool by scatter_(top_indices, True) in the same kernel.

Where it plugs in. Replace RouterGatingLinearFunction.forward (moe_utils.py:1294) and most of topk_routing_with_score_function (moe_utils.py:710), at least the dropless / no-group-topk branch, which is the common case. The Sinkhorn / group-topk / capacity-padding branches stay as-is, routed through the legacy path. The forward becomes a one-call replacement. The backward keeps the existing RouterGatingLinearFunction.backward, since the gradient path need not be fused on round one.

Predicted gain. Each of these on its own is microseconds, but at small mbs the kernel-launch tax dominates idle time. From the GBS=512 / mbs=4 trace, GPU idle was 11% of step. Collapsing 5 kernels into 1 across 47 MoE layers and 256 microbatches per step removes ~50k kernel launches. At 5μs per launch that's 250 ms per step, order 0.5–1% MFU at GBS=8192 / mbs=4, more at small mbs.

Falsification. Nsight should show Ekl,routing per step drop by ≥4×. If it doesn't, the dispatcher is launching them itself and the fusion is in the wrong place.

Risk. Numerics on the noise. Upstream apply_input_jitter draws from torch.distributions.Uniform on the host, so reproducing it exactly in a Triton kernel means matching its RNG stream. A counter-based RNG (Philox-4×32-10) seeded to track the same stream gives bit-identical fp32 floats. That is the path to take. A one-shot test sanity-checks that the new kernel's jitter matches the old path token-for-token when moe_input_jitter_eps > 0.

Why this and not "rewrite the whole MoE block in one Triton megakernel"? Diminishing returns and engineering cost. The two fusions above capture the two boundaries that dominate routing-side time.
The full megakernel (UniEP / FlashMoE) buys you the dispatch / grouped-GEMM boundary on top, which is where the expert time lives, not the routing time in focus here. That can be revisited once Experiment A is in.

References