Skip to content

Commit b5cc1a3

Browse files
dyzhengclaude
andcommitted
Feature: add MPI communicator injection for general-purpose parallel workflows
Replace hardcoded MPI_COMM_WORLD with configurable base_comm parameter in the MPI initialization path, enabling workflows like parallel NEB, phonon calculations, or replica exchange MD to split MPI ranks into independent groups, each running its own ESolver instance in a separate subprocess. Changes: - parallel_global: add base_comm parameter to divide_pools(), split_diag_world(), split_grid_world(), init_pools() - parallel_reduce: add comm parameter to all reduce_all, reduce_min, reduce_max, reduce_double_allpool, gather_int_all functions - parallel_common: add comm parameter to all bcast_* functions - PyESolverLCAO: add mpi_comm_handle parameter to initialize() - LCAOWorkflow: add mpi_comm parameter forwarded to ESolver All parameters default to MPI_COMM_WORLD for full backward compatibility. Existing code compiles and behaves identically without modification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c039e92 commit b5cc1a3

10 files changed

Lines changed: 434 additions & 110 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# MPI Communicator Injection Design
2+
3+
## Problem
4+
5+
ABACUS hardcodes `MPI_COMM_WORLD` as the base communicator throughout its initialization
6+
and communication paths. This prevents workflows like parallel NEB, phonon calculations,
7+
or replica exchange MD from splitting MPI ranks into independent groups, each running its
8+
own ESolver instance in a separate subprocess.
9+
10+
The pyabacus ESolver bindings have no mechanism to pass a custom MPI communicator from
11+
Python to C++. All sub-communicators (`POOL_WORLD`, `DIAG_WORLD`, `GRID_WORLD`, etc.)
12+
are derived from `MPI_COMM_WORLD`, and all global reduction/broadcast wrappers
13+
(`parallel_reduce`, `parallel_common`) hardcode `MPI_COMM_WORLD`.
14+
15+
## Goal
16+
17+
Enable general-purpose MPI communicator injection so that any workflow can pass a
18+
sub-communicator to the ESolver. One ESolver instance per process, but each process
19+
can use any communicator (not just `MPI_COMM_WORLD`). Default to `MPI_COMM_WORLD`
20+
when no communicator is provided, preserving full backward compatibility.
21+
22+
## Architecture
23+
24+
```
25+
Python (mpi4py) C++ pybind11 bindings ABACUS core
26+
------------------- --------------------- -----------
27+
comm = MPI.COMM_WORLD PyESolverLCAO::initialize( Parallel_Global::divide_pools(
28+
sub = comm.Split(color, key) input_dir, base_comm, ...)
29+
mpi_comm_handle) Parallel_Reduce::reduce_all(
30+
esolver.initialize( ..., base_comm)
31+
input_dir, Stores base_comm_ member Parallel_Common::bcast_*(
32+
comm_handle=sub.py2f()) Passes to Parallel_Global ..., base_comm)
33+
```
34+
35+
## Scope
36+
37+
### In scope (this PR)
38+
39+
1. Add `MPI_Comm base_comm` parameter to `Parallel_Global::divide_pools()`,
40+
`split_diag_world()`, `split_grid_world()` — defaulting to `MPI_COMM_WORLD`
41+
2. Add `MPI_Comm comm` parameter to all `Parallel_Reduce::reduce_all` and
42+
`Parallel_Common::bcast_*` functions — defaulting to `MPI_COMM_WORLD`
43+
3. Add `int mpi_comm_handle` parameter to `PyESolverLCAO::initialize()` and
44+
`PyESolverPW::initialize()` — defaulting to `-1` (meaning `MPI_COMM_WORLD`)
45+
4. Update pybind11 bindings to expose the new parameter
46+
5. Unit tests for communicator injection
47+
48+
### Out of scope (follow-up PRs)
49+
50+
- Parallel NEB orchestration in Python
51+
- Multi-instance-per-process support (would require encapsulating GlobalV)
52+
- Changing the ~280 scattered `MPI_COMM_WORLD` references in computation/IO modules
53+
(unnecessary for single-instance-per-process + subprocess model)
54+
55+
## Detailed Changes
56+
57+
### Layer 1: `source_base/parallel_global.h` and `.cpp`
58+
59+
Add `MPI_Comm base_comm = MPI_COMM_WORLD` as the last parameter to:
60+
61+
- `divide_pools()` — replace `MPI_COMM_WORLD` with `base_comm` in `MPICommGroup`
62+
constructor (line 337), `MPI_Comm_split` for `INT_BGROUP` (line 365),
63+
`MPI_Comm_dup` fallback (line 373), `MPI_Comm_split` fallback (line 374)
64+
- `split_diag_world()` — replace `MPI_COMM_WORLD` in `MPI_Comm_split` (line 60)
65+
- `split_grid_world()` — replace `MPI_COMM_WORLD` in `MPI_Comm_split` (line 80)
66+
67+
~10 lines changed, ~3 signature changes.
68+
69+
### Layer 2: `source_base/parallel_reduce.h` and `.cpp`
70+
71+
Add `MPI_Comm comm = MPI_COMM_WORLD` to all `reduce_all` overloads (~13 functions),
72+
`reduce_double_allpool` (~2), `gather_int_all` (~1), `reduce_min` (~3),
73+
`reduce_max` (~2). Total: ~19 signature changes, ~19 substitutions.
74+
75+
Functions using `POOL_WORLD`, `DIAG_WORLD`, `GRID_WORLD` are unchanged.
76+
77+
### Layer 3: `source_base/parallel_common.h` and `.cpp`
78+
79+
Add `MPI_Comm comm = MPI_COMM_WORLD` to all `bcast_*` functions (~10 functions).
80+
Total: ~10 signature changes, ~12 substitutions.
81+
82+
### Layer 4: Pybind11 bindings
83+
84+
**`py_esolver_lcao.hpp`**: Add `int mpi_comm_handle = -1` to `initialize()`.
85+
86+
**`py_esolver_lcao_impl.cpp`**: Convert handle via `MPI_Comm_f2c()`, use resulting
87+
communicator for `MPI_Comm_size`, `MPI_Comm_rank`, and pass to `divide_pools()` etc.
88+
89+
**`py_esolver_pw_impl.cpp`**: Same changes as LCAO.
90+
91+
**`py_esolver_bindings.cpp`**: Update `.def("initialize", ...)` to include
92+
`"mpi_comm_handle"_a = -1`.
93+
94+
~15 lines changed across 4 files.
95+
96+
### Layer 5: Python interface
97+
98+
**`esolver/workflow.py`**: Add optional `mpi_comm` parameter to `LCAOWorkflow` and
99+
`PWWorkflow` that forwards `comm.py2f()` to `esolver.initialize()`.
100+
101+
**`ase/calculator.py`**: Add optional `mpi_comm` parameter to `AbacusCalculator`.
102+
103+
### Scattered `MPI_COMM_WORLD` references (~280 in other modules)
104+
105+
**No changes.** In the single-instance-per-process + subprocess model, each
106+
subprocess's `MPI_COMM_WORLD` is already the correct sub-communicator. These
107+
references are correct as-is.
108+
109+
## Backward Compatibility
110+
111+
1. **Standalone ABACUS binary**: `main.cpp` calls `read_pal_param()` which still
112+
uses `MPI_COMM_WORLD` directly. Unaffected.
113+
2. **Existing pyabacus scripts**: `initialize(input_dir)` with no second argument
114+
defaults to `MPI_COMM_WORLD`. Identical behavior.
115+
3. **Existing `parallel_reduce`/`parallel_common` callers**: Default parameter
116+
means all call sites compile and behave identically without modification.
117+
118+
## Testing
119+
120+
1. **Unit test: `parallel_global` with sub-communicator** — Split `MPI_COMM_WORLD`,
121+
call `divide_pools()` with sub-communicator, verify derived communicators are
122+
correct relative to the sub-communicator.
123+
2. **Unit test: `parallel_reduce` with custom communicator** — Verify reduction
124+
scoped to sub-communicator.
125+
3. **Integration test: pyabacus ESolver with sub-communicator**`mpirun -np 4`,
126+
split into 2 groups of 2, each initializes ESolver independently.
127+
4. **Regression test**: Run existing tests without `mpi_comm_handle` argument,
128+
verify identical results.

python/pyabacus/src/ModuleESolver/py_esolver_lcao.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,15 +448,20 @@ PyESolverLCAO<TK, TR>::~PyESolverLCAO()
448448
}
449449

450450
template <typename TK, typename TR>
451-
void PyESolverLCAO<TK, TR>::initialize(const std::string& input_dir)
451+
void PyESolverLCAO<TK, TR>::initialize(const std::string& input_dir, int mpi_comm_handle)
452452
{
453453
// Placeholder: will be implemented in Phase 3
454454
// This will:
455-
// 1. Read INPUT file from input_dir
456-
// 2. Initialize UnitCell
457-
// 3. Create ESolver_KS_LCAO instance
455+
// 1. Convert mpi_comm_handle to MPI_Comm via MPI_Comm_f2c()
456+
// (if mpi_comm_handle >= 0, otherwise use MPI_COMM_WORLD)
457+
// 2. Use the resulting communicator for MPI_Comm_size/rank
458+
// 3. Pass base_comm to Parallel_Global::init_pools(), split_diag_world(), split_grid_world()
459+
// 4. Read INPUT file from input_dir
460+
// 5. Initialize UnitCell
461+
// 6. Create ESolver_KS_LCAO instance
458462
initialized_ = true;
459-
std::cout << "[PyESolverLCAO] Initialized with input directory: " << input_dir << std::endl;
463+
std::cout << "[PyESolverLCAO] Initialized with input directory: " << input_dir
464+
<< ", mpi_comm_handle: " << mpi_comm_handle << std::endl;
460465
}
461466

462467
template <typename TK, typename TR>
@@ -861,7 +866,10 @@ void bind_esolver_lcao(py::module& m, const std::string& suffix)
861866
----------
862867
input_dir : str
863868
Directory containing INPUT, STRU, and other input files
864-
)pbdoc", "input_dir"_a)
869+
mpi_comm_handle : int, optional
870+
Fortran MPI communicator handle from mpi4py comm.py2f().
871+
Use -1 (default) to use MPI_COMM_WORLD.
872+
)pbdoc", "input_dir"_a, "mpi_comm_handle"_a = -1)
865873
.def("before_all_runners", &ESolver::before_all_runners,
866874
"Initialize calculation environment")
867875

python/pyabacus/src/ModuleESolver/py_esolver_lcao.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,10 @@ class PyESolverLCAO
284284
// ==================== Initialization ====================
285285

286286
/// Initialize from INPUT file directory
287-
void initialize(const std::string& input_dir);
287+
/// @param input_dir Directory containing INPUT, STRU, and other input files
288+
/// @param mpi_comm_handle Fortran MPI communicator handle from mpi4py comm.py2f().
289+
/// Use -1 (default) to use MPI_COMM_WORLD.
290+
void initialize(const std::string& input_dir, int mpi_comm_handle = -1);
288291

289292
/// Call before_all_runners
290293
void before_all_runners();

python/pyabacus/src/pyabacus/esolver/workflow.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class LCAOWorkflow:
5454
'after_scf', # Called after after_scf()
5555
]
5656

57-
def __init__(self, input_dir: str, gamma_only: bool = True):
57+
def __init__(self, input_dir: str, gamma_only: bool = True, mpi_comm=None):
5858
"""
5959
Initialize LCAOWorkflow.
6060
@@ -64,9 +64,13 @@ def __init__(self, input_dir: str, gamma_only: bool = True):
6464
Directory containing input files
6565
gamma_only : bool
6666
Use gamma-only calculation if True, multi-k if False
67+
mpi_comm : mpi4py.MPI.Comm, optional
68+
MPI communicator to use. If None, uses MPI_COMM_WORLD.
69+
Pass a sub-communicator to run this ESolver on a subset of MPI ranks.
6770
"""
6871
self._input_dir = input_dir
6972
self._gamma_only = gamma_only
73+
self._mpi_comm = mpi_comm
7074
self._esolver = None
7175
self._initialized = False
7276
self._scf_running = False
@@ -96,10 +100,17 @@ def initialize(self) -> None:
96100
"Make sure pyabacus is properly installed with ESolver support."
97101
) from e
98102

99-
self._esolver.initialize(self._input_dir)
103+
self._esolver.initialize(self._input_dir, mpi_comm_handle=self._mpi_comm_handle)
100104
self._esolver.before_all_runners()
101105
self._initialized = True
102106

107+
@property
108+
def _mpi_comm_handle(self) -> int:
109+
"""Convert mpi4py communicator to Fortran handle, or -1 for default."""
110+
if self._mpi_comm is None:
111+
return -1
112+
return self._mpi_comm.py2f()
113+
103114
def register_callback(self, event: str, callback: Callable[['LCAOWorkflow'], None]) -> None:
104115
"""
105116
Register a callback function for a specific event.

source/source_base/parallel_common.cpp

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,75 +7,75 @@
77
#include <cstring>
88

99
#ifdef __MPI
10-
void Parallel_Common::bcast_string(std::string& object) // Peize Lin fix bug 2019-03-18
10+
void Parallel_Common::bcast_string(std::string& object, MPI_Comm comm) // Peize Lin fix bug 2019-03-18
1111
{
1212
int size = object.size();
13-
MPI_Bcast(&size, 1, MPI_INT, 0, MPI_COMM_WORLD);
14-
13+
MPI_Bcast(&size, 1, MPI_INT, 0, comm);
14+
1515
int my_rank;
16-
MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
17-
16+
MPI_Comm_rank(comm, &my_rank);
17+
1818
if (0 != my_rank)
1919
{
2020
object.resize(size);
2121
}
2222

23-
MPI_Bcast(&object[0], size, MPI_CHAR, 0, MPI_COMM_WORLD);
23+
MPI_Bcast(&object[0], size, MPI_CHAR, 0, comm);
2424
return;
2525
}
2626

27-
void Parallel_Common::bcast_string(std::string* object, const int n) // Peize Lin fix bug 2019-03-18
27+
void Parallel_Common::bcast_string(std::string* object, const int n, MPI_Comm comm) // Peize Lin fix bug 2019-03-18
2828
{
2929
for (int i = 0; i < n; i++)
30-
bcast_string(object[i]);
30+
bcast_string(object[i], comm);
3131
return;
3232
}
3333

34-
void Parallel_Common::bcast_complex_double(std::complex<double>& object)
34+
void Parallel_Common::bcast_complex_double(std::complex<double>& object, MPI_Comm comm)
3535
{
36-
MPI_Bcast(&object, 1, MPI_DOUBLE_COMPLEX, 0, MPI_COMM_WORLD);
36+
MPI_Bcast(&object, 1, MPI_DOUBLE_COMPLEX, 0, comm);
3737
}
3838

39-
void Parallel_Common::bcast_complex_double(std::complex<double>* object, const int n)
39+
void Parallel_Common::bcast_complex_double(std::complex<double>* object, const int n, MPI_Comm comm)
4040
{
41-
MPI_Bcast(object, n, MPI_DOUBLE_COMPLEX, 0, MPI_COMM_WORLD);
41+
MPI_Bcast(object, n, MPI_DOUBLE_COMPLEX, 0, comm);
4242
}
4343

44-
void Parallel_Common::bcast_double(double& object)
44+
void Parallel_Common::bcast_double(double& object, MPI_Comm comm)
4545
{
46-
MPI_Bcast(&object, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
46+
MPI_Bcast(&object, 1, MPI_DOUBLE, 0, comm);
4747
}
4848

49-
void Parallel_Common::bcast_double(double* object, const int n)
49+
void Parallel_Common::bcast_double(double* object, const int n, MPI_Comm comm)
5050
{
51-
MPI_Bcast(object, n, MPI_DOUBLE, 0, MPI_COMM_WORLD);
51+
MPI_Bcast(object, n, MPI_DOUBLE, 0, comm);
5252
}
5353

54-
void Parallel_Common::bcast_int(int& object)
54+
void Parallel_Common::bcast_int(int& object, MPI_Comm comm)
5555
{
56-
MPI_Bcast(&object, 1, MPI_INT, 0, MPI_COMM_WORLD);
56+
MPI_Bcast(&object, 1, MPI_INT, 0, comm);
5757
}
5858

59-
void Parallel_Common::bcast_int(int* object, const int n)
59+
void Parallel_Common::bcast_int(int* object, const int n, MPI_Comm comm)
6060
{
61-
MPI_Bcast(object, n, MPI_INT, 0, MPI_COMM_WORLD);
61+
MPI_Bcast(object, n, MPI_INT, 0, comm);
6262
}
6363

64-
void Parallel_Common::bcast_bool(bool& object)
64+
void Parallel_Common::bcast_bool(bool& object, MPI_Comm comm)
6565
{
6666
int swap = object;
6767
int my_rank;
68-
MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
68+
MPI_Comm_rank(comm, &my_rank);
6969
if (my_rank == 0)
7070
swap = object;
71-
MPI_Bcast(&swap, 1, MPI_INT, 0, MPI_COMM_WORLD);
71+
MPI_Bcast(&swap, 1, MPI_INT, 0, comm);
7272
if (my_rank != 0)
7373
object = static_cast<bool>(swap);
7474
}
7575

76-
void Parallel_Common::bcast_char(char* object, const int n)
76+
void Parallel_Common::bcast_char(char* object, const int n, MPI_Comm comm)
7777
{
78-
MPI_Bcast(object, n, MPI_CHAR, 0, MPI_COMM_WORLD);
78+
MPI_Bcast(object, n, MPI_CHAR, 0, comm);
7979
}
8080

8181
#endif

source/source_base/parallel_common.h

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,58 @@
1010
namespace Parallel_Common
1111
{
1212
//(1) bcast array
13-
void bcast_complex_double(std::complex<double>* object, const int n);
14-
void bcast_string(std::string* object, const int n);
15-
void bcast_double(double* object, const int n);
16-
void bcast_int(int* object, const int n);
17-
void bcast_char(char* object, const int n);
13+
void bcast_complex_double(std::complex<double>* object, const int n
14+
#ifdef __MPI
15+
, MPI_Comm comm = MPI_COMM_WORLD
16+
#endif
17+
);
18+
void bcast_string(std::string* object, const int n
19+
#ifdef __MPI
20+
, MPI_Comm comm = MPI_COMM_WORLD
21+
#endif
22+
);
23+
void bcast_double(double* object, const int n
24+
#ifdef __MPI
25+
, MPI_Comm comm = MPI_COMM_WORLD
26+
#endif
27+
);
28+
void bcast_int(int* object, const int n
29+
#ifdef __MPI
30+
, MPI_Comm comm = MPI_COMM_WORLD
31+
#endif
32+
);
33+
void bcast_char(char* object, const int n
34+
#ifdef __MPI
35+
, MPI_Comm comm = MPI_COMM_WORLD
36+
#endif
37+
);
1838

1939
//(2) bcast single
20-
void bcast_complex_double(std::complex<double>& object);
21-
void bcast_string(std::string& object);
22-
void bcast_double(double& object);
23-
void bcast_int(int& object);
24-
void bcast_bool(bool& object);
40+
void bcast_complex_double(std::complex<double>& object
41+
#ifdef __MPI
42+
, MPI_Comm comm = MPI_COMM_WORLD
43+
#endif
44+
);
45+
void bcast_string(std::string& object
46+
#ifdef __MPI
47+
, MPI_Comm comm = MPI_COMM_WORLD
48+
#endif
49+
);
50+
void bcast_double(double& object
51+
#ifdef __MPI
52+
, MPI_Comm comm = MPI_COMM_WORLD
53+
#endif
54+
);
55+
void bcast_int(int& object
56+
#ifdef __MPI
57+
, MPI_Comm comm = MPI_COMM_WORLD
58+
#endif
59+
);
60+
void bcast_bool(bool& object
61+
#ifdef __MPI
62+
, MPI_Comm comm = MPI_COMM_WORLD
63+
#endif
64+
);
2565

2666
} // namespace Parallel_Common
2767

0 commit comments

Comments
 (0)