Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/user-guide/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ context_parallel
megatron_fsdp
dist_optimizer
optimizer_cpu_offload
paged_stash
pipeline_parallel_layout
tokenizers
megatron_energon
Expand Down
59 changes: 59 additions & 0 deletions docs/user-guide/features/paged_stash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!---
Copyright (c) 2022-2026, NVIDIA CORPORATION. All rights reserved.
NVIDIA CORPORATION and its licensors retain all intellectual property
and proprietary rights in and to this software, related documentation
and any modifications thereto. Any use, reproduction, disclosure or
distribution of this software and related documentation without an express
license agreement from NVIDIA CORPORATION is strictly prohibited.
-->

# MoE Paged Stash

*This is an experimental feature and may change.*

**Paged stash** = **sync-free** expert execution + **paged stashing** (packing routed-expert activations for backward into paged buffers).

**Sync-free:** `--moe-flex-dispatcher-backend hybridep`, `--use-transformer-engine-op-fuser`, and `--moe-expert-rank-capacity-factor` pre-size dispatch and fused grouped expert buffers from a user-controlled capacity, avoiding a per-step device query / realloc loop for buffer sizing.

**Paged stashing:** `--moe-paged-stash` stores those activations in paged CUDA buffers (optional pinned host spill). It helps save activation memory; sync-free still works without it, at the cost of higher activation memory use.

Whenever `moe_expert_rank_capacity_factor` is set, a **runner** wraps forward-backward: after each pass it checks **stash overflow** (only with `--moe-paged-stash`) and **token over-budget**. If either hits any rank, the step **reruns once** without capacity padding and without paged stashing.

## Prerequisites

HybridEP + TE fused grouped experts are required whenever `moe_expert_rank_capacity_factor` is set. With `moe_paged_stash` enabled: capacity factor must be set; no `cpu_offloading`; `offload_modules` must not include `expert_fc1` or `moe_act`. The runner is active whenever capacity factor is set (even without `--moe-paged-stash`) for over-budget reruns; stash overflow is checked only when paged stashing is on.

## Configuration

```bash
# Sync-free
--moe-flex-dispatcher-backend hybridep
--use-transformer-engine-op-fuser
--moe-expert-rank-capacity-factor <float>

# Paged stashing (to avoid memory waste due to fragmentation)
--moe-paged-stash
```

## Tuning (paged stashing only)

```bash
# Page size for stashing
--moe-paged-stash-page-size 64
# CUDA stashing buffer scaling factor (default 1.10)
--moe-paged-stash-buffer-size-factor-cuda 1.10
# Host spill (0 = off); same sign rule as CUDA
--moe-paged-stash-buffer-size-factor-cpu 0.0
```

## What `moe_expert_rank_capacity_factor` and `moe_paged_stash_buffer_size_factor_cuda` mean

Both are **multipliers on buffer size relative to the perfectly balanced case**—the space you would need if routed tokens were evenly distributed across expert ranks. A larger factor adds headroom for real-world **skew**.

## Choosing `moe_expert_rank_capacity_factor` and stash buffer scales

Profile how far real routing departs from the **balanced** reference, then pick factors so **skew spikes** rarely exceed your margin (avoid constant reruns).

- **`moe_expert_rank_capacity_factor`:** pick from profiles so **over-budget token drop** is uncommon; set **slightly above** the profiled value so reruns stay rare.
- **`moe_paged_stash_buffer_size_factor_cuda`:** size from the **same stats** (peaks vs averages) so **stash overflow** is uncommon; undersizing triggers reruns like over-budget.
- **`moe_paged_stash_buffer_size_factor_cpu`:** set **> 0** to allow **spill to pinned host** when CUDA pages are full—often **avoids overflow / rerun** at the cost of host memory and more overhead from paged stashing.
19 changes: 17 additions & 2 deletions megatron/core/full_cuda_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

"""Full iteration CUDA graph for training."""

import gc
import logging

import torch
Expand Down Expand Up @@ -182,12 +183,10 @@ def __call__(self, *args, **kwargs):
torch.cuda.synchronize()
torch.distributed.barrier()
logger.info(f'CUDA graph capture done for {training_str}!!!')

if FullCudaGraphWrapper.cuda_graph[training_str] is None:
FullCudaGraphWrapper.result[training_str] = self.forward_backward_func(*args, **kwargs)
else:
FullCudaGraphWrapper.cuda_graph[training_str].replay()

self.next_iter(training_str)
return FullCudaGraphWrapper.result[training_str]

Expand All @@ -198,3 +197,19 @@ def curr_iter(self, stage):
def next_iter(self, stage):
"""Increment current training/validation iteration."""
FullCudaGraphWrapper.curr_iteration[stage] += 1

def reset_cuda_graph(self, stage=None):
"""Reset CUDA graph."""
if stage is None or stage == 'training':
if FullCudaGraphWrapper.cuda_graph['training'] is not None:
del FullCudaGraphWrapper.cuda_graph['training']
FullCudaGraphWrapper.cuda_graph['training'] = None
FullCudaGraphWrapper.result['training'] = None
FullCudaGraphWrapper.curr_iteration['training'] = 0
if stage is None or stage == 'validation':
if FullCudaGraphWrapper.cuda_graph['validation'] is not None:
del FullCudaGraphWrapper.cuda_graph['validation']
FullCudaGraphWrapper.cuda_graph['validation'] = None
FullCudaGraphWrapper.result['validation'] = None
FullCudaGraphWrapper.curr_iteration['validation'] = 0
gc.collect()
12 changes: 12 additions & 0 deletions megatron/core/models/gpt/gpt_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from megatron.core.quantization.utils import get_quant_config_or_none
from megatron.core.tensor_parallel import gather_from_sequence_parallel_region
from megatron.core.transformer.enums import ModelType
from megatron.core.transformer.moe.paged_stash import paged_stash_init_chunk_handler
from megatron.core.transformer.multi_token_prediction import (
MultiTokenPredictionBlock,
mtp_on_this_rank,
Expand Down Expand Up @@ -478,6 +479,12 @@ def preprocess_for_fine_grained_offloading(self):
off_interface.mark_not_offloadable(param)
self.disable_param_offloading = False

def preprocess_for_paged_stash(self):
"""Preprocess for paged stash."""
return paged_stash_init_chunk_handler(
vp_size=self.config.virtual_pipeline_model_parallel_size, vp_stage=self.vp_stage
)

def forward(
self,
input_ids: Tensor,
Expand Down Expand Up @@ -516,6 +523,9 @@ def forward(
if self.config.fine_grained_activation_offloading:
self.preprocess_for_fine_grained_offloading()

if self.config.moe_paged_stash:
self.preprocess_for_paged_stash()

inference_context = deprecate_inference_params(inference_context, inference_params)

preproc_output = self._preprocess(
Expand Down Expand Up @@ -799,6 +809,8 @@ def build_schedule_plan(

if self.config.fine_grained_activation_offloading:
self.preprocess_for_fine_grained_offloading()
if self.config.moe_paged_stash:
self.preprocess_for_paged_stash()

from ..common.model_chunk_schedule_plan import TransformerModelChunkSchedulePlan

Expand Down
10 changes: 10 additions & 0 deletions megatron/core/models/hybrid/hybrid_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from megatron.core.transformer import TransformerConfig
from megatron.core.transformer.enums import InferenceCudaGraphScope, ModelType
from megatron.core.transformer.module import GraphableMegatronModule
from megatron.core.transformer.moe.paged_stash import paged_stash_init_chunk_handler
from megatron.core.transformer.multi_token_prediction import (
MultiTokenPredictionBlock,
mtp_on_this_rank,
Expand Down Expand Up @@ -343,6 +344,12 @@ def preprocess_for_fine_grained_offloading(self):
off_interface.mark_not_offloadable(param)
self.disable_param_offloading = False

def preprocess_for_paged_stash(self):
"""Preprocess for paged stash."""
return paged_stash_init_chunk_handler(
vp_size=self.config.virtual_pipeline_model_parallel_size, vp_stage=self.vp_stage
)

def _should_call_local_cudagraph(self, *args, **kwargs):
"""
Check if we should call the local cudagraph path.
Expand Down Expand Up @@ -406,6 +413,9 @@ def forward(
if self.config.fine_grained_activation_offloading:
self.preprocess_for_fine_grained_offloading()

if self.config.moe_paged_stash:
self.preprocess_for_paged_stash()

inference_context = deprecate_inference_params(inference_context, inference_params)

in_inference_mode = InferenceMode.is_active()
Expand Down
10 changes: 10 additions & 0 deletions megatron/core/pipeline_parallel/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
ProcessGroupCollection,
)
from megatron.core.transformer.cuda_graphs import create_cudagraphs, set_current_microbatch
from megatron.core.transformer.moe.paged_stash import paged_stash_reset
from megatron.core.transformer.moe.router import MoEAuxLossAutoScaler
from megatron.core.utils import (
drain_embedding_wgrad_compute,
Expand Down Expand Up @@ -646,6 +647,9 @@ def forward_backward_no_pipelining(
if config.timers is not None:
config.timers('forward-backward', log_level=1).start(barrier=config.barrier_with_L1_time)

if getattr(config, "moe_paged_stash", False):
paged_stash_reset(enabled=not forward_only, config=config)

no_sync_func = config.no_sync_func
if no_sync_func is None:
no_sync_func = contextlib.nullcontext
Expand Down Expand Up @@ -968,6 +972,9 @@ def forward_backward_pipelining_with_interleaving(
adjust_tensor_shapes_fn is None
), "adjust_tensor_shapes_fn is not supported for interleaved pipeline parallelism"

if getattr(config, "moe_paged_stash", False):
paged_stash_reset(enabled=not forward_only, config=config)

if config.overlap_p2p_comm and config.batch_p2p_comm:
raise ValueError("Can not use both overlap_p2p_comm and batch_p2p_comm")

Expand Down Expand Up @@ -2141,6 +2148,9 @@ def forward_backward_pipelining_without_interleaving(
if config.timers is not None:
config.timers('forward-backward', log_level=1).start(barrier=config.barrier_with_L1_time)

if getattr(config, "moe_paged_stash", False):
paged_stash_reset(enabled=not forward_only, config=config)

# Disable async grad reductions
no_sync_func = config.no_sync_func
if no_sync_func is None:
Expand Down
47 changes: 37 additions & 10 deletions megatron/core/transformer/moe/experts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import logging
from collections.abc import Callable
from contextlib import nullcontext
from copy import deepcopy
from dataclasses import dataclass
from itertools import chain
Expand Down Expand Up @@ -37,6 +38,11 @@
get_align_size_for_quantization,
skip_routed_expert_padding,
)
from megatron.core.transformer.moe.paged_stash import (
get_paged_stash_context,
paged_stash_group_commit,
paged_stash_group_start,
)
from megatron.core.transformer.moe.token_dispatcher_inference import (
InferenceAllGatherDispatcherBase,
NVLSAllGatherVDispatcher,
Expand Down Expand Up @@ -505,19 +511,40 @@ def _fused_forward(
tokens_per_expert = torch.tensor(
tokens_per_expert, dtype=torch.int, device=permuted_probs.device
)

# Call fused impl
output = ops(
permuted_local_hidden_states,
tokens_per_expert, # FC1
permuted_probs, # Scaled SwiGLU
tokens_per_expert, # FC2
)

# if the number of tokens is 0, pad the hidden states to 256

if self.config.moe_paged_stash:
permuted_local_hidden_states = paged_stash_group_start(permuted_local_hidden_states)
max_num_tokens = permuted_local_hidden_states.shape[0]
# Average/expected tokens is a pre-padding estimate used by paged stashing heuristics.
# moe_expert_rank_capacity_factor is required when moe_paged_stash is enabled.
cap_factor = self.config.moe_expert_rank_capacity_factor
avg_num_tokens = (
int(max_num_tokens // cap_factor)
if cap_factor is not None and cap_factor > 0
else None
)
stash_context = get_paged_stash_context(
name="grouped_mlp",
max_num_tokens=max_num_tokens,
num_tokens_tensor=tokens_per_expert.sum(),
avg_num_tokens=avg_num_tokens,
)
else:
stash_context = nullcontext()
with stash_context:
# Call fused impl
output = ops(
permuted_local_hidden_states,
tokens_per_expert, # FC1
permuted_probs, # Scaled SwiGLU
tokens_per_expert, # FC2
)
# Remove padding if needed
if unpadded_tokens_per_expert is not None:
output = self.quantization_unpadding(output, unpadded_tokens_per_expert)

if self.config.moe_paged_stash:
output = paged_stash_group_commit(output, name="grouped_mlp")
return output

@staticmethod
Expand Down
Loading
Loading