Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
38f655c
Add all-to-all graph parallel communication primitives
May 2, 2026
530b685
Integrate A2A graph parallel into eSCN-MD backbone
May 2, 2026
f8c8388
Add A2A GP mode to MD consistency test
May 2, 2026
c45cb75
Harden A2A GP: multi-system guard, defensive assertions, expand test …
May 5, 2026
360f38c
Refactor build_gp_context and add allgather index exchange variant
Jun 3, 2026
3a06f49
Remove allgather index exchange variant, keep 2xA2A
Jun 3, 2026
45e218e
Remove all_to_all_collect_compiled (dead code)
Jun 5, 2026
13754ca
Replace _validate_gp_mappings with simple assertions
Jun 5, 2026
37f7393
Use torch._assert_async for GPU-only validation (no host sync)
Jun 5, 2026
1d6fefc
Add graph parallel verification doc from rgao_a2a_comms
Jun 5, 2026
b129bfa
Remove send_info precomputation (dead code with radius_pbc v2)
Jun 5, 2026
7558df4
Make all GPContext fields required (no optional defaults)
Jun 5, 2026
c20b024
Remove torchrun entrypoint; use fairchem CLI for multi-GPU tests
Jun 5, 2026
256a144
Add GPU/NCCL tests for A2A graph parallel primitives
Jun 5, 2026
7112ab8
Skip GPU/NCCL tests when fewer than 2 GPUs available
Jun 5, 2026
50ab32d
Use CI env var to skip multi-GPU tests instead of device_count check
Jun 5, 2026
af68f80
Add full-model GP correctness test (no-GP vs allgather vs A2A)
Jun 5, 2026
6168d23
Add GP correctness runner for fairchem CLI
Jun 5, 2026
1a87db0
Revert "Add GP correctness runner for fairchem CLI"
Jun 5, 2026
cc71ad7
Simplify build_gp_context: use remote_mask, rename needed_from_ranks_…
Jun 5, 2026
0467a4e
Remove redundant remote_mask filter in build_gp_context
Jun 5, 2026
88c776e
Update GPContext docstring with missing field descriptions
Jun 5, 2026
6dc9708
Slim GPContext to runtime-only fields
Jun 5, 2026
fb7ed9c
Increase Gloo process group init timeout from 10s to 120s
Jun 5, 2026
3e8059b
Remove AABB halo optimization from A2A graph parallel
Jun 7, 2026
83025be
Revert timeout_hr addition in slurm benchmark config
Jun 8, 2026
7b1e3b4
Merge main into rgao_a2a_model after rgao_a2a_comms landed
Jun 9, 2026
0a7fba1
Merge main into rgao_a2a_model
Jul 2, 2026
b2acb2c
Add GPMode enum to gp_utils and move compute_a2a_partition to paralle…
Jul 8, 2026
472b3f7
Always pre-compute scatter_target, eliminate branching at call sites
Jul 8, 2026
efcd9b9
Move rank_assignments into GPContext, remove from data_dict
Jul 8, 2026
4f5e6d7
Remove redundant send_indices parameter, read from gp_ctx directly
Jul 8, 2026
8341f1e
Replace gp_ctx is not None checks with gp_utils.is_a2a()
Jul 8, 2026
5c73ad7
Check gp_utils.initialized() before is_a2a() in block forward
Jul 8, 2026
8109b35
Consolidate GP config into GraphParallelConfig dataclass, remove GPMode
Jul 9, 2026
2a73638
Fix double __post_init__ conflict in deprecated graph_parallel_group_…
Jul 9, 2026
bc827f3
Remove is_a2a() helper, use get_gp_config().mode directly
Jul 9, 2026
a9a715b
Remove mixed PBC assertion reverted by earlier merge
Jul 9, 2026
72e2b90
Merge branch 'main' into rgao_a2a_model
rayg1234 Jul 9, 2026
0378c6b
Guard GP group setup behind gp_config check in predict worker
Jul 9, 2026
23dfe98
Remove stale send_indices arg from all_to_all_collect test calls
Jul 10, 2026
e3ea2b0
Fix stale 3-arg all_to_all_collect call in test_graph_parallel
Jul 10, 2026
c621eb4
Fix kwarg name gp_size -> gp_config in SPMDWorker.run()
Jul 10, 2026
6f30046
Merge remote-tracking branch 'origin/main' into rgao_a2a_model
Jul 21, 2026
cad5e06
Add GPMode and GPPartition StrEnums to GraphParallelConfig
Jul 24, 2026
bc45b3d
Move graph_partition imports to top-level in graph_parallel_a2a
Jul 24, 2026
15e49dc
Fix typo in filename: verfication -> verification
Jul 24, 2026
043c640
Always set scatter_target before dummy-edge handling
Jul 24, 2026
d0f49d2
Default to GP when workers > 1 in ParallelMLIPPredictUnit
Jul 24, 2026
2a293ef
Auto-set gp_config.group_size from num_workers when left at default
Jul 25, 2026
361aab9
Merge branch 'main' into rgao_a2a_model
rayg1234 Jul 26, 2026
c754660
Merge remote-tracking branch 'origin/main' into rgao_a2a_model
Jul 30, 2026
37e0352
Ensure GraphParallelConfig is set whenever GP groups are initialized
Jul 30, 2026
a931ec4
Fix isolated-atom crash under GP+A2A; add empty-graph unit tests
Jul 30, 2026
a6a4c07
Use dataclasses.replace instead of mutating caller's gp_config
Jul 30, 2026
934b853
Merge remote-tracking branch 'origin/main' into rgao_a2a_model
Jul 30, 2026
b49e3a0
Extract gp_config resolution helper to gp_utils.resolve_gp_config_for…
Jul 31, 2026
23d2c93
Move warnings import to top of launchers/api.py
Jul 31, 2026
ebd9871
Update src/fairchem/core/launchers/api.py
rayg1234 Jul 31, 2026
899307d
Fix IndentationError and use imported replace() in JobConfig.__post_i…
Jul 31, 2026
fb99ed7
Restore test_full_model_gp_correctness in test_predict.py
rayg1234 Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/fairchem/core/calculate/pretrained_mlip.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def get_predict_unit(
cache_dir: str = CACHE_DIR,
workers: int = 1,
seed: int = 41,
gp_config=None,
) -> MLIPPredictUnit:
"""
Retrieves a prediction unit for a specified model.
Expand Down Expand Up @@ -116,6 +117,7 @@ def get_predict_unit(
form_elem_refs,
workers,
seed,
gp_config=gp_config,
)


Expand Down
84 changes: 84 additions & 0 deletions src/fairchem/core/common/gp_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
from __future__ import annotations

import contextlib
import dataclasses
import logging
import threading
from dataclasses import dataclass

import torch
from torch import distributed as dist
from torch.distributed.nn.functional import all_reduce, reduce_scatter

from fairchem.core.common.utils import StrEnum

"""
Functions to support graph parallel training.
This is based on the Megatron-LM implementation:
Expand All @@ -26,6 +30,26 @@
_GRAPH_PARALLEL_GROUP = None
_DATA_PARALLEL_GROUP = None


class GPMode(StrEnum):
ALLGATHER = "allgather"
ALL_TO_ALL = "all_to_all"


class GPPartition(StrEnum):
INDEX_SPLIT = "index_split"
SPATIAL = "spatial"


@dataclass
class GraphParallelConfig:
group_size: int = 1
mode: GPMode = GPMode.ALLGATHER
partition: GPPartition = GPPartition.INDEX_SPLIT


_GP_CONFIG: GraphParallelConfig | None = None

_tls = threading.local()


Expand Down Expand Up @@ -97,6 +121,13 @@ def setup_graph_parallel_groups(
if i == found[0]:
_GRAPH_PARALLEL_GROUP = group

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


def setup_gp(config) -> None:
gp_size = config["gp_gpus"]
Expand Down Expand Up @@ -129,10 +160,18 @@ def setup_gp(config) -> None:
if i == found[0]:
_GRAPH_PARALLEL_GROUP = group

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


def cleanup_gp() -> None:
global _DATA_PARALLEL_GROUP
global _GRAPH_PARALLEL_GROUP
global _GP_CONFIG
assert _GRAPH_PARALLEL_GROUP is not None
assert _DATA_PARALLEL_GROUP is not None
with contextlib.suppress(ValueError):
Expand All @@ -141,12 +180,57 @@ def cleanup_gp() -> None:
dist.destroy_process_group(_GRAPH_PARALLEL_GROUP)
_DATA_PARALLEL_GROUP = None
_GRAPH_PARALLEL_GROUP = None
_GP_CONFIG = None


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


def set_gp_config(config: GraphParallelConfig) -> None:
global _GP_CONFIG
_GP_CONFIG = config


def get_gp_config() -> GraphParallelConfig | None:
return _GP_CONFIG


def resolve_gp_config_for_workers(
gp_config: GraphParallelConfig | None,
num_workers: int,
) -> GraphParallelConfig | None:
"""
Reconcile a user-provided GraphParallelConfig with the target number
of workers.

Behavior:
- ``num_workers <= 1``: no GP is used; return ``gp_config`` unchanged
(may be ``None``).
- ``num_workers > 1``:
* If ``gp_config`` is ``None``, build a default
``GraphParallelConfig(group_size=num_workers)``.
* If the config's ``group_size`` is still the default (1),
return a copy with ``group_size=num_workers``. The caller's
config is NOT mutated.
* If ``group_size == num_workers`` already, return it unchanged.
* Otherwise raise ``ValueError`` — an explicit mismatch is a
configuration error.
"""
if num_workers <= 1:
return gp_config
if gp_config is None:
return GraphParallelConfig(group_size=num_workers)
if gp_config.group_size == 1:
return dataclasses.replace(gp_config, group_size=num_workers)
if gp_config.group_size != num_workers:
raise ValueError(
f"gp_config.group_size ({gp_config.group_size}) must equal "
f"num_workers ({num_workers})"
)
return gp_config


def get_dp_group():
return _DATA_PARALLEL_GROUP

Expand Down
2 changes: 2 additions & 0 deletions src/fairchem/core/common/parallelism/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
GPContext,
all_to_all_collect,
build_gp_context,
compute_a2a_partition,
)
from fairchem.core.common.parallelism.graph_partition import (
PartitionStrategy,
Expand All @@ -25,6 +26,7 @@
"PartitionStrategy",
"all_to_all_collect",
"build_gp_context",
"compute_a2a_partition",
"partition_atoms_index_split",
"partition_atoms_spatial",
]
49 changes: 44 additions & 5 deletions src/fairchem/core/common/parallelism/graph_parallel_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
from torch.profiler import record_function

from fairchem.core.common import gp_utils
from fairchem.core.common.parallelism.graph_partition import (
PartitionStrategy,
partition_atoms_index_split,
partition_atoms_spatial,
)


def _safe_all_to_all(
Expand Down Expand Up @@ -61,8 +66,6 @@ class GPContext:
Graph parallel context holding communication metadata for all-to-all.

Runtime-only struct: every field is needed for the forward/backward pass.
Construction intermediates (node_partition, rank_assignments, needed_atoms,
global_to_local, etc.) are computed in build_gp_context but not stored.

Attributes:
rank: Current GP rank.
Expand All @@ -79,6 +82,7 @@ class GPContext:
total_recv: Total number of embeddings to receive (sum of recv_splits).
local_edge_idx: Indices into edge_index_local where source is a local atom.
remote_edge_idx: Indices into edge_index_local where source is a remote atom.
rank_assignments: Rank owner for each atom, shape (total_atoms,).
"""

rank: int
Expand All @@ -93,6 +97,7 @@ class GPContext:
total_recv: int
local_edge_idx: torch.Tensor
remote_edge_idx: torch.Tensor
rank_assignments: torch.Tensor


def _sparse_index_exchange(
Expand Down Expand Up @@ -191,6 +196,41 @@ def _sparse_index_exchange(
return send_counts, send_indices_global


@torch.compiler.disable
def compute_a2a_partition(
pos: torch.Tensor,
total_atoms: int,
device: torch.device,
world_size: int,
rank: int,
strategy: PartitionStrategy,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Compute rank assignments and local node partition for A2A graph parallel.

Args:
pos: Atom positions, shape (N, 3).
total_atoms: Total number of atoms.
device: Device for output tensors.
world_size: Number of GP ranks.
rank: Current GP rank.
strategy: Partitioning strategy (SPATIAL or INDEX_SPLIT).

Returns:
Tuple of (rank_assignments, node_partition) where rank_assignments
is shape (N,) mapping each atom to a rank, and node_partition is
the indices of atoms belonging to this rank.
"""
with record_function("a2a_partition"):
if strategy == PartitionStrategy.SPATIAL:
rank_assignments = partition_atoms_spatial(pos, world_size)
else:
rank_assignments = partition_atoms_index_split(
total_atoms, world_size, device
)
node_partition = (rank_assignments == rank).nonzero(as_tuple=True)[0]
return rank_assignments, node_partition


@torch.compiler.disable
def build_gp_context(
edge_index: torch.Tensor,
Expand Down Expand Up @@ -313,6 +353,7 @@ def build_gp_context(
total_recv=total_recv,
local_edge_idx=local_edge_idx,
remote_edge_idx=remote_edge_idx,
rank_assignments=rank_assignments,
)


Expand Down Expand Up @@ -524,7 +565,6 @@ def backward(ctx, grad_received: torch.Tensor):
def all_to_all_collect(
x_local: torch.Tensor,
gp_ctx: GPContext,
send_indices: torch.Tensor,
) -> torch.Tensor:
"""
High-level function to collect remote embeddings via all-to-all.
Expand All @@ -536,15 +576,14 @@ def all_to_all_collect(
Args:
x_local: Local atom embeddings, shape (local_atoms, *features).
gp_ctx: Graph parallel context.
send_indices: Local indices of atoms to send.

Returns:
x_received: Remote atom embeddings,
shape (total_needed, *features).
"""
return AllToAllCollect.apply(
x_local,
send_indices,
gp_ctx.send_indices,
gp_ctx.send_counts,
gp_ctx.recv_counts,
gp_utils.get_gp_group(),
Expand Down
Loading