NMFW-464: Distributed checkpoint save/load for the hetero MIMO training loop - #26
Conversation
Adds the standalone `examples/mimo/training/hetero/checkpointing.py` module
plus the CLI surface and loop wiring needed to round-trip MimoModel,
MimoOptimizer (ChainedOptimizer-of-DistributedOptimizers in the MoE recipe)
and the LR/WD scheduler through `megatron.core.dist_checkpointing` without
depending on the `parallel_state` singleton.
Layout stays compatible with `megatron/training/checkpointing.py` output:
`<save>/latest_checkpointed_iteration.txt` plus per-iteration directories
containing `common.pt`, `metadata.json`, `.metadata`, and torch_dist shards.
Common state now carries `args`, `checkpoint_version=3.0`, the LR scheduler
state, and a per-branch `mimo.{branch}.rng_state` ShardedObject; the tracker
read uses a cross-rank MAX reduce to mirror megatron's `read_metadata`.
Fixes three pre-existing dist-ckpt bugs that hetero usage uncovered:
- `megatron/core/ssm/mamba_mixer.py` was calling
`make_sharded_tensors_for_checkpoint` without passing `tp_group` and
`dp_cp_group`, which fell back to the parallel_state singleton and
asserted in hetero mode (gated_delta_net was already correct).
- `MimoOptimizer.sharded_state_dict` now applies
`add_prefix_for_sharding(module_sd, f'mimo.{name}.')` to each per-branch
optimizer sub-dict so two modules' identical internal ShardedObject keys
(e.g. `chained_0.optimizer.distributed.dp_group_idx_0.*`) don't collide.
- `_get_replica_id` now folds in `tp_rank` so two TP ranks within DP=0
don't both claim primary writer for the same shard.
Also routes DistributedOptimizer's per-module `param_state_sharding_type`
config string through a new ShardedObject (`_extract_*` helpers) so the
non-rank-0 module owner doesn't lose it when only rank 0's common.pt is
authoritative.
A `_propagate_tp_groups_for_checkpoint` walker stamps `self.tp_group` on
descendants that omit it (e.g. `ExtendedRMSNorm`, RADIO submodules) so the
default `MegatronModule.sharded_state_dict` path doesn't fall through to
`parallel_state.get_tensor_model_parallel_group`.
Validated end-to-end on cw-dfw 8-GPU 20L mock (stage2):
- Save iter 3 (DistributedOptimizer + EP=4 + TP=2 + 2-module Chained)
- Reload iter 3 → resume at iter 4 with cosine LR continuation
(1.59e-4 → 1.32e-4 → 1.01e-4), losses match prior trajectory.
New flags: `--save`, `--load`, `--save-interval`, `--no-save-optim`,
`--no-load-optim`, `--no-load-scheduler`, `--no-save-rng`, `--no-load-rng`,
`--finetune`, `--dist-ckpt-optim-fully-reshardable`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| return (0, pg_collection.pp.rank(), pg_collection.dp.rank()) | ||
| tp = getattr(pg_collection, 'tp', None) | ||
| tp_rank = tp.rank() if tp is not None else 0 | ||
| return (pg_collection.pp.rank(), tp_rank, pg_collection.dp.rank()) |
There was a problem hiding this comment.
should tp rank be first or pp rank? how do we know ? can we check existing megatron code for optimizer or other classes? whats the expectation here ?
| suffix = f'.{idx}' if idx > 0 else '' | ||
| _extract_param_groups(sub_sd, name, suffix, replica_id) | ||
| _extract_grad_scaler(sub_sd, name, suffix, replica_id) | ||
| _extract_param_state_sharding_type(sub_sd, name, suffix, replica_id) |
There was a problem hiding this comment.
just for my own understanding why do we need this ?
| _stamp_tp_group(submodule, topology.vision_pg.tp) | ||
|
|
||
|
|
||
| def _stamp_tp_group(module: torch.nn.Module, tp_group) -> None: |
There was a problem hiding this comment.
for my own understanding, why do we need this ? this walks over all modules and sets tp group for every module in self.tp_group? which modules are missing ? trying to think if this indirect way is better or we directly fix the module that does not have tp group set, depends on what modules are missing. now
| persist MimoModel + MimoOptimizer + LR scheduler state without depending on | ||
| `megatron.training.checkpointing` (which assumes the parallel_state singleton). | ||
|
|
||
| Stays intentionally close to the layout that `megatron/training/checkpointing.py` |
There was a problem hiding this comment.
can we reuse functions and functionality from megatron/training where ever possible ?
Three convergent simplifications to MimoOptimizer's distributed-checkpoint
path, matching Kamran's MimoOptimizer fixes PR:
1. Replace the ShardedObject-based round-trip for
`param_state_sharding_type` with a metadata stash. The sharding type is
not per-rank state — it's a load-time interpretation hint that the caller
supplies via the `metadata` kwarg on `sharded_state_dict()`. We stash that
metadata in `self._last_sharded_metadata` at save and re-inject the
sharding type into each per-module sub state-dict during
`load_state_dict()` for ranks that lost it via dist_checkpointing's
common-state path (i.e. non-rank-0 module owners in non-colocated
layouts). Drops `_extract_param_state_sharding_type` /
`_restore_param_state_sharding_type` along with their ShardedObject keys.
2. `_restore_param_groups` now uses `setdefault('optimizer', {})` before
writing back `param_groups`. After `_extract_param_groups` deletes
`param_groups` at save time, the leftover empty `'optimizer'` dict can be
dropped by the common-state round-trip on ranks whose active module
wasn't on rank 0 at save. The setdefault makes the restore path tolerant
of that drop.
3. `_get_replica_id` reorders to `(tp_rank, pp_rank, dp_rank)` to match the
convention used by `make_sharded_object_for_checkpoint` in
`megatron/core/transformer/utils.py:168-172`. Dedup math is unchanged —
`(0, 0, 0)` is still the primary replica — but the order is now
consistent with the rest of the codebase.
Validated on cw-dfw 1-node 8-GPU 20L mock (stage2, DistributedOptimizer +
ChainedOptimizer + EP=4 + TP=2): save iter 3, reload, resume iter 4 with
cosine LR continuation (1.59e-4 → 1.32e-4 → 1.01e-4) and matching loss
trajectory. Save exit 0, load exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Disabled `_propagate_tp_groups_for_checkpoint` and re-ran the 20L mock to enumerate exactly which modules fall through to `parallel_state.get_tensor_model_parallel_group()` and assert. Confirmed both branches escape: - RADIO encoder internals (first failure, reached via `nemotron_moe_vlm.RadioEncoder.sharded_state_dict` → HF radio_model leaves with no tp_group + no own sharded_state_dict). - `MambaLayer.__init__` in `megatron/core/ssm/mamba_layer.py` plumbs pg_collection to the mixer but never sets `self.tp_group`. - `ExtendedRMSNorm` at `megatron/core/ssm/mamba_mixer.py:93` never sees pg_collection at all. Fixing each at the source would mean patches across core (Mamba) plus a partial walk of RADIO's HF wrapper, validated against all existing non-hetero users of those modules. The walker is the smaller intervention: one place, hasattr-guarded, applied per branch with the correct pg. Re-enables the walker (it was already in PR1; this commit only updates the docstring to record the experiment's findings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kamran reverted the metadata-stash approach in NVIDIA#4801 (discussion r3250847203) and adopted Li Ding's PR NVIDIA#4791 pattern, which is the same ShardedObject round-trip we had originally. Align our MimoOptimizer with that final shape: - Restore `_extract_param_state_sharding_type` / `_restore_param_state_sharding_type` helpers. Hooks back into the existing `_iter_optimizer_sub_dicts` loop. - Add `if not opt_sub: del sub_sd['optimizer']` to `_extract_param_groups` (from NVIDIA#4791) so the now-empty `'optimizer'` wrapper doesn't round-trip through common-state with undefined behavior on the load side. - Drop `self._last_sharded_metadata` and the metadata-stash recover path from `load_state_dict` / `sharded_state_dict`. The ShardedObject route is self-contained and doesn't need caller-state coupling. Kept (not in NVIDIA#4791, specific to our non-colocated hetero layout): - `add_prefix_for_sharding(module_sd, f'mimo.{name}.')` so the two branches' identical inner ShardedObject keys (e.g. `chained_0.optimizer.distributed.dp_group_idx_0.*`) don't collide. - `_get_replica_id` returning `(tp_rank, pp_rank, dp_rank)` (from NVIDIA#4801). Validated on cw-dfw 1-node 8-GPU 20L mock (stage2, DistributedOptimizer + ChainedOptimizer + EP=4 + TP=2): save iter 3 exit 0, reload + resume iter 4 with cosine LR continuation (1.59e-4 → 1.32e-4 → 1.01e-4), matching loss trajectory across the boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three modules under our hetero save path don't store `self.tp_group` in their constructors and therefore trip `MegatronModule.sharded_state_dict`'s parallel_state fallback (`megatron/core/transformer/module.py:85`) in heterogeneous-parallelism layouts where parallel_state is intentionally not initialized. Fix them at the source instead of papering over with the hasattr-guarded walker: - `megatron/core/models/vision/radio.py:RADIOViTModel.__init__` — already extracts `tp_group` at line 129 for the embedder; now also stamps `self.tp_group = tp_group`. - `megatron/core/ssm/mamba_layer.py:MambaLayer.__init__` — takes pg_collection and plumbs it into the mixer; now also stores `self.tp_group = pg_collection.tp` on the layer itself. - `megatron/core/ssm/mamba_mixer.py:ExtendedRMSNorm` — adds an `__init__(*args, tp_group=None, **kwargs)` override that stores `self.tp_group` eagerly, and updates the single call site at line ~369 to pass `tp_group=self.pg_collection.tp`. The lazy `hasattr` fallback inside `sharded_state_dict` is preserved for callers that don't pass tp_group. With these three constructor fixes in place, the `_propagate_tp_groups_for_checkpoint` walker (and `_stamp_tp_group` helper) in `examples/mimo/training/hetero/runtime.py` is no longer needed. Removed entirely. Validated on cw-dfw 1-node 8-GPU 20L mock with the walker disabled: - save iter 3 exit 0 (DistributedOptimizer + ChainedOptimizer + EP=4 + TP=2) - reload iter 3 → resume iter 4-5 with cosine LR continuation (1.59e-4 → 1.32e-4 → 1.01e-4), exit 0 - losses match prior runs (iter 1: 12.187, iter 2: 12.190, iter 3: 12.177, resume iter 4: 11.817, iter 5: 11.264) The downstream check `if not hasattr(self, 'tp_group')` in subsequent descendants (TransformerBlock, TransformerLayer, Attention, MLP, ColumnParallelLinear) was already satisfied by their own constructors; verified by reading those files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
23bf04a
into
ykarnati/nmfw-464-nemotron-vlm-with-hetero-parallel
Summary
Adds distributed-checkpoint save/load to the standalone hetero MIMO training loop (
examples/mimo/train_hetero.py) so MimoModel + MimoOptimizer (Chained DistributedOptimizer in the MoE recipe) + LR/WD scheduler + Python/Torch/CUDA RNG state round-trip throughmegatron.core.dist_checkpointingwithout depending onparallel_state. Layout is intentionally compatible withmegatron/training/checkpointing.pyoutput.Validated end-to-end on cw-dfw 8-GPU 20L mock (stage2): save at iter 3 → reload → resume at iter 4 with cosine LR continuation (1.59e-4 → 1.32e-4 → 1.01e-4) and matching loss trajectory.
What's in this PR
New files
examples/mimo/training/hetero/checkpointing.py—save_checkpoint/load_checkpointwrappingdist_checkpointing.save/loadwith a tracker file, per-branch RNG ShardedObjects, content_metadata persistence, and atomic tracker writes.CLI surface (in
examples/mimo/training/hetero/args.py)--save,--load,--save-interval,--no-save-optim,--no-load-optim,--no-load-scheduler,--no-save-rng,--no-load-rng,--finetune,--dist-ckpt-optim-fully-reshardable(off by default; help text warns about save-time gather OOM on <80GB GPUs).Loop wiring (
examples/mimo/training/hetero/loop.py)--save-interval, always save final iter when--saveis set.Three pre-existing bug fixes uncovered by hetero usage
megatron/core/ssm/mamba_mixer.py:MambaMixer.sharded_state_dictnow passestp_group=self.tp_groupanddp_cp_group=metadata['dp_cp_group']tomake_sharded_tensors_for_checkpoint(was falling back to parallel_state; gated_delta_net already does this correctly).megatron/core/models/mimo/optimizer.py:MimoOptimizer.sharded_state_dict: now appliesadd_prefix_for_sharding(module_sd, f'mimo.{name}.')so the two branches' optimizers don't collide on identical internal ShardedObject keys likechained_0.optimizer.distributed.dp_group_idx_0.optimizer/shard_0_1.megatron/core/models/mimo/optimizer.py:_get_replica_id: now returns(pp_rank, tp_rank, dp_rank)instead of(0, pp_rank, dp_rank)— withouttp_rank, two TP ranks within the same DP rank both claimed primary writer anddist_checkpointing.saveerrored with 'Duplicate ShardedObject keys'.New
_extract_param_state_sharding_typehelpers inMimoOptimizerroute DistributedOptimizer's top-levelparam_state_sharding_typeconfig string through a per-module ShardedObject (with a unique key per branch), so the non-rank-0 module owner doesn't lose it when only rank 0'scommon.ptis authoritative. Without this,DistributedOptimizer.load_state_dictraisedAssertionError: 'param_state_sharding_type' in state_dict.TP group propagation (
examples/mimo/training/hetero/runtime.py): a new_propagate_tp_groups_for_checkpointwalker stampsself.tp_groupon every descendant that doesn't already have one (e.g.ExtendedRMSNormin mamba_mixer, RADIO encoder submodules) so the defaultMegatronModule.sharded_state_dictpath doesn't fall through toparallel_state.get_tensor_model_parallel_group. The walker useshasattrso a module that intentionally setstp_group = Noneis left alone.Megatron-parity additions in common state
args(dict(vars(args))) so the saved layout is inspectable / future-checkable.checkpoint_version = 3.0.content_metadata = _clean_metadata_for_serialization(_build_optim_metadata(args))passed todist_checkpointing.save, persistingdistrib_optim_sharding_typeinmetadata.json._read_tracker(mirrorsmegatron.training.checkpointing.read_metadata)..tmp+os.replace).Test coverage
tests/unit_tests/models/test_mimo_checkpoint.pyparameterized onuse_distributed_optimizer. Newtest_encoder_tp2_llm_tp2_pp3_distributed_optimizerexercises the MimoOptimizer prefix walk +param_state_sharding_typeextract that the existing Float16Optimizer variant doesn't reach.Test plan
optimizer=yes, scheduler=yes, rng=yes, finetune=Falseand LR continues cosine schedule across the reload boundary.tools/autoformat.sh(black, isort) clean.tests/unit_tests/models/test_mimo_checkpoint.py::TestMimoCheckpoint(existing Float16Optimizer + new DistributedOptimizer variant) — run on the next CI sweep.--dist-ckpt-optim-fully-reshardable— opt-in, deferred to a follow-up due to save-time memory cost.Notes
ykarnati/nmfw-464-nemotron-vlm-with-hetero-parallel(the NMFW-464 integration branch), not againstNVIDIA/Megatron-LM:main. Cluster sessionnmfw-464-ckpt-iter2retains the latest validation artifacts if you want to inspect the savedcommon.pt.--save-interval, B4 mkdir-on-all-ranks, R1 tp_group propagation comment, R2 OOM warning in--dist-ckpt-optim-fully-reshardablehelp) are all addressed in this commit.🤖 Generated with Claude Code