Say when ZenFlow will never re-select its important columns - #8456
Say when ZenFlow will never re-select its important columns#8456alanhuangyoo wants to merge 2 commits into
Conversation
ZenFlow re-picks the top-k 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 the number of steps in an epoch.
DeepSpeed reads that from engine.training_dataloader, which is only set when
the caller hands its data to deepspeed.initialize(). A caller driving its own
dataloader falls into `engine.select_interval = 0`, and
is_zenflow_select_boundary() treats 0 as "never again":
(micro_step - full_warm_up_rounds) == 0 or
(self.select_interval != 0 and self.micro_step % self.select_interval == 0)
so the columns chosen at the first step are the ones used for the whole run.
No error, no warning, and the default configuration takes this path.
Resolving the intervals directly, over 40 micro-steps:
select_strategy select_interval epoch length known resolved selections
auto (default) auto no 0 1
auto (default) auto yes 10 4
epoch 1 no 0 1
epoch 1 yes 10 4
step 10 n/a 10 4
The tests in tests/unit/runtime/zenflow/test_zf.py build their dataloader after
deepspeed.initialize() and never pass it in, so every ZenFlow test -- including
the auto and epoch parametrizations -- runs on select_interval 0. They assert
that training does not crash, which it does not.
Rather than refuse the configuration, which would force callers onto
DeepSpeed's own dataloader, this keeps the run going and:
- takes steps_per_epoch from the zenflow config when the user sets it, so a
caller with its own dataloader can say what an epoch is;
- warns, naming all three ways out, when neither source provides a length;
- documents steps_per_epoch as user-settable rather than "should not be set by
users".
Also replaces three `raise Warning(...)` calls with logger.warning. Warning is
an Exception, so select_strategy 'auto' with an integer select_interval raised
instead of warning, and the `select_interval = 1` line after it was dead.
tests/unit/runtime/zenflow/test_zf_select_interval.py covers all five rows
above plus the warning; five of its eight cases fail on master.
Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
| description="Number of steps in one epoch, used by the 'epoch' select strategy (which " | ||
| "'auto' resolves to) to turn select_interval into a number of steps. DeepSpeed fills this " | ||
| "in from the training dataloader when it owns one; set it explicitly when you drive your " | ||
| "own dataloader, otherwise the important columns are selected once and never re-selected.", |
There was a problem hiding this comment.
I ran this against 4ffc94ea in a clean container (python:3.12-slim, CPU torch), driving the real configure_zenflow the way test_zf_select_interval.py does. Your 8 new cases pass there.
This PR makes steps_per_epoch something a caller is now told to set, and the guard on the next line reads is not None, so steps_per_epoch: 0 multiplies straight through to select_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 the None branch.
$ docker run --rm -v $PWD:/src:ro -w /src -e PYTHONPATH=/src ds-hunt:cpu python zf_probe.py
deepspeed from: /src/deepspeed/__init__.py
steps_per_epoch unset (None) -> select_interval=0 selections_in_40_steps=1 warning=YES
steps_per_epoch=0 -> select_interval=0 selections_in_40_steps=1 warning=NO
steps_per_epoch=-5 -> select_interval=-5 selections_in_40_steps=8 warning=NO
steps_per_epoch=10 -> select_interval=10 selections_in_40_steps=4 warning=NO
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_fields already rejects an update_interval below 1 one field away. The same check on steps_per_epoch, or gt=0 on this Field, 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.
Fixed in ffb0860 — sorry for the silence, I pushed it the same day and never replied here.
validate_fields now rejects it one field away from the update_interval check you pointed at:
if self.steps_per_epoch is not None and self.steps_per_epoch < 1:
raise ValueError("If steps_per_epoch is set, it must be at least 1")and all three consumers moved from is not None to a truth guard, so None and a rejected value cannot take different paths through them.
Your probe against the current head:
steps_per_epoch result
unset (None) accepted, steps_per_epoch=None -> the branch that warns
0 ValidationError at config validation
-5 ValidationError at config validation
10 accepted, steps_per_epoch=10
The negative case was the worse of the two and I would not have looked for it: select_interval stayed negative and micro_step % -5 fired on a schedule nobody asked for — 8 selections in 40 steps rather than the 4 that 10 gives. 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.
… announce Reported by @ebarkhordar on deepspeedai#8456. This PR documents steps_per_epoch as something a caller sets, and the guard read `is not None`, so 0 multiplied straight through to select_interval = 0 -- the 'selected once and never re-selected' state the warning is for, reached silently through the knob that documents it, because the warning only fires in the None branch. A negative value was worse: select_interval stayed negative and micro_step % -5 fired on a schedule nobody asked for. steps_per_epoch resolved selections/40 warning None 0 1 YES 0 0 1 NO -5 -5 8 NO 10 10 4 NO validate_fields already rejects an update_interval below 1 one field away; the same check on steps_per_epoch keeps 0 and negatives out, and the three consumers guard on truth rather than None so nothing else can carry a zero through. Adding the validator surfaced a second path to the same 0, which the report did not cover: configure_zenflow assigns len(training_dataloader), the config model validates on assignment, so an empty dataloader turned into a ValidationError raised from inside DeepSpeed. A crash is the wrong answer for a degenerate but legal dataloader, so the length is only assigned when positive and an empty one falls through to the warning. tests/unit/runtime/zenflow/: 105 passed on 2xH20, against a 56 passed / 38 skipped / 0 failed baseline on master -- the skips are the multi-GPU cases this run reached, plus the 3 tests added here. Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
|
You were right, and I reproduced your table before changing anything — same four rows, same numbers. Fixed in Adding the validator surfaced a second path to the same 0 that your report did not cover, and I would rather say so than let it pass:
Worth naming the pattern, since this is the second time you have caught the same shape in my work: on #8435 the guard was per tensor and the invariant I wrote was global, and here I documented a knob and only guarded one of its values. Both times the fix was verified against the path that motivated it and described in wider language than that path supports. Your habit of sweeping the domain and printing the table is the thing that catches it, and it is cheap — I am adopting it for anything I add or document rather than waiting for review to do it. |
|
Thanks. I read the diff at The empty-dataloader case was not in my report. Assigning |
Problem
ZenFlow re-picks the top-k important gradient columns every
select_intervalmicro-steps. The'epoch'strategy — which'auto', the default, resolves to — expresses that interval in epochs, so it needs the number of steps in an epoch. DeepSpeed reads it fromengine.training_dataloader, which is only set when the caller hands its data todeepspeed.initialize():and
is_zenflow_select_boundary()reads 0 as "never again":So a caller that drives its own dataloader — the common case — gets the columns chosen at the first step for the whole run. No error, no warning, and the default configuration takes this path.
Resolving the intervals directly, over 40 micro-steps:
auto(default)autoauto(default)autoepochepochstepTwo things I want to flag rather than bury:
tests/unit/runtime/zenflow/test_zf.py::run_training_distributedbuilds its dataloader afterdeepspeed.initialize()and never passes it in, so every ZenFlow test — including the("auto", "auto", "auto")and("epoch", 1, 4)parametrizations — resolvesselect_intervalto 0. They assert that training does not crash, which it does not, so the suite cannot see this.raise Warning(...)raises.Warningis a subclass ofException, soselect_strategy: "auto"together with an integerselect_intervaldid not warn — it aborted, with a message that describes a fallback ("would be overwritten"), and theselect_interval = 1line right after it was unreachable. Three copies, inengine.py,engine_stage3.pyandzenflow_stage_1_and_2.py.Solution
My first attempt raised a
ValueErrorfor this configuration. That was wrong, and this repo's own tests said so: forcing callers ontotraining_data=also hands them DeepSpeed's dataloader, which builds2 * device_countworker processes — 36 of the 56 ZenFlow tests then died ondaemonic processes are not allowed to have children. Imposing a dataloader is not a bug fix.So the run keeps going, and instead:
steps_per_epochis taken from the zenflow config when the user sets it, so a caller with its own dataloader can say what an epoch is. The field already existed and was already settable; it was documented as "initialized during execution and should not be set by users", which is exactly the gap.logger.warningsays the columns will be selected once and never re-selected, and names all three ways out.raise Warning(...)becomelogger.warning.Nothing changes for a caller that already passes
training_data=.Verification
Full ZenFlow suite, 1×H20 (
DS_SKIP_CUDA_CHECK=1for the CPU-Adam JIT build):64 − 56 = the 8 tests added here; no existing test changes state.
tests/unit/runtime/zenflow/test_zf_select_interval.pydrives the realconfigure_zenflowagainst a stub engine, so it needs no GPU and no CPU-Adam build. It covers every row of the table, the user-suppliedsteps_per_epochpath, and the warning. Five of its eight cases fail on master.pre-commit runpasses on all five files.Note
I have not touched the
is_zenflow_select_boundarypredicate or the meaning ofselect_interval == 0. If you would rather the epoch strategy refuse to start without a length, that is a smaller change than this one — I went with the non-breaking side because the current default lands there and refusing would break every existing run.