MoE training deep-dive#
This guide covers Mixture-of-Experts (MoE) training in Primus on AMD Instinct GPUs: the bottlenecks unique to sparse models, the Primus/Primus-Turbo optimizations that address them, and a model-by-model tuning walkthrough. It is adapted from the AMD blog MoE Training Best Practices on AMD GPU (examples/moe_package/README.md) and grounded in the actual Primus configs and run scripts.
All flags shown here are the real CLI/YAML keys used by examples/moe_package/run_*_pretrain_mi355x.sh and the Megatron module configs (primus/configs/modules/megatron/). The Primus-Turbo MoE optimizations in this guide (DeepEP, sync-free MoE, Turbo grouped GEMM) are Megatron-backend features. TorchTitan also supports MoE via expert parallelism (expert_parallel_degree, expert_tensor_parallel_degree), but its tuning is out of scope here.
1. Why MoE training is different#
MoE scales model capacity by routing each token through a small subset of “expert” sub-networks instead of activating the whole network. A gating/router picks the top-k experts per token, so a model can hold many billions of parameters while only a fraction are active per token.
This sparsity creates performance challenges that dense models do not have:
Grouped GEMM overhead—each expert is a separate GEMM; naive multi-stream execution leaves scheduling gaps.
All-to-all (A2A) communication—token dispatch/combine across expert-parallel ranks can dominate runtime, especially with
EP >= 8and multi-node.CPU sync & launch delays—dynamic shapes (token counts per expert) force device-to-host syncs that stall the kernel launch queue.
Too many small kernels—fine-grained MoE ops stress the CPU launch path.
Pipeline load imbalance—uneven layer distribution across pipeline stages quietly degrades throughput.
Memory pressure—activations dominate memory at large scale, forcing recomputation.
2. Representative model configs#
Primus ships Megatron model presets for DeepSeek-style MoE models plus two ultra-large research configs:
Model |
Total / Active params |
Model config ( |
|---|---|---|
DeepSeek-V2-Lite |
16B / 2.4B |
|
DeepSeek-V2 |
236B / 21B |
|
DeepSeek-V3 |
671B / 37B |
|
MoE-1T |
1T / 44B |
|
MoE-2T |
2T / 80B |
|
Ready-to-run pretrain scripts live in examples/moe_package/, e.g.:
examples/moe_package/run_deepseek_v2_lite_pretrain_mi355x.shexamples/moe_package/run_deepseek_v2_pretrain_mi355x.shexamples/moe_package/run_deepseek_v3_pretrain_mi355x.sh
Each example script is a convenience wrapper: it sets environment + parallelism and selects an experiment YAML under examples/moe_package/configs/, then launches training. You can run the same training directly with the unified CLI, passing the experiment YAML with --config and the MoE feature toggles (Section 4) as overrides:
# DeepSeek-V2-Lite baseline + DeepEP + sync-free + loss fusion + manual GC, via primus-cli
export ENABLE_NUMA_BINDING=1 HSA_KERNARG_POOL_SIZE=12582912 # feature 6 (env, not CLI flags)
./runner/primus-cli direct -- train pretrain \
--config examples/moe_package/configs/MI355X/deepseek_v2_lite-pretrain-baseline.yaml \
--enable_primus_turbo True \
--use_turbo_deepep True --turbo_deepep_num_cu 64 --moe_router_dtype fp32 \
--turbo_sync_free_moe_stage 1 \
--cross_entropy_fusion_impl te --cross_entropy_loss_fusion True \
--manual_gc True --manual_gc_interval 1
Use ./runner/primus-cli slurm srun -N <nodes> -- train pretrain --config ... for multi-node. The feature tables below list the exact flags so you can compose your own command. (Optimizations that are environment variables—NUMA binding, HSA_KERNARG_POOL_SIZE, UCCL-EP—are exported before the command rather than passed as --flags.)
3. Profiling and analysis workflow#
Diagnose before optimizing. The recommended order:
Torch Profiler—capture operator times, memory, and GPU utilization. Enable through the Megatron profiling flags (
--profile,--use_pytorch_profiler,--profile_step_start,--profile_step_end,--disable_profiler_activity_cpu). Load the trace in Perfetto to inspect CPU/GPU overlap, launch delays, and idle gaps. See Profiling & observability.TraceLens—AMD’s automated trace analyzer for hierarchical breakdowns, roofline/efficiency, communication-vs-sync separation, and trace diffing. Wired into Primus via
generate_tracelens_report/mlflow_upload_tracelens_report(see the profiling guide).Memory projection—model VRAM across params, gradients, activations, and optimizer state before launching, via
./primus-cli direct -- projection memory --config <exp>.yaml. See Projection.Pipeline visualization—dump pipeline schedule data (
--dump_pp_data true) and render stage utilization withtools/visualization/pp_vis/vis.pyto find bubbles and stage imbalance.
4. Primus MoE optimizations#
The examples/moe_package/run_* scripts expose these as composable “MoE features.” The table maps each feature to the actual --flags (or environment variables) you pass to train pretrain—the same toggles the example scripts set.
Feature |
Flags (real keys) |
What it does |
|---|---|---|
Turbo attention |
|
Optimized attention kernels (Primus-Turbo). |
Turbo grouped GEMM |
|
Fused CK grouped GEMM processes all experts in one launch instead of multi-stream. |
Loss fusion |
|
Fuses large-vocab loss into one kernel to cut memory + launch overhead. |
DeepEP acceleration |
|
GPU-side index calc + sync-free dispatch to cut redundant cross-node A2A traffic. |
Sync-free MoE |
|
Removes CPU D2H syncs across Router → Dispatcher → Permutation → GroupMLP. |
NUMA binding |
|
Pins each GPU process to its NUMA socket for better memory bandwidth/stability. |
HIP kernarg pool |
|
Enlarges the kernel-argument pool (12 MB) to avoid launch stalls under many small kernels. |
Manual GC |
|
Periodic host GC to remove iteration-time jitter on long runs. |
UCCL-EP |
|
Use the UCCL transport for DeepEP dispatch/combine (sets |
Grouped GEMM keys. Enable Turbo grouped GEMM for MoE with
use_turbo_grouped_gemm(--use_turbo_grouped_gemm True). The olderuse_turbo_grouped_mlpalias has been removed—passing it now raises an assertion error (use_turbo_grouped_mlp has been removed; please use use_turbo_grouped_gemm instead).Legacy path. The legacy multi-stream grouped GEMM path is selected with
--moe_use_legacy_grouped_gemm True(the scripts defaultLEGACY_GG=True). Turbo grouped GEMM is incompatible with the legacy path—set--moe_use_legacy_grouped_gemm Falsewheneveruse_turbo_grouped_gemmis enabled (Primus raises an error otherwise).
Sync-free MoE stages#
turbo_sync_free_moe_stage is a single knob with four levels (0–3, validated in primus/backends/megatron/patches/args/rocm_arg_validation.py). Each stage auto-enables a set of fusion flags:
Level |
Auto-enabled flags |
Behavior |
|---|---|---|
|
— |
Disabled—standard baseline. |
|
|
Sync-free Router + Permutation fusion. |
|
stage 1 + |
Adds sync-free DeepEP dispatch and Turbo grouped GEMM. |
|
stage 2 + |
Full sync-free pipeline (adds fused activation). Per the blog this uses significantly more GPU memory—only enable with headroom. |
Requirements (enforced at startup):
All stages require
--enable_primus_turbo True.Stages
2and3require Turbo grouped GEMM and are therefore incompatible with--moe_use_legacy_grouped_gemm True.
Practical guidance from the run scripts:
MI355X:
--turbo_sync_free_moe_stage 1(compatible with the default legacy grouped GEMM path, since stage 1 does not enable Turbo grouped GEMM).MI300X / MI325X: the example scripts suggest stage
2(with--moe_shared_expert_overlap False --moe_router_dtype fp32). Because stage 2 auto-enables Turbo grouped GEMM, you must also set--moe_use_legacy_grouped_gemm False.
Scheduling and memory features#
1F1B A2A overlap—interleaves micro-batch N’s expert communication with micro-batch N-1’s backward compute on top of interleaved-1F1B pipeline parallelism, hiding A2A behind compute while preserving the bubble rate and roughly the same peak memory.
Arbitrary pipeline partition—manual stage layout instead of automatic even splits, to balance per-stage memory/compute. Use the Megatron-core
--pipeline_model_parallel_layoutflag (as the DeepSeek-V3 script does) or the Primusdecoder_pipeline_manual_split_listconfig key (primus/configs/modules/megatron/primus_megatron_module.yaml).Selective layer recompute—recompute specific transformer layers with
--recompute_layer_ids 0,1,2,3(keepRECOMPUTE_LAYERS=0so this is the only recompute control), or full block recompute via--recompute_granularity full --recompute_method block --recompute_num_layers N.MoE expert-parallel comm overlap—
overlap_moe_expert_parallel_comm: true(trainer_base.yaml).
See Performance tuning for the full Primus-Turbo flag reference.
5. Model-specific tuning#
DeepSeek-V2-Lite (16B / 2.4B, 27 layers)#
A compute/memory-efficient variant ideal for high-throughput pretraining. AMD Instinct’s large HBM (192 GB on MI300X, 288 GB on MI355X) lets you push micro-batch size (MBS) high to maximize throughput.
Recommended optimization stack (matches run_deepseek_v2_lite_pretrain_mi355x.sh, where MoE_Features=(3 4 5 6 7 8)):
Manual GC for stable iteration time.
Loss fusion for the large vocabulary.
DeepEP for A2A.
Sync-free mode (stage 1 on MI355X) to remove D2H syncs.
NUMA binding for CPU affinity (
ENABLE_NUMA_BINDING=1+HSA_KERNARG_POOL_SIZE).MBS scaling using the memory freed by the above (the blog reports peak memory dropping from ~99.8% to ~84.3% at MBS=12, enabling MBS=14).
The script’s default MoE_Features=(3 4 5 6 7 8) also enables feature 8 = UCCL-EP (see the feature table above). Default parallelism in the script: TP=1 ETP=1 PP=1 EP=8 CP=1, MBS=14 GBS=896 SEQ=4096.
DeepSeek-V2 (236B / 21B, 60 layers)#
Scale up with parallelism for max throughput across nodes. Recommended stack:
Manual GC, 2) Loss fusion, 3) DeepEP, 4) NUMA binding, 5) Sync-free mode, plus interleaved pipeline parallelism (VPP) to cut the pipeline bubble ratio. Enabling VPP (
--num_virtual_stages_per_pipeline_rank > 1) also improves sync-free mode effectiveness.
Default parallelism in the script: TP=1 ETP=1 PP=4 VPP=5 EP=8 CP=1 (interleaved PP), SEQ=4096.
1T+ parameter models (MoE-1T / MoE-2T, 96 layers)#
Ultra-large training combines every advanced technique. Use memory projection first—at this scale activations dominate memory, not parameters/optimizer state.
Findings from the blog’s projections (768–1024 GPUs):
Context parallelism (CP2) roughly halves activation memory (~76 GB/GPU saved for 1T, ~131 GB/GPU for 2T)—the most effective single lever.
Increasing EP (8→16) barely reduces memory but adds A2A time.
Increasing PP (24→48) doesn’t materially cut memory and raises pipeline bubbles + activation memory.
Suggested configs:
Model |
MI300X |
MI355X |
|---|---|---|
MoE-1T |
PP24 EP8 CP2 |
PP24 EP8 (no checkpointing) |
MoE-2T |
PP24 EP16 CP2 |
PP24 EP8 CP2 (benefits from larger DP) |
Pipeline bubble at scale. When the global batch is constrained, gradient-accumulation (GA) per iteration drops and the bubble ratio rises. Interleaved PP (VPP) mitigates this:
For PP=16, GA=16: VPP=1 gives ~48% bubble; VPP=6 gives ~14%—a large efficiency win verified on a 64-node setup.
Inter-node dispatch. Profiling on a 2T/1024-GPU run showed A2A consuming 25–30% of step time; DeepEP delivered roughly 1.05×–7.66× end-to-end speedup over plain A2A and kept EP scaling nearly flat.
6. Quick checklist#
Profile first—Torch Profiler + TraceLens; project memory before launching ultra-large runs.
Turn on Turbo—
--enable_primus_turbo True, then grouped GEMM, attention, DeepEP as needed (requires the externalprimus_turbopackage).Kill CPU syncs—
--turbo_sync_free_moe_stage(1 on MI355X, 2 on MI300X/MI325X).Stabilize + bind—
--manual_gc True,ENABLE_NUMA_BINDING=1,HSA_KERNARG_POOL_SIZE=12582912.Scale memory headroom into throughput—raise MBS; use CP2 + selective recompute for 1T+; use VPP to cut pipeline bubbles.