Repository navigation
fix(training): restore gradient synchronization for copied model configs - #6067
Conversation
Signed-off-by: Ali Roshan Ghias <aroshanghias@nvidia.com>
|
LGTM — clean, well-targeted correctness fix. The change resolves the callback-target config from the constructed model via Verified:
No functional changes needed. Suggested test cases
No perf tests impacted. |
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
|
Checked the new ModelConfig path. The production fix already targets the correct runtime config for both paths: GPTModelBuilder and HybridModelBuilder pass their nested transformer config into the constructed model, and the scheduler reads that same object through get_model_config(model[0]). I pushed 65bece8 to strengthen the regression coverage: the test now uses BridgeGPTModelConfig and actually traverses _build_distributed_model for both GPT and Hybrid instead of mocking the builder dispatch. @aroshanghias-nvd please review the added coverage. |
|
/ok to test 65bece8 |
Signed-off-by: Ali Roshan Ghias <aroshanghias@nvidia.com>
|
looks good to me. I'm removing some of the tests added, since they were not that helpful. I will make a commit soon. |
|
/ok to test 33c47e4 |
|
/ok to test 234789c |
|
/ok to test fe56c9e |
|
/ok to test fc71484 |
|
/ok to test 3e6c19a |
What does this PR do?
Install training callbacks on the configuration consumed by Megatron's scheduler. This fixes a silent gradient-synchronization failure when a provider, such as canonical Nemotron Omni, copies its configuration while constructing the model.
Why the existing binding is incorrect
setup()currently passes the provider configuration (or the GPT/Hybrid builder's transformer configuration) to_update_model_config_funcs(). This assumes that the selected object is also the runtime model configuration.Canonical Omni breaks that identity assumption:
language_cfgand passes it to the canonical model.model.config.finalize_model_grads_func,grad_scale_func, and applicable synchronization callbacks on the original provider.get_model_config(model). Its finalization callback is stillNone, so the conditional scheduler call is skipped.Conceptually, the existing path does this:
This PR obtains the callback target from
get_model_config(model[0]), matching the scheduler's lookup. The existing installer continues to construct the callbacks and per-chunk callback lists. The reduction algorithms and normalization settings are unchanged.Training consequence—and why a run can look healthy
In the reproduced PP=1 configuration with
overlap_grad_reduce=False, gradient finalization completes the required DP/CP reductions and applicable TP reductions for replicated parameters. The distributed optimizer assumes those operations have completed before it consumes its owned gradient slices. Without them, a shard can update from its owner's local contribution instead of the intended combined gradient.A small example uses 512 zero-initialized parameters and fresh distributed Adam state. Every parameter receives a local contribution of
+1on DP rank 0 and-3on DP rank 1. Rank 0 owns the first half of the optimizer update; rank 1 owns the second half.+1,-3+1and-3; 256 parameters decrease and 256 increase+1,-1Parameter all-gather can still give DP replicas identical final weights after the incorrect updates. It assembles the already-updated parameter shards; it does not combine the gradients or recompute the optimizer updates. Every replica therefore receives the same mixture of shard updates based on different ranks' local gradients. Backward also continues to produce nonzero gradients with useful learning signal. Consequently, matching DP weights, stable losses, or successful training completion do not establish that synchronization was correct.
This is a training-correctness finding. It does not establish that existing trained models are unusable, quantify model-quality degradation, or demonstrate a full-model TMPE improvement. Affected historical runs require their exact provider, entrypoint, runtime configuration, and overlap settings to be checked. This patch does not repair prior optimizer history or prescribe a checkpoint reset.
Changelog
setup()call and callback installer, with model construction and external services mocked.Validation
Validation base: Bridge
2eae3c971dd6971beb403e6536987427b7e8a3b2, with its pinned MCorec9b53d0a87cb926f47115259593ecfeb351ca29f.setup.pyproduces three expected failures and five passes. Both copied-Omni binding cases fail becauseconfig.finalize_model_grads_funcisNone; the training scheduler case fails because the finalizer is called zero times. All eight pass with the fix.git diff --checkpassed.The eight new unit-test cases check callback binding and scheduler invocation. The GPT/Hybrid ModelConfig cases traverse the real
_build_distributed_model()dispatch with a lightweight recording builder; other model construction is mocked. The scheduler test also mocks forward/backward computation and the finalizer itself. Numerical gradient correctness is covered by the separate two-GPU experiment described above, not by these unit tests.The targeted test commands, in an environment with Bridge and its pinned MCore installed, are:
To reproduce the original failure, run the new test file against unpatched
setup.py:The numerical check distinguishes both loss contracts: the two-item, already-normalized loss used by GRPO requires the sum
-2; the three-item loss with two global tokens produces-1. The corrected binding preserves both contracts. Numerical topology coverage is PP=1, CP=1, with DP and TP tested separately. Callback-list tests cover multiple chunks and overlap wiring; they are not a claim of full distributed PP>1, CP>1, or FSDP qualification.GitHub Actions CI
The new regression tests live under
tests/unit_tests/training/. Repository CI remains the merge gate; the standalone numerical experiment is additional validation, not a new CI job in this PR.Before your PR is "Ready for review"
Additional information
Related history: #4974 (July 23) explicitly preserved the provider configuration in the legacy Omni wrapper. #4885 (July 29 Pacific / July 30 UTC) introduced the canonical path using a copied runtime configuration and also removed the legacy wrapper's explicit
config=self. This identifies a recurrence in that rewrite relative to its parent; it is not a claim that July was the first-ever occurrence or that every subsequent training run was affected.