maybempi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maybempi-0.1.0/LICENSE.txt +21 -0
- maybempi-0.1.0/PKG-INFO +162 -0
- maybempi-0.1.0/README.md +94 -0
- maybempi-0.1.0/pyproject.toml +82 -0
- maybempi-0.1.0/setup.cfg +4 -0
- maybempi-0.1.0/src/maybempi/__init__.py +66 -0
- maybempi-0.1.0/src/maybempi/__main__.py +5 -0
- maybempi-0.1.0/src/maybempi/cli.py +142 -0
- maybempi-0.1.0/src/maybempi/launch.py +204 -0
- maybempi-0.1.0/src/maybempi/py.typed +0 -0
- maybempi-0.1.0/src/maybempi/serial.py +612 -0
- maybempi-0.1.0/src/maybempi/tests/mpi/__init__.py +0 -0
- maybempi-0.1.0/src/maybempi/tests/mpi/test_under_mpirun.py +77 -0
- maybempi-0.1.0/src/maybempi/tests/unit/__init__.py +0 -0
- maybempi-0.1.0/src/maybempi/tests/unit/test_cli.py +103 -0
- maybempi-0.1.0/src/maybempi/tests/unit/test_launch.py +144 -0
- maybempi-0.1.0/src/maybempi/tests/unit/test_serial.py +279 -0
- maybempi-0.1.0/src/maybempi.egg-info/PKG-INFO +162 -0
- maybempi-0.1.0/src/maybempi.egg-info/SOURCES.txt +21 -0
- maybempi-0.1.0/src/maybempi.egg-info/dependency_links.txt +1 -0
- maybempi-0.1.0/src/maybempi.egg-info/entry_points.txt +2 -0
- maybempi-0.1.0/src/maybempi.egg-info/requires.txt +23 -0
- maybempi-0.1.0/src/maybempi.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Max Lindqvist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
maybempi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maybempi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Use MPI only when launched under MPI, and a serial stand-in for mpi4py otherwise.
|
|
5
|
+
Author: Max
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Max Lindqvist
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Changelog, https://github.com/max-models/maybempi/blob/main/CHANGELOG.md
|
|
29
|
+
Project-URL: Documentation, https://max-models.github.io/maybempi/
|
|
30
|
+
Project-URL: Source, https://github.com/max-models/maybempi
|
|
31
|
+
Keywords: hpc,mpi,mpi4py,parallel,serial,slurm
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering
|
|
43
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
License-File: LICENSE.txt
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: jupyterlab; extra == "dev"
|
|
50
|
+
Requires-Dist: maybempi[docs,test]; extra == "dev"
|
|
51
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
52
|
+
Requires-Dist: pyproject-fmt; extra == "dev"
|
|
53
|
+
Requires-Dist: pyright; extra == "dev"
|
|
54
|
+
Requires-Dist: ruff; extra == "dev"
|
|
55
|
+
Provides-Extra: docs
|
|
56
|
+
Requires-Dist: griffe; extra == "docs"
|
|
57
|
+
Requires-Dist: ipykernel; extra == "docs"
|
|
58
|
+
Requires-Dist: nbclient>=0.10; extra == "docs"
|
|
59
|
+
Requires-Dist: nbformat>=5.10; extra == "docs"
|
|
60
|
+
Requires-Dist: numpy; extra == "docs"
|
|
61
|
+
Provides-Extra: mpi
|
|
62
|
+
Requires-Dist: mpi4py; extra == "mpi"
|
|
63
|
+
Provides-Extra: test
|
|
64
|
+
Requires-Dist: numpy; extra == "test"
|
|
65
|
+
Requires-Dist: pytest; extra == "test"
|
|
66
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
67
|
+
Dynamic: license-file
|
|
68
|
+
|
|
69
|
+
# maybempi
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
<!-- README.md is generated from README.qmd: edit the .qmd and run `make readme`. -->
|
|
73
|
+
|
|
74
|
+
[](https://github.com/max-models/maybempi/actions/workflows/test_pytest.yml)
|
|
75
|
+
[](https://github.com/max-models/maybempi/actions/workflows/static_analysis.yml)
|
|
77
|
+
[](https://max-models.github.io/maybempi/)
|
|
78
|
+
[](https://codecov.io/gh/max-models/maybempi)
|
|
79
|
+
[](https://pypi.org/project/maybempi/)
|
|
80
|
+
[](https://pypi.org/project/maybempi/)
|
|
81
|
+
|
|
82
|
+
Use MPI only when the process was launched under MPI, and a serial
|
|
83
|
+
stand-in for `mpi4py` otherwise. The same code runs under
|
|
84
|
+
`mpirun`/`srun` and as a plain `python script.py`, and the serial run
|
|
85
|
+
never starts MPI.
|
|
86
|
+
|
|
87
|
+
Documentation: <https://max-models.github.io/maybempi/>
|
|
88
|
+
|
|
89
|
+
``` python
|
|
90
|
+
from maybempi import MPI # instead of: from mpi4py import MPI
|
|
91
|
+
|
|
92
|
+
# mpi4py.MPI under mpirun/mpiexec/srun, a serial stand-in otherwise
|
|
93
|
+
comm = MPI.COMM_WORLD
|
|
94
|
+
total = comm.allreduce(local_total, op=MPI.SUM) # local_total in a serial run
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Why
|
|
98
|
+
|
|
99
|
+
Importing `mpi4py.MPI` calls `MPI_Init`, which costs close to a second
|
|
100
|
+
and makes every collective cost something, even on one process. maybempi
|
|
101
|
+
decides from the environment variables that MPI launchers export (Open
|
|
102
|
+
MPI, MPICH, Intel MPI, PMIx/`srun`, MVAPICH2, Cray), without importing
|
|
103
|
+
mpi4py. In a serial run it returns a stand-in with the module constants
|
|
104
|
+
and communicator methods of mpi4py, which give the results MPI gives on
|
|
105
|
+
one process: `allreduce(x)` is `x`, `gather(x)` is `[x]`,
|
|
106
|
+
`Allreduce(send, recv)` copies. Methods it does not implement raise
|
|
107
|
+
`AttributeError` instead of silently doing nothing.
|
|
108
|
+
|
|
109
|
+
- `MAYBEMPI=0`/`1` overrides the decision.
|
|
110
|
+
- `maybempi.is_serial(MPI)` tells the stand-in from mpi4py.
|
|
111
|
+
- `maybempi.local_rank()` gives the node-local rank before `MPI_Init`,
|
|
112
|
+
e.g. to pick a GPU.
|
|
113
|
+
- The `maybempi` command prints what it decides, and why:
|
|
114
|
+
`mpirun -n 2 maybempi --init`.
|
|
115
|
+
|
|
116
|
+
maybempi is pure Python and has no dependencies.
|
|
117
|
+
|
|
118
|
+
## Install
|
|
119
|
+
|
|
120
|
+
``` bash
|
|
121
|
+
pip install maybempi # serial runs only
|
|
122
|
+
pip install "maybempi[mpi]" # with mpi4py, for MPI runs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
With [uv](https://docs.astral.sh/uv/):
|
|
128
|
+
|
|
129
|
+
``` bash
|
|
130
|
+
make install # uv sync --extra dev, plus the pre-commit hooks
|
|
131
|
+
make lint # ruff check, ruff format --check, pyright
|
|
132
|
+
make test # pytest with coverage
|
|
133
|
+
mpiexec -n 2 .venv/bin/python -m pytest src/maybempi/tests/mpi # tests under a real launcher
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Commit messages follow [Conventional
|
|
137
|
+
Commits](https://www.conventionalcommits.org/); see
|
|
138
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
139
|
+
|
|
140
|
+
The documentation in `docs/` is an [Astro](https://astro.build/) +
|
|
141
|
+
[Starlight](https://starlight.astro.build/) site with the executed
|
|
142
|
+
notebooks from `tutorials/` and an API reference generated from the
|
|
143
|
+
docstrings. It needs Node 22 or newer:
|
|
144
|
+
|
|
145
|
+
``` bash
|
|
146
|
+
make docs-install # npm packages and the Python docs extra
|
|
147
|
+
make docs-notebooks # execute tutorials/*.ipynb and convert them to pages
|
|
148
|
+
make docs-dev # live preview at http://localhost:4321/maybempi/
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`README.md` is rendered from `README.qmd` with
|
|
152
|
+
[Quarto](https://quarto.org/): `make readme`.
|
|
153
|
+
|
|
154
|
+
## Releases
|
|
155
|
+
|
|
156
|
+
Before merging a release to `main`, update the version in
|
|
157
|
+
`pyproject.toml`, `src/maybempi/__init__.py` and `CITATION.cff`
|
|
158
|
+
(including its release date), and add the release notes to
|
|
159
|
+
`CHANGELOG.md`. The push to `main` creates a GitHub release with a
|
|
160
|
+
`vX.Y.Z` tag and publishes the package to PyPI with trusted publishing
|
|
161
|
+
(OIDC). See the [publishing
|
|
162
|
+
guide](https://max-models.github.io/maybempi/development/publishing/).
|
maybempi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# maybempi
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<!-- README.md is generated from README.qmd: edit the .qmd and run `make readme`. -->
|
|
5
|
+
|
|
6
|
+
[](https://github.com/max-models/maybempi/actions/workflows/test_pytest.yml)
|
|
7
|
+
[](https://github.com/max-models/maybempi/actions/workflows/static_analysis.yml)
|
|
9
|
+
[](https://max-models.github.io/maybempi/)
|
|
10
|
+
[](https://codecov.io/gh/max-models/maybempi)
|
|
11
|
+
[](https://pypi.org/project/maybempi/)
|
|
12
|
+
[](https://pypi.org/project/maybempi/)
|
|
13
|
+
|
|
14
|
+
Use MPI only when the process was launched under MPI, and a serial
|
|
15
|
+
stand-in for `mpi4py` otherwise. The same code runs under
|
|
16
|
+
`mpirun`/`srun` and as a plain `python script.py`, and the serial run
|
|
17
|
+
never starts MPI.
|
|
18
|
+
|
|
19
|
+
Documentation: <https://max-models.github.io/maybempi/>
|
|
20
|
+
|
|
21
|
+
``` python
|
|
22
|
+
from maybempi import MPI # instead of: from mpi4py import MPI
|
|
23
|
+
|
|
24
|
+
# mpi4py.MPI under mpirun/mpiexec/srun, a serial stand-in otherwise
|
|
25
|
+
comm = MPI.COMM_WORLD
|
|
26
|
+
total = comm.allreduce(local_total, op=MPI.SUM) # local_total in a serial run
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why
|
|
30
|
+
|
|
31
|
+
Importing `mpi4py.MPI` calls `MPI_Init`, which costs close to a second
|
|
32
|
+
and makes every collective cost something, even on one process. maybempi
|
|
33
|
+
decides from the environment variables that MPI launchers export (Open
|
|
34
|
+
MPI, MPICH, Intel MPI, PMIx/`srun`, MVAPICH2, Cray), without importing
|
|
35
|
+
mpi4py. In a serial run it returns a stand-in with the module constants
|
|
36
|
+
and communicator methods of mpi4py, which give the results MPI gives on
|
|
37
|
+
one process: `allreduce(x)` is `x`, `gather(x)` is `[x]`,
|
|
38
|
+
`Allreduce(send, recv)` copies. Methods it does not implement raise
|
|
39
|
+
`AttributeError` instead of silently doing nothing.
|
|
40
|
+
|
|
41
|
+
- `MAYBEMPI=0`/`1` overrides the decision.
|
|
42
|
+
- `maybempi.is_serial(MPI)` tells the stand-in from mpi4py.
|
|
43
|
+
- `maybempi.local_rank()` gives the node-local rank before `MPI_Init`,
|
|
44
|
+
e.g. to pick a GPU.
|
|
45
|
+
- The `maybempi` command prints what it decides, and why:
|
|
46
|
+
`mpirun -n 2 maybempi --init`.
|
|
47
|
+
|
|
48
|
+
maybempi is pure Python and has no dependencies.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
``` bash
|
|
53
|
+
pip install maybempi # serial runs only
|
|
54
|
+
pip install "maybempi[mpi]" # with mpi4py, for MPI runs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
With [uv](https://docs.astral.sh/uv/):
|
|
60
|
+
|
|
61
|
+
``` bash
|
|
62
|
+
make install # uv sync --extra dev, plus the pre-commit hooks
|
|
63
|
+
make lint # ruff check, ruff format --check, pyright
|
|
64
|
+
make test # pytest with coverage
|
|
65
|
+
mpiexec -n 2 .venv/bin/python -m pytest src/maybempi/tests/mpi # tests under a real launcher
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Commit messages follow [Conventional
|
|
69
|
+
Commits](https://www.conventionalcommits.org/); see
|
|
70
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
71
|
+
|
|
72
|
+
The documentation in `docs/` is an [Astro](https://astro.build/) +
|
|
73
|
+
[Starlight](https://starlight.astro.build/) site with the executed
|
|
74
|
+
notebooks from `tutorials/` and an API reference generated from the
|
|
75
|
+
docstrings. It needs Node 22 or newer:
|
|
76
|
+
|
|
77
|
+
``` bash
|
|
78
|
+
make docs-install # npm packages and the Python docs extra
|
|
79
|
+
make docs-notebooks # execute tutorials/*.ipynb and convert them to pages
|
|
80
|
+
make docs-dev # live preview at http://localhost:4321/maybempi/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`README.md` is rendered from `README.qmd` with
|
|
84
|
+
[Quarto](https://quarto.org/): `make readme`.
|
|
85
|
+
|
|
86
|
+
## Releases
|
|
87
|
+
|
|
88
|
+
Before merging a release to `main`, update the version in
|
|
89
|
+
`pyproject.toml`, `src/maybempi/__init__.py` and `CITATION.cff`
|
|
90
|
+
(including its release date), and add the release notes to
|
|
91
|
+
`CHANGELOG.md`. The push to `main` creates a GitHub release with a
|
|
92
|
+
`vX.Y.Z` tag and publishes the package to PyPI with trusted publishing
|
|
93
|
+
(OIDC). See the [publishing
|
|
94
|
+
guide](https://max-models.github.io/maybempi/development/publishing/).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = [ "setuptools", "wheel" ]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "maybempi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Use MPI only when launched under MPI, and a serial stand-in for mpi4py otherwise."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
keywords = [ "hpc", "mpi", "mpi4py", "parallel", "serial", "slurm" ]
|
|
11
|
+
license = { file = "LICENSE.txt" }
|
|
12
|
+
authors = [ { name = "Max" } ]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Scientific/Engineering",
|
|
26
|
+
"Topic :: System :: Distributed Computing",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = []
|
|
30
|
+
optional-dependencies.dev = [
|
|
31
|
+
"jupyterlab",
|
|
32
|
+
"maybempi[docs,test]",
|
|
33
|
+
"pre-commit",
|
|
34
|
+
"pyproject-fmt",
|
|
35
|
+
"pyright",
|
|
36
|
+
"ruff",
|
|
37
|
+
]
|
|
38
|
+
optional-dependencies.docs = [
|
|
39
|
+
"griffe",
|
|
40
|
+
"ipykernel",
|
|
41
|
+
"nbclient>=0.10",
|
|
42
|
+
"nbformat>=5.10",
|
|
43
|
+
"numpy",
|
|
44
|
+
]
|
|
45
|
+
optional-dependencies.mpi = [ "mpi4py" ]
|
|
46
|
+
optional-dependencies.test = [ "numpy", "pytest", "pytest-cov" ]
|
|
47
|
+
urls.Changelog = "https://github.com/max-models/maybempi/blob/main/CHANGELOG.md"
|
|
48
|
+
urls.Documentation = "https://max-models.github.io/maybempi/"
|
|
49
|
+
urls.Source = "https://github.com/max-models/maybempi"
|
|
50
|
+
scripts.maybempi = "maybempi.cli:main"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools]
|
|
53
|
+
package-data."*" = [ "py.typed" ]
|
|
54
|
+
packages.find.where = [ "src" ]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
target-version = "py310"
|
|
58
|
+
line-length = 88
|
|
59
|
+
extend-include = [ "*.ipynb" ]
|
|
60
|
+
# pycodestyle, pyflakes, isort, pyupgrade, bugbear and pydocstyle
|
|
61
|
+
lint.select = [ "B", "D", "E", "F", "I", "UP", "W" ]
|
|
62
|
+
lint.ignore = [ "E501" ] # line length is handled by the formatter
|
|
63
|
+
lint.per-file-ignores."**/tests/**" = [ "D" ]
|
|
64
|
+
lint.per-file-ignores."*.ipynb" = [ "D" ]
|
|
65
|
+
lint.per-file-ignores."docs/tools/*" = [ "D" ]
|
|
66
|
+
lint.pydocstyle.convention = "google"
|
|
67
|
+
|
|
68
|
+
[tool.pyright]
|
|
69
|
+
include = [ "src" ]
|
|
70
|
+
exclude = [ "**/tests" ]
|
|
71
|
+
pythonVersion = "3.10"
|
|
72
|
+
typeCheckingMode = "standard"
|
|
73
|
+
|
|
74
|
+
[tool.pytest]
|
|
75
|
+
ini_options.pythonpath = [ "src" ]
|
|
76
|
+
ini_options.testpaths = [ "src" ]
|
|
77
|
+
ini_options.addopts = "-ra"
|
|
78
|
+
|
|
79
|
+
[tool.coverage]
|
|
80
|
+
run.omit = [ "*/tests/*" ]
|
|
81
|
+
run.source = [ "maybempi" ]
|
|
82
|
+
report.show_missing = true
|
maybempi-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Use MPI only when the process was launched under MPI, and a serial stand-in otherwise.
|
|
2
|
+
|
|
3
|
+
Importing ``mpi4py.MPI`` starts MPI, which costs close to a second and makes every
|
|
4
|
+
collective cost something, even on one process. maybempi decides from the
|
|
5
|
+
environment the launcher sets up, without importing mpi4py, and returns either
|
|
6
|
+
``mpi4py.MPI`` or a serial stand-in with the same interface::
|
|
7
|
+
|
|
8
|
+
import maybempi
|
|
9
|
+
|
|
10
|
+
MPI = maybempi.get_mpi() # mpi4py.MPI under mpirun/srun, else the stand-in
|
|
11
|
+
comm = MPI.COMM_WORLD
|
|
12
|
+
total = comm.allreduce(local_total, op=MPI.SUM)
|
|
13
|
+
|
|
14
|
+
``from maybempi import MPI`` is the drop-in replacement for ``from mpi4py import
|
|
15
|
+
MPI``: the name resolves to ``get_mpi()`` when it is first imported.
|
|
16
|
+
|
|
17
|
+
:mod:`maybempi.launch` holds the detection, :mod:`maybempi.serial` the stand-in.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from maybempi.launch import (
|
|
23
|
+
LAUNCHER_VARIABLES,
|
|
24
|
+
LOCAL_RANK_VARIABLES,
|
|
25
|
+
OVERRIDE_VARIABLE,
|
|
26
|
+
get_mpi,
|
|
27
|
+
is_serial,
|
|
28
|
+
launched_under_mpi,
|
|
29
|
+
launcher_variable,
|
|
30
|
+
local_rank,
|
|
31
|
+
)
|
|
32
|
+
from maybempi.serial import (
|
|
33
|
+
SerialComm,
|
|
34
|
+
SerialMPI,
|
|
35
|
+
SerialPrequest,
|
|
36
|
+
SerialRequest,
|
|
37
|
+
SerialStatus,
|
|
38
|
+
set_copy_hook,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"LAUNCHER_VARIABLES",
|
|
45
|
+
"LOCAL_RANK_VARIABLES",
|
|
46
|
+
"OVERRIDE_VARIABLE",
|
|
47
|
+
"SerialComm",
|
|
48
|
+
"SerialMPI",
|
|
49
|
+
"SerialPrequest",
|
|
50
|
+
"SerialRequest",
|
|
51
|
+
"SerialStatus",
|
|
52
|
+
"__version__",
|
|
53
|
+
"get_mpi",
|
|
54
|
+
"is_serial",
|
|
55
|
+
"launched_under_mpi",
|
|
56
|
+
"launcher_variable",
|
|
57
|
+
"local_rank",
|
|
58
|
+
"set_copy_hook",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def __getattr__(name: str) -> Any:
|
|
63
|
+
"""Resolve ``maybempi.MPI`` (and ``from maybempi import MPI``) to :func:`get_mpi`."""
|
|
64
|
+
if name == "MPI":
|
|
65
|
+
return get_mpi()
|
|
66
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""The ``maybempi`` command: show what maybempi decides in this environment.
|
|
2
|
+
|
|
3
|
+
Run it the way the application is run, to see whether it would use MPI::
|
|
4
|
+
|
|
5
|
+
maybempi # serial: one table
|
|
6
|
+
mpirun -n 2 maybempi # one table per rank (MPI is not started)
|
|
7
|
+
mpirun -n 2 maybempi --init # start MPI: rank 0 prints one table for all ranks
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import importlib.util
|
|
14
|
+
import os
|
|
15
|
+
import socket
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from maybempi import __version__
|
|
20
|
+
from maybempi.launch import (
|
|
21
|
+
OVERRIDE_VARIABLE,
|
|
22
|
+
get_mpi,
|
|
23
|
+
is_serial,
|
|
24
|
+
launched_under_mpi,
|
|
25
|
+
launcher_variable,
|
|
26
|
+
local_rank,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# columns of the per-rank table, in this order; other items become columns only
|
|
30
|
+
# if they differ between ranks
|
|
31
|
+
_RANK_COLUMNS = ("rank", "host", "local rank", "launcher variable")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def info(init: bool = False) -> dict[str, Any]:
|
|
35
|
+
"""Collect what maybempi decides in this process, and why.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
init: Also call :func:`~maybempi.launch.get_mpi` (which starts MPI under a
|
|
39
|
+
launcher) and add the rank and size of ``COMM_WORLD``.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The items of the report, by name.
|
|
43
|
+
"""
|
|
44
|
+
items: dict[str, Any] = {
|
|
45
|
+
"maybempi": __version__,
|
|
46
|
+
"host": socket.gethostname(),
|
|
47
|
+
"launched under MPI": launched_under_mpi(),
|
|
48
|
+
"launcher variable": launcher_variable() or "-",
|
|
49
|
+
f"{OVERRIDE_VARIABLE} override": os.environ.get(OVERRIDE_VARIABLE, "-"),
|
|
50
|
+
"local rank": local_rank(),
|
|
51
|
+
"mpi4py installed": importlib.util.find_spec("mpi4py") is not None,
|
|
52
|
+
}
|
|
53
|
+
if init:
|
|
54
|
+
mpi = get_mpi()
|
|
55
|
+
comm = mpi.COMM_WORLD
|
|
56
|
+
items["MPI"] = "serial stand-in" if is_serial(mpi) else "mpi4py"
|
|
57
|
+
items["rank"] = comm.Get_rank()
|
|
58
|
+
items["size"] = comm.Get_size()
|
|
59
|
+
return items
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _table(header: Sequence[str], rows: Sequence[Sequence[Any]]) -> str:
|
|
63
|
+
"""Format `rows` as a plain-text table with aligned columns."""
|
|
64
|
+
cells = [[str(value) for value in row] for row in rows]
|
|
65
|
+
widths = [
|
|
66
|
+
max(len(str(h)), *(len(row[i]) for row in cells)) for i, h in enumerate(header)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
def line(values: Sequence[str]) -> str:
|
|
70
|
+
return " ".join(
|
|
71
|
+
v.ljust(w) for v, w in zip(values, widths, strict=True)
|
|
72
|
+
).rstrip()
|
|
73
|
+
|
|
74
|
+
return "\n".join(
|
|
75
|
+
[line(header), line(["-" * w for w in widths]), *(line(row) for row in cells)]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def format_report(items: dict[str, Any]) -> str:
|
|
80
|
+
"""Format the report of one process as a two-column table.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
items: What :func:`info` returned.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The table, one item per line.
|
|
87
|
+
"""
|
|
88
|
+
items = dict(items)
|
|
89
|
+
if "rank" in items:
|
|
90
|
+
items["rank"] = f"{items['rank']} of {items.pop('size')}"
|
|
91
|
+
return _table(("item", "value"), list(items.items()))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_reports(reports: Sequence[dict[str, Any]]) -> str:
|
|
95
|
+
"""Format the reports of all ranks: the common items, then one row per rank.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
reports: What :func:`info` returned on each rank, in rank order.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A table of the items that are the same on every rank, and a table with one
|
|
102
|
+
row per rank for the others (rank, host, local rank, launcher variable and
|
|
103
|
+
whatever else differs).
|
|
104
|
+
"""
|
|
105
|
+
names = list(reports[0])
|
|
106
|
+
differs = {n for n in names if any(r.get(n) != reports[0].get(n) for r in reports)}
|
|
107
|
+
columns = [n for n in _RANK_COLUMNS if n in names]
|
|
108
|
+
columns += [n for n in names if n in differs and n not in columns]
|
|
109
|
+
common = [(n, reports[0][n]) for n in names if n not in columns]
|
|
110
|
+
rows = [[report.get(n, "-") for n in columns] for report in reports]
|
|
111
|
+
return f"{_table(('item', 'value'), common)}\n\n{_table(columns, rows)}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def report(init: bool = False) -> str:
|
|
115
|
+
"""Return the report of this process as a table (see :func:`info`)."""
|
|
116
|
+
return format_report(info(init))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main(argv: list[str] | None = None) -> None:
|
|
120
|
+
"""Print the report; the entry point of the ``maybempi`` command."""
|
|
121
|
+
parser = argparse.ArgumentParser(
|
|
122
|
+
prog="maybempi",
|
|
123
|
+
description="Show whether this process would use MPI, and why.",
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--init",
|
|
127
|
+
action="store_true",
|
|
128
|
+
help="also start MPI (under a launcher); rank 0 then prints one table for all ranks",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
131
|
+
args = parser.parse_args(argv)
|
|
132
|
+
items = info(init=args.init)
|
|
133
|
+
if args.init and items["size"] > 1:
|
|
134
|
+
reports = get_mpi().COMM_WORLD.gather(items, root=0)
|
|
135
|
+
if items["rank"] == 0:
|
|
136
|
+
print(format_reports(reports), flush=True)
|
|
137
|
+
return
|
|
138
|
+
print(format_report(items), flush=True)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|