Make PipelineModule.set_checkpoint_interval actually change the interval - #8178
Make PipelineModule.set_checkpoint_interval actually change the interval#8178vineethsaivs wants to merge 2 commits into
Conversation
set_checkpoint_interval() assigned self.checkpoint_interval, but forward() and _precompute_checkpointable_values() both read self.activation_checkpoint_interval, so the call wrote an attribute nothing reads and silently left the schedule unchanged. Assigning the right attribute is not enough on its own. _precompute_checkpointable_values() appends to self.is_checkpointable_results without clearing it, and its guard exists to recompute when the interval changes, so a second run leaves results from the previous interval at the front of the list. forward() zips the layer blocks against that list, so the blocks get the flags of a different partitioning: with 8 layers and interval 4 -> 1 the list holds 10 entries instead of 8 and blocks are checkpointed against stale decisions. Clear the cached results before recomputing them, and have the setter both assign activation_checkpoint_interval and rebuild the cache, so it cannot leave a list sized for the old interval behind. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a0966a8dc
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self.activation_checkpoint_interval = interval | ||
| # forward() zips the layer blocks against the cached results, so they have to be rebuilt | ||
| # for the new interval before the next forward pass | ||
| self._precompute_checkpointable_values() |
There was a problem hiding this comment.
Sync engine state when enabling checkpointing
When this setter is used to change a module from interval 0 to a positive interval while the PipelineEngine config still has the default interval 0, it now enables the reentrant checkpoint path but leaves PipelineEngine._config.pipeline['activation_checkpoint_interval'] unchanged. The load path in PipelineEngine._exec_load_micro_batch() only marks first-stage inputs as requires_grad when that config value is >0 (deepspeed/runtime/pipe/engine.py:904-916), so the default reentrant checkpoint function runs with detached inputs and the first checkpointed segment is not connected to autograd, dropping or failing gradients for that training run. Either keep the engine/config flag in sync or have the engine key this requires_grad decision off the module interval.
Useful? React with 👍 / 👎.
tohtana
left a comment
There was a problem hiding this comment.
Hi @vineethsaivs,
Thank you for submitting this PR! I think the comment from Codex bot is reasonable. Can you address it?
_exec_load_micro_batch marks the first stage's inputs as requiring grad only when self._config.pipeline['activation_checkpoint_interval'] > 0, but the pipeline config only seeds the module at __init__. Now that set_checkpoint_interval() actually changes the interval, a module switched from 0 to a positive interval starts checkpointing while that config value is still 0, so the inputs are left detached and the first checkpointed segment is cut off from autograd. The same hole already existed for a PipelineModule constructed with its own activation_checkpoint_interval under a config of 0, so key the decision off the module instead of syncing two copies. forward() and the _precompute_checkpointable_values() call in __init__ already branch on the module attribute, and the module's activation_checkpoint_func is what the use_reentrant flag ends up selecting, so the two now agree by construction. Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
|
Thanks @tohtana. Agreed, the Codex comment is a real bug and I have addressed it in cb8dae6. I checked it against the source before fixing, and it holds:
So once the setter really changes the interval, the module starts checkpointing while the config still reads 0, and under reentrant checkpointing the first stage's inputs stay detached. The first checkpointed segment is then cut off from autograd. Codex offered two options: keep the config in sync, or key the decision off the module. I went with the second, for two reasons. The first is that the module is already the source of truth everywhere else. The second is that this hole is not actually new. I also had to fold def _reentrant_activation_checkpointing(self):
if self.module.activation_checkpoint_interval <= 0:
return False
return self.module.activation_checkpoint_func is not ds_checkpointing.non_reentrant_checkpointTwo tests added to |
Problem
PipelineModule.set_checkpoint_interval()does not change the activation checkpoint interval.Every reader uses
self.activation_checkpoint_interval:forward()branches on it and steps the layer loop by it (module.pyL370-379),_precompute_checkpointable_values()keys its cache on it (L223-230), andPipelineEngineassigns it directly (pipe/engine.pyL212).self.checkpoint_intervalis read nowhere in the repo, so the call assigns a dead attribute and the schedule silently stays as it was.Assigning the right attribute is not sufficient on its own, because the recompute path it feeds is also broken:
The
is_checkpointable_results_interval != activation_checkpoint_intervalguard exists precisely so the values are recomputed when the interval changes, but the loop appends toself.is_checkpointable_resultswithout clearing it, so results computed for the previous interval stay at the front of the list.forward()then pairs the layer blocks with that list positionally:so each block is checkpointed according to a decision made for a different partitioning of the layers.
Reproduction
8 layers, the first 4 without parameters and the last 4 with, so
_is_checkpointablegenuinely differs per block. Going from interval 4 to interval 1:Blocks 1, 4 and 5 are checkpointed against the wrong decision: block 1 holds no parameters and is checkpointed anyway, blocks 4 and 5 hold parameters and are not.
Fix
Clear the cached results before recomputing them, and have the setter assign
activation_checkpoint_intervaland rebuild the cache:The setter has to do both. Assigning the interval alone would leave
forward()zipping the new, longer block range against a list still sized for the old interval, andzipstops at the shorter one, so trailing layer blocks would be dropped from the forward pass entirely.Nothing changes for the normal path:
PipelineEngineassigns the interval once and calls_precompute_checkpointable_values()while the cache is still empty, so clearing an empty list is a no-op and the guard still skips the recompute when the interval is unchanged.Testing
TestPipeModuleCheckpointIntervalintests/unit/pipe/test_pipe_module.pybuilds aPipelineModuleat interval 4, callsset_checkpoint_interval(1), and asserts the interval is updated and the results match a module constructed at interval 1 directly. It fails on master on both assertions (the interval stays 4, and the results stay[False, True]) and passes with the fix. The mixedReLU/Linearmodel is deliberate: with a uniformly parameterised model only the length of the list is wrong, and the misalignment would not show.yapfandflake8are clean on both changed files.