MoE Performance Guide#

MoE Architecture#

An MoE module contains num_experts experts and a router that selects top_k of them for each token. To accelerate LLM execution, a multi-GPU deployment usually partitions these experts across ranks, which makes each rank hold only part of them. The module therefore additionally pays for a dispatch (send each token to the rank that owns its target expert) and a combine (bring the results back).

The consequence is that MoE execution time is usually not decided by the expert GEMM alone, but by the combination of how the experts are partitioned, how the tokens are exchanged, and which kernel implementation runs the GEMM:

                    hidden states
                          |
                +-------------------+
                |   Router (top_k)  |
                +-------------------+
                          |
                +-------------------+
                |     Dispatch      | <--- Communication
                +-------------------+
                          |
                +-------------------+
                |    Expert GEMM    | <--- Backend
                +-------------------+
                          |
                +-------------------+
                |      Combine      | <--- Communication
                +-------------------+
                          |
                        output

     Parallelism decides which experts each rank owns,
     and therefore how much there is to dispatch and to compute.

Option

What it controls

Parallelism

How experts are sharded across GPUs: EP (expert parallelism, split by expert) vs TP (tensor parallelism, split inside every expert).

Communication

Which path implements dispatch / combine.

Backend

Which kernel implementation runs the expert GEMM. Each one supports a different set of quantization formats and GPU architectures.

The three can be combined in many ways, and no single combination is always the best one. The right choice changes with the world size and with the number of tokens in flight, which is why prefill and decode of the same deployment often need different answers. It also changes with the quantization format, with the GPU architecture, and with whether expert parallelism stays inside one NVLink domain. The large number of options, together with the many factors they depend on, makes it difficult to find the best MoE configuration for a given deployment.

This guide aims to help you reach a high level of MoE performance in practice. It first gives a recommended configuration for a given platform, model, quantization, world size and workload. If you want to push the performance of a specific workload further, it then shows how to benchmark it yourself. Finally, it introduces the advanced techniques that are available when the recommended configuration is not enough.

Configuration Options#

Parallelism#

The parallelism scheme determines the communication volume and the computation each GPU performs, and therefore the execution efficiency of the MoE module. It also depends on how the attention module is parallelized, and that in turn is decided by whether the deployment targets low latency or high throughput.

The parallelism scheme of MoE is expressed as a combination of moe_ep_size (experts partitioned across ranks) and moe_tp_size (every rank holds a shard of every expert), with moe_ep_size * moe_tp_size = world_size.

The equations and recommendations in this guide assume pp_size = 1 and cp_size = 1. Pipeline-parallel deployments are not covered; they must use the stage-local MoE group size instead of deployment-wide world_size.

Expert parallel vs tensor parallel vs hybrid, 4 experts on 4 ranks

Scheme

Token-expert assignments per rank (balanced routing)

Per-rank GEMM N

Communication

Expert Parallelism (EP)

num_tokens * top_k / ep_size

full intermediate_size

dispatch + combine of activations

Tensor Parallelism (TP)

num_tokens * top_k

intermediate_size / tp_size

AllReduce of outputs, no cross-rank dispatch

hybrid (EP+TP)

num_tokens * top_k / ep_size

intermediate_size / tp_size

both, at reduced scale each

Here, num_tokens counts unique input tokens globally across the MoE parallel group, without counting TP replicas. Each token contributes top_k token-expert assignments. The table sums these assignments over all experts on a rank, and an individual expert’s average GEMM M is num_tokens * top_k / num_experts under balanced routing.

EP keeps each rank’s GEMM full-width and does redundant work on no token, but pays the routing exchange and becomes load-sensitive when the router is skewed. TP still performs router/top-k selection, but needs no cross-rank token dispatch. It stays efficient when there are too few tokens to fill an EP dispatch, although every rank redundantly reads every token and each expert’s GEMM gets narrower as tp_size grows.

The scheme is notated together with the attention parallelism:

Notation

Attention

MoE

DEP

DP

expert parallelism

TEP

TP

expert parallelism

DTP

DP

tensor parallelism

TTP<k>EP<m>

TP

moe_tp_size=k, moe_ep_size=m

Communication#

Under expert parallel every rank holds only part of the experts, so a token whose target expert lives elsewhere has to be dispatched there. Each MoE forward therefore sends every token to the ranks owning its top_k experts and brings the partial results back. These are the Dispatch and Combine in the diagram above.

TensorRT-LLM provides several implementations of this exchange. They differ in the physical link (NVLink inside a domain, or the network across it), how the data is moved (one-sided writes into the peer’s memory, or a two-sided handshake), and whether tokens are routed at all. The best communication choice varies with specific workload and deployment environment.

Strategy

Mechanism

NVLINK_ONE_SIDED

One-sided writes into a symmetric MNNVL workspace

NVLINK_TWO_SIDED

Two-sided alltoall over MNNVL

DEEPEP

DeepEP normal-mode kernels over the network

DEEPEPLOWLATENCY

DeepEP low-latency kernels over the network

NCCL_EP

NCCL-EP 0.2 low-latency rank-major exchange, BF16, FP8 QDQ, or NVFP4 dispatch

ALLGATHER

AllGather the tokens, ReduceScatter the outputs

The MEGAMOE backends fuse dispatch and combine into the expert GEMM kernel over a symmetric memory heap, writing straight into a peer’s buffer with no host-side exchange, which forces the communication to NONE.

How to assign a communication strategy?#

By default TensorRT-LLM picks the strategy itself (AUTO). The choice is made from the maximum shapes rather than the live token count.

Default selection (`AUTO`)
│
├─ backend == MEGAMOE_*, or DWDP enabled     → NONE
│     (the fused path owns the exchange)
├─ attention TP, or dp_size == 1             → NONE
│     (nothing to dispatch)
├─ moe_tp_size != 1                          → ALLGATHER
│     (an alltoall cannot serve MoE TP)
├─ TRTLLM_FORCE_COMM_METHOD set              → that method
│     (forced: no fallback if it fails to initialize)
│
└─ else, try in order — keep the first that initializes on this platform
       │
       ├─ NVLINK_ONE_SIDED     needs MNNVL, and moe_ep_size == world_size
       │
       ├─ num_experts % moe_ep_size != 0     → ALLGATHER
       │     (candidates below are skipped when not divisible)
       │
       ├─ NVLINK_TWO_SIDED     needs MNNVL
       ├─ NCCL_EP              needs NCCL-EP 0.2+, BF16 activations
       ├─ DEEPEP               needs BF16 activations
       ├─ DEEPEPLOWLATENCY     needs BF16 activations, top_k <= 16
       │
       └─ ALLGATHER            final fallback

To override the default, set TRTLLM_FORCE_COMM_METHOD on every rank:

TRTLLM_FORCE_COMM_METHOD=DEEPEPLOWLATENCY \
  trtllm-serve <model> --tp_size 16 --ep_size 16 --enable_attention_dp \
    --extra_llm_api_options config.yaml

The value must be one of the strategy names in the table above.

Implementation Notes: MOE_DEVELOPER_GUIDE.md.

Backend#

TensorRT-LLM provides many MoE implementations, differing in supported quantization formats, target GPU architecture, and expert-GEMM scheduling. This is the Expert GEMM box in the diagram above.

Backend

GPU arch

Quantization

CUTLASS

SM80+

unquantized, FP8 QDQ, FP8 block-scale, NVFP4, W4A16_NVFP4, W4A8_AWQ, W8A16, MXFP4, MXFP8

TRTLLM

SM100 / SM103

NVFP4, FP8 block-scale, W4A8_NVFP4_FP8, W4A16_MXFP4, W4A8_MXFP4_FP8, W4A8_MXFP4_MXFP8, BF16 (a separate FlashInfer path)

CUTEDSL

SM100 / SM103 / SM107, SM120 / SM121 (CUTLASS prefill / FlashInfer decode)

NVFP4, BF16 (SM107), W4A16_NVFP4 (SM120 / SM121)

CUTEDSL_FC12

SM107

NVFP4

DEEPGEMM

SM100 / SM103 / SM107

FP8 block-scale

DENSEGEMM

SM100 / SM103

NVFP4

MEGAMOE_CUTEDSL

SM100 / SM103 / SM107

NVFP4

MEGAMOE_DEEPGEMM

SM100 / SM103

W4A8_MXFP4_MXFP8

MARLIN

SM89 – SM99

NVFP4, W4A16_NVFP4 (BF16 activations)

TRITON (Deprecated)

SM90 (GPT-OSS)

BF16, FP8 QDQ, W4A16_MXFP4, W4A8_MXFP4_FP8

How to assign a backend?#

Set moe_config.backend in the trtllm-serve config yaml:

moe_config:
  backend: TRTLLM          # AUTO (default), CUTLASS, CUTEDSL, CUTEDSL_FC12, TRTLLM, DEEPGEMM,
                           # DENSEGEMM, MEGAMOE_CUTEDSL, MEGAMOE_DEEPGEMM, MARLIN, ...

The Backend Capability Matrix and Quantization Support tables: MOE_DEVELOPER_GUIDE.md.

Terminology#

  • world_size: number of ranks (GPUs) the model is served on.

  • NVLink domain: the set of GPUs reachable over NVLink. GB300 (NVL72) forms one domain of up to 72 GPUs, so EP crosses the domain only when world_size > 72. B300 forms one domain per 8-GPU node, so EP crosses the domain when world_size > 8 and falls back to the slower network.

  • prefill / decode: prefill processes the whole prompt at once (large GEMM M, compute bound), decode produces one token per request per step (small M, communication and memory bound). The two phases usually need different configurations.

Tuning a Specific Workload#

The decision trees give a general configuration that works well across a wide range of deployments. For a specific workload and world size, searching the whole configuration space can find a better one.

bench_moe runs the MoE module on its own, without deploying the full model. With the model, quantization, world size and token counts fixed, --search full expands backend, communication, parallel layout, and CUDA Graph. This includes layouts the decision trees do not select, such as MoE TP and the even-split hybrid (D|T)TP<k>EP<m> modes. It can be executed as:

PYTHONPATH=tests/microbenchmarks:${PYTHONPATH:-} \
mpirun --allow-run-as-root --oversubscribe --bind-to none --map-by slot -np 8 \
  python3 -m bench_moe \
  --world_size 8 \
  --model deepseek_v4_pro \
  --quant NVFP4 \
  --search full \
  --balanced_total_num_tokens 256 8192 \
  --use_low_precision_moe_combine \
  --output_file out/deepseek_v4_pro_ws8.json

This writes out/deepseek_v4_pro_ws8.json and out/deepseek_v4_pro_ws8.analysis.xlsx. If attention parallelism is already decided, pin --parallel_mode DEP or TEP and --search backend comm instead.

The JSON has results (one object per candidate) and rankings grouped by (num_tokens, parallel_mode). Start at rankings: best is the status=success candidate with the lowest score_ms in that group.

{
  "num_tokens": 256,
  "parallel_mode": "DEP",
  "best": {
    "backend": "TRTLLM",
    "comm_method": "NVLinkOneSided",
    "cuda_graph": true,
    "score_ms": 0.85,
    "status": "success"
  }
}

score_ms is the trimmed mean of per-iteration slowest-rank forward times; lower is better. In the workbook, best_by_workload lists those winners and all_results is the full table. Take the DEP or TEP group that matches the serving job. CUDA Graph on/off share that group, so if the job pins Graph (on for decode, off for prefill), filter cuda_graph in all_results. Set the winner with moe_config.backend and TRTLLM_FORCE_COMM_METHOD as in Configuration Options.

Search axes and further recipes: BENCH_MOE_USER_GUIDE.md.

One thing to know before trusting a bench_moe ranking: it has been cross-validated against end-to-end disaggregated serving, detailed in Appendix.

Advanced Techniques#

The two techniques below go beyond picking a configuration: they change how experts are placed or how their weights move, and each targets a cost that the options above cannot remove.

Expert Parallelism Load Balancer (EPLB)#

Expert Parallelism Load Balancer (EPLB), introduced in DeepSeek-V3, is proposed to remove the straggler effect caused by uneven expert activation under large-scale expert parallelism, where the ranks holding the most-activated experts dominate the latency of the whole module while the rest idle. It replicates hot experts and packs the resulting slots onto ranks so that the expected load is even, deriving the assignment from measured routing statistics.

It is recommended to enable EPLB on MoE models when expert parallelism is large (ep_size typically > 8), the MoE is a significant fraction of runtime, and ranks are imbalanced. This setting usually corresponds to the decode phase of a disaggregated deployment.

EPLB can use an offline plan or recompute the placement online. Offline EPLB is suitable when the traffic pattern is known and stable. Online EPLB is for production when the traffic mix can drift, because hot experts differ across datasets and requests. The online path follows that drift without regenerating a placement. The cost is a full host copy of the experts, background weight-update threads, and platform requirements (GDRCopy, huge pages). At small per-GPU batches, the statistics and update work may not hide behind the MoE GEMMs.

Usage: examples/wide_ep/ep_load_balancer/README.md.

Distributed Weight Data Parallelism (DWDP)#

Distributed Weight Data Parallelism (DWDP) targets layer-wise synchronization and token-exchange overhead in the prefill phase, where differences in sequence lengths and KV-cache hits can leave workers waiting for the slowest rank. DWDP keeps a resident subset of experts on each worker and asynchronously prefetches the remaining expert weights from its NVLink peers. Each worker computes on its own tokens, replacing synchronized token dispatch/combine with weight transfers that overlap with computation.

DWDP can be applied to the prefill phase of a serving system when the context batch is large enough for computation to hide the weight prefetch. Decode typically has too little computation to hide these transfers, so generation keeps the normal EP dispatch. Experimental cost of DWDP is a TTFT regression, from a lower service rate on the context stage, plus extra prefetch memory: each rank stores only its local experts, but adds a double-buffered region of about two layers of remote experts. DWDP cannot run on the same MoE path as EPLB. A disaggregated deployment can use DWDP on context servers and EPLB on generation servers.

Usage: examples/dwdp/README.md and blog19.

Appendix#

MoE Perf Dashboard#

The MoE Perf Dashboard publishes MoE performance of typical workloads on key models. The recommended configuration in this guide is summarized from these sweeps. Browse it at MoE Perf Dashboard.

Covered Models with MoE Configuration:

Model

num_experts

top_k

hidden_size

intermediate_size

DeepSeek-V4-Pro

384

6

7168

3072

GLM-5

256

8

6144

2048

Kimi-K2

384

8

7168

2048

Kimi-K3

896

16

3584

3072

Qwen3.8

512

10

8192

2048

Cross Validation of Microbenchmark with End-to-end Performance#

The microbenchmark ranks MoE configurations by module latency, without serving the full model. This ranking is validated against disaggregated serving.

Baseline: the Pareto serving configuration#

The baseline is the MoE configuration on the Pareto Curve of the InferenceX Dashboard. The curve comes from InferenceX’s public benchmark (fixed ISL/OSL, sweeping concurrency and parallelism), and each point is Pareto-optimal for system throughput and per-user interactivity (tps_per_user).

These configurations are published in InferenceX:

  • Dashboard: each point’s tooltip shows the recipe (model, framework, precision, parallelism, ISL/OSL, concurrency) and a link to the GitHub Actions run.

  • nvidia-master.yaml: lists the disagg topology (num-worker, tp, ep, dp-attn, ISL/OSL, concurrency) and the CONFIG_FILE path.

  • NVIDIA/srt-slurm: the CONFIG_FILE YAML holds the serving configuration (moe_config.backend, EP/TP, CUDA Graph, batch, MTP), for example ctx1_gen4_tep8_batch1_eplb0_mtp3.yaml.

How a Pareto point is compared to bench_moe microbenchmark?#

Each serving case (ISL, OSL, instance layout, batch, MTP, concurrency) is mapped to a bench_moe shape: the same model configuration, quantization, hardware, world size, num_tokens of the MoE forward, and CUDA Graph on or off. Candidates with the same deployed attention parallelism (DP or TP) are then ranked by module latency (score_ms, lower is better).

The deployed MoE config is compared with that best bench_moe candidate and labeled Match if they are the same, Mismatch otherwise. Each Mismatch is evaluated in disaggregated serving with both the Pareto-curve configuration and the bench_moe candidate, and compared on tps_per_user (tokens per second per user).

  • Improvement: the bench_moe candidate raises tps_per_user by more than 3%.

  • Flat: the change is within ±3%.

  • Regression: the bench_moe candidate lowers tps_per_user by more than 3%.

Model

Hardware

Match

Mismatch

Improvement

Flat

Regression

Regression after Perfect Routing

DeepSeek-V4-Pro

GB300

92/108 (85.19%, prefill 54/54, decode 38/54)

16/108

9/16

6/16

1/16

0/16

Kimi-K2

GB200

33/50 (66%, prefill 25/25, decode 8/25)

17/50

4/17

10/17

3/17

0/17

Kimi-K2

GB300

35/50 (70%, prefill 25/25, decode 10/25)

15/50

1/15

13/15

1/15

0/15

GLM-5

GB200

75/92 (81.52%, prefill 43/46, decode 32/46)

17/92

3/17

13/17

1/17

0/17

GLM-5

GB300

36/90 (40.00%, prefill 1/45, decode 35/45)

54/90

5/54

49/54

0/54

0/54

DeepSeek-R1

GB200

92/104 (88.46%, prefill 49/52, decode 43/52)

12/104

5/12

7/12

0/12

0/12

Regression cases are re-run with perfect routing (balanced expert load). They go to zero in the table, indicating the end-to-end gap came from router skew, as bench_moe ranks under balanced expert load.