[CUDA] Port ShardedMoE to the new MoE GEMM backend - #31191
Draft
tianleiwu wants to merge 2 commits into
Draft
Conversation
tianleiwu
marked this pull request as draft
July 30, 2026 20:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The MoE GEMM refactor (#28467) moved the CUDA MoE backend to
contrib_ops/cuda/llm/moe_gemm/and deletedcontrib_ops/cuda/moe/ft_moe/moe_kernel.htogether withort_fastertransformer::CutlassMoeFCRunner.moe.cc/moe_base.hwere ported to the newonnxruntime::llm::kernels::cutlass_kernelsAPI, butsharded_moe.{h,cc}were not, soShardedMoEstill included the deleted header and any build withonnxruntime_USE_NCCL=ONfailed to compile.This PR ports
ShardedMoEto the new MoE runner, shares the kernel invocation with the non-shardedMoEop, and fixes two correctness bugs in that shared path that were exposed while validating expert parallelism end to end.Related PR: #31139 tried to remove the op to avoid build error.
Summary of Changes
Port ShardedMoE to the new MoE runner
contrib_ops/cuda/moe/moe_base.hMoEBase::RunMoe<T>()that holds the whole kernel invocation (routing, weight packing, workspace,runMoe) and takes anMOEParallelismConfig, a GEMM profiler and the output pointer.MoEandShardedMoEnow share one implementation.contrib_ops/cuda/moe/moe.ccMoE<T>::ComputeInternalintoMoEBase::RunMoe<T>;ComputeInternalonly validates inputs and calls it with a default (non-parallel) config. Adds explicit instantiations forfloat/MLFloat16/BFloat16.contrib_ops/cuda/collective/sharded_moe.hft_moe/moe_kernel.hinclude; keeps only the shard attributes plus a GEMM profiler and its mutex.contrib_ops/cuda/collective/sharded_moe.ccRunMoe<T>. Builds theMOEParallelismConfigfrom the parsed parallelism type (EP:ep_size = world_size,ep_rank = local_experts_start_index / local_num_experts; TP:tp_size = world_size,tp_rank = nccl_->Rank()), validates the sharding attributes, and combines the per-rank results with a singleFuncAllReduce(ncclAllReduceSUM) over a temporary partial-output tensor. The oldncclBroadcast/get_total_rows_infocombine and thencclSend/ncclRecvpath are gone.The all-reduce combine is correct because the new runner already produces a partial result on each rank:
finalizeMoeRoutingKernelskips experts outside[start_expert, start_expert + num_experts_per_node)and writes zeros for tokens with no local expert, and the FC2 bias is only added for locally routed experts.Per-rank shape validation
contrib_ops/cpu/moe/moe_helper.hfc1/fc2/fc3weights, scales, zero points andfc1_experts_bias) are now validated againstlocal_num_expertsinstead of the globalnum_experts.fc2_experts_biasstays global because it is applied after the ranks are combined. Also enforcesnum_experts % local_num_experts == 0when expert parallelism is inferred.Gated-activation fixes in the shared MoE path
Both bugs affect the existing non-sharded
MoEop too, and are the reason expert parallelism did not match the unsharded reference:fc3_experts_weightsinput is present, FC1 and FC3 are packed into a single[E, 2 * inter_size, hidden_size]buffer, but the activation was still passed as the non-gatedSilu/Gelu. The kernel then usedinter_sizeas the per-expert stride of a buffer with2 * inter_sizerows per expert and read another expert's weights.Siluis now mapped toSwigluandGelutoGegluin that case.2 * inter_sizeper-expert stride (bias_offset = expert * inter_size * gated_size_mul), so an[E, inter_size]bias aliased across experts. The bias is now padded into[E, 2 * inter_size]with the gate half holding the bias and the linear half zero.Test
test/python/transformers/sharded_moe/test_sharded_moe.pyTHRESHOLD_EPrelaxed from1e-6to3e-2(same asTHRESHOLD_TP). Expert parallelism is no longer bit-exact: the sharded and unsharded sessions run the same CUTLASS kernels with a different number of experts per rank, so they may select a different GEMM tactic and accumulate in a different order.Testing
Built with NCCL enabled (
--use_cuda --enable_nccl --nccl_home ..., SM80) and validated on a 4×A100 node:mpirun -n 4 python test_sharded_moe.py— OK, 8/8 parity checks (EP and TP forhidden_size∈ {128, 1024} ×inter_size∈ {512, 2048}, 64 experts, 1024 rows). Max observed diff is 1–2 fp16 ulp (0.000977…0.03125).fc3 + fc1_biashad a max diff of3.735before the fix and0.000977after; all other input combinations were unaffected.test_moe_cuda.py48 passed / 15 skipped,test_qmoe_cuda.py109 passed / 13 skipped.lintrunner -aclean.Note for anyone reproducing the MPI test: ORT is not built with
USE_MPI, soNcclContextbootstraps over TCP and each rank needsLOCAL_RANK,LOCAL_WORLD_SIZE,RANK0_IP=127.0.0.1,NCCL_IB_DISABLE=1,NCCL_SOCKET_IFNAME=lo.Motivation and Context
onnxruntime_USE_NCCL=ONbuild break introduced by QMoE CUDA EP — FP4/FP8/WFP4AFP8 Quantized Mixture-of-Experts + MoE GEMM Refactor #28467.MoEParallelType::EPAndTPstill returns "Expert and Tensor Parallelism is not supported yet", as before.FuncCustomAllReduceis intentionally not used: its CUDA IPC handle exchange inipc_utils.ccis#ifdef USE_MPI, which no ORT CMake configuration defines, so it fails at runtime in every shipped build.