已合并
test: verify community 2.11.0 features and fixes #36311
test: verify community 2.11.0 features and fixes #36311
已合并
kuhn7创建于 5月21日
5 个文件变更+887-7
@@ -1,3 +1,6 @@
1+# Copyright (c) Meta Platforms, Inc. and affiliates
2+# Owner(s): ["oncall: distributed"]
3+ 
1import unittest4import unittest
2 5 
3import torch6import torch
@@ -6,14 +9,15 @@ import torch.nn.functional as F
6from numpy.testing import assert_array_equal9from numpy.testing import assert_array_equal
7from torch.distributed._functional_collectives import AsyncCollectiveTensor10from torch.distributed._functional_collectives import AsyncCollectiveTensor
8import torch.distributed._functional_collectives as funcol11import torch.distributed._functional_collectives as funcol
9- 12+from torch.distributed.device_mesh import init_device_mesh
10-from torch.distributed._tensor import (13+from torch.distributed.tensor import (
11 DeviceMesh,14 DeviceMesh,
12 distribute_tensor,15 distribute_tensor,
13 DTensor,16 DTensor,
14- init_device_mesh,17+ Partial,
18+ Replicate,
19+ Shard,
15)20)
16-from torch.distributed._tensor.placement_types import _Partial, Replicate, Shard
17from torch.distributed.tensor.parallel import (21from torch.distributed.tensor.parallel import (
18 ColwiseParallel,22 ColwiseParallel,
19 parallelize_module,23 parallelize_module,
@@ -177,7 +181,7 @@ class DTensorTest(DTensorTestBase):
177 ddp_tensor = DTensor.from_local(local_tensor, device_mesh, replica_spec)181 ddp_tensor = DTensor.from_local(local_tensor, device_mesh, replica_spec)
178 self.assertEqual(ddp_tensor.size(), local_tensor.size())182 self.assertEqual(ddp_tensor.size(), local_tensor.size())
179 183 
180- partial_spec = [_Partial()]184+ partial_spec = [Partial()]
181 partial_tensor = DTensor.from_local(local_tensor, device_mesh, partial_spec)185 partial_tensor = DTensor.from_local(local_tensor, device_mesh, partial_spec)
182 self.assertEqual(partial_tensor.size(), local_tensor.size())186 self.assertEqual(partial_tensor.size(), local_tensor.size())
183 187 
@@ -336,7 +340,7 @@ class DTensorTest(DTensorTestBase):
336 340 
337 sharded_dtensor = distribute_tensor(global_tensor, device_mesh, placements)341 sharded_dtensor = distribute_tensor(global_tensor, device_mesh, placements)
338 local_out = sharded_dtensor.redistribute(placements=[Replicate()]).to_local(342 local_out = sharded_dtensor.redistribute(placements=[Replicate()]).to_local(
339- grad_placements=[_Partial()]343+ grad_placements=[Partial()]
340 )344 )
341 local_out.sum().backward()345 local_out.sum().backward()
342 346 
@@ -363,12 +367,51 @@ class DTensorTest(DTensorTestBase):
363 global_tensor = torch.ones(8, 3, requires_grad=True)367 global_tensor = torch.ones(8, 3, requires_grad=True)
364 368 
365 sharded_dtensor = distribute_tensor(global_tensor, device_mesh, placements)369 sharded_dtensor = distribute_tensor(global_tensor, device_mesh, placements)
366- local_out = sharded_dtensor.full_tensor(grad_placements=[_Partial()])370+ local_out = sharded_dtensor.full_tensor(grad_placements=[Partial()])
367 local_out.sum().backward()371 local_out.sum().backward()
368 372 
369 replica_grad = sharded_dtensor.grad.full_tensor()373 replica_grad = sharded_dtensor.grad.full_tensor()
370 self.assertEqual(replica_grad, global_tensor * self.world_size)374 self.assertEqual(replica_grad, global_tensor * self.world_size)
371 375 
376+ @skipIfUnsupportMultiNPU(4)
377+ @with_comms
378+ def test_to_local_from_local_backward(self):
379+ """
380+ Test that to_local() followed by from_local() with Partial placement
381+ produces correct gradients. This validates the gradient placement mapping:
382+ Partial forward → Replicate gradient (see DTensor.from_local docstring).
383+ """
384+ device_mesh = self.build_device_mesh()
385+ 
386+ # Create a sharded tensor [1., 2., ...] across ranks
387+ # rank 0 gets [1.], rank 1 gets [2.], etc.
388+ global_tensor = torch.arange(
389+ 1.0, self.world_size + 1, device=self.device_type, requires_grad=True
390+ )
391+ sharded_tensor = distribute_tensor(global_tensor, device_mesh, [Shard(0)])
392+ 
393+ # sum() produces a Partial DTensor
394+ out = sharded_tensor.sum()
395+ 
396+ # to_local() + from_local() round-trip with same Partial placement
397+ out = DTensor.from_local(
398+ out.to_local(),
399+ out.device_mesh,
400+ out.placements,
401+ run_check=False,
402+ )
403+ 
404+ # full_tensor() reduces the Partial, then compute loss
405+ loss = out.full_tensor().sum()
406+ loss.backward()
407+ 
408+ # Expected: each element contributes equally to the sum, so gradient is 1.0
409+ # The full gradient should be [1., 1., ...] (world_size elements)
410+ expected_grad = torch.ones(self.world_size, device=self.device_type)
411+ actual_grad = sharded_tensor.grad.full_tensor()
412+ 
413+ self.assertEqual(actual_grad, expected_grad)
414+ 
372 @skipIfUnsupportMultiNPU(4)415 @skipIfUnsupportMultiNPU(4)
373 @with_comms416 @with_comms
374 def test_dtensor_new_empty_strided(self):417 def test_dtensor_new_empty_strided(self):
@@ -16,6 +16,12 @@ from torch.distributed._tensor.placement_types import (
16 Replicate,16 Replicate,
17 Shard,17 Shard,
18)18)
19+from torch.distributed.tensor.debug import CommDebugMode
20+from torch.testing._internal.common_utils import (
21+ instantiate_parametrized_tests,
22+ parametrize,
23+ run_tests,
24+)
19from torch.distributed.distributed_c10d import ReduceOp25from torch.distributed.distributed_c10d import ReduceOp
20from torch.testing._internal.common_utils import run_tests26from torch.testing._internal.common_utils import run_tests
21from torch.testing._internal.distributed._tensor.common_dtensor import DTensorTestBase27from torch.testing._internal.distributed._tensor.common_dtensor import DTensorTestBase
@@ -293,5 +299,30 @@ class DistElementwiseOpsTest(DTensorTestBase):
293 self.assertEqual(dist_res.to_local().dtype, dst_dtype)299 self.assertEqual(dist_res.to_local().dtype, dst_dtype)
294 self.assertEqual(dist_res.to_local(), local_result)300 self.assertEqual(dist_res.to_local(), local_result)
295 301 
302+ @with_comms
303+ @parametrize("op,reduce_op", [(torch.maximum, "max"), (torch.minimum, "min")])
304+ def test_partial_propagation(self, op, reduce_op):
305+ # Test that torch.maximum/minimum preserves Partial("max"/"min") placements
306+ # since max(max(a), max(b)) == max(a, b) and min(min(a), min(b)) == min(a, b)
307+ device_mesh = self.build_device_mesh()
308+ comm_mode = CommDebugMode()
309+ 
310+ input1 = torch.rand(8, 8) * self.rank
311+ input2 = torch.rand(8, 8) * (self.world_size - self.rank)
312+ 
313+ d_input1 = DTensor.from_local(input1, device_mesh, [Partial(reduce_op)])
314+ d_input2 = DTensor.from_local(input2, device_mesh, [Partial(reduce_op)])
315+ 
316+ with comm_mode:
317+ result = op(d_input1, d_input2)
318+ 
319+ # Should not require any communication
320+ self.assertEqual(comm_mode.get_total_counts(), 0)
321+ # Result should still be Partial with the same reduce_op
322+ self.assertEqual(result.placements, (Partial(reduce_op),))
323+ 
324+instantiate_parametrized_tests(DistElementwiseOpsTest)
325+ 
326+ 
296if __name__ == "__main__":327if __name__ == "__main__":
297 run_tests()328 run_tests()
@@ -0,0 +1,28 @@
1+# Owner(s): ["oncall: distributed"]
2+ 
3+import torch
4+from torch._subclasses.fake_tensor import FakeTensorMode
5+from torch.testing._internal.common_utils import run_tests, TestCase
6+from torch.testing._internal.distributed.fake_pg import FakeStore
7+ 
8+ 
9+class TestFakeDTensor(TestCase):
10+ def test_fake_collectives(self):
11+ # Ensure that we can run (non-functional) collectives under FakeTensorMode.
12+ # This requires the meta impls for non-functional collectives
13+ # to be registered at impor
14+ fake_mode = FakeTensorMode()
15+ world_size = 4
16+ 
17+ fake_store = FakeStore()
18+ torch.distributed.init_process_group(
19+ "fake", store=fake_store, rank=0, world_size=world_size
20+ )
21+ default_pg = torch.distributed.distributed_c10d._get_default_group()
22+ with fake_mode:
23+ x = torch.randn(2, 2, device="npu")
24+ torch.distributed.all_reduce(x, group=default_pg)
25+ 
26+ 
27+if __name__ == "__main__":
28+ run_tests()
@@ -0,0 +1,600 @@
1+# Owner(s): ["oncall: distributed"]
2+ 
3+import sys
4+from functools import partial, wraps
5+ 
6+import torch
7+import torch.distributed as dist
8+from torch.distributed import _functional_collectives as fcols
9+from torch.testing._internal.common_distributed import (
10+ MultiThreadedTestCase,
11+ TEST_SKIPS,
12+)
13+from torch.testing._internal.common_utils import (
14+ instantiate_parametrized_tests,
15+ parametrize,
16+ run_tests,
17+)
18+ 
19+ 
20+if not dist.is_available():
21+ print("Distributed not available, skipping tests", file=sys.stderr)
22+ sys.exit(0)
23+ 
24+ 
25+# Determine available devices
26+DEVICE = "npu"
27+devices = ["cpu"]
28+if acc := torch.accelerator.current_accelerator(True):
29+ devices += [acc.type]
30+ 
31+ 
32+def with_comms(func=None):
33+ if func is None:
34+ return partial(with_comms)
35+ 
36+ @wraps(func)
37+ def wrapper(self, *args, **kwargs):
38+ if (
39+ torch.npu.is_available()
40+ and torch.accelerator.device_count() < self.world_size
41+ ):
42+ sys.exit(TEST_SKIPS[f"multi-gpu-{self.world_size}"].exit_code)
43+ 
44+ self.pg = self.create_pg(device=DEVICE)
45+ self.device = DEVICE
46+ try:
47+ return func(self, *args, **kwargs)
48+ finally:
49+ torch.distributed.destroy_process_group()
50+ 
51+ return wrapper
atomgit-bot
atomgit-botatomgit-bot5月21日

🟠 High Priority

代码库中所有其他调用均为 current_accelerator() 无参数形式(见 test_pg_wrapper.py:28、test_accelerator.py:22 等)。PyTorch 标准 API 签名为 current_accelerator(device=None),传入布尔值 True 不是有效的 device 参数。该行在模块顶层执行,NPU 可用时将直接触发 TypeError,导致整个测试文件无法导入运行。

同一段代码其他问题

likedislike
52+ 
53+ 
54+@instantiate_parametrized_tests
55+class TestFunctionalDifferentials(MultiThreadedTestCase):
56+ @property
57+ def world_size(self):
58+ return 4
59+ 
60+ def setUp(self):
61+ super().setUp()
62+ self._spawn_threads()
63+ 
64+ # ============================================================
65+ # Forward Correctness Tests
66+ # ============================================================
67+ 
68+ @parametrize("device", devices)
69+ def test_all_reduce_forward(self, device):
70+ """Test all_reduce does all_reduce in forward.
71+ 
72+ Tensor is VARYING (different across ranks).
73+ Forward aggregates varying tensors via all_reduce(sum).
74+ """
75+ group_name = dist.group.WORLD.group_name
76+ rank = dist.get_rank()
77+ 
78+ # Each rank contributes its rank value (tensor is varying)
79+ input_tensor = torch.full((3, 3), fill_value=float(rank), device=device)
80+ output = fcols.all_reduce(input_tensor, "sum", group=group_name)
81+ 
82+ # Forward does all_reduce: sum of 0+1+2+3 = 6
83+ expected = torch.full(
84+ (3, 3),
85+ fill_value=self.world_size * (self.world_size - 1) / 2,
86+ device=device,
87+ )
88+ self.assertEqual(output, expected)
89+ 
90+ @parametrize("device", devices)
91+ @parametrize("gather_dim", [0, 1, 2])
92+ def test_all_gather_tensor_forward(self, device, gather_dim):
93+ """Test all_gather_tensor produces correct output shape.
94+ 
95+ Tensor is VARYING (different across ranks).
96+ Forward gathers tensors from all ranks along gather_dim.
97+ """
98+ group_name = dist.group.WORLD.group_name
99+ rank = dist.get_rank()
100+ 
101+ # Each rank has tensor with its rank value
102+ input_tensor = torch.full((3, 3, 3), fill_value=float(rank), device=device)
103+ output = fcols.all_gather_tensor(
104+ input_tensor, gather_dim=gather_dim, group=group_name
105+ )
106+ 
107+ # Verify output shape
108+ expected_shape = list(input_tensor.shape)
109+ expected_shape[gather_dim] *= self.world_size
110+ self.assertEqual(list(output.shape), expected_shape)
111+ 
112+ # Verify output contains all ranks' data
113+ # Check each chunk along gather_dim contains the correct rank value
114+ for r in range(self.world_size):
115+ chunk = output.narrow(gather_dim, r * 3, 3)
116+ expected_chunk = torch.full((3, 3, 3), fill_value=float(r), device=device)
117+ self.assertEqual(chunk, expected_chunk)
118+ 
119+ @parametrize("device", devices)
120+ @parametrize("scatter_dim", [0, 1])
121+ def test_reduce_scatter_tensor_forward(self, device, scatter_dim):
122+ """Test reduce_scatter_tensor produces correct output shape.
123+ 
124+ Tensor is VARYING (different across ranks).
125+ Forward reduces and scatters chunks to ranks.
126+ """
127+ group_name = dist.group.WORLD.group_name
128+ rank = dist.get_rank()
129+ 
130+ # Create input with appropriate size
131+ if scatter_dim == 0:
132+ input_tensor = torch.full(
133+ (4 * self.world_size, 3), fill_value=float(rank), device=device
134+ )
135+ else: # scatter_dim == 1
136+ input_tensor = torch.full(
137+ (3, 4 * self.world_size), fill_value=float(rank), device=device
138+ )
139+ 
140+ output = fcols.reduce_scatter_tensor(
141+ input_tensor, "sum", scatter_dim=scatter_dim, group=group_name
142+ )
143+ 
144+ # Verify output shape
145+ expected_shape = list(input_tensor.shape)
146+ expected_shape[scatter_dim] //= self.world_size
147+ self.assertEqual(list(output.shape), expected_shape)
148+ 
149+ # Each rank should receive sum of all ranks' values: 0+1+2+3 = 6
150+ expected_value = self.world_size * (self.world_size - 1) / 2
151+ expected = torch.full_like(output, fill_value=expected_value)
152+ self.assertEqual(output, expected)
153+ 
154+ @parametrize("device", devices)
155+ def test_all_to_all_single_forward(self, device):
156+ """Test all_to_all_single with uniform splits.
157+ 
158+ Tensor is VARYING (different across ranks).
159+ Forward exchanges tensor chunks between ranks.
160+ """
161+ group_name = dist.group.WORLD.group_name
162+ rank = dist.get_rank()
163+ 
164+ # Each rank contributes its rank value
165+ input_tensor = torch.full(
166+ (2 * self.world_size, 3), fill_value=float(rank), device=device
167+ )
168+ 
169+ # Uniform split
170+ output = fcols.all_to_all_single(
171+ input_tensor,
172+ output_split_sizes=None,
173+ input_split_sizes=None,
174+ group=group_name,
175+ )
176+ 
177+ # Output should have same shape as input for uniform splits
178+ self.assertEqual(output.shape, input_tensor.shape)
179+ 
180+ # Verify each rank receives data from all other ranks
181+ for r in range(self.world_size):
182+ chunk = output[r * 2 : (r + 1) * 2, :]
183+ expected_chunk = torch.full((2, 3), fill_value=float(r), device=device)
184+ self.assertEqual(chunk, expected_chunk)
185+ 
186+ @parametrize("device", devices)
187+ def test_all_reduce_coalesced_forward(self, device):
188+ """Test all_reduce_coalesced does all_reduce on each tensor.
189+ 
190+ Tensors are VARYING (different across ranks).
191+ Forward aggregates varying tensors via all_reduce(sum).
192+ """
193+ group_name = dist.group.WORLD.group_name
194+ rank = dist.get_rank()
195+ 
196+ # Each rank contributes its rank value
197+ input_tensors = [
198+ torch.full((3, 3), fill_value=float(rank), device=device),
199+ torch.full((2, 2), fill_value=float(rank), device=device),
200+ ]
201+ outputs = fcols.all_reduce_coalesced(input_tensors, "sum", group=group_name)
202+ 
203+ # Forward does all_reduce: sum of 0+1+2+3 = 6
204+ expected_value = self.world_size * (self.world_size - 1) / 2
205+ for output, input_tensor in zip(outputs, input_tensors):
206+ expected = torch.full_like(input_tensor, fill_value=expected_value)
207+ self.assertEqual(output, expected)
208+ 
209+ @parametrize("device", devices)
210+ def test_all_gather_into_tensor_coalesced_forward(self, device):
211+ """Test all_gather_into_tensor_coalesced gathers each tensor.
212+ 
213+ Tensors are VARYING (different across ranks).
214+ Forward gathers tensors from all ranks.
215+ """
216+ group_name = dist.group.WORLD.group_name
217+ rank = dist.get_rank()
218+ 
219+ # Each rank has tensors with its rank value
220+ input_tensors = [
221+ torch.full((3, 3), fill_value=float(rank), device=device),
222+ torch.full((2, 2), fill_value=float(rank), device=device),
223+ ]
224+ outputs = fcols.all_gather_into_tensor_coalesced(
225+ input_tensors, group=group_name
226+ )
227+ 
228+ # Verify output shapes
229+ for output, input_tensor in zip(outputs, input_tensors):
230+ expected_shape = list(input_tensor.shape)
231+ expected_shape[0] *= self.world_size
232+ self.assertEqual(list(output.shape), expected_shape)
233+ 
234+ @parametrize("device", devices)
235+ def test_reduce_scatter_tensor_coalesced_forward(self, device):
236+ """Test reduce_scatter_tensor_coalesced reduces and scatters each tensor.
237+ 
238+ Tensors are VARYING (different across ranks).
239+ Forward reduces and scatters chunks to ranks.
240+ """
241+ group_name = dist.group.WORLD.group_name
242+ rank = dist.get_rank()
243+ 
244+ # Create inputs with appropriate size (divisible by world_size)
245+ input_tensors = [
246+ torch.full((4 * self.world_size, 3), fill_value=float(rank), device=device),
247+ torch.full((2 * self.world_size, 2), fill_value=float(rank), device=device),
248+ ]
249+ scatter_dims = [0, 0]
250+ 
251+ outputs = fcols.reduce_scatter_tensor_coalesced(
252+ input_tensors, "sum", scatter_dims, group=group_name
253+ )
254+ 
255+ # Each rank should receive sum of all ranks' values: 0+1+2+3 = 6
256+ expected_value = self.world_size * (self.world_size - 1) / 2
257+ for output, input_tensor in zip(outputs, input_tensors):
258+ expected_shape = list(input_tensor.shape)
259+ expected_shape[0] //= self.world_size
260+ self.assertEqual(list(output.shape), expected_shape)
261+ expected = torch.full_like(output, fill_value=expected_value)
262+ self.assertEqual(output, expected)
263+ 
264+ # ============================================================
265+ # Backward Correctness Tests
266+ # ============================================================
267+ 
268+ @parametrize("device", devices)
269+ def test_all_reduce_backward(self, device):
270+ """Test all_reduce backward does all_reduce.
271+ 
272+ Both tensor AND gradients are VARYING (different across ranks).
273+ Backward aggregates gradients via all_reduce(sum).
274+ """
275+ group_name = dist.group.WORLD.group_name
276+ 
277+ input_tensor = torch.randn(3, 3, requires_grad=True, device=device)
278+ output = fcols.all_reduce(input_tensor, "sum", group=group_name)
279+ 
280+ # Backward with ones
281+ output.sum().backward()
282+ 
283+ # Gradient should be aggregated (backward is all_reduce)
284+ expected_grad = torch.full(
285+ (3, 3), fill_value=float(self.world_size), device=device
286+ )
287+ self.assertEqual(input_tensor.grad, expected_grad)
288+ 
289+ # Backward is all_reduce (sum)
290+ grad_outputs = torch.rand_like(output, device=device)
291+ (grad_input,) = torch.autograd.grad(
292+ output, input_tensor, grad_outputs=grad_outputs
293+ )
294+ expected_grad_input = fcols.all_reduce(grad_outputs, "sum", group=group_name)
295+ self.assertEqual(grad_input, expected_grad_input)
296+ 
297+ @parametrize("device", devices)
298+ @parametrize("gather_dim", [0, 1, 2])
299+ def test_all_gather_tensor_backward(self, device, gather_dim):
300+ """Test all_gather_tensor backward does reduce_scatter.
301+ 
302+ Both tensor AND gradients are VARYING (different across ranks).
303+ Forward gathers tensors, backward reduces and scatters gradients.
304+ """
305+ group_name = dist.group.WORLD.group_name
306+ 
307+ input_tensor = torch.randn(3, 3, 3, requires_grad=True, device=device)
308+ output = fcols.all_gather_tensor(
309+ input_tensor, gather_dim=gather_dim, group=group_name
310+ )
311+ 
312+ # Backward with ones
313+ output.sum().backward()
314+ 
315+ # Gradient should be reduce_scatter of ones
316+ self.assertIsNotNone(input_tensor.grad)
317+ # Gradient should be all world_size (sum from all ranks)
318+ expected_grad = torch.full(
319+ (3, 3, 3), fill_value=float(self.world_size), device=device
320+ )
321+ self.assertEqual(input_tensor.grad, expected_grad)
322+ 
323+ # Backward is reduce_scatter (sum)
324+ grad_outputs = torch.rand_like(output, device=device)
325+ (grad_input,) = torch.autograd.grad(
326+ output, input_tensor, grad_outputs=grad_outputs
327+ )
328+ expected_grad_input = fcols.reduce_scatter_tensor(
329+ grad_outputs, "sum", gather_dim, group=group_name
330+ )
331+ self.assertEqual(grad_input, expected_grad_input)
332+ 
333+ @parametrize("device", devices)
334+ @parametrize("scatter_dim", [0, 1])
335+ def test_reduce_scatter_tensor_backward(self, device, scatter_dim):
336+ """Test reduce_scatter_tensor backward does all_gather.
337+ 
338+ Both tensor AND gradients are VARYING (different across ranks).
339+ Forward reduces and scatters, backward gathers gradients.
340+ """
341+ group_name = dist.group.WORLD.group_name
342+ 
343+ # Create input with appropriate size
344+ if scatter_dim == 0:
345+ input_tensor = torch.randn(
346+ 4 * self.world_size, 3, requires_grad=True, device=device
347+ )
348+ else:
349+ input_tensor = torch.randn(
350+ 3, 4 * self.world_size, requires_grad=True, device=device
351+ )
352+ 
353+ output = fcols.reduce_scatter_tensor(
354+ input_tensor, "sum", scatter_dim=scatter_dim, group=group_name
355+ )
356+ 
357+ # Backward with ones
358+ output.sum().backward()
359+ 
360+ # Gradient should be all_gather of ones
361+ self.assertIsNotNone(input_tensor.grad)
362+ self.assertEqual(input_tensor.grad.shape, input_tensor.shape)
363+ 
364+ # All gradients should be 1 (gathered from all ranks)
365+ expected_grad = torch.ones_like(input_tensor)
366+ self.assertEqual(input_tensor.grad, expected_grad)
367+ 
368+ # Backward is all_gather (sum)
369+ grad_outputs = torch.rand_like(output, device=device)
370+ (grad_input,) = torch.autograd.grad(
371+ output, input_tensor, grad_outputs=grad_outputs
372+ )
373+ expected_grad_input = fcols.all_gather_tensor(
374+ grad_outputs, scatter_dim, group=group_name
375+ )
376+ self.assertEqual(grad_input, expected_grad_input)
377+ 
378+ @parametrize("device", devices)
379+ def test_all_to_all_single_backward(self, device):
380+ """Test all_to_all_single backward reverses split sizes.
381+ 
382+ Both tensor AND gradients are VARYING (different across ranks).
383+ Forward does all_to_all, backward does all_to_all with reversed splits.
384+ """
385+ group_name = dist.group.WORLD.group_name
386+ 
387+ input_tensor = torch.randn(
388+ 4 * self.world_size, 3, requires_grad=True, device=device
389+ )
390+ output = fcols.all_to_all_single(
391+ input_tensor,
392+ output_split_sizes=None,
393+ input_split_sizes=None,
394+ group=group_name,
395+ )
396+ 
397+ # Backward
398+ output.sum().backward()
399+ 
400+ # Gradient should have same shape as input
401+ self.assertIsNotNone(input_tensor.grad)
402+ self.assertEqual(input_tensor.grad.shape, input_tensor.shape)
403+ expected_grad = torch.ones_like(input_tensor)
404+ self.assertEqual(input_tensor.grad, expected_grad)
405+ 
406+ # Backward is all_gather (sum)
407+ grad_outputs = torch.rand_like(output, device=device)
408+ (grad_input,) = torch.autograd.grad(
409+ output, input_tensor, grad_outputs=grad_outputs
410+ )
411+ expected_grad_input = fcols.all_to_all_single(
412+ grad_outputs, None, None, group=group_name
413+ )
414+ self.assertEqual(grad_input, expected_grad_input)
415+ 
416+ @parametrize("device", devices)
417+ def test_all_reduce_coalesced_backward(self, device):
418+ """Test all_reduce_coalesced backward does all_reduce on each gradient.
419+ 
420+ Tensors AND gradients are VARYING (different across ranks).
421+ Backward aggregates each gradient via all_reduce(sum).
422+ """
423+ group_name = dist.group.WORLD.group_name
424+ 
425+ input_tensors = [
426+ torch.randn(3, 3, requires_grad=True, device=device),
427+ torch.randn(2, 2, requires_grad=True, device=device),
428+ ]
429+ outputs = fcols.all_reduce_coalesced(input_tensors, "sum", group=group_name)
430+ 
431+ # Backward with ones
432+ loss = sum(output.sum() for output in outputs)
433+ loss.backward()
434+ 
435+ # Each gradient should be aggregated (backward is all_reduce)
436+ for input_tensor in input_tensors:
437+ self.assertIsNotNone(input_tensor.grad)
438+ expected_grad = torch.full_like(
439+ input_tensor, fill_value=float(self.world_size)
440+ )
441+ self.assertEqual(input_tensor.grad, expected_grad)
442+ 
443+ @parametrize("device", devices)
444+ def test_all_gather_into_tensor_coalesced_backward(self, device):
445+ """Test all_gather_into_tensor_coalesced backward does reduce_scatter on each gradient.
446+ 
447+ Tensors AND gradients are VARYING (different across ranks).
448+ Forward gathers each tensor, backward reduce_scatters each gradient.
449+ """
450+ group_name = dist.group.WORLD.group_name
451+ 
452+ input_tensors = [
453+ torch.randn(3, 3, requires_grad=True, device=device),
454+ torch.randn(2, 2, requires_grad=True, device=device),
455+ ]
456+ outputs = fcols.all_gather_into_tensor_coalesced(
457+ input_tensors, group=group_name
458+ )
459+ 
460+ # Backward with ones
461+ loss = sum(output.sum() for output in outputs)
462+ loss.backward()
463+ 
464+ # Each gradient should be reduce_scatter of ones
465+ for input_tensor in input_tensors:
466+ self.assertIsNotNone(input_tensor.grad)
467+ expected_grad = torch.full_like(
468+ input_tensor, fill_value=float(self.world_size)
469+ )
470+ self.assertEqual(input_tensor.grad, expected_grad)
471+ 
472+ @parametrize("device", devices)
473+ def test_reduce_scatter_tensor_coalesced_backward(self, device):
474+ """Test reduce_scatter_tensor_coalesced backward does all_gather on each gradient.
475+ 
476+ Tensors AND gradients are VARYING (different across ranks).
477+ Forward reduces and scatters each tensor, backward gathers each gradient.
478+ """
479+ group_name = dist.group.WORLD.group_name
480+ 
481+ input_tensors = [
482+ torch.randn(4 * self.world_size, 3, requires_grad=True, device=device),
483+ torch.randn(2 * self.world_size, 2, requires_grad=True, device=device),
484+ ]
485+ scatter_dims = [0, 0]
486+ 
487+ outputs = fcols.reduce_scatter_tensor_coalesced(
488+ input_tensors, "sum", scatter_dims, group=group_name
489+ )
490+ 
491+ # Backward with ones
492+ loss = sum(output.sum() for output in outputs)
493+ loss.backward()
494+ 
495+ # Each gradient should be all_gather of ones
496+ for input_tensor in input_tensors:
497+ self.assertIsNotNone(input_tensor.grad)
498+ expected_grad = torch.ones_like(input_tensor)
499+ self.assertEqual(input_tensor.grad, expected_grad)
500+ 
501+ # ============================================================
502+ # torch.library.opcheck Tests
503+ # ============================================================
504+ 
505+ test_utils = [
506+ "test_schema",
507+ "test_autograd_registration",
508+ "test_faketensor",
509+ # "test_aot_dispatch_dynamic" - Open issue with check: TBD
510+ ]
511+ 
512+ @parametrize("test_utils", test_utils)
513+ def test_all_reduce_opcheck(self, test_utils):
514+ """Test all_reduce op registration with torch.library.opcheck.
515+ 
516+ Verifies all aspects of op registration including:
517+ - Fake tensor support
518+ - Autograd support (backward does all_reduce)
519+ - Schema validation
520+ """
521+ group_name = dist.group.WORLD.group_name
522+ 
523+ input_tensor = torch.ones(3, 3, requires_grad=True)
524+ 
525+ # opcheck verifies all aspects of op registration
526+ torch.library.opcheck(
527+ torch.ops._c10d_functional.all_reduce,
528+ (input_tensor, "sum", group_name),
529+ test_utils=test_utils,
530+ )
531+ 
532+ @parametrize("test_utils", test_utils)
533+ def test_all_gather_into_tensor_opcheck(self, test_utils):
534+ """Test all_gather_into_tensor op registration with torch.library.opcheck.
535+ 
536+ Verifies all aspects of op registration including:
537+ - Fake tensor support
538+ - Autograd support (backward does reduce_scatter)
539+ - Schema validation
540+ """
541+ group_name = dist.group.WORLD.group_name
542+ 
543+ input_tensor = torch.ones(3, 3, 3, requires_grad=True)
544+ 
545+ # opcheck verifies all aspects of op registration
546+ torch.library.opcheck(
547+ torch.ops._c10d_functional.all_gather_into_tensor,
548+ (input_tensor, self.world_size, group_name),
549+ test_utils=test_utils,
550+ )
551+ 
552+ @parametrize("test_utils", test_utils)
553+ def test_reduce_scatter_tensor_opcheck(self, test_utils):
554+ """Test reduce_scatter_tensor op registration with torch.library.opcheck.
555+ 
556+ Verifies all aspects of op registration including:
557+ - Fake tensor support
558+ - Autograd support (backward does all_gather)
559+ - Schema validation
560+ """
561+ group_name = dist.group.WORLD.group_name
562+ 
563+ # Input should be divisible by world_size
564+ input_tensor = torch.ones(4 * self.world_size, 3, requires_grad=True)
565+ 
566+ # opcheck verifies all aspects of op registration
567+ torch.library.opcheck(
568+ torch.ops._c10d_functional.reduce_scatter_tensor,
569+ (input_tensor, "sum", self.world_size, group_name),
570+ test_utils=test_utils,
571+ )
572+ 
573+ @parametrize("test_utils", test_utils)
574+ def test_all_to_all_single_opcheck(self, test_utils):
575+ """Test all_to_all_single op registration with torch.library.opcheck.
576+ 
577+ Verifies all aspects of op registration including:
578+ - Fake tensor support
579+ - Autograd support (backward reverses split sizes)
580+ - Schema validation
581+ """
582+ group_name = dist.group.WORLD.group_name
583+ group_size = dist.group.WORLD.size()
584+ 
585+ # Input should be divisible by world_size
586+ input_tensor = torch.ones(4 * self.world_size, 3, requires_grad=True)
587+ 
588+ output_split_sizes = [input_tensor.shape[0] // group_size] * group_size
589+ input_split_sizes = output_split_sizes
590+ 
591+ # opcheck verifies all aspects of op registration
592+ torch.library.opcheck(
593+ torch.ops._c10d_functional.all_to_all_single,
594+ (input_tensor, output_split_sizes, input_split_sizes, group_name),
595+ test_utils=test_utils,
596+ )
597+ 
598+ 
599+if __name__ == "__main__":
600+ run_tests()
@@ -0,0 +1,178 @@
1+# Owner(s): ["oncall: distributed"]
2+ 
3+import os
4+import sys
5+from datetime import timedelta
6+ 
7+import torch
8+import torch_npu
9+import torch.distributed as c10d
10+from torch._C._distributed_c10d import _ProcessGroupWrapper
11+from torch_npu.testing.common_distributed import skipIfUnsupportMultiNPU
12+ 
13+ 
14+if not c10d.is_available():
15+ print("c10d not available, skipping tests", file=sys.stderr)
16+ sys.exit(0)
17+ 
18+from torch.testing._internal.common_distributed import (
19+ MultiProcessTestCase,
20+ with_dist_debug_levels,
21+)
22+from torch.testing._internal.common_utils import (
23+ run_tests,
24+ TEST_WITH_DEV_DBG_ASAN,
25+)
26+ 
27+ 
28+device_type = acc.type if (acc := torch.accelerator.current_accelerator()) else "cpu"
29+backend = c10d.get_default_backend_for_device(device_type)
30+ 
31+ 
32+class AbstractProcessGroupWrapperTest(MultiProcessTestCase):
33+ def setUp(self):
34+ super().setUp()
35+ self._spawn_processes()
36+ 
37+ def _validate_error(self, exception, op_type, rank, tensor, verify_diff=True):
38+ err = str(exception)
39+ self.assertTrue(
40+ op_type in err, f"Got {err} but expected {op_type} to be in error."
41+ )
42+ # User doesn't call barrier with tensor.
43+ if op_type != "BARRIER":
44+ self.assertTrue(
45+ f"{list(tensor.shape)}" in err,
46+ f"Did not find shapes {list(tensor.shape)} in error {err}",
47+ )
48+ # For NPU, only assert on device type, not index
49+ if device_type in str(tensor.device):
50+ self.assertTrue(
51+ device_type in err,
52+ f"Did not find {device_type} device in error {err}",
53+ )
54+ else:
55+ self.assertTrue(
56+ str(tensor.device) in err,
57+ f"Did not find tensor device {str(tensor.device)} in error {err}",
58+ )
59+ # C++ and python type strings are not exactly the same.
60+ if "float" in str(tensor.dtype):
61+ self.assertTrue("Float" in err, "Expected Float type")
62+ elif "int" in str(tensor.dtype):
63+ self.assertTrue("Long" in err, "Expected Long type")
64+ else:
65+ self.fail(f"Unexpected dtype {str(tensor.dtype)} for error {err}")
66+ 
67+ # Ensure sequence number is logged in error
68+ self.assertTrue("SequenceNumber" in err)
69+ # Ensure info about how collectives diff is in the error.
70+ if verify_diff:
71+ self.assertTrue(
72+ "Collectives differ in the following" in err, f"Got error {err}"
73+ )
74+ 
75+ 
76+# ASAN is not safe since we are spawning processes.
77+if not TEST_WITH_DEV_DBG_ASAN:
78+ class ProcessGroupHCCLWrapperTest(AbstractProcessGroupWrapperTest):
79+ def setUp(self):
80+ super(AbstractProcessGroupWrapperTest, self).setUp()
81+ self._spawn_processes()
82+ # TORCH_HCCL_BLOCKING_WAIT overrides TORCH_HCCL_ASYNC_ERROR_HANDLING hence tests
83+ # that use TORCH_HCCL_BLOCKING_WAIT will test it as expected.
84+ os.environ["TORCH_HCCL_ASYNC_ERROR_HANDLING"] = "1"
85+ 
86+ @property
87+ def world_size(self) -> int:
88+ return 2
89+ 
90+ def _create_wrapper_pg(self, with_new_group=False, timeout=10.0):
91+ store = c10d.FileStore(self.file_name, self.world_size)
92+ c10d.init_process_group(
93+ backend=backend,
94+ rank=self.rank,
95+ world_size=self.world_size,
96+ store=store,
97+ timeout=timedelta(seconds=timeout),
98+ )
99+ if with_new_group:
100+ pg = c10d.new_group(backend=backend, timeout=timedelta(seconds=timeout))
101+ else:
102+ if device_type == "xpu":
103+ _pg = c10d.ProcessGroupXCCL(
104+ store,
105+ self.rank,
106+ self.world_size,
107+ )
108+ else:
109+ _pg = torch_npu._C._distributed_c10d.ProcessGroupHCCL(
110+ store,
111+ self.rank,
112+ self.world_size,
113+ timeout=timedelta(seconds=timeout),
114+ )
115+ pg = c10d._create_process_group_wrapper(
116+ _pg,
117+ "unused",
118+ store,
119+ self.rank,
120+ self.world_size,
121+ timeout=timeout,
122+ )
123+ return pg
124+ 
125+ @skipIfUnsupportMultiNPU(2)
126+ @with_dist_debug_levels(levels=["DETAIL"])
127+ def test_wrapper_forwards_hccl_methods(self):
128+ """
129+ Tests that ProcessGroupWrapper correctly forwards HCCL-specific
130+ utility methods to the wrapped backend. See issue #173538.
131+ """
132+ torch.npu.set_device(self.rank)
133+ device = torch.device(f"npu:{self.rank}")
134+ wrapper = self._create_wrapper_pg(with_new_group=False)
135+ 
136+ # Verify we're testing the wrapper
137+ self.assertIsInstance(wrapper, _ProcessGroupWrapper)
138+ unwrapped = wrapper.wrapped_pg
139+ 
140+ # Verify wrapper forwards property/method calls to wrapped backend
141+ self.assertEqual(wrapper.supports_splitting, unwrapped.supports_splitting)
142+ self.assertEqual(wrapper.supports_coalescing, unwrapped.supports_coalescing)
143+ self.assertEqual(
144+ wrapper.supports_time_estimate, unwrapped.supports_time_estimate
145+ )
146+ self.assertEqual(
147+ wrapper.supports_tensor_alloc(device),
148+ unwrapped.supports_tensor_alloc(device),
149+ )
150+ if hasattr(unwrapped, 'get_error'):
151+ self.assertEqual(wrapper.get_error(), unwrapped.get_error())
152+ try:
153+ wrapper_options = wrapper.options
154+ except RuntimeError as e:
155+ self.assertIn("does not implement getBackendOptions", str(e))
156+ wrapper_options = None
157+ 
158+ if wrapper_options is not None:
159+ self.assertIs(wrapper_options, unwrapped.options)
160+ 
161+ # Test eager_connect_single_device forwarding (should not raise)
162+ wrapper.eager_connect_single_device(device)
163+ 
164+ # HCCL does not support mem_allocator and allocate_tensor
165+ if wrapper.supports_tensor_alloc(device):
166+ self.assertIs(wrapper.mem_allocator, unwrapped.mem_allocator)
167+ tensor = wrapper.allocate_tensor(
168+ 1024, dtype=torch.float32, device=device
169+ )
170+ self.assertEqual(tensor.shape, torch.Size([1024]))
171+ 
172+ 
173+if __name__ == "__main__":
174+ assert not (torch.npu.is_initialized() or torch.xpu.is_initialized()), (
175+ "test_pg_wrapper must not have initialized NPU/XPU context on main process"
176+ )
177+ 
178+ run_tests()