Nemotron RL support - #1284
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new offline conversion tool convert_hf_to_torch_dist_bridge.py for Nemotron-H models, simplifies conditional checks in model.py using walrus operators, and updates the MambaModel.forward shim to route MTP labels. The review feedback highlights two issues: a potential TypeError in the MambaModel.forward shim due to passing an unsupported loss_mask argument to the original forward method, and a potential directory nesting issue in the conversion tool if the target directory already exists during shutil.move.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def forward(self, *args, loss_mask=None, mtp_kwargs=None, **kwargs): | ||
| # Route miles' GPT-style mtp_kwargs['mtp_labels'] into MambaModel.forward's | ||
| # mtp_labels arg so MTP actually trains in RL: the MTP loss is gated on | ||
| # mtp_labels (mirroring gpt_model.forward), while the main path keeps | ||
| # labels=None and returns logits for the policy-gradient loss. | ||
| mtp_labels = (mtp_kwargs or {}).get('mtp_labels') | ||
| return _orig_forward(self, *args, loss_mask=loss_mask, mtp_labels=mtp_labels, **kwargs) |
There was a problem hiding this comment.
The original MambaModel.forward (captured in _orig_forward) does not accept loss_mask as an argument (which is the entire reason this shim exists, as documented in the docstring). Passing loss_mask=loss_mask to _orig_forward will raise a TypeError: forward() got an unexpected keyword argument 'loss_mask'. You should drop loss_mask when calling _orig_forward just like the original shim did.
| def forward(self, *args, loss_mask=None, mtp_kwargs=None, **kwargs): | |
| # Route miles' GPT-style mtp_kwargs['mtp_labels'] into MambaModel.forward's | |
| # mtp_labels arg so MTP actually trains in RL: the MTP loss is gated on | |
| # mtp_labels (mirroring gpt_model.forward), while the main path keeps | |
| # labels=None and returns logits for the policy-gradient loss. | |
| mtp_labels = (mtp_kwargs or {}).get('mtp_labels') | |
| return _orig_forward(self, *args, loss_mask=loss_mask, mtp_labels=mtp_labels, **kwargs) | |
| def forward(self, *args, loss_mask=None, mtp_kwargs=None, **kwargs): | |
| # Route miles' GPT-style mtp_kwargs['mtp_labels'] into MambaModel.forward's | |
| # mtp_labels arg so MTP actually trains in RL: the MTP loss is gated on | |
| # mtp_labels (mirroring gpt_model.forward), while the main path keeps | |
| # labels=None and returns logits for the policy-gradient loss. | |
| mtp_labels = (mtp_kwargs or {}).get('mtp_labels') | |
| return _orig_forward(self, *args, mtp_labels=mtp_labels, **kwargs) |
| if dist.get_rank() == 0: | ||
| source_dir = get_checkpoint_name(args.save, 1, False, return_base_dir=True) | ||
| target_dir = get_checkpoint_name(args.save, -1, True, return_base_dir=True) | ||
| shutil.move(source_dir, target_dir) |
There was a problem hiding this comment.
If target_dir already exists (e.g., from a previous run of this conversion tool), shutil.move(source_dir, target_dir) will move source_dir inside target_dir as a subdirectory (e.g., target_dir/iter_0000001), rather than replacing it. To ensure consistent behavior across multiple runs, you should remove target_dir if it already exists before calling shutil.move.
| if dist.get_rank() == 0: | |
| source_dir = get_checkpoint_name(args.save, 1, False, return_base_dir=True) | |
| target_dir = get_checkpoint_name(args.save, -1, True, return_base_dir=True) | |
| shutil.move(source_dir, target_dir) | |
| if dist.get_rank() == 0: | |
| source_dir = get_checkpoint_name(args.save, 1, False, return_base_dir=True) | |
| target_dir = get_checkpoint_name(args.save, -1, True, return_base_dir=True) | |
| if os.path.exists(target_dir): | |
| shutil.rmtree(target_dir) | |
| shutil.move(source_dir, target_dir) |
Code: minimal MTP fixes only (+21/-4): - forward_step: pre-shift mtp_labels (next-token) so MTP depth-k targets token[i+k+2] as the pretrained head expects (raw tokens were off-by-one). - train(): init mtp_losses=None and gate the metric (tracker can be empty). - bridge: clear mtp_hybrid_override_pattern when dropping MTP (finalize() would hit the nonexistent Symbols.MTP_SEPARATOR), and route miles mtp_kwargs[mtp_labels] into MambaModel.forward(mtp_labels=...). Scripts follow the existing per-model convention (sh+py runner, models/ config, bridge converter tool). The old branch walrus-style churn is gone (main already adopted that style). (cherry picked from commit 2ea1e9b224a45831e595b4e74e032cd59132ea7a)
Colocated weight sync IPC-shares the megatron->HF conversion transients with the engines; after del they sit in the producer IPC pending-free list, and under torch_memory_saver offload nothing triggers the lazy reap -- the freed pages accumulate as unpausable memory (+~full export size per training round) until rollout KV onload OOMs. Reap explicitly: once after update_weights() in the actor (covers every updater path, incl. the new atomic-group/direct-iterator ones) and after each tensor bucket sync (bounds peak pinning). Pairs with the engine-side ipc_collect in sglang (#27110).
16ddc6f to
3d811f4
Compare
# Conflicts: # miles/backends/megatron_utils/update_weight/update_weight_from_tensor.py
Slices the 550B checkpoint down to source layers 0,1,7,8 (renumbered 0..3), which is the cheapest selection covering every block type the 108-layer model has: mamba, moe, attention, moe -> 'ME*E'. A prefix cut would need 8 layers to reach the first attention layer and pull in 4 MoE layers (~44B params) instead of 2. Everything else (512 experts, top-22, moe_latent_size=2048, sigmoid router + expert bias, AutoBridge path) matches the full model, so the runner exercises the same weight-conversion path on one node and asserts it with --check-weight-update-equal.
| # Reap CUDA-IPC pending frees; under torch_memory_saver | ||
| # offload nothing else triggers the lazy reap (leaks ~full | ||
| # export size per round otherwise). | ||
| torch.cuda.ipc_collect() |
There was a problem hiding this comment.
is this needed? in my understanding ipc should be automatically collected in weight update (so other models work well)
There was a problem hiding this comment.
repeated ipc collect, will delete this and that one in sglang
…550b_a55b.py
The three .sh launchers (full run, 4-layer run, offline convert) duplicated the
same arg blocks with the differences hardcoded. Merge them into the existing
Python launcher using the ScriptArgs/typer convention the other model scripts
use (run_qwen3_30b_a3b.py, run_glm5_744b_a40b.py), so every setting is a flag
with MILES_SCRIPT_* env binding instead of an edit-the-file constant.
Settings are unchanged: parallelism (TP8/PP4/EP32 for the full model on 128
GPUs, TP1/PP1/EP8 for the pruned slice), SGLang DP-attention sizing, GRPO,
optimizer, and rollout args all reproduce the shell scripts verbatim. Two
deliberate deltas, both to match the other launchers: --save goes to
{output_dir}/{run_id}/checkpoints, and --dump-details is now passed.
scripts/models/*.sh stay -- execute_train sources them for MODEL_ARGS.
Also add the 4-layer slice as a model-scripts CI test on stage-c-8-gpu-h200.
8 GPUs rather than 4 because SGLang DP-attention needs attn_tp to divide Mamba
n_groups=8, and --ci-test turns on the Megatron -> SGLang weight equality check
that this test exists to guard.
There was a problem hiding this comment.
qq: why do we need this script? given that we usually just load hf weight directly when we use megatron bridge
There was a problem hiding this comment.
(I removed this file, but correct me if it is needed)
Zero bytes (the canonical empty git blob), not on main, nothing references it. Swept into 2e63629 alongside the nemotron work -- 'core' is what Linux names a coredump in cwd.
…nvert miles' load_checkpoint dispatches on what --load points at: an HF directory goes to _load_checkpoint_hf, which is the same megatron.bridge path nemotron_h is mapped through, and args.load falls back to ref_load/hf_checkpoint. So bridge mode already loads HF directly and the offline conversion was never needed for correctness -- only to amortize the bridge mapping across runs. That optimization also came with a constraint the direct path does not have: the converted layout has to match the run's TP/PP/EP or the load reshards. Drop --use-torch-dist-ckpt and _prepare_torch_dist_ckpt, and remove tools/convert_hf_to_torch_dist_bridge.py, which nothing references now. It was a copy of tools/convert_hf_to_torch_dist.py with the mbridge backend swapped for megatron.bridge, minus the ROCm writer patch and --custom-model-provider-path. If the 550B's startup cost turns out to matter, the right shape is a backend branch on the existing tool's --megatron-to-hf-mode flag, which it already parses and currently ignores.
The test itself is correct but hf download of the pruned slice 404s, so it fails in 40s on every run. Re-enable by publishing the checkpoint and dropping the disabled= flag.
…ne reap point --use-kl-loss (coef 0) loads a ref model, which the noop weights backuper rejects under --disable-weights-backuper, so the CI test config could never run; run_deepseek.py already dropped the flag for the same reason. The per-chunk ipc_collect in the base sync loop is redundant (the next chunk's sends reap the previous chunk's frees); keep a single reap after update_weights(), the one point where nothing else runs before the trainer pauses for the rollout.
The pruned checkpoint is published at CharyZeng/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16-4layer (layers 0,1,7,8 of the 550B BF16 renumbered 0..3, MTP head kept).
The 4-layer A/B (all ipc_collect calls removed, 6 rounds, offload on, with and without --ci-test) shows no memory growth; the automatic pending-free collection on the next round's IPC activity is sufficient. actor.py is now identical to main.
ci-sglang-pr: #27110
ci-megatron-pr: #51
sgl-project/sglang#27110
radixark/Megatron-LM#51