1313# limitations under the License.
1414
1515import logging
16- from functools import partial
16+ from functools import partial , wraps
1717from typing import Iterable
1818
1919import modelopt .torch .distill as mtd
2020import torch
21- from megatron .core import parallel_state
21+ from megatron .core import parallel_state , tensor_parallel
2222from megatron .core .models .gpt import GPTModel
2323from megatron .core .pipeline_parallel .utils import (
2424 is_pp_first_stage ,
2727 is_vp_last_stage ,
2828)
2929from megatron .core .transformer .enums import LayerType
30+ from megatron .core .transformer .moe .router import TopKRouter
3031from megatron .core .transformer .pipeline_parallel_layer_layout import PipelineParallelLayerLayout
3132from megatron .core .utils import (
3233 get_batch_on_this_cp_rank ,
4344from megatron .bridge .training .utils .flop_utils import accumulate_flops_metadata , get_model_chunk_vp_stage
4445from megatron .bridge .training .utils .packed_seq_utils import get_packed_seq_params , get_thd_cp_partition_indices
4546from megatron .bridge .training .utils .pg_utils import get_pg_collection
47+ from megatron .bridge .utils .cuda_graph import cuda_graph_module_names
4648
4749
4850logger = logging .getLogger (__name__ )
5557_LEGACY_PACKED_SEQ_HOST_KEYS = ("cu_seqlens_argmin" , "max_seqlen" , "cu_seqlens_unpadded_argmin" )
5658_LEGACY_PACKED_SEQ_PARAM_KEYS = (* _LEGACY_PACKED_SEQ_DEVICE_KEYS , * _LEGACY_PACKED_SEQ_HOST_KEYS , "total_tokens" )
5759_PackedMetadataValue = torch .Tensor | int | None
60+ _MCORE_EXPERT_BIAS_PADDING_MASK_PATCHED = "_mbridge_expert_bias_padding_mask_compatible"
61+ _MCORE_SCHEDULE_PADDING_MASK_PATCHED = "_mbridge_schedule_padding_mask_compatible"
5862
5963
6064def _trim_padded_cu_seqlens_for_cp (cu_seqlens : torch .Tensor , cu_seqlens_argmin : torch .Tensor | None ) -> torch .Tensor :
@@ -81,14 +85,144 @@ def _has_packed_sequence_metadata(batch: dict[str, torch.Tensor]) -> bool:
8185
8286
8387def _packed_metadata_for_forward (batch : dict [str , torch .Tensor ]) -> dict [str , _PackedMetadataValue ] | None :
84- """Extract packed-sequence metadata accepted by ``get_packed_seq_params`` ."""
88+ """Extract packed-sequence metadata needed by the forward step ."""
8589 if batch .get ("cu_seqlens_q" ) is not None :
86- return {key : batch [key ] for key in _CURRENT_PACKED_SEQ_PARAM_KEYS if batch .get (key ) is not None }
90+ metadata : dict [str , _PackedMetadataValue ] = {
91+ key : batch [key ] for key in _CURRENT_PACKED_SEQ_PARAM_KEYS if batch .get (key ) is not None
92+ }
93+ if batch .get ("padding_mask" ) is not None :
94+ metadata ["padding_mask" ] = batch ["padding_mask" ]
95+ return metadata
8796 if batch .get ("cu_seqlens" ) is not None :
8897 return {key : batch [key ] for key in _LEGACY_PACKED_SEQ_PARAM_KEYS if batch .get (key ) is not None }
8998 return None
9099
91100
101+ def _patch_mcore_expert_bias_padding_mask () -> None :
102+ """Adapt MCore expert-bias routing to accept a flat padding mask.
103+
104+ TODO(https://github.com/NVIDIA/Megatron-LM/issues/6111): Remove this
105+ compatibility patch after the MCore dev fix reaches the pinned main commit.
106+ """
107+ current_apply_expert_bias = TopKRouter ._apply_expert_bias
108+ if getattr (current_apply_expert_bias , _MCORE_EXPERT_BIAS_PADDING_MASK_PATCHED , False ):
109+ return
110+
111+ def _apply_expert_bias (
112+ self : TopKRouter ,
113+ routing_map : torch .Tensor ,
114+ padding_mask : torch .Tensor | None = None ,
115+ ) -> None :
116+ if padding_mask is not None :
117+ flat_mask = padding_mask .reshape (- 1 )
118+ assert flat_mask .shape [0 ] == routing_map .shape [0 ], (
119+ f"padding_mask flat { flat_mask .shape } vs routing_map { routing_map .shape } "
120+ )
121+ padding_mask = flat_mask .unsqueeze (- 1 )
122+ current_apply_expert_bias (self , routing_map , padding_mask = padding_mask )
123+
124+ setattr (_apply_expert_bias , _MCORE_EXPERT_BIAS_PADDING_MASK_PATCHED , True )
125+ TopKRouter ._apply_expert_bias = _apply_expert_bias
126+
127+
128+ def _patch_mcore_schedule_plan_padding_mask () -> None :
129+ """Forward the schedule-plan chunk mask into the pinned MCore MoE router.
130+
131+ TODO: Remove this compatibility patch after MCore dev's schedule-plan
132+ padding-mask forwarding reaches the pinned main commit.
133+ """
134+ from megatron .core .models .common import fine_grained_callables
135+
136+ current_builder = fine_grained_callables .build_transformer_layer_callables
137+ if getattr (current_builder , _MCORE_SCHEDULE_PADDING_MASK_PATCHED , False ):
138+ return
139+
140+ @wraps (current_builder )
141+ def build_transformer_layer_callables (layer ):
142+ forward_funcs , backward_dw = current_builder (layer )
143+ if not hasattr (layer .mlp , "route" ):
144+ return forward_funcs , backward_dw
145+
146+ forward_funcs = list (forward_funcs )
147+ current_pre_dispatch = forward_funcs [0 ]
148+
149+ @wraps (current_pre_dispatch )
150+ def pre_dispatch_with_padding_mask (node , * args , ** kwargs ):
151+ chunk_padding_mask = getattr (node .chunk_state , "padding_mask" , None )
152+ if chunk_padding_mask is None :
153+ return current_pre_dispatch (node , * args , ** kwargs )
154+
155+ had_instance_route = "route" in layer .mlp .__dict__
156+ instance_route = layer .mlp .__dict__ .get ("route" )
157+ current_route = layer .mlp .route
158+
159+ @wraps (current_route )
160+ def route_with_padding_mask (hidden_states , padding_mask = None , * args , ** kwargs ):
161+ if padding_mask is None :
162+ padding_mask = chunk_padding_mask
163+ return current_route (hidden_states , padding_mask , * args , ** kwargs )
164+
165+ layer .mlp .route = route_with_padding_mask
166+ try :
167+ return current_pre_dispatch (node , * args , ** kwargs )
168+ finally :
169+ if had_instance_route :
170+ layer .mlp .route = instance_route
171+ else :
172+ delattr (layer .mlp , "route" )
173+
174+ forward_funcs [0 ] = pre_dispatch_with_padding_mask
175+ return forward_funcs , backward_dw
176+
177+ setattr (build_transformer_layer_callables , _MCORE_SCHEDULE_PADDING_MASK_PATCHED , True )
178+ fine_grained_callables .build_transformer_layer_callables = build_transformer_layer_callables
179+
180+
181+ def _validate_packed_moe_cuda_graph (config ) -> None :
182+ """Reject router-scoped TE graphs that cannot consume packed padding masks."""
183+ if (
184+ not getattr (config , "num_moe_experts" , None )
185+ or getattr (config , "cuda_graph_impl" , "none" ) != "transformer_engine"
186+ ):
187+ return
188+
189+ graph_modules = set (cuda_graph_module_names (config ))
190+ captures_router = not graph_modules or bool (graph_modules & {"moe" , "moe_router" , "moe_preprocess" })
191+ if captures_router :
192+ raise ValueError (
193+ "Packed MoE padding masks do not support router-scoped Transformer Engine CUDA graphs in the pinned "
194+ "MCore. Use an attention-only CUDA graph scope or disable scoped CUDA graphs."
195+ )
196+
197+
198+ def _prepare_packed_padding_mask (
199+ padding_mask : torch .Tensor | None ,
200+ * ,
201+ config ,
202+ model : GPTModel ,
203+ pg_collection ,
204+ ) -> torch .Tensor | None :
205+ """Prepare an alignment-padding mask for the current model stage."""
206+ if padding_mask is None :
207+ return None
208+ if getattr (config , "moe_router_enable_expert_bias" , False ):
209+ _patch_mcore_expert_bias_padding_mask ()
210+
211+ # A pre-process GPT stage scatters this mask alongside its embeddings. HybridModel
212+ # scatters only its embeddings in the pinned MCore, while later PP stages already
213+ # receive SP-local activations, so both paths need the matching local mask here.
214+ needs_sp_scatter = not unwrap_model (model ).pre_process or getattr (config , "is_hybrid_model" , False )
215+ if getattr (config , "sequence_parallel" , False ) and needs_sp_scatter :
216+ padding_mask = (
217+ tensor_parallel .scatter_to_sequence_parallel_region (
218+ padding_mask .transpose (0 , 1 ).contiguous (), group = pg_collection .tp
219+ )
220+ .transpose (0 , 1 )
221+ .contiguous ()
222+ )
223+ return padding_mask
224+
225+
92226def _cu_seqlens_for_cp_partition (batch : dict [str , torch .Tensor ]) -> torch .Tensor :
93227 """Return the cu-seqlens tensor TE should use to partition packed THD tokens."""
94228 if batch .get ("cu_seqlens_q" ) is not None :
@@ -250,6 +384,8 @@ def get_batch_from_iterator(
250384 if "cu_seqlens_q" in batch :
251385 required_device_keys .update (key for key in _CURRENT_PACKED_SEQ_DEVICE_KEYS if key in batch )
252386 required_host_keys .update (key for key in _CURRENT_PACKED_SEQ_HOST_KEYS if key in batch )
387+ if batch .get ("padding_mask" ) is not None :
388+ required_device_keys .add ("padding_mask" )
253389 elif "cu_seqlens" in batch :
254390 required_device_keys .update (key for key in _LEGACY_PACKED_SEQ_DEVICE_KEYS if key in batch )
255391 required_host_keys .update (key for key in _LEGACY_PACKED_SEQ_HOST_KEYS if key in batch )
@@ -438,6 +574,19 @@ def _forward_step_common(
438574
439575 # Add packed sequence support
440576 if packed_seq_metadata is not None :
577+ padding_mask = packed_seq_metadata .get ("padding_mask" )
578+ # Supported producers make mask presence configuration-driven and stable
579+ # across DP ranks. Never branch on mask contents here: an all-false local
580+ # batch must follow the same path as a padded batch on another rank.
581+ if padding_mask is not None :
582+ _validate_packed_moe_cuda_graph (config )
583+ padding_mask = _prepare_packed_padding_mask (
584+ padding_mask ,
585+ config = config ,
586+ model = model ,
587+ pg_collection = pg_collection ,
588+ )
589+ packed_seq_metadata = {key : value for key , value in packed_seq_metadata .items () if key != "padding_mask" }
441590 # total_tokens drives seq_idx computation in PackedSeqParams.__post_init__,
442591 # which is only needed for Mamba/hybrid SSM layers. Skip it for pure
443592 # transformer models to avoid per-step CUDA overhead.
@@ -449,14 +598,24 @@ def _forward_step_common(
449598 else :
450599 packed_seq_metadata ["total_tokens" ] = getattr (config , "seq_length" , None )
451600 forward_args ["packed_seq_params" ] = get_packed_seq_params (packed_seq_metadata )
601+ if padding_mask is not None :
602+ forward_args ["padding_mask" ] = padding_mask
452603
453604 with straggler_timer :
454605 if return_schedule_plan :
455606 assert config .overlap_moe_expert_parallel_comm , (
456607 "overlap_moe_expert_parallel_comm must be enabled to return the schedule plan"
457608 )
609+ if forward_args .get ("padding_mask" ) is not None :
610+ _patch_mcore_schedule_plan_padding_mask ()
458611 schedule_plan = model .build_schedule_plan (
459- tokens , position_ids , attention_mask , labels = labels , loss_mask = loss_mask
612+ tokens ,
613+ position_ids ,
614+ attention_mask ,
615+ labels = labels ,
616+ loss_mask = loss_mask ,
617+ packed_seq_params = forward_args .get ("packed_seq_params" ),
618+ padding_mask = forward_args .get ("padding_mask" ),
460619 )
461620 return schedule_plan , loss_mask
462621 else :
0 commit comments