Skip to content

Commit 35b4262

Browse files
committed
ZeRO 1/2: wait on all IPG-bucket producer streams in average_tensor (#8061)
With overlap_comm, the per-parameter gradient copies into the contiguous IPG bucket can be issued on multiple streams (e.g. under torch.compile, gradient hooks run on different autograd streams). average_tensor waited the reduction stream on only the current stream before reducing the bucket, so the reduction could read the bucket before another producer finished, corrupting gradients (NaN loss). Track the set of producer streams per IPG bucket and wait on all of them. The single-stream path is unchanged (the set is just {current_stream}), so there is no behavior change when overlap_comm copies stay on one stream. Adds CPU unit tests in tests/unit/v1/zero/test_overlap_comm_record_stream.py for the producer-stream wait, the empty fallback to current_stream, and the IPGBucket.copy_streams reset. Fixes #8061. Signed-off-by: Arun Sharma <sharm485@umn.edu>
1 parent dc0fd29 commit 35b4262

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

deepspeed/runtime/zero/stage_1_and_2.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,18 @@ class IPGBucket:
115115
elements: int = 0
116116
index: int = 0
117117
has_moe_params: bool = False
118+
# Streams that issued copies into buffer[index] for the current bucket fill.
119+
# average_tensor must wait on all of them before reducing the bucket, since the
120+
# copies can be produced on multiple streams (e.g. under torch.compile gradient
121+
# hooks run on different autograd streams), not just the current one (#8061).
122+
copy_streams: set = field(default_factory=set)
118123

119124
def clear(self):
120125
self.params.clear()
121126
self.grads.clear()
122127
self.elements = 0
123128
self.has_moe_params = False
129+
self.copy_streams.clear()
124130

125131

126132
class DeepSpeedZeroOptimizer(ZeROOptimizer):
@@ -1119,6 +1125,10 @@ def reduce_independent_p_g_buckets_and_remove_grads(self, param, i):
11191125
grad_reduc.data = new_grad_tensor.data.view_as(grad_reduc) if (
11201126
not self.zenflow or grad_reduc.dim() == 1) else new_grad_tensor.data.view_as(
11211127
grad_reduc.transpose(0, 1))
1128+
# Record the stream this copy ran on so average_tensor can wait on
1129+
# every producer of the bucket, not just the current stream (#8061).
1130+
if self.overlap_comm and not get_accelerator().resolves_data_dependency():
1131+
bucket.copy_streams.add(get_accelerator().current_stream())
11221132

11231133
bucket.elements += param.numel()
11241134

@@ -1227,7 +1237,15 @@ def average_tensor(self, tensor: torch.Tensor, communication_data_type: torch.dt
12271237
if self.overlap_comm:
12281238
stream = self.reduction_stream
12291239
if not get_accelerator().resolves_data_dependency():
1230-
stream.wait_stream(get_accelerator().current_stream())
1240+
# The contiguous IPG bucket may have been filled by copies issued on
1241+
# several streams (e.g. under torch.compile, gradient hooks run on
1242+
# different autograd streams). Waiting only on the current stream lets
1243+
# the reduction read the bucket before the other producers finish
1244+
# (#8061), so wait on every stream that produced a copy into it.
1245+
bucket = self.ipg_buckets[communication_data_type]
1246+
producer_streams = bucket.copy_streams or {get_accelerator().current_stream()}
1247+
for producer_stream in producer_streams:
1248+
stream.wait_stream(producer_stream)
12311249
get_accelerator().current_stream().wait_stream(stream)
12321250
else:
12331251
stream = get_accelerator().current_stream()

tests/unit/v1/zero/test_overlap_comm_record_stream.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,75 @@ def test_allreduce_and_copy_with_multiple_ranks_records_only_local_buffers(monke
9595
assert bucket[0].recorded_streams == [optimizer.reduction_stream]
9696
assert bucket[1].copied_from is None
9797
assert bucket[1].recorded_streams == []
98+
99+
100+
class _FakeWaitStream:
101+
"""A stream stand-in that records which streams it was told to wait on."""
102+
103+
def __init__(self):
104+
self.waited_on = []
105+
106+
def wait_stream(self, other):
107+
self.waited_on.append(other)
108+
109+
110+
class _FakeAcceleratorWithCurrentStream(_FakeAccelerator):
111+
112+
def __init__(self, resolves_data_dependency, current_stream):
113+
super().__init__(resolves_data_dependency)
114+
self._current_stream = current_stream
115+
116+
def current_stream(self):
117+
return self._current_stream
118+
119+
120+
def _build_average_tensor_optimizer(monkeypatch, *, copy_streams):
121+
optimizer = DeepSpeedZeroOptimizer.__new__(DeepSpeedZeroOptimizer)
122+
optimizer.overlap_comm = True
123+
optimizer.reduce_scatter = False # take the early-return reduce path, isolating the wait logic
124+
optimizer.reduction_stream = _FakeWaitStream()
125+
comm_dtype = torch.float16
126+
bucket = zero_stage12.IPGBucket()
127+
bucket.copy_streams = set(copy_streams)
128+
optimizer.ipg_buckets = {comm_dtype: bucket}
129+
reduced = []
130+
optimizer.gradient_reduction_w_predivide = lambda tensor, dt: reduced.append(dt)
131+
current = _FakeWaitStream()
132+
monkeypatch.setattr(
133+
zero_stage12,
134+
"get_accelerator",
135+
lambda: _FakeAcceleratorWithCurrentStream(False, current),
136+
)
137+
return optimizer, comm_dtype, current, reduced
138+
139+
140+
def test_average_tensor_waits_on_all_ipg_bucket_producer_streams(monkeypatch):
141+
# #8061: the reduction stream must wait on every stream that produced a copy into
142+
# the contiguous IPG bucket, not just the current stream, because under
143+
# torch.compile those copies can be issued on multiple autograd streams.
144+
s1, s2 = object(), object()
145+
optimizer, comm_dtype, _, reduced = _build_average_tensor_optimizer(monkeypatch, copy_streams=[s1, s2])
146+
147+
optimizer.average_tensor(torch.zeros(4), comm_dtype)
148+
149+
assert set(optimizer.reduction_stream.waited_on) == {s1, s2}
150+
assert reduced == [comm_dtype]
151+
152+
153+
def test_average_tensor_falls_back_to_current_stream_without_producers(monkeypatch):
154+
# The extra-large-param path reduces without copying into the bucket, so
155+
# copy_streams is empty: preserve the original behavior of waiting on the
156+
# current stream.
157+
optimizer, comm_dtype, current, _ = _build_average_tensor_optimizer(monkeypatch, copy_streams=[])
158+
159+
optimizer.average_tensor(torch.zeros(4), comm_dtype)
160+
161+
assert optimizer.reduction_stream.waited_on == [current]
162+
163+
164+
def test_ipg_bucket_clear_resets_copy_streams():
165+
bucket = zero_stage12.IPGBucket()
166+
assert bucket.copy_streams == set()
167+
bucket.copy_streams.add(object())
168+
bucket.clear()
169+
assert bucket.copy_streams == set()

0 commit comments

Comments
 (0)