Skip to content

Commit db29b90

Browse files
authored
test Open MPI and matching mpi4py on Linux and macOS (#75)
test Open MPI against matched mpi4py on Linux and macOS and speed up generated module lookups Open MPI integration CI now runs the mpi_f08 tutorial on Linux and macOS against Open MPI 4.1.8 and 5.0.11, built with a paired GNU C and Fortran 13 toolchain. Each job builds mpi4py from source against the same installation, runs the tutorial's two-rank program through PRIK and through mpi4py, and requires identical output. Generated extension modules now serve module variables through get/set descriptors on the module type instead of a getattro that compared every attribute name with each module variable before the ordinary lookup. Looking up a function on a generated module drops from about 100 ns to about 25 ns, as on a plain module; read-only and deletion errors are unchanged.
1 parent 1644269 commit db29b90

9 files changed

Lines changed: 438 additions & 481 deletions

File tree

‎.github/workflows/openmpi-integration.yml‎

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ concurrency:
1313

1414
jobs:
1515
openmpi:
16-
name: Open MPI mpi_f08 · ${{ matrix.version }} · Ubuntu 24.04
17-
runs-on: ubuntu-24.04
16+
name: Open MPI mpi_f08 · ${{ matrix.version }} · ${{ matrix.target }}
17+
runs-on: ${{ matrix.runner }}
1818
timeout-minutes: 90
1919
strategy:
2020
fail-fast: false
@@ -24,13 +24,25 @@ jobs:
2424
# mpi_f08_types, 5.0 re-exports them from a configured mpi_types.
2525
- version: "4.1.8"
2626
series: "v4.1"
27+
target: Ubuntu 24.04
28+
runner: ubuntu-24.04
2729
- version: "5.0.11"
2830
series: "v5.0"
31+
target: Ubuntu 24.04
32+
runner: ubuntu-24.04
33+
- version: "4.1.8"
34+
series: "v4.1"
35+
target: macOS 15 ARM64
36+
runner: macos-15
37+
- version: "5.0.11"
38+
series: "v5.0"
39+
target: macOS 15 ARM64
40+
runner: macos-15
2941
env:
3042
OPENMPI_VERSION: ${{ matrix.version }}
3143
OPENMPI_SERIES: ${{ matrix.series }}
3244
PRIK_GFORTRAN_BINARY: gfortran-13
33-
PRIK_GFORTRAN_PACKAGE: gfortran-13
45+
PRIK_GCC_BINARY: gcc-13
3446
PYTHONPATH: .
3547
steps:
3648
- name: Checkout repository
@@ -43,24 +55,43 @@ jobs:
4355
run: |
4456
python -m pip install --upgrade pip
4557
python -m pip install -e ".[qa]"
46-
- name: Install pinned GFortran
58+
- name: Install pinned GFortran on Ubuntu
59+
if: runner.os == 'Linux'
4760
shell: bash
4861
run: |
49-
if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1; then
62+
if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1 || \
63+
! command -v "$PRIK_GCC_BINARY" >/dev/null 2>&1; then
5064
sudo apt-get update
51-
sudo apt-get install --yes "$PRIK_GFORTRAN_PACKAGE"
65+
sudo apt-get install --yes gfortran-13 gcc-13
66+
fi
67+
compiler_dir="$RUNNER_TEMP/prik-gfortran"
68+
mkdir -p "$compiler_dir"
69+
ln -sf "$(command -v "$PRIK_GFORTRAN_BINARY")" "$compiler_dir/gfortran"
70+
ln -sf "$(command -v "$PRIK_GCC_BINARY")" "$compiler_dir/gcc"
71+
echo "$compiler_dir" >> "$GITHUB_PATH"
72+
"$compiler_dir/gfortran" --version
73+
"$compiler_dir/gcc" --version
74+
- name: Install pinned GFortran on macOS
75+
if: runner.os == 'macOS'
76+
shell: bash
77+
run: |
78+
if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1 || \
79+
! command -v "$PRIK_GCC_BINARY" >/dev/null 2>&1; then
80+
brew install gcc@13
5281
fi
5382
compiler_dir="$RUNNER_TEMP/prik-gfortran"
5483
mkdir -p "$compiler_dir"
5584
ln -sf "$(command -v "$PRIK_GFORTRAN_BINARY")" "$compiler_dir/gfortran"
85+
ln -sf "$(command -v "$PRIK_GCC_BINARY")" "$compiler_dir/gcc"
5686
echo "$compiler_dir" >> "$GITHUB_PATH"
5787
"$compiler_dir/gfortran" --version
88+
"$compiler_dir/gcc" --version
5889
- name: Restore the Open MPI source, configured build, and installation
5990
id: openmpi-cache
6091
uses: actions/cache@v4
6192
with:
6293
path: ~/prik-openmpi/${{ matrix.version }}
63-
key: openmpi-${{ matrix.version }}-ubuntu-24.04-gfortran-13-v1
94+
key: openmpi-${{ matrix.version }}-${{ matrix.runner }}-gcc-gfortran-13-v2
6495
- name: Build and install Open MPI
6596
if: steps.openmpi-cache.outputs.cache-hit != 'true'
6697
shell: bash
@@ -71,23 +102,42 @@ jobs:
71102
| tar -xj -C "$root"
72103
mv "$root/openmpi-$OPENMPI_VERSION" "$root/source"
73104
cd "$root/build"
74-
../source/configure --prefix="$root/install" --enable-mpi-fortran=usempif08 FC=gfortran
75-
make -j"$(nproc)"
105+
../source/configure --prefix="$root/install" --enable-mpi-fortran=usempif08 CC=gcc FC=gfortran
106+
make -j2
76107
make install
77108
# The test reads sources and generated headers from these trees and
78109
# links the installation, so build objects are not cached.
79110
find . \( -name '*.o' -o -name '*.lo' -o -name '*.a' -o -name '*.la' \) -delete
80111
find . -type d -name .libs -prune -exec rm -rf {} +
112+
- name: Build mpi4py against this Open MPI installation
113+
shell: bash
114+
run: |
115+
root="$HOME/prik-openmpi/$OPENMPI_VERSION"
116+
export PATH="$root/install/bin:$PATH"
117+
export LD_LIBRARY_PATH="$root/install/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
118+
export DYLD_LIBRARY_PATH="$root/install/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
119+
export MPI4PY_BUILD_MPICC="$root/install/bin/mpicc"
120+
python -m pip install --no-cache-dir --no-binary=mpi4py mpi4py==4.1.2
81121
- name: Run the Open MPI mpi_f08 workflow test
82122
shell: bash
83123
env:
84124
PRIK_OPENMPI_REQUIRED: "1"
125+
PRIK_OPENMPI_BENCHMARK: "1"
126+
PRIK_OPENMPI_BENCHMARK_DIR: ${{ runner.temp }}/openmpi-benchmark
85127
run: |
86128
root="$HOME/prik-openmpi/$OPENMPI_VERSION"
87129
export PATH="$root/install/bin:$PATH"
88130
export LD_LIBRARY_PATH="$root/install/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
131+
export DYLD_LIBRARY_PATH="$root/install/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
89132
export PRIK_OPENMPI_SOURCE="$root/source"
90133
export PRIK_OPENMPI_BUILD="$root/build"
91134
export PRIK_OPENMPI_MPIFORT="$root/install/bin/mpifort"
92135
export PRIK_OPENMPI_LAUNCHER="$root/install/bin/mpirun"
93-
python -m pytest -q -rs tests/fortran/assumed_types/end_to_end/test_openmpi_f08.py
136+
python -m pytest -q -rs -s tests/fortran/assumed_types/end_to_end/test_openmpi_f08.py
137+
- name: Upload matched Open MPI benchmark results
138+
if: always()
139+
uses: actions/upload-artifact@v4
140+
with:
141+
name: openmpi-benchmark-${{ matrix.version }}-${{ matrix.runner }}
142+
path: ${{ runner.temp }}/openmpi-benchmark/*.json
143+
if-no-files-found: ignore

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ release tags add a leading `v` to the package version.
77

88
## Unreleased
99

10+
- Generated extension modules serve their module variables through
11+
descriptors on the module type, so looking up a function or any other
12+
ordinary attribute costs what it costs on a plain module instead of first
13+
being compared with every module variable name. `mpi.comm_rank(comm)` on
14+
the Open MPI tutorial's extension drops from 245 to 164 ns.
15+
- Open MPI integration CI now runs the `mpi_f08` tutorial on Linux and macOS
16+
against Open MPI 4.1 and 5.0 with paired GNU C/Fortran compilers, and
17+
compares its two-rank result with mpi4py built from the same installation.
18+
The tutorial provides a repeatable matched-installation benchmark and a
19+
labeled local results table comparing its wrapped API and mpi4py-style
20+
Python API with mpi4py, including relative timings. The benchmark binds
21+
each callable and fixes buffer placement before timing; CI uploads separate
22+
results for each platform and Open MPI version.
1023
- The test suite consolidates overlapping checks around compiled workflows and
1124
retains focused parser, semantic, diagnostic, and ABI boundary coverage;
1225
contributor guidance now favors observable behavior over implementation shape.

‎benchmarks/openmpi_f08.py‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Compare the tutorial's three APIs against one Open MPI installation.
2+
3+
Run each backend separately with two ranks from the directory containing the
4+
generated extension and ``prik_mpi.py``. Rank zero prints one JSON record.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import argparse
10+
import atexit
11+
import json
12+
import os
13+
import timeit
14+
15+
import mpi4py
16+
import numpy as np
17+
18+
19+
def aligned_int32(size: int, byte_offset: int) -> np.ndarray:
20+
"""Give each backend the same buffer placement modulo 4 KiB."""
21+
page_bytes = 4_096
22+
item_bytes = np.dtype(np.int32).itemsize
23+
backing = np.empty(size + page_bytes // item_bytes, dtype=np.int32)
24+
start = ((byte_offset - backing.ctypes.data % page_bytes) % page_bytes) // item_bytes
25+
return backing[start : start + size]
26+
27+
28+
def main() -> None:
29+
parser = argparse.ArgumentParser(description=__doc__)
30+
parser.add_argument("backend", choices=("wrapped", "python", "mpi4py"))
31+
backend = parser.parse_args().backend
32+
33+
if backend != "mpi4py":
34+
mpi4py.rc.initialize = False
35+
mpi4py.rc.finalize = False
36+
from mpi4py import MPI as timer_mpi
37+
38+
if backend == "wrapped":
39+
from prik_openmpi_f08 import mpi_f08 as mpi
40+
41+
mpi.init()
42+
atexit.register(mpi.finalize)
43+
comm = mpi.mpi_comm_world
44+
45+
def barrier() -> None:
46+
mpi.barrier(comm)
47+
48+
rank = int(mpi.comm_rank(comm))
49+
ranks = int(mpi.comm_size(comm))
50+
datatype = mpi.mpi_int
51+
op = mpi.mpi_sum
52+
rank_fn = mpi.comm_rank
53+
barrier_fn = mpi.barrier
54+
allreduce_fn = mpi.allreduce
55+
rank_call = "rank_fn(comm)"
56+
barrier_call = "barrier_fn(comm)"
57+
allreduce_call = "allreduce_fn(send, recv, datatype, op, comm)"
58+
elif backend == "python":
59+
import prik_mpi as mpi
60+
61+
comm = mpi.COMM_WORLD
62+
barrier = comm.Barrier
63+
rank = int(comm.Get_rank())
64+
ranks = int(comm.Get_size())
65+
datatype = None
66+
op = mpi.SUM
67+
rank_fn = comm.Get_rank
68+
barrier_fn = comm.Barrier
69+
allreduce_fn = comm.Allreduce
70+
rank_call = "rank_fn()"
71+
barrier_call = "barrier_fn()"
72+
allreduce_call = "allreduce_fn(send, recv, op=op)"
73+
else:
74+
mpi = timer_mpi
75+
comm = mpi.COMM_WORLD
76+
barrier = comm.Barrier
77+
rank = int(comm.Get_rank())
78+
ranks = int(comm.Get_size())
79+
datatype = None
80+
op = mpi.SUM
81+
rank_fn = comm.Get_rank
82+
barrier_fn = comm.Barrier
83+
allreduce_fn = comm.Allreduce
84+
rank_call = "rank_fn()"
85+
barrier_call = "barrier_fn()"
86+
allreduce_call = "allreduce_fn(send, recv, op=op)"
87+
88+
results: dict[str, float] = {}
89+
90+
def measure(
91+
statement: str, iterations: int, *, send: np.ndarray | None = None, recv: np.ndarray | None = None
92+
) -> float:
93+
timer = timeit.Timer(
94+
statement,
95+
timer=timer_mpi.Wtime,
96+
globals={
97+
"comm": comm,
98+
"send": send,
99+
"recv": recv,
100+
"datatype": datatype,
101+
"op": op,
102+
"rank_fn": rank_fn,
103+
"barrier_fn": barrier_fn,
104+
"allreduce_fn": allreduce_fn,
105+
},
106+
)
107+
barrier()
108+
timer.timeit(number=min(iterations, 100))
109+
samples = []
110+
for _ in range(5):
111+
barrier()
112+
samples.append(timer.timeit(number=iterations) * 1e9 / iterations)
113+
barrier()
114+
return min(samples)
115+
116+
for size, iterations in ((1, 20_000), (1_024, 20_000), (1_048_576, 8)):
117+
send = aligned_int32(size, 0)
118+
send.fill(rank + 1)
119+
recv = aligned_int32(size, 2_048)
120+
results[f"allreduce_{size}"] = measure(allreduce_call, iterations, send=send, recv=recv)
121+
expected = ranks * (ranks + 1) // 2
122+
if int(recv[0]) != expected or int(recv[-1]) != expected:
123+
raise AssertionError(f"Allreduce produced an incorrect result for {size} values")
124+
125+
results["barrier"] = measure(barrier_call, 20_000)
126+
results["get_rank"] = measure(rank_call, 200_000)
127+
if rank == 0:
128+
print(
129+
json.dumps(
130+
{
131+
"backend": backend,
132+
"mpi_version": os.environ.get("OPENMPI_VERSION"),
133+
"timer": "mpi4py.MPI.Wtime",
134+
"ranks": ranks,
135+
"ns_per_call": results,
136+
}
137+
)
138+
)
139+
140+
141+
if __name__ == "__main__":
142+
main()

‎docs/developer/workflows/ci.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ contributors need to administer.
1717
| --- | --- |
1818
| Static analysis | Linting, formatting, security, dead code, and changed-code complexity policy. |
1919
| Compiler and platform tests | Supported Python versions, Linux and macOS, GNU Fortran, IFX, and Flang. |
20-
| Open MPI Integration | The Open MPI `mpi_f08` workflow on Ubuntu for one Open MPI 4.1 and one 5.0 release: each is built from source, a restricted contract is generated from `mpi-f08.F90` with module discovery, and a two-rank program runs against the built wrapper. |
20+
| Open MPI Integration | The Open MPI `mpi_f08` workflow on Linux and macOS for one Open MPI 4.1 and one 5.0 release: each is built from source, a restricted contract is generated from `mpi-f08.F90` with module discovery, and a two-rank program runs against the built wrapper and mpi4py compiled with that installation. Matched-installation call timings are uploaded as benchmark artifacts. |
2121
| Real Libraries Portability | Maintained real-library examples across the hosted Linux and macOS architecture/compiler matrix, with deep BLAS and LAPACK audits on Linux x86-64. |
2222
| Documentation and benchmarks | Required performance benchmark and generated snapshot, documentation tests, and a strict site build. |
2323

0 commit comments

Comments
 (0)