-
Notifications
You must be signed in to change notification settings - Fork 5k
Say when ZenFlow will never re-select its important columns #8456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alanhuangyoo
wants to merge
2
commits into
deepspeedai:master
Choose a base branch
from
alanhuangyoo:fix/zenflow-epoch-selection-never-repeats
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Copyright (c) DeepSpeed Team. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| # DeepSpeed Team | ||
| """ZenFlow's select interval, and what it takes for re-selection to happen. | ||
|
|
||
| ZenFlow re-picks the important gradient columns every ``select_interval`` | ||
| micro-steps. The 'epoch' strategy -- which 'auto', the default, resolves to -- | ||
| expresses that interval in epochs, so it needs to know how many steps an epoch | ||
| is. DeepSpeed can only read that from a dataloader it owns, so a caller driving | ||
| its own dataloader has to say. These pin what each combination produces. | ||
| """ | ||
|
|
||
| import types | ||
|
|
||
| import pytest | ||
|
|
||
| from deepspeed.runtime.zenflow.engine import configure_zenflow | ||
| from deepspeed.runtime.zenflow.zenflow_config import ZenFlowConfig | ||
|
|
||
|
|
||
| class _StubEngine: | ||
| """The attributes ``configure_zenflow`` reads, and nothing else.""" | ||
|
|
||
| def __init__(self, config, training_dataloader=None): | ||
| self._zenflow_config = config | ||
| self.training_dataloader = training_dataloader | ||
| self._config = types.SimpleNamespace(gradient_accumulation_steps=1) | ||
|
|
||
| def zenflow_config(self): | ||
| return self._zenflow_config | ||
|
|
||
|
|
||
| class _Loader: | ||
|
|
||
| def __init__(self, steps): | ||
| self.steps = steps | ||
|
|
||
| def __len__(self): | ||
| return self.steps | ||
|
|
||
|
|
||
| def _select_boundaries(select_interval, steps, full_warm_up_rounds=0): | ||
| """Count the boundaries ``is_zenflow_select_boundary`` would report.""" | ||
| return sum(1 for micro_step in range(steps) if (micro_step - full_warm_up_rounds) >= 0 and ( | ||
| (micro_step - full_warm_up_rounds) == 0 or (select_interval != 0 and micro_step % select_interval == 0))) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("select_strategy,select_interval", [("auto", "auto"), ("epoch", 1)]) | ||
| def test_epoch_length_from_a_dataloader_deepspeed_owns(select_strategy, select_interval): | ||
| config = ZenFlowConfig(select_strategy=select_strategy, select_interval=select_interval, update_interval="auto") | ||
| engine = _StubEngine(config, training_dataloader=_Loader(10)) | ||
|
|
||
| configure_zenflow(engine) | ||
|
|
||
| assert config.steps_per_epoch == 10 | ||
| assert engine.select_interval == 10 | ||
| assert _select_boundaries(engine.select_interval, steps=40) == 4 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("select_strategy,select_interval", [("auto", "auto"), ("epoch", 1)]) | ||
| def test_epoch_length_given_by_the_user_needs_no_dataloader(select_strategy, select_interval): | ||
| # The path for a caller that drives its own dataloader. | ||
| config = ZenFlowConfig(select_strategy=select_strategy, | ||
| select_interval=select_interval, | ||
| update_interval="auto", | ||
| steps_per_epoch=10) | ||
| engine = _StubEngine(config, training_dataloader=None) | ||
|
|
||
| configure_zenflow(engine) | ||
|
|
||
| assert engine.select_interval == 10 | ||
| assert _select_boundaries(engine.select_interval, steps=40) == 4 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("select_strategy,select_interval", [("auto", "auto"), ("epoch", 1)]) | ||
| def test_no_epoch_length_selects_once_and_says_so(select_strategy, select_interval, caplog): | ||
| # select_interval 0 makes is_zenflow_select_boundary true exactly once, so | ||
| # the columns chosen at the first step are used for the whole run. That is | ||
| # the shape of the bug; the point of the test is that it is now announced. | ||
| config = ZenFlowConfig(select_strategy=select_strategy, select_interval=select_interval, update_interval="auto") | ||
| engine = _StubEngine(config, training_dataloader=None) | ||
|
|
||
| with caplog.at_level("WARNING"): | ||
| configure_zenflow(engine) | ||
|
|
||
| assert engine.select_interval == 0 | ||
| assert _select_boundaries(engine.select_interval, steps=40) == 1 | ||
| assert any("never re-selected" in record.getMessage() for record in caplog.records) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("steps_per_epoch", [0, -5]) | ||
| def test_a_non_positive_epoch_length_is_refused(steps_per_epoch): | ||
| """Reported by @ebarkhordar on #8456. | ||
|
|
||
| 0 multiplies select_interval straight to 0, which is_zenflow_select_boundary | ||
| reads as "never re-select" -- the state the warning exists to announce, reached | ||
| silently through the knob that documents it, because the warning only fires in | ||
| the None branch. -5 is worse: select_interval stays negative and | ||
| `micro_step % -5` fires on a schedule nobody asked for, 8 times in 40 steps. | ||
|
|
||
| validate_fields already rejects an update_interval below 1 one field away. | ||
| """ | ||
| with pytest.raises(ValueError, match="steps_per_epoch"): | ||
| ZenFlowConfig(select_strategy="auto", | ||
| select_interval="auto", | ||
| update_interval="auto", | ||
| steps_per_epoch=steps_per_epoch) | ||
|
|
||
|
|
||
| def test_an_empty_dataloader_warns_rather_than_selecting_once_in_silence(caplog): | ||
| """The programmatic path to the same 0: len(dataloader) == 0. | ||
|
|
||
| configure_zenflow fills steps_per_epoch from the dataloader it owns, and the | ||
| config validator never sees that assignment. | ||
| """ | ||
| config = ZenFlowConfig(select_strategy="auto", select_interval="auto", update_interval="auto") | ||
| engine = _StubEngine(config, training_dataloader=_Loader(0)) | ||
|
|
||
| with caplog.at_level("WARNING"): | ||
| configure_zenflow(engine) | ||
|
|
||
| assert engine.select_interval == 0 | ||
| assert any("never re-selected" in record.getMessage() for record in caplog.records) | ||
|
|
||
|
|
||
| def test_step_strategy_needs_no_epoch_length(): | ||
| config = ZenFlowConfig(select_strategy="step", select_interval=10, update_interval="auto") | ||
| engine = _StubEngine(config, training_dataloader=None) | ||
|
|
||
| configure_zenflow(engine) | ||
|
|
||
| assert engine.select_interval == 10 | ||
| assert _select_boundaries(engine.select_interval, steps=40) == 4 | ||
|
|
||
|
|
||
| def test_auto_strategy_warns_rather_than_raising_on_an_ignored_interval(): | ||
| # `raise Warning(...)` raises: Warning is an Exception. This combination | ||
| # could not run at all, and the `select_interval = 1` line after it was dead. | ||
| config = ZenFlowConfig(select_strategy="auto", select_interval=5, update_interval="auto") | ||
| engine = _StubEngine(config, training_dataloader=_Loader(10)) | ||
|
|
||
| configure_zenflow(engine) | ||
|
|
||
| assert engine.select_interval == 10 # one epoch, not the ignored 5 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran this against
4ffc94eain a clean container (python:3.12-slim, CPU torch), driving the realconfigure_zenflowthe waytest_zf_select_interval.pydoes. Your 8 new cases pass there.This PR makes
steps_per_epochsomething a caller is now told to set, and the guard on the next line readsis not None, sosteps_per_epoch: 0multiplies straight through toselect_interval = 0. That is the same "selected once and never re-selected" state this PR exists to announce, reached silently through the knob it documents, because the new warning only fires in theNonebranch.All three copies guard the same way, so a zero passes all of them:
engine.py:61,engine_stage3.py:51,zenflow_stage_1_and_2.py:140.validate_fieldsalready rejects anupdate_intervalbelow 1 one field away. The same check onsteps_per_epoch, orgt=0on thisField, keeps the zero case in the branch that warns and costs one line.I only exercised
configure_zenflow, so I have not run the rest of the ZenFlow suite here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
ffb0860— sorry for the silence, I pushed it the same day and never replied here.validate_fieldsnow rejects it one field away from theupdate_intervalcheck you pointed at:and all three consumers moved from
is not Noneto a truth guard, soNoneand a rejected value cannot take different paths through them.Your probe against the current head:
The negative case was the worse of the two and I would not have looked for it:
select_intervalstayed negative andmicro_step % -5fired on a schedule nobody asked for — 8 selections in 40 steps rather than the 4 that10gives. Your table is in the commit message.Full suite on 2×H20 after the change:
tests/unit/runtime/zenflow/105 passed, against a 56 passed / 38 skipped baseline on one GPU.