Skip to content

Drop the documented grad_hooks ZeRO option, which does not exist - #8242

Merged
sfc-gh-truwase merged 2 commits into
deepspeedai:masterfrom
vineethsaivs:fix/zero-config-grad-hooks-doc
Aug 23, 2026
Merged

sfc-gh-truwase merged 2 commits into
deepspeedai:masterfrom
vineethsaivs:fix/zero-config-grad-hooks-doc

Conversation

@vineethsaivs

@vineethsaivs vineethsaivs commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem

config-json.md documents grad_hooks as a ZeRO option with a default of True:

grad_hooks: [boolean]
For use with ZeRO stage 1, enable backward hooks to reduce gradients during the backward pass or wait until the end of the backward pass. Default True

DeepSpeedZeroConfig has no such field, and DeepSpeedConfigModel sets extra="forbid", so anyone who follows the docs gets a hard failure out of deepspeed.initialize:

DeepSpeedZeroConfig(**{"stage": 1, "grad_hooks": False})

pydantic_core._pydantic_core.ValidationError: 1 validation error for DeepSpeedZeroConfig
grad_hooks
  Extra inputs are not permitted [type=extra_forbidden, input_value=False, input_type=bool]

The option was never wired up rather than removed later, so there is nothing to restore. cfa63f5da ("ZeRO stage 1 refresh", #1042, thanks @ebarkhordar for pinning it down) added the doc entry and DeepSpeedEngine.zero_grad_hooks() in the same commit and never added the field to deepspeed/runtime/zero/config.py or to the ZeRO constants module. It is absent from both in every release back to v0.3.0. zero_grad_hooks() reads self._config.zero_config.grad_hooks, which can only raise AttributeError, and nothing in the package or the tests calls it.

Fix

Remove the doc entry and the dead accessor. If the intent is that the option should exist, that is a feature rather than a fix and I would rather leave it to you than invent a semantic for it.

Test

No test. An earlier revision of this PR added a guard to tests/unit/runtime/zero/test_zero_config.py that checked every ZeRO key config-json.md documents is one DeepSpeedZeroConfig accepts. @tohtana asked for it to be dropped as too fragile for CI, which is fair, so it is gone and this is now a two file deletion.

What was checked instead, on CPU:

  • The ValidationError above reproduces on master and the doc entry is what invites it.
  • An ast sweep over every .py in the tree finds zero_grad_hooks exactly once, its own definition at deepspeed/runtime/engine.py:1290, and .grad_hooks exactly once, on the line inside it. Removing it cannot break a caller, since any call raises AttributeError today.
  • python -m pytest tests/unit/runtime/zero/test_zero_config.py gives 6 passed before and after, unchanged, and that file is now untouched by this PR.
  • yapf --style .style.yapf and flake8 --config .flake8 are clean on deepspeed/runtime/engine.py, and clean on the unmodified tree as a control.

How it was found

Cross-checking every <i>**key**</i> in config-json.md against a grep for that literal in deepspeed/. Of 126 documented keys only two came back unreferenced: Compression, which is a section name, and this one.

For scope rather than as a request: the drift runs the other way too, with 28 fields DeepSpeedZeroConfig declares having no entry in that doc section. That direction misleads rather than crashes, so it is left alone here.

@ebarkhordar ebarkhordar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The central claim holds, and the history makes it stronger than the body puts it. grad_hooks was not removed at some point, it was never wired up. cfa63f5da ("ZeRO stage 1 refresh", #1042) added the doc entry and zero_grad_hooks() in the same commit, and added the field to neither the ZeRO config nor the constants module:

$ git show cfa63f5da:deepspeed/runtime/zero/config.py    | grep -i grad_hook   # no output
$ git show cfa63f5da:deepspeed/runtime/zero/constants.py | grep -i grad_hook   # no output
$ git show cfa63f5da -- deepspeed/runtime/engine.py docs/_pages/config-json.md | grep grad_hooks
+    def zero_grad_hooks(self):
+        return self._config.zero_config.grad_hooks
+<i>**grad_hooks**</i>: [boolean]

Naming that commit in the body would answer the "did we break this, and should we restore it instead" question up front.

The dead-accessor claim holds too. An ast sweep over every .py in the master tree finds zero_grad_hooks exactly once, its own definition at deepspeed/runtime/engine.py:1290, and .grad_hooks exactly once, on the line inside it. Removing it cannot break a caller, since any call raises AttributeError: 'DeepSpeedZeroConfig' object has no attribute 'grad_hooks' today.

I ran your new guard, because CI has not. On this head sha cpu-torch-latest, Formatting, python, nv-pre-compile-ops and DCO / required are all sitting at action_required, so modal-torch-latest is the only leg that has executed. In a clean python:3.11-slim container with torch 2.13.0+cpu and pydantic 2.13.4:

$ python -m pytest tests/unit/runtime/zero/test_zero_config.py -q
7 passed

It also bites, which is the part worth knowing: re-adding the grad_hooks doc block and rerunning gives AssertionError: documented but not accepted by DeepSpeedZeroConfig: ['grad_hooks']. Your parser finds 22 keys against its floor of 15, and the six stage3_* entries among them are accepted through their pydantic aliases rather than as declared fields, so filtering on extra_forbidden is the right call there.

For scope, not a request: the drift runs both ways. 28 fields DeepSpeedZeroConfig declares have no entry in that doc section, zenflow, leaf_module, sub_group_size and mics_shard_size among them. This PR guards the direction that actually breaks users, which seems like the right half to take on here.

@tohtana tohtana left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @vineethsaivs,
Thank you for this PR! It is good to remove grad_hooks.

The test could be useful to prevent regressions, but it's a bit fragile and can cause false failures. I think this type of validation shouldn't run in CI. Would it be okay to remove the test?

config-json.md documents grad_hooks as a ZeRO option with a default of True, but
DeepSpeedZeroConfig has no such field and DeepSpeedConfigModel sets
extra="forbid", so following the docs is a hard failure:

    DeepSpeedZeroConfig(**{"stage": 1, "grad_hooks": False})
    pydantic_core._pydantic_core.ValidationError: 1 validation error
    grad_hooks
      Extra inputs are not permitted

The option was never wired up rather than removed later. cfa63f5 ("ZeRO stage 1
refresh", deepspeedai#1042) added the doc entry and DeepSpeedEngine.zero_grad_hooks() in the
same commit without ever adding the field to the config or to the constants
module, and it is absent from deepspeed/runtime/zero/config.py in every release
back to v0.3.0. So there is nothing to restore, and zero_grad_hooks() reads
zero_config.grad_hooks, which can only raise AttributeError. Nothing in the
package or the tests calls it.

Remove the doc entry and the dead accessor.

Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
@vineethsaivs
vineethsaivs force-pushed the fix/zero-config-grad-hooks-doc branch from 6eefc91 to 0449b34 Compare August 12, 2026 17:59
@vineethsaivs

Copy link
Copy Markdown
Contributor Author

Thanks @tohtana, that is a fair call and the test is gone. Pushed as 0449b34.

You are right about the fragility. The guard parsed config-json.md with a regex, so a change to the doc format would have broken it in CI on a PR that had nothing to do with ZeRO config, and the person paying for that would not have been the person who caused it. The floor assertion I added to catch that case turns a silent no-op into a red build, which is not obviously the better failure of the two.

The PR is now a two file deletion: the doc entry and DeepSpeedEngine.zero_grad_hooks().

@ebarkhordar, thank you for finding cfa63f5da, that answers the question I could not. I had only established that grad_hooks was absent from config.py back to v0.3.0, which left open whether it had been dropped by accident and should be restored instead. Since #1042 added the doc entry and the accessor in the same commit and never added the field, it was never wired up, so there is nothing to restore and deleting is the right direction. I have put that in the PR body and the commit message.

Both of your verification notes match what I get, including the documented but not accepted: ['grad_hooks'] failure and the six stage3_* aliases, so at least the removed test agreed with itself while it existed.

@tohtana tohtana left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the update!

@sfc-gh-truwase
sfc-gh-truwase added this pull request to the merge queue Aug 23, 2026
Merged via the queue into deepspeedai:master with commit f76ab88 Aug 23, 2026
13 checks passed
pull Bot pushed a commit to Mu-L/DeepSpeed that referenced this pull request Sep 17, 2026
)

## Problem

`sub_group_size` has no description entry in the ZeRO section of
`config-json.md`. It appears only as a bare value in the example JSON
block, so nothing tells a user what it does or when to change it. That
is the original report in deepspeedai#1145.

The default is also documented wrongly.
`deepspeed/runtime/zero/config.py:182` is authoritative:

```python
sub_group_size: int = Field(pp_int(1e9), ge=0)
```

`pp_int` (`deepspeed/runtime/config_utils.py:127`) subclasses `int` and
overrides only `__repr__`, so the default really is 1e9. @samyam's
comment in deepspeedai#1145 says the same thing independently: *"Most users can
leave sub_group_size to its default value of 1e9."*

Two places said 1e12:

- `docs/_pages/config-json.md:381`, the ZeRO example block.
- `deepspeed/runtime/zero/config.py:39`, inside `ZERO_FORMAT`. That one
is not a comment — `ZERO_FORMAT` is interpolated into the
`logger.warning` in `read_zero_config_deprecated()` (`config.py:58-66`),
so the wrong value was printed at runtime to anyone on the deprecated
config format.

`docs/_tutorials/zero.md:139` already says 1e9, which is the
inconsistency deepspeedai#1145 points at.

## Fix

- Adds a `***sub_group_size***` entry to the ZeRO section, between
`stage3_param_persistence_threshold` and
`stage3_gather_16bit_weights_on_model_save`, matching the ordering in
the example block above it. The `***key***` form matches every other
entry in that section.
- Corrects `config-json.md:381` from 1e12 to 1e9.
- Corrects `ZERO_FORMAT` in `config.py:39` from 1000000000000 to
1000000000, keeping the full-digit style of its neighbours.

The prose is @samyam's explanation from deepspeedai#1145, relayed by @stas00,
edited only for tense, to fix "each buckets" → "each bucket", and to
drop the inline "1e9" now that the Default column carries it. It keeps
the guidance on when to raise or lower the value, which seemed like the
most useful part of that comment.

## Where the 1e12 probably came from, and what I left alone

`DeepSpeedZeroOptimizer_Stage3.__init__` declares
`sub_group_size=1000000000000` (`deepspeed/runtime/zero/stage3.py:186`).
It is the only keyword default in that signature that disagrees with its
config counterpart — `reduce_bucket_size=500000000`,
`max_reuse_distance=1000000000`, `max_live_parameters=1000000000` and
`param_persistence_threshold=100000` all match `DeepSpeedZeroConfig`.
The sole in-tree caller, `engine.py:2726`, always passes
`sub_group_size=self.zero_sub_group_size()` from the config, so the 1e12
is only reachable by constructing the optimizer directly, and no
released behaviour depends on it via the normal path.

I have **not** touched it, because changing a public constructor's
default is a behaviour change and does not belong in a docs fix.
Flagging it in case you want it aligned separately — happy to send that
as its own PR if so.

Also left alone for the same reason:
`deepspeed/autotuning/config_templates/template_zero3.json:15` and
`examples/sdma_allgather/ds_config_zero3.json:39`, both of which also
say 1e12. Those are real configs rather than documentation, so their
values may be deliberate.

## Test

No test. This is a documentation entry plus two literal corrections, so
there is no contract to assert that a different correct implementation
would not also satisfy. (Noting that deepspeedai#8242 added a docs/config
consistency guard in this same file and @tohtana asked for it to be
dropped as too fragile for CI.)

What was checked instead, on CPU:

- `pre-commit run --files docs/_pages/config-json.md
deepspeed/runtime/zero/config.py` — all hooks pass, including `yapf`,
`flake8` and `codespell`.
- Rendered the added block through GitHub's GFM API and confirmed the
output: `<em><strong>sub_group_size</strong></em>` for the heading, a
well-formed `<table>` with `Description`/`Default` headers and
`<code>1e9</code>` in the default cell, and an `<ol>` with both tuning
cases. I did not build the Jekyll site locally; the addition uses no
Minimal Mistakes-specific syntax, so plain GFM rendering covers it.
- Grepped the whole tree for `sub_group_size`. After this change,
`docs/` states the default in exactly one place and it agrees with
`config.py:182`. The remaining occurrences are runtime code
(`zero/stage3.py`, `runtime/engine.py`,
`superoffload/superoffload_stage3.py`), tests, and the three non-doc
configs noted above.
- I could not exercise the pydantic default at runtime — no GPU and no
torch install here — so the 1e9 claim rests on reading `config.py:182`
plus `pp_int`, and on @samyam's own statement in the issue.

Fixes deepspeedai#1145

Signed-off-by: Shravani Nikam <shravanibharatnikam@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants