feat(megatron): support GTP weight rematerialization for training + refit - #3857
feat(megatron): support GTP weight rematerialization for training + refit#3857shanmugamr1992 wants to merge 2 commits into
Conversation
…efit Megatron-LM PR 6133 adds Generalized Tensor Parallelism (GTP) refit support: each weight is sharded along dim 0 across a `gtp_remat` group carved out of the DP axis and all-gathered on demand. Megatron-Bridge does not yet forward the `gtp_remat_size`/`expert_gtp_remat_size` arguments to `parallel_state.initialize_model_parallel`, so enable it from NeMo-RL with a scoped, self-removing shim rather than taking a cross-repo dependency. - New optional `megatron_cfg` keys `tensor_parallel_num_weight_shards` and `expert_tensor_parallel_num_weight_shards` (null = GTP off), documented in the exemplar config. - `setup.py` resolves them onto the model config and wraps Bridge's init to inject the derived remat sizes, restoring the original on exit. - Inference pins GTP off: refit reassembles the training model's dim-0 shards into whole inference weights, so the inference model never shards. `dedicated_inference_megatron_cfg` did not compare weight-shard counts, so a TP1 x GTP2 training model looked layout-identical to a TP1 inference model and the reshard/refit path was silently skipped -- generation still produced tokens, so the existing GPU tests would have passed while testing nothing. Include the shard counts in the layout comparison (treating null and an explicit TP-equal value as the same unsharded layout) and pin the decision with a CPU unit test. The new functional test is registered behind a `HAVE_GTP` capability guard. MCore's GTP core requires TransformerEngine >= 2.19.0.dev0 -- above the floor MCore itself declares -- and NeMo-RL currently pins TE release_v2.15. Below the floor MCore degrades to import stubs instead of raising, so the run died six frames deep on `isinstance() arg 2 must be a type` with nothing pointing at TE. Preflight `HAVE_GTP` in the shim to fail with the actual requirement, and skip the suite entry rather than leaving L1 permanently red. Both the guard and the skip retire themselves once the TE pin is bumped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: shanmugamr1992 <shanmugamr1992@gmail.com>
TransformerEngine 2.19 run on GB200 — GTP now activates, but a pre-existing upstream bug blocks the testFollow-up to the "TE too old" note in the description. I upgraded TransformerEngine on the Result
So the TE floor is cleared and the NeMo-RL shim in this PR does its job — the parallel state Two fixes were needed to get TE 2.19 built at allBoth are worth knowing for anyone who bumps TE in this repo; neither is obvious from the failure. 1.
2. TE 2.19 needs Installing The remaining blocker: GTP-sharded models can't load a
|
…e layout Forcing gtp_weight_remat_size=1 on the inference provider does not by itself keep the inference model off the training GTP layout: megatron's layers resolve the axis from the process-group collection, which falls back to the MPU globals unless build_inference_pg_collection declares it off (NVIDIA/Megatron-LM#6940). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: shanmugamr1992 <shanmugamr1992@gmail.com>
|
/ok to test 95fb64d |
What this does
Adds support for Megatron-Core Generalized Tensor Parallelism (GTP) in NeMo-RL, including the refit path, so a GRPO run can train with GTP-sharded weights and still refit into a whole-weight inference model.
GTP shards each weight along dim 0 across a
gtp_rematprocess group carved out of the DP axis, and all-gathers ("rematerializes") it on demand in both forward and backward. The world decomposition becomesworld = TP x GTP x CP x DP x PP. It is exercised against Megatron-LM#6133, which added the refit support on the MCore side and has since merged.User-facing config
Two new optional
policy.megatron_cfgkeys, both defaulting tonull(= GTP off, i.e. no behaviour change for existing configs):These mirror MCore's own
ModelParallelConfignames.setup.pyresolves them via MCore'sresolve_tensor_parallel_weight_shards()into the derivedgtp_weight_remat_size = num_weight_shards // tp_size. Followingconfig-conventions, the keys areNotRequiredon the (still v1TypedDict)MegatronConfigwith the default documented in the exemplar YAML only -- no.get(key, default)at any call site.Why the enablement is a shim in NeMo-RL
Megatron-Bridge does not yet forward
gtp_remat_size/expert_gtp_remat_sizetoparallel_state.initialize_model_parallel(src/megatron/bridge/training/initialize.py:805). Rather than take a cross-repo dependency on an unreleased Bridge change,setup_model_and_optimizerwraps Bridge's init in a narrowly-scoped context manager that monkeypatchesparallel_state.initialize_model_parallelto inject the two sizes, then restores the original in afinally.The patch uses
functools.wrapsdeliberately: Bridge probesinspect.signature(parallel_state.initialize_model_parallel).parametersa few lines earlier (initialize.py:794), and an unwrapped wrapper would change what that probe sees.It is marked with a TODO to delete once Bridge forwards these natively. Reviewers should feel free to push back if you would rather this wait for Bridge.
Inference deliberately pins GTP off (
build_inference_modelsets the shard counts equal to their TP degree,gtp_weight_remat_size = 1): refit reassembles the training model's dim-0 shards into whole inference weights, so the inference model never shards.Bug found along the way: the refit path was being skipped silently
This is the part most worth reviewing, and it is a pre-existing bug, not one this feature introduced.
dedicated_inference_megatron_cfg()decides whether colocated generation runs directly on the shared training model, or whether the worker must build a second, resharded inference model (and therefore refit). Its layout comparison did not include the weight-shard counts. So aTP1 x GTP2training model compared equal to aTP1inference model, the function returnedNone, no dedicated inference model was built, and no refit ever happened.The failure mode is silent, which is what makes it nasty: generation still produces tokens, the metrics still look fine, the test still exits 0. The new functional test would have passed while exercising exactly none of the code it was written for.
Fixed by adding
tensor_parallel_num_weight_shardsandexpert_tensor_parallel_num_weight_shardstolayout_keys, normalizingnull-> TP degree before comparing so thatnulland an explicit TP-equal value are treated as the same unsharded layout (otherwise writing the default out longhand would cost you a pointless dedicated inference model and a full refit every wake).Pinned by a new CPU-only unit test,
tests/unit/models/generation/test_megatron_generation_layout.py(8 assertions, no GPU, runs in the normal unit suite) so this cannot silently regress again.The functional test
tests/functional/grpo_megatron_generation_gtp.sh-- GRPO onQwen/Qwen2.5-0.5B,cluster.gpus_per_node=2, trainingTP=1+tensor_parallel_num_weight_shards=2(GTP=2), megatron generation backend, colocated,refit_backend=nccl, 2 steps.Asserts
max(train/token_mult_prob_error) < 1.05, plus two anti-vacuity greps on the run log, added precisely because of the bug above:[gtp] enabling GTP weight rematerialization (gtp_remat_size=2-- proves GTP actually got turned on[colocated-reshard] building dedicated inference model-- proves the reshard/refit path actually ranThe feature is code-complete here, but the configuration it enables cannot run on the pinned dependencies. Three things must land first; none of them belong in this PR.
1. TransformerEngine >= 2.19.0.dev0
_GTP_TE_MIN_VERSION = Version("2.19.0.dev0")is declared inmegatron/core/tensor_parallel/generalized_tensor_parallelism.py. NeMo-RL pins TEv2.14.1/release_v2.15. This floor predates PR 6133 -- it is present at the image's own mcore pin.Below the floor MCore does not raise. It catches its own ImportError and rebinds every TE name to a stub, including
QuantizedTensor = MagicMock()-- an instance, not a type -- and setsHAVE_TE = False->HAVE_GTP = False. Nothing downstream re-checks, because MCore's layer constructors gate GTP on the process-group size alone. The result was a run dying six frames deep on:which says nothing whatsoever about TransformerEngine.
Two mitigations, both self-retiring, are in this PR:
HAVE_GTPpreflight in the shim (setup.py) turns that into a named error stating the actual requirement and how to opt out.megatron_gtp_supported()capability guard inL1_Functional_Tests_Megatron_4.shskips the test rather than leaving L1 permanently red. It asks MCore directly (from megatron.core.tensor_parallel.gtp_api import HAVE_GTP) instead of hardcoding a version, so the moment the TE pin is bumped, the test re-enables itself with no edit to this branch.Bumping TE needs a container rebuild and affects every backend, so it is deliberately not in this PR. TE
release_v2.19was built into the test venvs out-of-band to get the validation below.2. NVIDIA/Megatron-LM#6940 (draft) -- four GTP bugs
Found while validating this branch. All four are megatron-core bugs, none are NeMo-RL's:
out_features_gtp_pre_initpasses it, so the bias comes out1/gtp_remattoo small -- but a bias is GTP-replicated. Breaks the GEMM epilogue and declares a wrong global shape to dist-checkpointing.GPTModelpassed onlytp_group(notpg_collection) toLanguageModelEmbeddingand the output layer, so both resolved their GTP axis from the global MPU groups instead of the model's own.TELMHeadColumnParallelLineardid not acceptpg_collectionat all.build_inference_pg_collectionleftgtp_rematunset, andresolve_gtp_remat_groupfalls back to the MPU globals for an absent field -- so the dedicated inference model came out GTP-sharded from the training layout.ColumnParallelLinear's supplied-weight shape guard compared a GTP shard's local shape against the logical one. With tied embeddings,shared_embedding_or_output_weight()hands the output layer exactly that shard. This one breaks any GTP run withshare_embeddings_and_output_weights=True, independent of NeMo-RL.The last two both surfaced as
supplied weight's shape is (75968, 896), not (151936, 896) as expected.3. NVIDIA-NeMo/Megatron-Bridge#5862 (draft) -- checkpoint replica group
Bridge derives sharded-checkpoint
replica_idfrompg_collection.dp_cp, which is the GTP replicate group and excludes thegtp_remataxis. Everygtp_rematpeer then claimsreplica_id 0for the same non-GTP-sharded tensor (norms, biases,_extra_state), and dist-checkpointing rejects it withInvalid access pattern for ShardedTensor(...). No-op when GTP is off.What was validated on hardware
OCI HSG GB200 cluster (aarch64, 4 GPUs/node), with TE
release_v2.19installed into the megatron venvs and the two draft fix PRs above applied.With all of the above in place the test runs end-to-end: GTP group built, dedicated inference model built, reshard + NCCL refit, two full GRPO steps, both anti-vacuity greps hit. Before the megatron-core fixes it could not even construct the models.
Earlier attempts, kept for the record:
uvre-resolved and failed to buildtorch-memory-saver(TMS_CUDA_MAJOR env var must be set). Test-harness issue, not product: mcore is an editable uv workspace member, so checking out the revision under test staled the image'suv.lock. Fixed in the harness withUV_FROZEN=1.isinstance()TypeError above. Crucially, both ranks first printed[gtp] enabling GTP weight rematerialization (gtp_remat_size=2 expert_gtp_remat_size=1)-- so Bridge forwarded the sizes and MCore built thegtp_rematgroup. The shim works; only the TE-gated kernels fail.HAVE_GTPpreflight fires cleanly on both ranks with the actionable message instead of the cryptic one.No assertion was loosened, no threshold moved, no test skipped or shortened to manufacture a green run.
One open question the test does not answer
The end-to-end run's
train/token_mult_prob_errormeasures 1.1011, against the< 1.05threshold this test inherited by copy from its non-GTP sibling. The value is bit-identical across repeated runs, so it is a deterministic train-vs-generation logprob disagreement rather than noise.I could not attribute it. The non-GTP control cannot exercise this code path -- with GTP off, colocated generation takes the shared-training-model branch instead of building a dedicated inference model, and that branch hits an unrelated CUDA-graph capture error -- so there is no measured baseline for the 1.05 gate on this configuration. It was never validated here; it was copied. Several other non-vanilla Megatron generation variants sit similarly above their own 1.05-class gates.
Flagging it rather than moving it: whether the GTP path adds real numerical error, or the inherited threshold is simply wrong for it, is a call for a reviewer who owns this path. If the answer is "the threshold is wrong", say so on this PR and I will change it with that rationale recorded.
Review notes
nemo_rl/models/generation/megatron/config.pyis inpyrefly.toml;nemo_rl/models/megatron/setup.pyis not (pre-existing).tests/unit/reference_configs/, so no reference-config update was needed.uvx ruff format --diffanduvx ruff checkare clean;bash -nclean on both shell scripts.