Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 60 additions & 10 deletions .github/workflows/openmpi-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ concurrency:

jobs:
openmpi:
name: Open MPI mpi_f08 · ${{ matrix.version }} · Ubuntu 24.04
runs-on: ubuntu-24.04
name: Open MPI mpi_f08 · ${{ matrix.version }} · ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 90
strategy:
fail-fast: false
Expand All @@ -24,13 +24,25 @@ jobs:
# mpi_f08_types, 5.0 re-exports them from a configured mpi_types.
- version: "4.1.8"
series: "v4.1"
target: Ubuntu 24.04
runner: ubuntu-24.04
- version: "5.0.11"
series: "v5.0"
target: Ubuntu 24.04
runner: ubuntu-24.04
- version: "4.1.8"
series: "v4.1"
target: macOS 15 ARM64
runner: macos-15
- version: "5.0.11"
series: "v5.0"
target: macOS 15 ARM64
runner: macos-15
env:
OPENMPI_VERSION: ${{ matrix.version }}
OPENMPI_SERIES: ${{ matrix.series }}
PRIK_GFORTRAN_BINARY: gfortran-13
PRIK_GFORTRAN_PACKAGE: gfortran-13
PRIK_GCC_BINARY: gcc-13
PYTHONPATH: .
steps:
- name: Checkout repository
Expand All @@ -43,24 +55,43 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[qa]"
- name: Install pinned GFortran
- name: Install pinned GFortran on Ubuntu
if: runner.os == 'Linux'
shell: bash
run: |
if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1; then
if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1 || \
! command -v "$PRIK_GCC_BINARY" >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install --yes "$PRIK_GFORTRAN_PACKAGE"
sudo apt-get install --yes gfortran-13 gcc-13
fi
compiler_dir="$RUNNER_TEMP/prik-gfortran"
mkdir -p "$compiler_dir"
ln -sf "$(command -v "$PRIK_GFORTRAN_BINARY")" "$compiler_dir/gfortran"
ln -sf "$(command -v "$PRIK_GCC_BINARY")" "$compiler_dir/gcc"
echo "$compiler_dir" >> "$GITHUB_PATH"
"$compiler_dir/gfortran" --version
"$compiler_dir/gcc" --version
- name: Install pinned GFortran on macOS
if: runner.os == 'macOS'
shell: bash
run: |
if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1 || \
! command -v "$PRIK_GCC_BINARY" >/dev/null 2>&1; then
brew install gcc@13
fi
compiler_dir="$RUNNER_TEMP/prik-gfortran"
mkdir -p "$compiler_dir"
ln -sf "$(command -v "$PRIK_GFORTRAN_BINARY")" "$compiler_dir/gfortran"
ln -sf "$(command -v "$PRIK_GCC_BINARY")" "$compiler_dir/gcc"
echo "$compiler_dir" >> "$GITHUB_PATH"
"$compiler_dir/gfortran" --version
"$compiler_dir/gcc" --version
- name: Restore the Open MPI source, configured build, and installation
id: openmpi-cache
uses: actions/cache@v4
with:
path: ~/prik-openmpi/${{ matrix.version }}
key: openmpi-${{ matrix.version }}-ubuntu-24.04-gfortran-13-v1
key: openmpi-${{ matrix.version }}-${{ matrix.runner }}-gcc-gfortran-13-v2
- name: Build and install Open MPI
if: steps.openmpi-cache.outputs.cache-hit != 'true'
shell: bash
Expand All @@ -71,23 +102,42 @@ jobs:
| tar -xj -C "$root"
mv "$root/openmpi-$OPENMPI_VERSION" "$root/source"
cd "$root/build"
../source/configure --prefix="$root/install" --enable-mpi-fortran=usempif08 FC=gfortran
make -j"$(nproc)"
../source/configure --prefix="$root/install" --enable-mpi-fortran=usempif08 CC=gcc FC=gfortran
make -j2
make install
# The test reads sources and generated headers from these trees and
# links the installation, so build objects are not cached.
find . \( -name '*.o' -o -name '*.lo' -o -name '*.a' -o -name '*.la' \) -delete
find . -type d -name .libs -prune -exec rm -rf {} +
- name: Build mpi4py against this Open MPI installation
shell: bash
run: |
root="$HOME/prik-openmpi/$OPENMPI_VERSION"
export PATH="$root/install/bin:$PATH"
export LD_LIBRARY_PATH="$root/install/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export DYLD_LIBRARY_PATH="$root/install/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
export MPI4PY_BUILD_MPICC="$root/install/bin/mpicc"
python -m pip install --no-cache-dir --no-binary=mpi4py mpi4py==4.1.2
- name: Run the Open MPI mpi_f08 workflow test
shell: bash
env:
PRIK_OPENMPI_REQUIRED: "1"
PRIK_OPENMPI_BENCHMARK: "1"
PRIK_OPENMPI_BENCHMARK_DIR: ${{ runner.temp }}/openmpi-benchmark
run: |
root="$HOME/prik-openmpi/$OPENMPI_VERSION"
export PATH="$root/install/bin:$PATH"
export LD_LIBRARY_PATH="$root/install/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export DYLD_LIBRARY_PATH="$root/install/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
export PRIK_OPENMPI_SOURCE="$root/source"
export PRIK_OPENMPI_BUILD="$root/build"
export PRIK_OPENMPI_MPIFORT="$root/install/bin/mpifort"
export PRIK_OPENMPI_LAUNCHER="$root/install/bin/mpirun"
python -m pytest -q -rs tests/fortran/assumed_types/end_to_end/test_openmpi_f08.py
python -m pytest -q -rs -s tests/fortran/assumed_types/end_to_end/test_openmpi_f08.py
- name: Upload matched Open MPI benchmark results
if: always()
uses: actions/upload-artifact@v4
with:
name: openmpi-benchmark-${{ matrix.version }}-${{ matrix.runner }}
path: ${{ runner.temp }}/openmpi-benchmark/*.json
if-no-files-found: ignore
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ release tags add a leading `v` to the package version.

## Unreleased

- Generated extension modules serve their module variables through
descriptors on the module type, so looking up a function or any other
ordinary attribute costs what it costs on a plain module instead of first
being compared with every module variable name. `mpi.comm_rank(comm)` on
the Open MPI tutorial's extension drops from 245 to 164 ns.
- Open MPI integration CI now runs the `mpi_f08` tutorial on Linux and macOS
against Open MPI 4.1 and 5.0 with paired GNU C/Fortran compilers, and
compares its two-rank result with mpi4py built from the same installation.
The tutorial provides a repeatable matched-installation benchmark and a
labeled local results table comparing its wrapped API and mpi4py-style
Python API with mpi4py, including relative timings. The benchmark binds
each callable and fixes buffer placement before timing; CI uploads separate
results for each platform and Open MPI version.
- The test suite consolidates overlapping checks around compiled workflows and
retains focused parser, semantic, diagnostic, and ABI boundary coverage;
contributor guidance now favors observable behavior over implementation shape.
Expand Down
142 changes: 142 additions & 0 deletions benchmarks/openmpi_f08.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Compare the tutorial's three APIs against one Open MPI installation.

Run each backend separately with two ranks from the directory containing the
generated extension and ``prik_mpi.py``. Rank zero prints one JSON record.
"""

from __future__ import annotations

import argparse
import atexit
import json
import os
import timeit

import mpi4py
import numpy as np


def aligned_int32(size: int, byte_offset: int) -> np.ndarray:
"""Give each backend the same buffer placement modulo 4 KiB."""
page_bytes = 4_096
item_bytes = np.dtype(np.int32).itemsize
backing = np.empty(size + page_bytes // item_bytes, dtype=np.int32)
start = ((byte_offset - backing.ctypes.data % page_bytes) % page_bytes) // item_bytes
return backing[start : start + size]


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("backend", choices=("wrapped", "python", "mpi4py"))
backend = parser.parse_args().backend

if backend != "mpi4py":
mpi4py.rc.initialize = False
mpi4py.rc.finalize = False
from mpi4py import MPI as timer_mpi

if backend == "wrapped":
from prik_openmpi_f08 import mpi_f08 as mpi

mpi.init()
atexit.register(mpi.finalize)
comm = mpi.mpi_comm_world

def barrier() -> None:
mpi.barrier(comm)

rank = int(mpi.comm_rank(comm))
ranks = int(mpi.comm_size(comm))
datatype = mpi.mpi_int
op = mpi.mpi_sum
rank_fn = mpi.comm_rank
barrier_fn = mpi.barrier
allreduce_fn = mpi.allreduce
rank_call = "rank_fn(comm)"
barrier_call = "barrier_fn(comm)"
allreduce_call = "allreduce_fn(send, recv, datatype, op, comm)"
elif backend == "python":
import prik_mpi as mpi

comm = mpi.COMM_WORLD
barrier = comm.Barrier
rank = int(comm.Get_rank())
ranks = int(comm.Get_size())
datatype = None
op = mpi.SUM
rank_fn = comm.Get_rank
barrier_fn = comm.Barrier
allreduce_fn = comm.Allreduce
rank_call = "rank_fn()"
barrier_call = "barrier_fn()"
allreduce_call = "allreduce_fn(send, recv, op=op)"
else:
mpi = timer_mpi
comm = mpi.COMM_WORLD
barrier = comm.Barrier
rank = int(comm.Get_rank())
ranks = int(comm.Get_size())
datatype = None
op = mpi.SUM
rank_fn = comm.Get_rank
barrier_fn = comm.Barrier
allreduce_fn = comm.Allreduce
rank_call = "rank_fn()"
barrier_call = "barrier_fn()"
allreduce_call = "allreduce_fn(send, recv, op=op)"

results: dict[str, float] = {}

def measure(
statement: str, iterations: int, *, send: np.ndarray | None = None, recv: np.ndarray | None = None
) -> float:
timer = timeit.Timer(
statement,
timer=timer_mpi.Wtime,
globals={
"comm": comm,
"send": send,
"recv": recv,
"datatype": datatype,
"op": op,
"rank_fn": rank_fn,
"barrier_fn": barrier_fn,
"allreduce_fn": allreduce_fn,
},
)
barrier()
timer.timeit(number=min(iterations, 100))
samples = []
for _ in range(5):
barrier()
samples.append(timer.timeit(number=iterations) * 1e9 / iterations)
barrier()
return min(samples)

for size, iterations in ((1, 20_000), (1_024, 20_000), (1_048_576, 8)):
send = aligned_int32(size, 0)
send.fill(rank + 1)
recv = aligned_int32(size, 2_048)
results[f"allreduce_{size}"] = measure(allreduce_call, iterations, send=send, recv=recv)
expected = ranks * (ranks + 1) // 2
if int(recv[0]) != expected or int(recv[-1]) != expected:
raise AssertionError(f"Allreduce produced an incorrect result for {size} values")

results["barrier"] = measure(barrier_call, 20_000)
results["get_rank"] = measure(rank_call, 200_000)
if rank == 0:
print(
json.dumps(
{
"backend": backend,
"mpi_version": os.environ.get("OPENMPI_VERSION"),
"timer": "mpi4py.MPI.Wtime",
"ranks": ranks,
"ns_per_call": results,
}
)
)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion docs/developer/workflows/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contributors need to administer.
| --- | --- |
| Static analysis | Linting, formatting, security, dead code, and changed-code complexity policy. |
| Compiler and platform tests | Supported Python versions, Linux and macOS, GNU Fortran, IFX, and Flang. |
| 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. |
| 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. |
| 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. |
| Documentation and benchmarks | Required performance benchmark and generated snapshot, documentation tests, and a strict site build. |

Expand Down
Loading
Loading