Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
35b33b8
docs(parallelism): 📝 add rank-local OpenMP design and implementation …
robertodr Sep 18, 2026
9bede89
chore: spec and plan revision after grill-with-docs
robertodr Sep 22, 2026
35f359f
chore: spec and plan revision after rebase, add mini-app experiment
robertodr Sep 24, 2026
e88a436
chore: further refinement of spec and plan
robertodr Sep 24, 2026
7007658
feat(index-miniapp): 🧪 add Task 0 MPI-off index mini-app
Sep 25, 2026
1ebd05f
docs(parallelism): 📝 record Task 0 outcome and defer the ownership de…
Sep 25, 2026
7962864
feat(bench): ✨ add rank-local OpenMP parity evidence tooling
Sep 26, 2026
8fc2564
docs(parallelism): 📝 freeze the Task 1 baseline campaign
Sep 26, 2026
af430da
docs(parallelism): 📝 record where Task 0 and Task 1 artifacts live
Sep 27, 2026
ec59ad9
feat(parallel): ✨ add the OpenMP block worksharing primitive and requ…
Sep 27, 2026
d749787
docs(parallelism): 📝 record the Task 2 outcome
Sep 27, 2026
8cd7b62
feat(parallel): ✨ plumb per-object thread budgets and distributed fai…
Sep 28, 2026
ef39488
fix(bench): 🐛 read the process allocation before importing monoprop
Sep 28, 2026
1f87c01
docs(parallelism): 📝 record the Task 3 outcome
Sep 28, 2026
e96af6e
docs(parallelism): 📝 record the baseline tool sync and add a Task 11 …
Sep 28, 2026
ea42df6
feat(parallel): ✨ thread cosine scaling and fused coefficient updates
Sep 28, 2026
a3631cb
docs(parallelism): 📝 record the Task 4 outcome and plan a std::fma ev…
Sep 28, 2026
29acf78
feat(parallel): ✨ thread the bitmap scan with a source-ordered merge
Sep 28, 2026
24bb70e
docs(parallelism): 📝 record the Task 5 outcome
Sep 28, 2026
43efb16
feat(parallel): ✨ thread frozen lookup with checked incoming decode
Sep 29, 2026
a6a995a
docs(parallelism): 📝 record Task 6 and the decision to re-design with…
Sep 29, 2026
f31174c
docs(parallelism): 📝 plan the sharded OpenMP replacement
robertodr Sep 29, 2026
1cbcf2c
feat(parallel): ✨ add the fixed-team phase primitive for sharded OpenMP
Sep 29, 2026
4cdffac
docs(parallelism): 📝 record S0 acceptance and the notebook lint waiver
Sep 29, 2026
37d1ced
feat(parallel): ✨ add owner-initialized shard state for sharded OpenMP
Sep 30, 2026
a56daac
docs(parallelism): 📝 record the S1 outcome in the sharded OpenMP plan
Sep 30, 2026
ffb3a36
feat(parallel): ✨ add direct-buffer sharded construction and propagation
Sep 30, 2026
0c2503b
docs(parallelism): 📝 record the S2 outcome in the sharded OpenMP plan
Sep 30, 2026
910fde3
feat(parallel): ✨ add snapshot-safe sharded evaluation
Sep 30, 2026
f347e6f
docs(parallelism): 📝 record the S3 outcome in the sharded OpenMP plan
Sep 30, 2026
a790362
feat(parallel): ✨ integrate the sharded OpenMP prototype behind Monom…
Oct 1, 2026
7c4bc92
docs(parallelism): 📝 record the S4 outcome in the sharded OpenMP plan
Oct 1, 2026
b9dc2b3
style(parallel): 🎨 format the S4 prototype files and exempt the shard…
Oct 1, 2026
2f24091
refactor(parallel): ⚡️ cut partner lookup and handoff overhead in the…
Oct 1, 2026
567da52
docs(parallelism): 📝 record the S4 checkpoint, the rework decision an…
Oct 1, 2026
554c3c8
refactor(parallel): ⚡️ single-barrier checkpoints, staged partner rea…
Oct 2, 2026
dc2e102
docs(parallelism): 📝 record the second S4 rework, its checkpoint and …
Oct 2, 2026
1a22a8f
feat(parallel): ✨ run the sharded OpenMP prototype over P MPI ranks
Oct 2, 2026
ef103e9
docs(parallelism): 📝 record S5, its MPI+OpenMP checkpoint and the fla…
Oct 2, 2026
454830f
feat(parallel)!: ✨ final sharded OpenMP interface and test migration
Oct 3, 2026
c91eeae
docs(parallelism): 📝 record S6 and its acceptance
Oct 3, 2026
bb71b9a
perf(parallel): ⚡️ lay replay rounds out owner-parallel
Oct 4, 2026
2ab9b5e
docs(parallelism): 📝 record the post-S6 re-check, B-replay and B-memory
Oct 4, 2026
e877419
perf(parallel): ⚡️ cheaper exchange rounds, persistent staging and a …
Oct 7, 2026
b69152d
docs(parallelism): 📝 record S7 and the post-S7 remedy rounds
Oct 7, 2026
61ebbb1
docs(parallelism): 📝 note that the post-S7 remedy artifacts were deleted
Oct 7, 2026
50c56a1
perf(parallel): ⚡️ stagger the final shrink, reuse paring buffers, 8-…
Oct 7, 2026
62173a5
docs(parallelism): 📝 record round A and the open parity points
Oct 7, 2026
42ee0e4
docs(parallelism): 📝 schedule the open parity optimizations at the en…
Oct 7, 2026
d023e67
docs(parallelism): 📝 record the owner's waiver of the S7-pass prerequ…
Oct 7, 2026
1fcbae0
refactor(parallel)!: 🔥 remove the legacy partition runtime and requir…
Oct 8, 2026
d359c15
perf(sharded): ⚡️ close the remaining parity misses after the cutover
Oct 8, 2026
747b562
docs(parallelism): 📝 record S8's optimization rounds and the campaign…
Oct 8, 2026
5ce60df
docs(parallelism): 📝 record S8's final parity campaign
Oct 9, 2026
4fdddbc
perf(sharded): ⚡️ cut the per-process memory the sharded runtime adds…
Oct 10, 2026
393f172
docs(parallelism): 📝 record S8's optimization round 5
Oct 10, 2026
94cbf92
chore(merge): 🔀 merge origin/main into the sharded OpenMP refactor
Oct 11, 2026
1a4573d
perf(sharded): ⚡️ keep main's fold off the per-thread memory and the …
Oct 11, 2026
a28bc43
docs(parallelism): 📝 record the merge of main and step 6
Oct 12, 2026
1eaebd4
chore(merge): 🔀 merge origin/main's test refactors into the sharded O…
Oct 15, 2026
4e54b2e
docs(parallelism): 📝 record the open optimization points and the item…
Oct 15, 2026
3cec097
fix(tests): 🐛 build the stack-residency probe on macOS
Oct 16, 2026
7daa511
fix(build): 🐛 limit GCC's floating-point contraction to one expression
Oct 16, 2026
1c2aa8e
fix(tests): 🐛 let the multi-rank probes oversubscribe small runners
Oct 16, 2026
96756d4
test(ci): ✅ widen the TSan leg and close the review's CI gaps
Oct 16, 2026
43dce9c
fix(sharded): 🐛 keep argument errors recoverable and tighten failure …
Oct 16, 2026
c9cfa84
chore(merge): 🔀 merge origin/main's per-term expectation values into …
Oct 23, 2026
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
1 change: 1 addition & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
| grep -v '^$' \
| xargs apt-get -y install \
clang \
libomp-dev \
clang-tidy \
clang-format \
cmake \
Expand Down
8 changes: 8 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,16 @@ runs:
esac
fi

# OpenMP is required. g++ ships libgomp; a Linux Clang needs the libomp of its own
# version, even when the runner already provides the compiler.
if [[ "$RUNNER_OS" != "macOS" && "${CXX:-}" == clang++* ]]; then
packages+=("libomp${CXX#clang++}-dev")
fi

if [[ "$RUNNER_OS" == "macOS" ]]; then
brew install "${packages[@]}"
# Homebrew's libomp is keg-only; CMake's FindOpenMP searches OpenMP_ROOT.
echo "OpenMP_ROOT=$(brew --prefix libomp)" >> "$GITHUB_ENV"
else
sudo apt-get update
sudo apt-get install -y "${packages[@]}"
Expand Down
4 changes: 4 additions & 0 deletions .github/tsan-libomp.supp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ThreadSanitizer suppressions for LLVM's OpenMP runtime: its internal locking is not instrumented, so races
# reported from inside libomp itself are not evidence (docs/content/docs/building.mdx). Archer
# (OMP_TOOL_LIBRARIES=.../libarcher.so) supplies the OpenMP synchronization TSan needs for monoprop's own frames.
called_from_lib:libomp.so.5
39 changes: 21 additions & 18 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ on:
type: string
rungs:
description: >-
One rung per line, `<name> <ranks> <partitions> <rounds> | <pytest args>`.
`partitions` is an integer or `per-rank` for (physical cores / ranks).
One rung per line, `<name> <ranks> <threads> <rounds> | <pytest args>`.
`threads` (OpenMP threads, and so shards, per rank) is an integer or `per-rank` for
(physical cores / ranks).
Blank lines and `#` comments are ignored. A name may carry a `.suffix`: the
Bencher testbed is the part before the first `.`, so rungs sharing a prefix
share a plot, which is how one shape measured by two processes keeps its rows
Expand Down Expand Up @@ -47,7 +48,7 @@ jobs:
with:
# A cache filled by a non-MPI build would restore a monoprop without MPI, which every
# rung below then shares.
cache-suffix: benchmark-mpi
cache-suffix: benchmark-mpi-sharded
# The build below requires MPI::MPI_CXX, and the multi-rank rungs need mpiexec.
mpi: "on"

Expand Down Expand Up @@ -79,7 +80,7 @@ jobs:
mkdir -p benches/results
# fd 3, because mpiexec forwards its own stdin to rank 0 and would eat the rung list.
while IFS='|' read -r head args <&3; do
IFS=$' \t' read -r rung ranks partitions rounds <<<"$head"
IFS=$' \t' read -r rung ranks threads rounds <<<"$head"
case "${rung:-}" in '' | '#'*) continue ;; esac
# The name reaches a result filename, a Bencher testbed and the prefix split
# below, so hold it to what all three accept. A leading `.` would split to an
Expand All @@ -94,23 +95,23 @@ jobs:
# `0` divides by zero at per-rank and reaches `mpiexec -n 0` otherwise; and `08` is
# an invalid octal literal there. Rejecting leading zeros keeps that arithmetic
# base 10.
for field in ranks partitions rounds; do
for field in ranks threads rounds; do
value="${!field:-}"
[[ "$field" == partitions && "$value" == per-rank ]] && continue
[[ "$field" == threads && "$value" == per-rank ]] && continue
case "$value" in
'') echo "::error::rung $rung has no $field"; exit 1 ;;
*[!0-9]* | 0*)
echo "::error::rung $rung needs a positive decimal $field, not '$value'"
exit 1 ;;
esac
done
if [[ "$partitions" == per-rank ]]; then
partitions=$((BENCH_CORES / ranks))
if [[ "$threads" == per-rank ]]; then
threads=$((BENCH_CORES / ranks))
fi
# Oversubscription reports a plausible wall time and would silently corrupt the
# testbed.
if ((partitions < 1 || ranks * partitions > BENCH_CORES)); then
echo "::error::rung $rung wants ${ranks}x${partitions} on $BENCH_CORES physical cores"
if ((threads < 1 || ranks * threads > BENCH_CORES)); then
echo "::error::rung $rung wants ${ranks}x${threads} on $BENCH_CORES physical cores"
exit 1
fi
# xargs honours quotes in `-k "a and b"` without evaluating the input.
Expand All @@ -123,23 +124,25 @@ jobs:
readarray -t argv <<< "$parsed"
fi
label="$BENCH_LABEL-$rung"
echo "::group::$rung: ${ranks}x${partitions}, $rounds rounds -> $label"
# `monoprop_PARTITIONS` also sizes the pinning below -- unpinned ranks vary by more
echo "::group::$rung: ${ranks}x${threads}, $rounds rounds -> $label"
# An exact team of T per rank, one thread per core of the rank's slot: placement is
# the launcher's and OpenMP's, never the library's. Unpinned threads vary by more
# than the effects being tracked.
export monoprop_PARTITIONS="$partitions" monoprop_NUM_THREADS="$partitions"
export monoprop_NUM_THREADS="$threads" OMP_NUM_THREADS="$threads" OMP_DYNAMIC=FALSE
export OMP_PROC_BIND=close OMP_PLACES=cores
export monoprop_BENCH_LABEL="$label" monoprop_BENCH_RESULTS=benches/results
if ((ranks == 1)); then
uv run --no-sync python -m pytest benches -o filterwarnings=default \
--benchmark-json="benches/results/time-$label.json" \
--runtime-shape=openmp --benchmark-json="benches/results/time-$label.json" \
-m "${monoprop_BENCH_MARKERS-not slow}" \
--bench-rounds "$rounds" "${argv[@]}"
else
uv run --no-sync mpiexec -n "$ranks" \
--map-by "slot:PE=$monoprop_PARTITIONS" --bind-to core \
--map-by "slot:PE=$threads" --bind-to core \
-x monoprop_BENCH_LABEL -x monoprop_BENCH_RESULTS \
-x monoprop_PARTITIONS -x monoprop_NUM_THREADS \
-x monoprop_NUM_THREADS -x OMP_NUM_THREADS -x OMP_DYNAMIC -x OMP_PROC_BIND -x OMP_PLACES \
python -m pytest benches -o filterwarnings=default \
--benchmark-json="benches/results/time-$label.json" \
--runtime-shape=openmp --benchmark-json="benches/results/time-$label.json" \
-m "${monoprop_BENCH_MARKERS-not slow}" \
--bench-rounds "$rounds" "${argv[@]}"
fi
Expand All @@ -148,7 +151,7 @@ jobs:
# To the log too: a rung that OOMs the instance takes the artifact with it.
cat "bmf-$rung.json"
uv run --no-sync python .github/workflows/scripts/check_shape.py \
"benches/results/$label.json" "$ranks" "$partitions"
"benches/results/$label.json" "$ranks" "$threads"
echo "::endgroup::"
done 3<<< "$RUNGS"

Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/nix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ on:
- VERSION
- pyproject.toml
- CMakeLists.txt
- cmake/**
- cpp/**/CMakeLists.txt
- cpp/tests/find_package_smoke/**
- cpp/tests/link_export_probe/**
- src/monoprop/bindings/CMakeLists.txt

concurrency:
Expand Down Expand Up @@ -67,10 +71,18 @@ jobs:
ninja --version
uv --version
just --version
pkg-config --modversion hwloc
mpicc --version
'

# Configures, links and runs the installed-package consumer, which instantiates the
# OpenMP worksharing template, with the shell's compiler and OpenMP runtime.
- name: Check the OpenMP consumer from the dev shell
run: |
nix develop --command bash -euo pipefail -c '
monoprop_ENABLE_MPI=OFF just build --group workspace-test
just test-find-package
'

flake-result:
name: All flake checks
if: always()
Expand Down
28 changes: 19 additions & 9 deletions .github/workflows/qa-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: Set up the build environment
uses: ./.github/actions/setup
with:
cache-suffix: coverage-mpi-${{ matrix.mpi }}
cache-suffix: coverage-mpi-${{ matrix.mpi }}-sharded
mpi: ${{ matrix.mpi }}

- name: Collect coverage
Expand Down Expand Up @@ -142,7 +142,7 @@ jobs:
- name: Set up the build environment
uses: ./.github/actions/setup
with:
cache-suffix: gcc-asan-ubsan
cache-suffix: gcc-asan-ubsan-sharded

- name: Install package
run: just build --group workspace-test
Expand All @@ -157,16 +157,23 @@ jobs:
if: failure()
run: just sanitizer-reports

gcc-tsan:
clang-tsan:
# TSan cannot load instrumented _core into stock CPython; Python coverage needs an
# instrumented interpreter. C++ tests cover the relevant partition and ShmComm paths.
name: GCC ThreadSanitizer (C++)
# instrumented interpreter. The C++ tests cover the sharded runtime's concurrency surface,
# whose OpenMP teams need LLVM's libomp with its Archer tool: GCC's libgomp is not
# TSan-aware, so its reports are not evidence either way (docs/content/docs/building.mdx).
name: Clang ThreadSanitizer with Archer (C++)
runs-on: ubuntu-26.04
permissions:
contents: read
env:
CXX: clang++-18
CC: clang-18
SKBUILD_CMAKE_BUILD_TYPE: "Tsan"
SKBUILD_CMAKE_DEFINE: "monoprop_SANITIZER=tsan"
OMP_TOOL_LIBRARIES: /usr/lib/llvm-18/lib/libarcher.so
# libomp's own internal locking is not instrumented.
TSAN_OPTIONS: "suppressions=${{ github.workspace }}/.github/tsan-libomp.supp"

steps:
- uses: actions/checkout@v7.0.1
Expand All @@ -176,12 +183,12 @@ jobs:
- name: Set up the build environment
uses: ./.github/actions/setup
with:
cache-suffix: gcc-tsan
cache-suffix: clang-tsan-sharded

- name: Install package
run: just build --group workspace-test

- name: Run C++ partition and ShmComm tests
- name: Run the sharded runtime's concurrency tests
run: just test-cpp-tsan

sonarqube-analysis:
Expand All @@ -197,7 +204,7 @@ jobs:
- name: Set up the build environment
uses: ./.github/actions/setup
with:
cache-suffix: sonarqube-mpi
cache-suffix: sonarqube-mpi-sharded
mpi: "on"

- name: Install package
Expand Down Expand Up @@ -239,8 +246,11 @@ jobs:

- name: Set up the build environment
uses: ./.github/actions/setup
# The build below uses clang++, so the setup installs its matching libomp.
env:
CXX: clang++
with:
cache-suffix: clang-tidy-mpi
cache-suffix: clang-tidy-mpi-sharded
mpi: "on"

- name: Build (generates compile_commands.json)
Expand Down
30 changes: 14 additions & 16 deletions .github/workflows/scripts/check_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@

"""Fail a rung that did not run at the shape it was asked for.

A knob that fails to reach the ranks does not fail the run: it measures one partition per
rank at a plausible wall time. Neither does `partitions` above a rank's visible cores --
that disables placement entirely, at 13.8x to 24.6x, warning only on C++ stderr, which
pytest's capture eats.
A knob that fails to reach the ranks does not fail the run: it measures a different team at a
plausible wall time. So the recorded rank count and per-rank thread count must be the rung's,
and the removed partition runtime's variable must not be set at all. Placement is the launcher's
and OpenMP's (`--bind-to core`, `OMP_PROC_BIND`/`OMP_PLACES`); its summary is printed for the log.

Usage: check_shape.py <results/<label>.json> <ranks> <partitions>
Usage: check_shape.py <results/<label>.json> <ranks> <threads>
"""

from __future__ import annotations
Expand All @@ -30,28 +30,26 @@


def main(argv: list[str]) -> str | None:
path, ranks, partitions = Path(argv[1]), int(argv[2]), argv[3]
path, ranks, threads = Path(argv[1]), int(argv[2]), argv[3]
meta = json.loads(path.read_text())["meta"]
pin = meta.get("pinning", {})
threads = pin.get("threads", 0)
placed = pin.get("single_cpu_threads_min", 0)

print(
f" ranks={meta['ranks']} partitions_env={meta['partitions_env']} "
f"threads={meta['monoprop_threads']} pinned>={placed} of {threads} "
f" ranks={meta['ranks']} threads={meta['monoprop_threads']} "
f"partitions_env={meta['partitions_env']} "
f"single-cpu threads>={pin.get('single_cpu_threads_min', 0)} of {pin.get('threads', 0)} "
f"mask={pin.get('affinity_cpus_min')}..{pin.get('affinity_cpus_max')}"
)

if meta["ranks"] != ranks or meta["partitions_env"] != partitions:
if meta["ranks"] != ranks or meta["monoprop_threads"] != threads:
return (
f"::error::{path.stem} ran at ranks={meta['ranks']} "
f"partitions_env={meta['partitions_env']}, asked for {ranks}x{partitions}"
f"threads={meta['monoprop_threads']}, asked for {ranks}x{threads}"
)
# An all-zero summary means /proc was unreadable, not that nothing was pinned.
if int(partitions) > 1 and threads and not placed:
if meta["partitions_env"] != "unset":
return (
f"::error::{path.stem} placed no thread on a CPU of its own. "
"partitions above the rank's visible cores return an empty placement order."
f"::error::{path.stem} ran with monoprop_PARTITIONS={meta['partitions_env']}; "
"the sharded runtime ignores it, so a rung must not set it"
)
return None

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/scripts/resolve_cores.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def main() -> str | None:
if not monoprop.has_mpi:
return "monoprop was built without MPI; every rung shares this binary"

# PHYSICAL cores, and only the ones this process may run on. Physical, because that is what
# the engine enumerates and the logical count would cross the `partitions > visible cores`
# threshold on any SMT machine. Visible, because psutil counts the whole machine: under a
# PHYSICAL cores, and only the ones this process may run on. Physical, because the rungs size
# their thread counts in cores and an SMT sibling is not a second core; monoprop itself takes
# whatever OpenMP reports. Visible, because psutil counts the whole machine: under a
# mask -- a cpuset, taskset, or a shared runner -- that number oversubscribes every rung and
# names the testbed after cores the run never had.
visible = os.sched_getaffinity(0)
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ jobs:
uses: ./.github/actions/setup
with:
python-version: ${{ matrix.python-version }}
cache-suffix: ${{ matrix.compiler }}-mpi-${{ matrix.mpi }}
# The MPI-off and MPI builds are ABI-incompatible, and so is every build from before the partition runtime was
# removed: the `sharded` part retires the caches of the dual-runtime matrix.
cache-suffix: ${{ matrix.compiler }}-mpi-${{ matrix.mpi }}-sharded
mpi: ${{ matrix.mpi }}

- name: Install package
Expand All @@ -87,6 +89,9 @@ jobs:
- name: Get monoprop information
run: just info

- name: Verify the runtime and MPI identity of the build
run: just check-runtime

- name: Verify that find_package(monoprop) works
run: just test-find-package

Expand Down
30 changes: 30 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,38 @@ just build-docs
- Python reference data lives in `tests/reference_data.py` (`load_problem()` reads `tests/data/*.msgpack`); C++ uses
`test_utils::load_case()` from `cpp/tests/TestData.h`.
- Pytest's fd capture hides C++ stderr such as `COMMPROF`; rerun with `-s`.
- Benchmark evidence: `pytest benches --runtime-shape=... --build-mode=...` refuses contradictory
runs. `tools/benchmark-rank-local-openmp.py` has exactly three modes, `observe`, `validate` and
`compare` (see `docs/content/docs/benchmarks.mdx`). Launch MPI-off runs directly: `mpi4py` is
never imported when `monoprop.has_mpi` is false.
- `uv sync` does not relink `bin/monoprop_unit_tests.x` -- compare mtimes and use the standalone
recipe in `docs/content/docs/building.mdx`.
- OpenMP is a required build dependency in every configuration (`OpenMP::OpenMP_CXX`, linked PUBLIC on
both `monoprop-objs` and `monoprop`, `find_dependency` in the package config). The worksharing
primitive is `detail/parallel/Workshare.h::for_blocks`. Never call `omp_set_*` from the library.
The library calls the runtime (`ThreadBudget.cpp`), so `import monoprop` loads it: with `OMP_PLACES`
set and binding on, the importing thread is bound to the first place.
- One runtime: each rank's `MonomialPropagator` owns T shards (`detail/sharded/`), T captured once at
construction from `monoprop_NUM_THREADS` (strict parser; unset falls back to `omp_get_max_threads()`).
There is no partition runtime, selector or `monoprop_PARTITIONS` reader. Every MPI-using operation
enforces MPI's initializing thread; only that thread (the team's primary) calls MPI, so FUNNELED is
required. Every state operation separates validation from mutation (`run_operation_`): a failure after
mutation starts invalidates the object and aborts a multi-rank communicator (`mpi::operation_failed`).
Catch failures inside posted `Ticket`/`PendingAlltoallv`/physical-round lifetimes, never only at the
outermost level. `detail::build_layer` (with `run_exchange` and `mpi::begin_alltoallv`) is the low-level
one-owner-per-rank engine kept only as the tests' reference; production builds layers through
`detail/sharded/Construction.h`.
- Threaded kernels (`scale_cos_*`, `apply_fused_contract`, the `fused_find_and_collect` scan, the resolve
phases: incoming decode, `probe_frozen_positions`, certified-sink scatter) take a defaulted test-only
range observer; participation tests read it (`cpp/tests/KernelTestSupport.h`, `ScanTestSupport.h`).
Serial and threaded runs must agree bitwise. Scan workers only traverse frozen storage and merge in range
order; only typed cutoffs (`CutoffEvaluator::parallel_safe`) run on them. Probes read a frozen store;
received query streams are validated on the caller (`QueryWire::checked_extent`) before any decode;
miss IDs, insertion (`bulk_insert_hashed`) and inverted-index maintenance stay single-writer. A sink's
`on_resolved` runs on workers only with `static constexpr bool parallel_resolve = true`. TSan evidence
needs Clang + libomp + Archer; GCC's libgomp gives false reports (`building.mdx`).
- `cpp/tests/mpi_failure_driver.cpp` has its own `main()`, excluded from the unit-runner glob;
`ctest -L mpi_failure` runs its two-rank abort scenarios (30 s timeout; a timeout is a hang).
- Slow CTest startup in MPI builds is `MPI_Init` fabric probing; see
`monoprop_TEST_EXCLUDE_MPI_FABRIC` in `cpp/tests/CMakeLists.txt`.
- Sanitizer rebuild and test:
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ find_package(

find_package(Threads REQUIRED)

# Required in every configuration, including MPI-off and single-thread builds: there is no
# OpenMP-disabled variant of the library.
find_package(OpenMP REQUIRED COMPONENTS CXX)

if(monoprop_ENABLE_MPI)
find_package(MPI REQUIRED COMPONENTS CXX)
endif()
Expand Down
Loading
Loading