fix: stop DeepSpeedConfig writing max_grad_norm back into the caller's config dict - #8289
fix: stop DeepSpeedConfig writing max_grad_norm back into the caller's config dict#8289ebarkhordar wants to merge 1 commit into
Conversation
…s config dict _do_warning_check assigned 0.0 into self.optimizer_params, which is the caller's own config["optimizer"]["params"] rather than a copy, so deepspeed.initialize() rewrote a dict it does not own. With a client optimizer passed in, _configure_basic_optimizer is never reached, so initialization succeeded and the change went unreported. The zeroed value has no reader: get_optimizer_gradient_clipping is its only consumer and has no callers. The engine side of this behaviour was removed in abe2204 (deepspeedai#232) and replaced by a ValueError; the config side predates that and was not revisited. The FP16 and FP32 branches existed only to gate the assignment, so they collapse into a single warning carrying the same remedy _configure_basic_optimizer already raises. engine.py:2088 states the same invariant for its own pop() calls. Signed-off-by: Ehsan Barkhordar <realbarkhordar@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 675a8726b7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| assert ds_config.eigenvalue_verbose is False | ||
|
|
||
|
|
||
| def test_max_grad_norm_leaves_caller_config_untouched(): |
There was a problem hiding this comment.
Add the required sign-off trailer
This is a single-parent, non-merge commit, but its commit message has no Signed-off-by trailer. Add the author identity from git config user.name and git config user.email using --signoff so the commit satisfies the repository's mandatory commit policy.
AGENTS.md reference: AGENTS.md:L8-L8
Useful? React with 👍 / 👎.
What happens
deepspeed.initialize()writes into the dict the caller passed asconfig. Whenoptimizer.params.max_grad_normis set to a positive value,DeepSpeedConfig._do_warning_checkassigns
0.0intoself.optimizer_params, and that is the caller's ownconfig["optimizer"]["params"]object rather than a copy.Measured in a clean
python:3.11-slimcontainer at HEAD11b518a00, torch2.13.0+cpu,deepspeed installed with
pip install -e .from the checkout(
deepspeed.__file__ = /src/deepspeed/__init__.py,deepspeed.__version__ = 0.19.6+unknown):Observed:
Expected:
initializeleaves the caller's dict as it found it.Passing a client optimizer is what makes this visible, because
_configure_basic_optimizeristhen never called and the usual
ValueErrornever fires, so initialization succeeds with thecaller's config quietly rewritten. The same run without a client optimizer still raises the
ValueError, and still leaves0.0behind in the caller's dict, because the zeroing happensduring config construction and the engine's check tests for the key's presence rather than its
value.
The warning is also no longer accurate. The value it claims to zero is not read by anything:
get_optimizer_gradient_clipping(config.py:458) is its only reader and has no callersanywhere in
deepspeed/ortests/(checked with an AST scan forCallnodes, not a textsearch). The clipping actually applied comes from
gradient_clipping, which is why the runabove reports
1.0. The engine side of this behaviour was removed inabe2204d(#232, 2020)and replaced by the hard
ValueError; the config side predates that change and was notrevisited with it.
Why the fix looks like this
_configure_basic_optimizeralready declares the invariant this line breaks, atengine.py:2088, added three months ago in3c337b542(#8010):Enumerating every writer of that dict across
deepspeed/by AST (subscript assignment plusupdate/pop/setdefault/clear/popitemcalls):config.py:1071on the caller's dict, plusengine.py:2096,2097,2115on the copyengine.py:2096,2097,2115, all on the copyThe FP16 and FP32 branches were a pair with the zeroing, so removing it leaves them without a
distinction to draw. Nothing passes
max_grad_normto an FP16 wrapper today: the onlyconsumers of a
max_grad_normparam group are the Lamb and OneBit optimizers, which take itas a constructor argument. The two branches therefore collapse into one warning carrying the
same remedy that
_configure_basic_optimizeralready raises.If you would rather keep both original messages and drop only the assignment, or split the
message change into its own PR, say so and I will rework it.
Tests
tests/unit/runtime/test_ds_config_dict.py::test_max_grad_norm_leaves_caller_config_untouchedpins the caller's dict directly. In the same container, on master it fails with
assert 0.0 == 1.0; with this change it passes.The rest of that file is unaffected: 27 passed, 5 skipped.
TestArgsneeds--shm-sizeabovethe Docker default and fails with
OSError: [Errno 28] No space left on devicewithout it, onmaster and on this branch alike.
yapf --style .style.yapf --diffandflake8 --config .flake8are both clean on the twochanged files.
One limit worth stating: the unit test covers config construction, which is where the write
happens. The full
deepspeed.initializepath is covered by the container run above ratherthan by a unit test, since it needs a built comm extension.