Skip to content

Commit ee635a0

Browse files
committed
gp test fixes
1 parent a3fa357 commit ee635a0

2 files changed

Lines changed: 68 additions & 17 deletions

File tree

src/fairchem/core/common/test_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ def spawn_multi_process(
109109
Spawn single node, multi-rank function.
110110
Uses a shared file for process group initialization to avoid port races.
111111
112+
IMPORTANT: ``test_method`` must return CPU tensors or plain Python values.
113+
Results are passed back through a ``multiprocessing.Manager()`` dict whose
114+
server process is forked from the caller, so unpickling a CUDA tensor there
115+
fails with "Cannot re-initialize CUDA in forked subprocess" (the CUDA IPC
116+
rebuild needs a CUDA context the forked server cannot create). Call
117+
``.detach().cpu()`` on any tensor before returning it from a NCCL worker.
118+
112119
Args:
113120
world_size: number of processes
114121
backend: backend to use. for example, "nccl", "gloo", etc

tests/core/common/parallelism/test_graph_parallel.py

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
829861
def _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
889925
def 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+
)
920962
def 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
950994
def test_a2a_spatial_partition_gpu():
951995
num_atoms = 8

0 commit comments

Comments
 (0)