Skip to content

Commit 2fb4b16

Browse files
committed
Merge remote-tracking branch 'origin/main' into workflow-align-main
2 parents d83d99e + 86f36c7 commit 2fb4b16

5 files changed

Lines changed: 50 additions & 228 deletions

File tree

.github/workflows/sync-fork.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ concurrency:
1515

1616
jobs:
1717
push-to-private:
18-
if: github.repository == facebookresearch/fairchem
18+
if: github.repository == 'facebookresearch/fairchem'
1919
runs-on: ubuntu-latest
2020
environment: sync-private
2121
steps:

src/fairchem/core/common/gp_utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import torch
1717
import torch.distributed._functional_collectives as funcol
18+
from omegaconf import OmegaConf
1819
from torch import distributed as dist
1920
from torch.distributed.nn.functional import all_reduce
2021

@@ -189,7 +190,16 @@ def initialized() -> bool:
189190

190191

191192
def set_gp_config(config: GraphParallelConfig) -> None:
193+
"""
194+
Store the graph parallel config as a plain dataclass.
195+
196+
Hydra passes a DictConfig, whose attribute reads torch.compile cannot
197+
trace; the per-layer read then splits the backbone forward into a frame
198+
per layer.
199+
"""
192200
global _GP_CONFIG
201+
if OmegaConf.is_config(config):
202+
config = OmegaConf.to_object(config)
193203
_GP_CONFIG = config
194204

195205

@@ -286,6 +296,30 @@ def backward(ctx, grad_output: torch.Tensor) -> torch.Tensor:
286296
return grad_output
287297

288298

299+
def all_reduce_sum_with_grad(input: torch.Tensor) -> torch.Tensor:
300+
assert initialized(), "Cannot use graph parallel with initializing gp group, must call setup_gp from gp_utils.py!"
301+
return AllReduceSumWithGrad.apply(input)
302+
303+
304+
class AllReduceSumWithGrad(torch.autograd.Function):
305+
"""
306+
Sum a tensor across the graph parallel group, differentiably.
307+
308+
The backward is a second sum, where ReduceFromModelParallelRegion's is the
309+
identity: every rank consumes the reduced value independently, so one
310+
rank's contribution collects gradient from all of them.
311+
"""
312+
313+
@staticmethod
314+
def forward(ctx, input: torch.Tensor) -> torch.Tensor:
315+
ctx.group = get_gp_group()
316+
return funcol.all_reduce(input, "sum", ctx.group)
317+
318+
@staticmethod
319+
def backward(ctx, grad_output: torch.Tensor) -> torch.Tensor:
320+
return funcol.all_reduce(grad_output, "sum", ctx.group)
321+
322+
289323
def scatter_to_model_parallel_region(input: torch.Tensor) -> torch.Tensor:
290324
assert initialized(), "Cannot use graph parallel with initializing gp group, must call setup_gp from gp_utils.py!"
291325
return ScatterToModelParallelRegion.apply(input)

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from __future__ import annotations
99

1010
from fairchem.core.common.parallelism.graph_parallel_a2a import (
11-
AllToAllCollect,
1211
GPContext,
1312
all_to_all_collect,
1413
build_gp_context,
@@ -21,7 +20,6 @@
2120
)
2221

2322
__all__ = [
24-
"AllToAllCollect",
2523
"GPContext",
2624
"PartitionStrategy",
2725
"all_to_all_collect",

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

Lines changed: 12 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from dataclasses import dataclass
1111

1212
import torch
13+
import torch.distributed._functional_collectives as funcol
1314
from torch import distributed as dist
1415
from torch.profiler import record_function
1516

@@ -357,239 +358,29 @@ def build_gp_context(
357358
)
358359

359360

360-
class AllToAllCollect(torch.autograd.Function):
361-
"""
362-
Autograd function that uses all-to-all to collect only the needed
363-
remote atom embeddings, replacing the all-gather approach.
364-
365-
Forward: Sends local atom embeddings to ranks that need them,
366-
receives remote atom embeddings that we need. Returns only the
367-
received remote embeddings (NOT concatenated with local).
368-
369-
Backward: Reverses the communication — sends gradient of received
370-
embeddings back to their owners, receives gradient of sent
371-
embeddings.
372-
373-
Optimizations over naive all-to-all:
374-
- Uses ``all_to_all_single`` on NCCL to avoid Python list creation
375-
from ``split()`` — communicates packed tensors directly.
376-
- Returns the pre-allocated receive buffer directly instead of
377-
``torch.cat(recv_list)`` — avoids a redundant copy.
378-
- Accepts precomputed ``send_splits``/``recv_splits`` to avoid
379-
repeated ``.tolist()`` calls per layer.
380-
"""
381-
382-
@staticmethod
383-
@torch.compiler.disable
384-
def forward(
385-
ctx,
386-
x_local: torch.Tensor,
387-
send_indices: torch.Tensor,
388-
send_counts: torch.Tensor,
389-
recv_counts: torch.Tensor,
390-
gp_group: dist.ProcessGroup,
391-
rank: int,
392-
world_size: int,
393-
precomputed_send_splits: list[int] | None = None,
394-
precomputed_recv_splits: list[int] | None = None,
395-
precomputed_total_recv: int | None = None,
396-
) -> torch.Tensor:
397-
"""
398-
Forward all-to-all embedding collection.
399-
400-
Args:
401-
x_local: Local atom embeddings,
402-
shape (local_atoms, *feature_dims).
403-
send_indices: Local indices of atoms to send,
404-
ordered by dest rank.
405-
send_counts: Number of atoms to send to each rank.
406-
recv_counts: Number of atoms to receive from each rank.
407-
gp_group: GP process group.
408-
rank: GP rank.
409-
world_size: GP world size.
410-
precomputed_send_splits: Optional cached
411-
send_counts.tolist().
412-
precomputed_recv_splits: Optional cached
413-
recv_counts.tolist().
414-
precomputed_total_recv: Optional cached
415-
sum(recv_splits).
416-
417-
Returns:
418-
Received remote embeddings,
419-
shape (sum(recv_counts), *feature_dims).
420-
"""
421-
ctx.send_indices = send_indices
422-
ctx.send_counts = send_counts
423-
ctx.recv_counts = recv_counts
424-
ctx.gp_group = gp_group
425-
ctx.rank = rank
426-
ctx.world_size = world_size
427-
ctx.local_size = x_local.shape[0]
428-
# Cache precomputed splits for backward
429-
ctx.precomputed_send_splits = precomputed_send_splits
430-
ctx.precomputed_recv_splits = precomputed_recv_splits
431-
432-
feature_shape = x_local.shape[1:]
433-
434-
# Gather atoms to send (index_select into contiguous buffer)
435-
if send_indices.numel() > 0:
436-
x_send = x_local[send_indices].contiguous()
437-
else:
438-
x_send = torch.empty(
439-
0,
440-
*feature_shape,
441-
device=x_local.device,
442-
dtype=x_local.dtype,
443-
)
444-
445-
# Use precomputed splits if available
446-
send_splits = (
447-
precomputed_send_splits
448-
if precomputed_send_splits is not None
449-
else send_counts.tolist()
450-
)
451-
recv_splits = (
452-
precomputed_recv_splits
453-
if precomputed_recv_splits is not None
454-
else recv_counts.tolist()
455-
)
456-
total_recv = (
457-
precomputed_total_recv
458-
if precomputed_total_recv is not None
459-
else sum(recv_splits)
460-
)
461-
x_recv = torch.empty(
462-
total_recv,
463-
*feature_shape,
464-
device=x_local.device,
465-
dtype=x_local.dtype,
466-
)
467-
468-
# Perform all-to-all communication
469-
backend = dist.get_backend(gp_group)
470-
if backend == "nccl":
471-
# Use all_to_all_single for NCCL
472-
dist.all_to_all_single(
473-
x_recv,
474-
x_send,
475-
output_split_sizes=recv_splits,
476-
input_split_sizes=send_splits,
477-
group=gp_group,
478-
)
479-
else:
480-
# Gloo fallback: use list-based pairwise send/recv
481-
send_list = list(x_send.split(send_splits))
482-
recv_list = list(x_recv.split(recv_splits))
483-
_safe_all_to_all(recv_list, send_list, group=gp_group)
484-
485-
# x_recv already contains all received data in rank order
486-
return x_recv
487-
488-
@staticmethod
489-
@torch.compiler.disable
490-
def backward(ctx, grad_received: torch.Tensor):
491-
"""
492-
Reverse the all-to-all: send gradients back to the ranks that
493-
originally sent us the embeddings.
494-
"""
495-
send_counts = ctx.send_counts
496-
recv_counts = ctx.recv_counts
497-
send_indices = ctx.send_indices
498-
gp_group = ctx.gp_group
499-
local_size = ctx.local_size
500-
501-
feature_shape = grad_received.shape[1:]
502-
503-
# In backward, the roles are reversed
504-
bwd_send_splits = (
505-
ctx.precomputed_recv_splits
506-
if ctx.precomputed_recv_splits is not None
507-
else recv_counts.tolist()
508-
)
509-
bwd_recv_splits = (
510-
ctx.precomputed_send_splits
511-
if ctx.precomputed_send_splits is not None
512-
else send_counts.tolist()
513-
)
514-
515-
total_bwd_recv = sum(bwd_recv_splits)
516-
grad_send_back = torch.empty(
517-
total_bwd_recv,
518-
*feature_shape,
519-
device=grad_received.device,
520-
dtype=grad_received.dtype,
521-
)
522-
523-
# Reverse all-to-all
524-
backend = dist.get_backend(gp_group)
525-
if backend == "nccl":
526-
dist.all_to_all_single(
527-
grad_send_back,
528-
grad_received.contiguous(),
529-
output_split_sizes=bwd_recv_splits,
530-
input_split_sizes=bwd_send_splits,
531-
group=gp_group,
532-
)
533-
else:
534-
# Gloo fallback
535-
bwd_send_list = list(grad_received.split(bwd_send_splits))
536-
bwd_recv_list = list(grad_send_back.split(bwd_recv_splits))
537-
_safe_all_to_all(bwd_recv_list, bwd_send_list, group=gp_group)
538-
539-
# Scatter received gradients back to local positions
540-
grad_local = torch.zeros(
541-
local_size,
542-
*feature_shape,
543-
device=grad_received.device,
544-
dtype=grad_received.dtype,
545-
)
546-
547-
if total_bwd_recv > 0:
548-
grad_local.index_add_(0, send_indices, grad_send_back)
549-
550-
# Return gradients for x_local only; None for all other inputs
551-
return (
552-
grad_local,
553-
None,
554-
None,
555-
None,
556-
None,
557-
None,
558-
None,
559-
None,
560-
None,
561-
None,
562-
)
563-
564-
565361
def all_to_all_collect(
566362
x_local: torch.Tensor,
567363
gp_ctx: GPContext,
568364
) -> torch.Tensor:
569365
"""
570-
High-level function to collect remote embeddings via all-to-all.
366+
Collect the remote atom embeddings this rank's edges need.
571367
572-
Returns the received remote embeddings (NOT including local).
573-
The caller should concatenate [x_local, received] and use
574-
gp_ctx.global_to_local to index into this combined tensor.
368+
Returns only the received embeddings; the caller concatenates them with
369+
the local ones to form the tensor ``edge_index_local`` indexes into.
370+
Autograd supplies the reverse exchange and the scatter-add back into the
371+
local embeddings.
575372
576373
Args:
577374
x_local: Local atom embeddings, shape (local_atoms, *features).
578375
gp_ctx: Graph parallel context.
579376
580377
Returns:
581-
x_received: Remote atom embeddings,
582-
shape (total_needed, *features).
378+
Remote atom embeddings, shape (total_recv, *features).
583379
"""
584-
return AllToAllCollect.apply(
585-
x_local,
586-
gp_ctx.send_indices,
587-
gp_ctx.send_counts,
588-
gp_ctx.recv_counts,
589-
gp_utils.get_gp_group(),
590-
gp_ctx.rank,
591-
gp_ctx.world_size,
592-
gp_ctx.send_splits,
380+
x_send = x_local[gp_ctx.send_indices]
381+
return funcol.all_to_all_single_autograd(
382+
x_send,
593383
gp_ctx.recv_splits,
594-
gp_ctx.total_recv,
384+
gp_ctx.send_splits,
385+
gp_utils.get_gp_group(),
595386
)

src/fairchem/core/models/uma/escn_md.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import torch
1616
import torch.nn as nn
1717
from omegaconf import DictConfig, ListConfig
18-
from torch.distributed.nn.functional import all_reduce as all_reduce_with_grad
1918
from torch.profiler import record_function
2019

2120
from fairchem.core.common import gp_utils
@@ -185,8 +184,8 @@ def balance_channels_batched(
185184
Returns:
186185
Modified embeddings with the specified channel range balanced to sum to target.
187186
188-
Supports graph parallel (GP) mode using torch.distributed.nn.functional.all_reduce
189-
which provides correct gradients in both forward and backward passes.
187+
Under graph parallelism the per-system sums are partial, so they are
188+
reduced across the group with a differentiable all-reduce.
190189
"""
191190
out_emb = emb.clone()
192191
num_systems = len(natoms)
@@ -203,7 +202,7 @@ def balance_channels_batched(
203202

204203
# Reduce partial sums across all graph parallel ranks
205204
if gp_utils.initialized():
206-
system_sums = all_reduce_with_grad(system_sums, group=gp_utils.get_gp_group())
205+
system_sums = gp_utils.all_reduce_sum_with_grad(system_sums)
207206

208207
# Batched correction: broadcast target to all channels
209208
target_sums = (target - target_offset).unsqueeze(1).expand(-1, n_channels)

0 commit comments

Comments
 (0)