@@ -328,13 +328,16 @@ def a2a_vs_allgather_test(atomic_numbers, edge_index):
328328 natoms = atomic_numbers .shape [0 ]
329329
330330 # Partition atoms (same as gp_utils does)
331- node_partition = torch .tensor_split (torch .arange (natoms ), world_size )[rank ]
331+ device = atomic_numbers .device
332+ node_partition = torch .tensor_split (
333+ torch .arange (natoms , device = device ), world_size
334+ )[rank ]
332335 node_offset = node_partition .min ().item ()
333336
334- # Create rank assignments
335- rank_assignments = partition_atoms_index_split (
336- natoms , world_size , torch . device ( "cpu" )
337- )
337+ # Create rank assignments on the same device as the data: build_gp_context
338+ # derives its working device from rank_assignments, so a CPU tensor here
339+ # would make it index a CPU mask with CUDA edge indices.
340+ rank_assignments = partition_atoms_index_split ( natoms , world_size , device )
338341
339342 # Filter edges: keep edges where target is in our partition
340343 target_in_partition = (edge_index [1 ] >= node_partition .min ()) & (
@@ -351,10 +354,12 @@ def a2a_vs_allgather_test(atomic_numbers, edge_index):
351354 # Run all-to-all version
352355 result_a2a = _a2a_simple_layer (x_local , local_edge_index , rank_assignments , natoms )
353356
357+ # Results travel back through spawn_multi_process's forked Manager
358+ # server, which cannot unpickle CUDA tensors. Move to CPU first.
354359 return {
355360 "rank" : rank ,
356- "allgather" : result_ag .detach (),
357- "all_to_all" : result_a2a .detach (),
361+ "allgather" : result_ag .detach (). cpu () ,
362+ "all_to_all" : result_a2a .detach (). cpu () ,
358363 "match" : torch .allclose (result_ag , result_a2a , atol = 1e-6 ),
359364 }
360365
@@ -414,12 +419,13 @@ def a2a_backward_test(atomic_numbers, edge_index):
414419 natoms = atomic_numbers .shape [0 ]
415420
416421 # Partition atoms
417- node_partition = torch .tensor_split (torch .arange (natoms ), world_size )[rank ]
422+ device = atomic_numbers .device
423+ node_partition = torch .tensor_split (
424+ torch .arange (natoms , device = device ), world_size
425+ )[rank ]
418426 node_offset = node_partition .min ().item ()
419427
420- rank_assignments = partition_atoms_index_split (
421- natoms , world_size , torch .device ("cpu" )
422- )
428+ rank_assignments = partition_atoms_index_split (natoms , world_size , device )
423429
424430 # Filter edges
425431 target_in_partition = (edge_index [1 ] >= node_partition .min ()) & (
@@ -454,8 +460,10 @@ def a2a_backward_test(atomic_numbers, edge_index):
454460 create_graph = False ,
455461 )[0 ]
456462
457- results [f"{ method } _energy" ] = energy .detach ()
458- results [f"{ method } _forces" ] = forces .detach ()
463+ # .cpu() so the results survive the forked Manager server used by
464+ # spawn_multi_process, which cannot rebuild CUDA tensors.
465+ results [f"{ method } _energy" ] = energy .detach ().cpu ()
466+ results [f"{ method } _forces" ] = forces .detach ().cpu ()
459467
460468 results ["rank" ] = rank
461469 results ["energy_match" ] = torch .allclose (
@@ -551,7 +559,9 @@ def a2a_spatial_partition_test(atomic_numbers, edge_index, pos):
551559 natoms = atomic_numbers .shape [0 ]
552560
553561 # --- All-gather with index-based partitioning (baseline) ---
554- node_partition_idx = torch .tensor_split (torch .arange (natoms ), world_size )[rank ]
562+ node_partition_idx = torch .tensor_split (
563+ torch .arange (natoms , device = atomic_numbers .device ), world_size
564+ )[rank ]
555565 node_offset_idx = node_partition_idx .min ().item ()
556566
557567 target_in_partition_idx = (edge_index [1 ] >= node_partition_idx .min ()) & (
@@ -589,10 +599,12 @@ def a2a_spatial_partition_test(atomic_numbers, edge_index, pos):
589599 full_ag = gather_from_model_parallel_region_sum_grad (result_ag , natoms )
590600 full_a2a = gather_from_model_parallel_region_sum_grad (result_a2a , natoms )
591601
602+ # .cpu() so the results survive the forked Manager server used by
603+ # spawn_multi_process, which cannot rebuild CUDA tensors.
592604 return {
593605 "rank" : rank ,
594- "allgather_full" : full_ag .detach (),
595- "all_to_all_full" : full_a2a .detach (),
606+ "allgather_full" : full_ag .detach (). cpu () ,
607+ "all_to_all_full" : full_a2a .detach (). cpu () ,
596608 "match" : torch .allclose (full_ag , full_a2a , atol = 1e-5 ),
597609 }
598610
@@ -826,6 +838,26 @@ def test_energy_forces_stress_gp(world_size):
826838)
827839
828840
841+ def _requires_gpus (n : int ):
842+ """
843+ Skip unless at least n CUDA devices are visible.
844+
845+ ``_to_cuda`` places rank r on ``cuda:r``, so a test spawning n ranks
846+ needs n distinct devices. The ``gpu`` marker only covers the
847+ zero-device case (see ``pytest_runtest_setup`` in tests/conftest.py).
848+
849+ Args:
850+ n: Number of CUDA devices the test requires.
851+
852+ Returns:
853+ A pytest skipif marker.
854+ """
855+ return pytest .mark .skipif (
856+ torch .cuda .device_count () < n ,
857+ reason = f"requires { n } CUDA devices" ,
858+ )
859+
860+
829861def _to_cuda (* tensors ):
830862 device = torch .device (f"cuda:{ gp_utils .get_gp_rank ()} " )
831863 return tuple (t .to (device ) for t in tensors )
@@ -846,6 +878,8 @@ def a2a_spatial_partition_test_gpu(atomic_numbers, edge_index, pos):
846878 return a2a_spatial_partition_test (atomic_numbers , edge_index , pos )
847879
848880
881+ @pytest .mark .gpu ()
882+ @_requires_gpus (2 )
849883@_skip_if_ci
850884@pytest .mark .parametrize (
851885 "num_atoms, edges" ,
@@ -885,6 +919,8 @@ def test_a2a_vs_allgather_gpu(num_atoms, edges):
885919 )
886920
887921
922+ @pytest .mark .gpu ()
923+ @_requires_gpus (2 )
888924@_skip_if_ci
889925def test_a2a_backward_gpu ():
890926 atomic_numbers = torch .tensor ([2.0 , 3.0 , 5.0 , 7.0 ])
@@ -915,8 +951,14 @@ def test_a2a_backward_gpu():
915951 )
916952
917953
954+ @pytest .mark .gpu ()
955+ @_requires_gpus (2 )
918956@_skip_if_ci
919- @pytest .mark .parametrize ("world_size" , [2 , 3 ])
957+ @pytest .mark .parametrize (
958+ "world_size" ,
959+ # Gate the 3-rank case separately so 2-GPU hosts still run the 2-rank one.
960+ [2 , pytest .param (3 , marks = _requires_gpus (3 ))],
961+ )
920962def test_a2a_multi_rank_gpu (world_size ):
921963 num_atoms = 6
922964 src = list (range (num_atoms ))
@@ -946,6 +988,8 @@ def test_a2a_multi_rank_gpu(world_size):
946988 )
947989
948990
991+ @pytest .mark .gpu ()
992+ @_requires_gpus (2 )
949993@_skip_if_ci
950994def test_a2a_spatial_partition_gpu ():
951995 num_atoms = 8
0 commit comments