Skip to content

Commit bd73fb4

Browse files
Merge branch 'main' into fast-default
2 parents 0b2b836 + 9547a5b commit bd73fb4

23 files changed

Lines changed: 1145 additions & 163 deletions

src/fairchem/core/calculate/pretrained_mlip.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def get_predict_unit(
7474
cache_dir: str = CACHE_DIR,
7575
workers: int = 1,
7676
seed: int = 41,
77+
gp_config=None,
7778
) -> MLIPPredictUnit:
7879
"""
7980
Retrieves a prediction unit for a specified model.
@@ -118,6 +119,7 @@ def get_predict_unit(
118119
form_elem_refs,
119120
workers,
120121
seed,
122+
gp_config=gp_config,
121123
)
122124

123125

src/fairchem/core/common/gp_utils.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
from __future__ import annotations
99

1010
import contextlib
11+
import dataclasses
1112
import logging
1213
import threading
14+
from dataclasses import dataclass
1315

1416
import torch
1517
from torch import distributed as dist
1618
from torch.distributed.nn.functional import all_reduce, reduce_scatter
1719

20+
from fairchem.core.common.utils import StrEnum
21+
1822
"""
1923
Functions to support graph parallel training.
2024
This is based on the Megatron-LM implementation:
@@ -26,6 +30,26 @@
2630
_GRAPH_PARALLEL_GROUP = None
2731
_DATA_PARALLEL_GROUP = None
2832

33+
34+
class GPMode(StrEnum):
35+
ALLGATHER = "allgather"
36+
ALL_TO_ALL = "all_to_all"
37+
38+
39+
class GPPartition(StrEnum):
40+
INDEX_SPLIT = "index_split"
41+
SPATIAL = "spatial"
42+
43+
44+
@dataclass
45+
class GraphParallelConfig:
46+
group_size: int = 1
47+
mode: GPMode = GPMode.ALLGATHER
48+
partition: GPPartition = GPPartition.INDEX_SPLIT
49+
50+
51+
_GP_CONFIG: GraphParallelConfig | None = None
52+
2953
_tls = threading.local()
3054

3155

@@ -97,6 +121,13 @@ def setup_graph_parallel_groups(
97121
if i == found[0]:
98122
_GRAPH_PARALLEL_GROUP = group
99123

124+
# Ensure a GP config exists so downstream code can read
125+
# `get_gp_config().mode` without a None check. Callers that want
126+
# non-default settings (A2A, spatial partition) should call
127+
# `set_gp_config` explicitly after this.
128+
if _GP_CONFIG is None:
129+
set_gp_config(GraphParallelConfig(group_size=graph_parallel_group_size))
130+
100131

101132
def setup_gp(config) -> None:
102133
gp_size = config["gp_gpus"]
@@ -129,10 +160,18 @@ def setup_gp(config) -> None:
129160
if i == found[0]:
130161
_GRAPH_PARALLEL_GROUP = group
131162

163+
# Every entry point that sets up GP groups must also set a GP config so
164+
# downstream code (e.g. escn_md.py) can read `get_gp_config().mode`
165+
# without a None check. setup_graph_parallel_groups()'s callers set the
166+
# config alongside; do the same here for parity.
167+
if _GP_CONFIG is None:
168+
set_gp_config(GraphParallelConfig(group_size=gp_size))
169+
132170

133171
def cleanup_gp() -> None:
134172
global _DATA_PARALLEL_GROUP
135173
global _GRAPH_PARALLEL_GROUP
174+
global _GP_CONFIG
136175
assert _GRAPH_PARALLEL_GROUP is not None
137176
assert _DATA_PARALLEL_GROUP is not None
138177
with contextlib.suppress(ValueError):
@@ -141,12 +180,57 @@ def cleanup_gp() -> None:
141180
dist.destroy_process_group(_GRAPH_PARALLEL_GROUP)
142181
_DATA_PARALLEL_GROUP = None
143182
_GRAPH_PARALLEL_GROUP = None
183+
_GP_CONFIG = None
144184

145185

146186
def initialized() -> bool:
147187
return _GRAPH_PARALLEL_GROUP is not None
148188

149189

190+
def set_gp_config(config: GraphParallelConfig) -> None:
191+
global _GP_CONFIG
192+
_GP_CONFIG = config
193+
194+
195+
def get_gp_config() -> GraphParallelConfig | None:
196+
return _GP_CONFIG
197+
198+
199+
def resolve_gp_config_for_workers(
200+
gp_config: GraphParallelConfig | None,
201+
num_workers: int,
202+
) -> GraphParallelConfig | None:
203+
"""
204+
Reconcile a user-provided GraphParallelConfig with the target number
205+
of workers.
206+
207+
Behavior:
208+
- ``num_workers <= 1``: no GP is used; return ``gp_config`` unchanged
209+
(may be ``None``).
210+
- ``num_workers > 1``:
211+
* If ``gp_config`` is ``None``, build a default
212+
``GraphParallelConfig(group_size=num_workers)``.
213+
* If the config's ``group_size`` is still the default (1),
214+
return a copy with ``group_size=num_workers``. The caller's
215+
config is NOT mutated.
216+
* If ``group_size == num_workers`` already, return it unchanged.
217+
* Otherwise raise ``ValueError`` — an explicit mismatch is a
218+
configuration error.
219+
"""
220+
if num_workers <= 1:
221+
return gp_config
222+
if gp_config is None:
223+
return GraphParallelConfig(group_size=num_workers)
224+
if gp_config.group_size == 1:
225+
return dataclasses.replace(gp_config, group_size=num_workers)
226+
if gp_config.group_size != num_workers:
227+
raise ValueError(
228+
f"gp_config.group_size ({gp_config.group_size}) must equal "
229+
f"num_workers ({num_workers})"
230+
)
231+
return gp_config
232+
233+
150234
def get_dp_group():
151235
return _DATA_PARALLEL_GROUP
152236

src/fairchem/core/common/parallelism/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
GPContext,
1313
all_to_all_collect,
1414
build_gp_context,
15+
compute_a2a_partition,
1516
)
1617
from fairchem.core.common.parallelism.graph_partition import (
1718
PartitionStrategy,
@@ -25,6 +26,7 @@
2526
"PartitionStrategy",
2627
"all_to_all_collect",
2728
"build_gp_context",
29+
"compute_a2a_partition",
2830
"partition_atoms_index_split",
2931
"partition_atoms_spatial",
3032
]

src/fairchem/core/common/parallelism/graph_parallel_a2a.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
from torch.profiler import record_function
1515

1616
from fairchem.core.common import gp_utils
17+
from fairchem.core.common.parallelism.graph_partition import (
18+
PartitionStrategy,
19+
partition_atoms_index_split,
20+
partition_atoms_spatial,
21+
)
1722

1823

1924
def _safe_all_to_all(
@@ -61,8 +66,6 @@ class GPContext:
6166
Graph parallel context holding communication metadata for all-to-all.
6267
6368
Runtime-only struct: every field is needed for the forward/backward pass.
64-
Construction intermediates (node_partition, rank_assignments, needed_atoms,
65-
global_to_local, etc.) are computed in build_gp_context but not stored.
6669
6770
Attributes:
6871
rank: Current GP rank.
@@ -79,6 +82,7 @@ class GPContext:
7982
total_recv: Total number of embeddings to receive (sum of recv_splits).
8083
local_edge_idx: Indices into edge_index_local where source is a local atom.
8184
remote_edge_idx: Indices into edge_index_local where source is a remote atom.
85+
rank_assignments: Rank owner for each atom, shape (total_atoms,).
8286
"""
8387

8488
rank: int
@@ -93,6 +97,7 @@ class GPContext:
9397
total_recv: int
9498
local_edge_idx: torch.Tensor
9599
remote_edge_idx: torch.Tensor
100+
rank_assignments: torch.Tensor
96101

97102

98103
def _sparse_index_exchange(
@@ -191,6 +196,41 @@ def _sparse_index_exchange(
191196
return send_counts, send_indices_global
192197

193198

199+
@torch.compiler.disable
200+
def compute_a2a_partition(
201+
pos: torch.Tensor,
202+
total_atoms: int,
203+
device: torch.device,
204+
world_size: int,
205+
rank: int,
206+
strategy: PartitionStrategy,
207+
) -> tuple[torch.Tensor, torch.Tensor]:
208+
"""Compute rank assignments and local node partition for A2A graph parallel.
209+
210+
Args:
211+
pos: Atom positions, shape (N, 3).
212+
total_atoms: Total number of atoms.
213+
device: Device for output tensors.
214+
world_size: Number of GP ranks.
215+
rank: Current GP rank.
216+
strategy: Partitioning strategy (SPATIAL or INDEX_SPLIT).
217+
218+
Returns:
219+
Tuple of (rank_assignments, node_partition) where rank_assignments
220+
is shape (N,) mapping each atom to a rank, and node_partition is
221+
the indices of atoms belonging to this rank.
222+
"""
223+
with record_function("a2a_partition"):
224+
if strategy == PartitionStrategy.SPATIAL:
225+
rank_assignments = partition_atoms_spatial(pos, world_size)
226+
else:
227+
rank_assignments = partition_atoms_index_split(
228+
total_atoms, world_size, device
229+
)
230+
node_partition = (rank_assignments == rank).nonzero(as_tuple=True)[0]
231+
return rank_assignments, node_partition
232+
233+
194234
@torch.compiler.disable
195235
def build_gp_context(
196236
edge_index: torch.Tensor,
@@ -313,6 +353,7 @@ def build_gp_context(
313353
total_recv=total_recv,
314354
local_edge_idx=local_edge_idx,
315355
remote_edge_idx=remote_edge_idx,
356+
rank_assignments=rank_assignments,
316357
)
317358

318359

@@ -524,7 +565,6 @@ def backward(ctx, grad_received: torch.Tensor):
524565
def all_to_all_collect(
525566
x_local: torch.Tensor,
526567
gp_ctx: GPContext,
527-
send_indices: torch.Tensor,
528568
) -> torch.Tensor:
529569
"""
530570
High-level function to collect remote embeddings via all-to-all.
@@ -536,15 +576,14 @@ def all_to_all_collect(
536576
Args:
537577
x_local: Local atom embeddings, shape (local_atoms, *features).
538578
gp_ctx: Graph parallel context.
539-
send_indices: Local indices of atoms to send.
540579
541580
Returns:
542581
x_received: Remote atom embeddings,
543582
shape (total_needed, *features).
544583
"""
545584
return AllToAllCollect.apply(
546585
x_local,
547-
send_indices,
586+
gp_ctx.send_indices,
548587
gp_ctx.send_counts,
549588
gp_ctx.recv_counts,
550589
gp_utils.get_gp_group(),

0 commit comments

Comments
 (0)