@@ -95,6 +95,66 @@ def wait(self):
9595 return None
9696
9797
98+ class StagedWork :
99+ """Completes a staged collective by copying the CPU results back to the original device tensors."""
100+
101+ def __init__ (self , work , copy_back ):
102+ self .work = work
103+ self .copy_back = copy_back
104+
105+ def wait (self ):
106+ if self .work is not None :
107+ self .work .wait ()
108+ self .copy_back ()
109+ return None
110+
111+
112+ def _needs_cpu_staging (tensor ):
113+ # gloo (the only torch backend on macOS) cannot operate on MPS tensors.
114+ return isinstance (tensor , torch .Tensor ) and tensor .device .type == 'mps'
115+
116+
117+ def stage_on_cpu (func ):
118+ """Runs a collective on CPU copies of any MPS tensor arguments, then copies the results back.
119+
120+ This is what lets DeepSpeed use the gloo backend on Apple Silicon, where device tensors are
121+ not supported by any torch.distributed backend. Unified memory keeps the copies cheap.
122+ """
123+
124+ def _stage (arg , pairs ):
125+ if _needs_cpu_staging (arg ):
126+ cpu_tensor = arg .to ('cpu' )
127+ pairs .append ((arg , cpu_tensor ))
128+ return cpu_tensor
129+ if isinstance (arg , list ) and any (_needs_cpu_staging (t ) for t in arg ):
130+ return [_stage (t , pairs ) for t in arg ]
131+ return arg
132+
133+ signature = inspect .signature (func )
134+
135+ def wrapper (self , * args , ** kwargs ):
136+ pairs = []
137+ args = [_stage (arg , pairs ) for arg in args ]
138+ kwargs = {key : _stage (arg , pairs ) for key , arg in kwargs .items ()}
139+ if not pairs :
140+ return func (self , * args , ** kwargs )
141+
142+ work = func (self , * args , ** kwargs )
143+
144+ def copy_back ():
145+ for device_tensor , cpu_tensor in pairs :
146+ device_tensor .copy_ (cpu_tensor )
147+
148+ # async_op is usually forwarded positionally, so resolve it against the real signature.
149+ bound_args = signature .bind (self , * args , ** kwargs )
150+ if bound_args .arguments .get ('async_op' , False ):
151+ return StagedWork (work , copy_back )
152+ copy_back ()
153+ return work
154+
155+ return wrapper
156+
157+
98158class TorchBackend (Backend ):
99159 """
100160 A light-weight wrapper class for torch.distributed API.
@@ -179,6 +239,7 @@ def init_process_group(self, backend, timeout, init_method, rank, world_size):
179239 self .using_mpi = torch .distributed .get_backend () == 'mpi'
180240
181241 @disable_compiler_collective
242+ @stage_on_cpu
182243 def all_reduce (self , tensor , op = torch .distributed .ReduceOp .SUM , group = None , async_op = False ):
183244 op = self ._reduce_op (op )
184245 return torch .distributed .all_reduce (tensor = tensor , op = op , group = group , async_op = async_op )
@@ -195,6 +256,7 @@ def inference_all_reduce(self, tensor, op, group=None):
195256 return torch .ops .deepspeed .inference_all_reduce_ (tensor )
196257
197258 @disable_compiler_collective
259+ @stage_on_cpu
198260 def all_reduce_coalesced (self , tensors , op = torch .distributed .ReduceOp .SUM , group = None , async_op = False ):
199261 """ proxy func to torch.distributed.all_reduce_coalesced,
200262 which is included in PyTorch 1.13 and above
@@ -206,6 +268,7 @@ def all_reduce_coalesced(self, tensors, op=torch.distributed.ReduceOp.SUM, group
206268 return torch .distributed .all_reduce_coalesced (tensors = tensors , op = op , group = group , async_op = async_op )
207269
208270 @disable_compiler_collective
271+ @stage_on_cpu
209272 def reduce (self , tensor , dst , op = ReduceOp .SUM , group = None , async_op = False ):
210273 if DS_COMM_REDUCE_OFF :
211274 if int (os .getenv ('RANK' , '0' )) == 0 :
@@ -214,6 +277,7 @@ def reduce(self, tensor, dst, op=ReduceOp.SUM, group=None, async_op=False):
214277 return torch .distributed .reduce (tensor = tensor , dst = dst , op = self ._reduce_op (op ), group = group , async_op = async_op )
215278
216279 @disable_compiler_collective
280+ @stage_on_cpu
217281 def reduce_scatter (self , output , input_list , op = ReduceOp .SUM , group = None , async_op = False ):
218282 if DS_COMM_REDUCE_SCATTER_OFF :
219283 if int (os .getenv ('RANK' , '0' )) == 0 :
@@ -227,6 +291,7 @@ def reduce_scatter(self, output, input_list, op=ReduceOp.SUM, group=None, async_
227291 async_op = async_op )
228292
229293 @disable_compiler_collective
294+ @stage_on_cpu
230295 def broadcast (self , tensor , src , group = None , async_op = False ):
231296 if DS_COMM_BROADCAST_OFF :
232297 if int (os .getenv ('RANK' , '0' )) == 0 :
@@ -240,6 +305,7 @@ def broadcast_object_list(self, object_list, src, group=None, device=None):
240305 return torch .distributed .broadcast_object_list (object_list = object_list , src = src , group = group , device = device )
241306
242307 @disable_compiler_collective
308+ @stage_on_cpu
243309 def all_gather (self , tensor_list , tensor , group = None , async_op = False ):
244310 if DS_COMM_ALL_GATHER_OFF :
245311 if int (os .getenv ('RANK' , '0' )) == 0 :
@@ -249,6 +315,7 @@ def all_gather(self, tensor_list, tensor, group=None, async_op=False):
249315 return torch .distributed .all_gather (tensor_list = tensor_list , tensor = tensor , group = group , async_op = async_op )
250316
251317 @disable_compiler_collective
318+ @stage_on_cpu
252319 def all_gather_into_tensor (self , output_tensor , input_tensor , group = None , async_op = False ):
253320 # Transparent SDMA fast-path on AMD/ROCm: when the mori backend is
254321 # available and the call is on the WORLD process group, route
@@ -266,6 +333,7 @@ def all_gather_into_tensor(self, output_tensor, input_tensor, group=None, async_
266333 async_op = async_op )
267334
268335 @disable_compiler_collective
336+ @stage_on_cpu
269337 def all_gather_base (self , output_tensor , input_tensor , group = None , async_op = False ):
270338 if DS_COMM_ALL_GATHER_OFF :
271339 if int (os .getenv ('RANK' , '0' )) == 0 :
@@ -284,6 +352,7 @@ def all_gather_base(self, output_tensor, input_tensor, group=None, async_op=Fals
284352 pass
285353
286354 @disable_compiler_collective
355+ @stage_on_cpu
287356 def all_gather_coalesced (self , output_tensors , input_tensors , group = None , async_op = False ):
288357 """"""
289358 assert len (output_tensors ) == len (input_tensors ), ""
@@ -312,6 +381,7 @@ def all_gather_object(self, object_list, obj, group=None):
312381 return torch .distributed .all_gather_object (object_list = object_list , obj = obj , group = group )
313382
314383 @disable_compiler_collective
384+ @stage_on_cpu
315385 def reduce_scatter_tensor (self , output_tensor , input_tensor , op = ReduceOp .SUM , group = None , async_op = False ):
316386 if self .has_reduce_scatter_tensor ():
317387 return self .reduce_scatter_function (output_tensor ,
@@ -326,6 +396,7 @@ def reduce_scatter_tensor(self, output_tensor, input_tensor, op=ReduceOp.SUM, gr
326396 pass
327397
328398 @disable_compiler_collective
399+ @stage_on_cpu
329400 def all_to_all_single (self ,
330401 output ,
331402 input ,
@@ -341,26 +412,32 @@ def all_to_all_single(self,
341412 async_op = async_op )
342413
343414 @disable_compiler_collective
415+ @stage_on_cpu
344416 def all_to_all (self , output_tensor_list , input_tensor_list , group = None , async_op = False ):
345417 return torch .distributed .all_to_all (output_tensor_list , input_tensor_list , group = group , async_op = async_op )
346418
347419 @disable_compiler_collective
420+ @stage_on_cpu
348421 def send (self , tensor , dst , group = None , tag = 0 ):
349422 return torch .distributed .send (tensor = tensor , dst = dst , group = group , tag = tag )
350423
351424 @disable_compiler_collective
425+ @stage_on_cpu
352426 def recv (self , tensor , src = None , group = None , tag = 0 ):
353427 return torch .distributed .recv (tensor = tensor , src = src , group = group , tag = tag )
354428
355429 @disable_compiler_collective
430+ @stage_on_cpu
356431 def isend (self , tensor , dst , group = None , tag = 0 ):
357432 return torch .distributed .isend (tensor = tensor , dst = dst , group = group , tag = tag )
358433
359434 @disable_compiler_collective
435+ @stage_on_cpu
360436 def irecv (self , tensor , src = None , group = None , tag = 0 ):
361437 return torch .distributed .irecv (tensor = tensor , src = src , group = group , tag = tag )
362438
363439 @disable_compiler_collective
440+ @stage_on_cpu
364441 def gather (self , tensor , gather_list = None , dst = 0 , group = None , async_op = False ):
365442 return torch .distributed .gather (tensor = tensor ,
366443 gather_list = gather_list ,
@@ -369,6 +446,7 @@ def gather(self, tensor, gather_list=None, dst=0, group=None, async_op=False):
369446 async_op = async_op )
370447
371448 @disable_compiler_collective
449+ @stage_on_cpu
372450 def scatter (self , tensor , scatter_list = None , src = 0 , group = None , async_op = False ):
373451 return torch .distributed .scatter (tensor = tensor ,
374452 scatter_list = scatter_list ,
0 commit comments