Skip to content

Commit d99a642

Browse files
authored
Fix repeated gradient scaling across pipeline stages (#8154)
## Summary Fix repeated gradient accumulation scaling when pipeline parallelism is combined with gradient accumulation. Every pipeline stage registers an output backward hook that scales gradients by `gradient_accumulation_steps`. The last stage therefore scales the gradient correctly before sending it upstream, but each preceding stage scales the already-scaled gradient again. Fixes #8152 ## Changes - Apply gradient accumulation scaling only at the last pipeline stage. - Preserve the existing behavior for single-stage pipelines and `gradient_accumulation_steps=1`. - Add a deterministic two-stage regression test that verifies both stages receive correctly averaged gradients. - Disable gradient clipping in the regression test so it cannot mask the scaling difference. ## Testing - `DS_ACCELERATOR=cpu LOCAL_SIZE=2 OMP_NUM_THREADS=1 pytest -q tests/unit/runtime/pipe/test_pipe.py::TestPipeGradientAccumulationScaling::test_gradients_are_scaled_once` - `pre-commit run --files deepspeed/runtime/pipe/engine.py tests/unit/runtime/pipe/test_pipe.py` - Two-process CPU/Gloo numerical comparison for `gradient_accumulation_steps=1,2,8` Signed-off-by: Yuchen Fan <functionhx@gmail.com>
1 parent 75f395f commit d99a642

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

deepspeed/runtime/pipe/engine.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,12 @@ def is_last_stage(self):
534534
"""True if this process is in the last stage in the pipeline."""
535535
return self.stage_id == self.num_stages - 1
536536

537+
def _backward_prologue_per_tensor(self, grad):
538+
# The last stage applies GAS scaling before sending gradients upstream.
539+
if self.is_last_stage():
540+
return super()._backward_prologue_per_tensor(grad)
541+
return grad
542+
537543
def get_pipeline_parallel_rank(self):
538544
return self.stage_id
539545

tests/unit/runtime/pipe/test_pipe.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import deepspeed.comm as dist
1414
from deepspeed.runtime.pipe.topology import PipeDataParallelTopology
1515
from deepspeed.runtime.pipe.module import PipelineModule
16+
from deepspeed.utils import RepeatingLoader
1617
from unit.alexnet_model import AlexNetPipe, train_cifar
1718
from unit.common import DistributedTest
1819
from unit.util import skip_on_arch, no_child_process_in_deepspeed_io
@@ -49,6 +50,44 @@ def rel_diff(A, B):
4950
return abs(A - B) / abs(A)
5051

5152

53+
class TestPipeGradientAccumulationScaling(DistributedTest):
54+
world_size = 2
55+
56+
def test_gradients_are_scaled_once(self):
57+
gas = 2
58+
learning_rate = 0.1
59+
config = {
60+
"train_batch_size": gas,
61+
"train_micro_batch_size_per_gpu": 1,
62+
"gradient_accumulation_steps": gas,
63+
"gradient_clipping": 0.0,
64+
"optimizer": {
65+
"type": "SGD",
66+
"params": {
67+
"lr": learning_rate
68+
}
69+
},
70+
"zero_allow_untested_optimizer": True,
71+
"pipeline": {
72+
"activation_checkpoint_interval": 0
73+
},
74+
}
75+
76+
layers = [nn.Linear(1, 1, bias=False), nn.Linear(1, 1, bias=False)]
77+
for layer in layers:
78+
layer.weight.data.fill_(1.0)
79+
80+
model = PipelineModule(layers=layers, num_stages=2, loss_fn=nn.MSELoss())
81+
engine, _, _, _ = deepspeed.initialize(config=config, model=model, model_parameters=model.parameters())
82+
engine.set_dataiterator(RepeatingLoader([(torch.ones(1, 1), torch.zeros(1, 1))]))
83+
84+
engine.train_batch()
85+
86+
expected_weight = torch.tensor([[1.0 - 2.0 * learning_rate]], device=engine.device)
87+
actual_weight = next(engine.module.parameters())
88+
assert torch.allclose(actual_weight, expected_weight)
89+
90+
5291
@pytest.mark.parametrize('topo_config', [
5392
{
5493
"num_pp": 1,

0 commit comments

Comments
 (0)