Repository navigation
Support heterogeneous DDP overlap in multimodal model training - #6284
Conversation
9dd93a0 to
140c0db
Compare
140c0db to
87d9792
Compare
|
/claude strict-review |
| 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 |
There was a problem hiding this comment.
[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.
| # Match the standalone language-model RNG; only encoders need role offsets. | ||
| _LANGUAGE_SEED_OFFSET = 0 |
There was a problem hiding this comment.
[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:
- The constant is now a no-op addend, so
role_seed_offset = _LANGUAGE_SEED_OFFSETat 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. examples/mimo/training/data.pystill 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_OFFSETgets 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_OFFSETindata.py); if they were meant to be shared, hoist them to one module and import.
| # 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 |
| 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 |
There was a problem hiding this comment.
[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.
| 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) |
There was a problem hiding this comment.
[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
basesaying this mirrors the bare-argscontract ofget_megatron_ddp_configand must be extended when that function grows a field, so the next person hitting theAttributeErrorknows 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.
Signed-off-by: ykarnati <ykarnati@nvidia.com>
87d9792 to
79004d8
Compare
Signed-off-by: ykarnati <ykarnati@nvidia.com>
| if forward_pre_hook_enabled or full_cg_captured: | ||
| for optim_instance in optimizer.chained_optimizers: | ||
| if isinstance(optim_instance, DistributedOptimizer): | ||
| if ( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>
|
/ok to test 1670ebb |
Signed-off-by: ykarnati <ykarnati@nvidia.com>
|
/ok to test de0ca4e |
|
/ok to test de0ca4e |
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA/Megatron-LM/actions/runs/32630524662 |
…A#6284) Signed-off-by: ykarnati <ykarnati@nvidia.com>
…A#6284) Signed-off-by: ykarnati <ykarnati@nvidia.com>
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>
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>
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
tools/autoformat.shon my PRValidation
3188397: 20 focused tests passed; 3 expected 8-GPU tests skipped (1 node / 4 allocated GPUs / 1 pytest process).git diff --checkpass on the changed scope.training.pyimports are unchanged and retain pre-existing full-file isort debt.