Skip to content

Support heterogeneous DDP overlap in multimodal model training - #6284

Merged
deepakn94 merged 5 commits into
NVIDIA:mainfrom
yashaswikarnati:agent/mimo-mxfp8-lifecycle
Aug 23, 2026
Merged

deepakn94 merged 5 commits into
NVIDIA:mainfrom
yashaswikarnati:agent/mimo-mxfp8-lifecycle

Conversation

@yashaswikarnati

@yashaswikarnati yashaswikarnati commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor
  • I, the PR author, have personally reviewed every line of this PR.

What does this PR do?

Allows multimodal models to use DDP-wrapped submodules with different
parameter-gather overlap policies while participating in the standard
Megatron training, checkpoint, rerun, and paged-stash lifecycles.

Issue tracking

Linked issue: N/A — compatibility gaps found during MIMO training integration.

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • I have added proper typing to my code
  • I have added relevant documentation
  • I have run tools/autoformat.sh on my PR

Validation

  • CMH job 3188397: 20 focused tests passed; 3 expected 8-GPU tests skipped (1 node / 4 allocated GPUs / 1 pytest process).
  • Black 26.3.0, isort 5.13.2, Ruff 0.9.10, Pylint 3.2.6, Python compilation, and git diff --check pass on the changed scope. training.py imports are unchanged and retain pre-existing full-file isort debt.

@copy-pr-bot

copy-pr-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@yashaswikarnati
yashaswikarnati force-pushed the agent/mimo-mxfp8-lifecycle branch from 9dd93a0 to 140c0db Compare August 18, 2026 03:54
@yashaswikarnati yashaswikarnati changed the title Support MXFP8 parameter gather in MIMO training Align MIMO DDP, MXFP8, and RNG behavior Aug 18, 2026
@yashaswikarnati
yashaswikarnati force-pushed the agent/mimo-mxfp8-lifecycle branch from 140c0db to 87d9792 Compare August 20, 2026 03:48
@yashaswikarnati

Copy link
Copy Markdown
Contributor Author

/claude strict-review

Comment thread megatron/core/models/mimo/optimizer.py Resolved
Comment thread examples/mimo/training/runtime.py Resolved
Comment thread examples/mimo/training/runtime.py Resolved
Comment thread megatron/core/models/mimo/optimizer.py Resolved
Comment thread megatron/core/models/mimo/optimizer.py Outdated
Comment on lines +359 to +364
module_config = copy(config)
module_config.overlap_param_gather = overlap_param_gather
module_config.overlap_param_gather_with_optimizer_step = (
config.overlap_param_gather_with_optimizer_step and overlap_param_gather
)
return module_config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION Simplification] copy(config) on a dataclass does not re-run __post_init__, so the derived config skips every validation in OptimizerConfig.__post_init__ (megatron/core/optimizer/optimizer_config.py:398-431) — including the reuse_grad_buf_for_mxfp8_param_ag + overlap_param_gather_with_optimizer_step ValueError.

For the two fields actually mutated here that's benign today (neither feeds use_precision_aware_optimizer_no_fp8_or_ds_fp8 or any other derived field), and skipping re-validation is arguably what you want since you're relaxing the flags. But it means a future field added to this function bypasses validation silently. dataclasses.replace re-runs __post_init__ and expresses the intent as "derive a variant" rather than "shallow-copy then poke fields":

def _optimizer_config_for_module(
    config: OptimizerConfig, module: torch.nn.Module
) -> OptimizerConfig:
    """Match optimizer overlap behavior to the module's DDP lifecycle."""
    ddp_config = getattr(module, 'ddp_config', None)
    if ddp_config is None:
        return config
    overlap_param_gather = ddp_config.overlap_param_gather
    return replace(
        config,
        overlap_param_gather=overlap_param_gather,
        overlap_param_gather_with_optimizer_step=(
            config.overlap_param_gather_with_optimizer_step and overlap_param_gather
        ),
    )

If __post_init__ re-validation is deliberately unwanted, keep copy but add a one-line comment saying so — otherwise the next reader will assume the derived config was validated. Note also that OptimizerConfig.overlap_param_gather's own docstring (optimizer_config.py:325-341) says it is "intended to have the same value as" the DDP-level flag; this function is the mechanism that now enforces that per module, which is worth stating in the docstring.

Comment thread examples/mimo/training/builder.py Outdated
Comment on lines +27 to +28
# Match the standalone language-model RNG; only encoders need role offsets.
_LANGUAGE_SEED_OFFSET = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION Naming] Setting this to 0 is correct for RNG parity with a standalone LM run (offsets 0 and 10_000 cannot collide with the 100 * pp_rank + 10 * dp_rank terms in _set_random_seed), but it leaves two problems:

  1. The constant is now a no-op addend, so role_seed_offset = _LANGUAGE_SEED_OFFSET at line 147 reads as "apply an offset" while applying none. That is fine as documentation of intent, but the comment should say the value is intentionally zero because it is the baseline, not just that encoders need offsets.
  2. examples/mimo/training/data.py still defines its own _LANGUAGE_SEED_OFFSET = 20_000 (and _ENCODER_SEED_OFFSET = 10_000). Two module-level constants with the same name in the same package now mean different things, and a reader grepping _LANGUAGE_SEED_OFFSET gets contradictory answers. If the data-loader offsets are a separate namespace on purpose, rename one pair to say which seed stream it offsets (e.g. _LANGUAGE_DATA_SEED_OFFSET in data.py); if they were meant to be shared, hoist them to one module and import.
Suggested change
# Match the standalone language-model RNG; only encoders need role offsets.
_LANGUAGE_SEED_OFFSET = 0
# Zero: the language role keeps the baseline seed so its RNG matches a standalone
# language-model run bit-for-bit. Only encoders need a role offset.
_LANGUAGE_SEED_OFFSET = 0

Comment on lines +96 to +113
ddp_average_in_collective=True,
use_distributed_optimizer=True,
fp8_param_gather=True,
reuse_grad_buf_for_mxfp8_param_ag=True,
)

enabled = _ddp_config_from_args(args, enable_overlap=True)
disabled = _ddp_config_from_args(args, enable_overlap=False)

assert enabled.overlap_grad_reduce
assert enabled.overlap_param_gather
assert enabled.check_for_nan_in_grad
assert enabled.average_in_collective
assert enabled.use_distributed_optimizer
assert not disabled.overlap_grad_reduce
assert not disabled.overlap_param_gather
assert disabled.check_for_nan_in_grad
assert disabled.average_in_collective

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] ddp_average_in_collective=True asserts a configuration MIMO can never reach, which makes these two assertions misleading rather than protective.

MIMO requires calculate_per_token_loss=True (examples/mimo/model_providers/nemotron_moe_vlm.py:133-134, enforced again in examples/mimo/training/grad_sync.py:143-146), and DistributedDataParallel.__init__ asserts not average_in_collective whenever calculate_per_token_loss is set (megatron/core/distributed/distributed_data_parallel.py:244-247). So a MIMO DDP wrap with average_in_collective=True fails at construction — the test proves the flag is forwarded, but a reader will take it as proof the combination is supported.

Either drop the flag from this test and leave a comment explaining why it can't be exercised, or turn it into an explicit negative test that the wrap rejects it. The use_distributed_optimizer / fp8_param_gather / reuse_grad_buf_for_mxfp8_param_ag assertions are the valuable ones here and should stay.

Comment on lines +35 to 58
ddp_defaults = vars(DistributedDataParallelConfig())
base = dict(
seed=1234, image_token_id=100, fp32=True, ddp_num_buckets=None, ddp_bucket_size=None
ddp_defaults,
seed=1234,
image_token_id=100,
fp32=True,
accumulate_allreduce_grads_in_fp32=ddp_defaults["grad_reduce_in_fp32"],
check_for_nan_in_loss_and_grad=ddp_defaults["check_for_nan_in_grad"],
check_for_large_grads=ddp_defaults["check_for_large_grads"],
ddp_num_buckets=ddp_defaults["num_buckets"],
ddp_bucket_size=ddp_defaults["bucket_size"],
ddp_pad_buckets_for_high_nccl_busbw=ddp_defaults["pad_buckets_for_high_nccl_busbw"],
ddp_reduce_scatter_with_fp32_accumulation=ddp_defaults[
"reduce_scatter_with_fp32_accumulation"
],
ddp_param_name_patterns_for_fp32_local_accumulation=list(
ddp_defaults["param_name_patterns_for_fp32_local_accumulation"]
),
ddp_average_in_collective=ddp_defaults["average_in_collective"],
use_precision_aware_optimizer=ddp_defaults["megatron_fsdp_use_decoupled_grad"],
cuda_graph_impl="none",
)
base.update(overrides)
return argparse.Namespace(**base)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION Simplification] This helper hand-mirrors get_megatron_ddp_config's argument contract: vars(DistributedDataParallelConfig()) covers the same-named fields, and the ten explicit entries cover the renamed ones. That works today, but get_megatron_ddp_config reads bare args.X (no getattr default) at megatron/training/training.py:2617-2658, so any new bare read added there breaks these MIMO tests with an AttributeError that names a field unrelated to MIMO — a maintenance trap for whoever touches that function.

Two options, either is fine:

  • Add a short comment above base saying this mirrors the bare-args contract of get_megatron_ddp_config and must be extended when that function grows a field, so the next person hitting the AttributeError knows where to look.
  • Better: have get_megatron_ddp_config (or a small helper next to it) expose the arg-name mapping, and build the namespace from that, so the mapping lives in one place instead of being duplicated in a MIMO test.

Also use_precision_aware_optimizer=ddp_defaults["megatron_fsdp_use_decoupled_grad"] is confusing enough to warrant a word — it reads like a typo until you check that get_megatron_ddp_config maps megatron_fsdp_use_decoupled_grad=args.use_precision_aware_optimizer.

Comment thread examples/mimo/training/builder.py Resolved
Signed-off-by: ykarnati <ykarnati@nvidia.com>
@yashaswikarnati
yashaswikarnati force-pushed the agent/mimo-mxfp8-lifecycle branch from 87d9792 to 79004d8 Compare August 20, 2026 21:14
Signed-off-by: ykarnati <ykarnati@nvidia.com>
@yashaswikarnati
yashaswikarnati marked this pull request as ready for review August 21, 2026 19:06
@yashaswikarnati
yashaswikarnati requested a review from a team as a code owner August 21, 2026 19:06
@yashaswikarnati
yashaswikarnati requested a review from a team August 21, 2026 19:06
@yashaswikarnati
yashaswikarnati requested review from a team as code owners August 21, 2026 19:06
@yashaswikarnati yashaswikarnati changed the title Align MIMO DDP, MXFP8, and RNG behavior Support heterogeneous DDP overlap in multimodal model training Aug 21, 2026
Comment thread megatron/training/training.py Outdated
if forward_pre_hook_enabled or full_cg_captured:
for optim_instance in optimizer.chained_optimizers:
if isinstance(optim_instance, DistributedOptimizer):
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment on why this changed?

from megatron.training.models.base import ModelBuilder, ModelConfig, compose_hooks

_LANGUAGE_SEED_OFFSET = 20_000
_LANGUAGE_SEED_OFFSET = 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the seed passed to _set_random_seed(),and which derives the data/tensor/expert parallel seeds for the rng tracker. I changed this to match pretrain_hrybrid, which passes seed 0. and the actual historic reason for having differnt encoder and language model seed offset is that in mimo encoders and LLM can be colocated or non colocated with different paralleisms

Comment thread megatron/training/training.py Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the outer loop uses args to check for overlap settings which are global, but we want to have encoders and LLM independently choose different overlap settings. the below checks uses the module's optimizer instance's ddp config to indepdently derive the overlap and reuse grad buffer settings

Signed-off-by: ykarnati <ykarnati@nvidia.com>
@deepakn94
deepakn94 enabled auto-merge August 22, 2026 21:47
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the Approved All necessary approvals have been made label Aug 22, 2026
@yashaswikarnati

Copy link
Copy Markdown
Contributor Author

/ok to test 1670ebb

Signed-off-by: ykarnati <ykarnati@nvidia.com>
@yashaswikarnati

Copy link
Copy Markdown
Contributor Author

/ok to test de0ca4e

@yashaswikarnati

Copy link
Copy Markdown
Contributor Author

/ok to test de0ca4e

@nemo-automation-bot

Copy link
Copy Markdown

🔄 Merge queue validation started!

You can track the progress here: https://github.com/NVIDIA/Megatron-LM/actions/runs/32630524662

Merged via the queue into NVIDIA:main with commit b964d16 Aug 23, 2026
92 checks passed
cspades pushed a commit to cspades/Megatron-LM that referenced this pull request Aug 25, 2026
ksivaman pushed a commit to ksivaman/Megatron-LM that referenced this pull request Sep 1, 2026
nemo-autobot-origin Bot pushed a commit to svcnemo-autobot/Megatron-LM that referenced this pull request Oct 19, 2026
Single repair commit for the 2026-10-16 main->dev sync of the 100-commit
main slice ending at 302ed23 onto dev
dee456a.

Dependencies
- pyproject.toml: add gigatoken to the training and mlm extras, which
  code imported from main requires.
- uv.lock: regenerated with uv 0.7.2 (Dockerfile pin); adds gigatoken
  0.10.0, awkward 2.14.0 and awkward-cpp 57; no removals.

Tests adapted from main's API to dev's API
- transformer/test_multi_token_prediction.py: dev's MTPLossLoggingHelper
  exposes reduced values via reduce_loss_in_tracker()/tracker["values"].
- data/test_get_batch.py: dev's pretrain_hybrid.get_batch reads
  args.linear_cp_layout and args.context_parallel_size.
- inference/contexts/test_dynamic_prefix_caching.py: on dev the engine
  records the Mamba match count before admission; set it like the engine.
- models/mimo/test_mimo_1f1b_schedule.py: pass use_layer_wise_param_layout
  so DistributedDataParallelConfig agrees with the layer-wise optimizer.
- transformer/experimental_attention_variant/test_attention_variant_dsa.py:
  dynamic context parallel on dev requires max_seqlen_per_dp_cp_rank.
- transformer/test_multi_token_prediction.py (main NVIDIA#6119 speculative-decoding
  capture test): dev stores the MTP input on model._decoder_hidden_states_cache,
  not on inference_context.mtp_decoder_hidden_states.

Golden values
- moe/gpt3_moe_mcore_te_ep8_resume_torch_dist_dist_muon dev H100 golden
  refreshed from the exact merge-candidate run: main 0560fbb (NVIDIA#6192)
  makes the LayerWise Muon child grad statistics span the intra
  distributed-optimizer group; reverting only that line reproduces dev's
  previous golden values exactly.

- tests: set max_seqlen_per_dp_cp_rank in dev dynamic-CP/scheduler test configs
  (transformer_layer, transformer_config_dynamic_cp, dsv4_hybrid_attention_cp) to
  satisfy main NVIDIA#5902 validation; dev arguments.py already derives it when a
  sequence-packing scheduler is set.
- tests: transformer/test_thd_cuda_graph.py balanced DSA dynamic-pack case with
  max_seqlen_per_dp_cp_rank=None now expects main NVIDIA#5902's earlier
  ModelParallelConfig validation message.
- tests: extend main NVIDIA#6284 mimo overlap-lifecycle optimizer mock with the dev
  _optimizer_state_offloader/_layer_wise_non_distopt_child attributes (NVIDIA#6244).
- tests: extend main NVIDIA#6119 HybridModel MTP forward stub with dev
  moe_n_hash_layers and pg_collection.dp_cp used by the dev forward.

Megatron-FSDP v2 symmetric memory (requester-approved alignment with main)
- experimental/dbuffer.py, experimental/parameter_group.py: take main's
  version. Symmetric-memory MFSDP raises "requires PyTorch 2.12 or later"
  on PyTorch builds without torch.distributed._symmetric_memory
  .is_symm_mem_tensor, as on main. This drops dev's allocation-flag
  fallback (ef8d090, 4a534df); with it, the symmetric path's losses
  diverged across ranks on dev's PyTorch 2.12 pre-release image and
  test_fully_shard_symmetric_memory_matches_default_and_profiles_nccl[3]
  failed on the merge candidate.
- mfsdp_v2 tests: skip the symmetric-memory cases on such builds
  (test_symmetric_memory.py, the symmetric_memory params of test_overlap.py
  and test_cuda_graph.py, and test_mcore_adapter.py nccl_ub). They run again
  once the image provides is_symm_mem_tensor.

Hybrid context parallelism (review 5570704991)
- models/hybrid/hybrid_block.py: HybridStack rejects
  linear_cp_layout='contiguous' with context parallelism when the segment
  contains GDN ('G') or KDA ('K') layers. main NVIDIA#6776 feeds contiguous
  shards to linear layers, but dev's GDN/KDA chunkwise and headwise paths
  still read their input layout from cp_partition_mode (zigzag by default)
  and would scramble token order. Contiguous Mamba/GDP and the zigzag
  GDN/KDA paths are unchanged.
- ssm/test_hybrid_block.py: cover the GDN/KDA rejection and the zigzag GDN
  and contiguous Mamba builds under CP=2.

Signed-off-by: svcnemo-autobot <svcnemo-autobot@nvidia.com>
nemo-autobot-origin Bot pushed a commit to svcnemo-autobot/Megatron-LM that referenced this pull request Oct 19, 2026
Single repair commit for the 2026-10-16 main->dev sync of the 100-commit
main slice ending at 302ed23 onto dev
dee456a.

Dependencies
- pyproject.toml: add gigatoken to the training and mlm extras, which
  code imported from main requires.
- uv.lock: regenerated with uv 0.7.2 (Dockerfile pin); adds gigatoken
  0.10.0, awkward 2.14.0 and awkward-cpp 57; no removals.

Tests adapted from main's API to dev's API
- transformer/test_multi_token_prediction.py: dev's MTPLossLoggingHelper
  exposes reduced values via reduce_loss_in_tracker()/tracker["values"].
- data/test_get_batch.py: dev's pretrain_hybrid.get_batch reads
  args.linear_cp_layout and args.context_parallel_size.
- inference/contexts/test_dynamic_prefix_caching.py: on dev the engine
  records the Mamba match count before admission; set it like the engine.
- models/mimo/test_mimo_1f1b_schedule.py: pass use_layer_wise_param_layout
  so DistributedDataParallelConfig agrees with the layer-wise optimizer.
- transformer/experimental_attention_variant/test_attention_variant_dsa.py:
  dynamic context parallel on dev requires max_seqlen_per_dp_cp_rank.
- transformer/test_multi_token_prediction.py (main NVIDIA#6119 speculative-decoding
  capture test): dev stores the MTP input on model._decoder_hidden_states_cache,
  not on inference_context.mtp_decoder_hidden_states.

Golden values
- moe/gpt3_moe_mcore_te_ep8_resume_torch_dist_dist_muon dev H100 golden
  refreshed from the exact merge-candidate run: main 0560fbb (NVIDIA#6192)
  makes the LayerWise Muon child grad statistics span the intra
  distributed-optimizer group; reverting only that line reproduces dev's
  previous golden values exactly.

- tests: set max_seqlen_per_dp_cp_rank in dev dynamic-CP/scheduler test configs
  (transformer_layer, transformer_config_dynamic_cp, dsv4_hybrid_attention_cp) to
  satisfy main NVIDIA#5902 validation; dev arguments.py already derives it when a
  sequence-packing scheduler is set.
- tests: transformer/test_thd_cuda_graph.py balanced DSA dynamic-pack case with
  max_seqlen_per_dp_cp_rank=None now expects main NVIDIA#5902's earlier
  ModelParallelConfig validation message.
- tests: extend main NVIDIA#6284 mimo overlap-lifecycle optimizer mock with the dev
  _optimizer_state_offloader/_layer_wise_non_distopt_child attributes (NVIDIA#6244).
- tests: extend main NVIDIA#6119 HybridModel MTP forward stub with dev
  moe_n_hash_layers and pg_collection.dp_cp used by the dev forward.

Megatron-FSDP v2 symmetric memory (requester-approved alignment with main)
- experimental/dbuffer.py, experimental/parameter_group.py: take main's
  version. Symmetric-memory MFSDP raises "requires PyTorch 2.12 or later"
  on PyTorch builds without torch.distributed._symmetric_memory
  .is_symm_mem_tensor, as on main. This drops dev's allocation-flag
  fallback (ef8d090, 4a534df); with it, the symmetric path's losses
  diverged across ranks on dev's PyTorch 2.12 pre-release image and
  test_fully_shard_symmetric_memory_matches_default_and_profiles_nccl[3]
  failed on the merge candidate.
- mfsdp_v2 tests: skip the symmetric-memory cases on such builds
  (test_symmetric_memory.py, the symmetric_memory params of test_overlap.py
  and test_cuda_graph.py, and test_mcore_adapter.py nccl_ub). They run again
  once the image provides is_symm_mem_tensor.

Hybrid context parallelism (review 5570704991)
- models/hybrid/hybrid_block.py: HybridStack rejects
  linear_cp_layout='contiguous' with context parallelism when the segment
  contains GDN ('G') or KDA ('K') layers. main NVIDIA#6776 feeds contiguous
  shards to linear layers, but dev's GDN/KDA chunkwise and headwise paths
  still read their input layout from cp_partition_mode (zigzag by default)
  and would scramble token order. Contiguous Mamba/GDP and the zigzag
  GDN/KDA paths are unchanged.
- ssm/test_hybrid_block.py: cover the GDN/KDA rejection and the zigzag GDN
  and contiguous Mamba builds under CP=2.
- tests: transformer/test_multi_token_prediction.py TestMultiTokenPredictionHybrid
  (main NVIDIA#6119) stub config sets cross_entropy_loss_fusion=False and
  cross_entropy_fusion_impl="native" (the TransformerConfig defaults);
  dev NVIDIA#8105 HybridModel.forward reads them when labels are given
  (review 5573599695).

Signed-off-by: svcnemo-autobot <svcnemo-autobot@nvidia.com>

This branch was previously deployed

2 inactive deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Approved All necessary approvals have been made complexity: low nemotron

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants