Skip to content
Open
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
3 changes: 3 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"source=monoprop-docs-next,target=${containerWorkspaceFolder}/docs/.next,type=volume",
"source=monoprop-docs-node-modules,target=${containerWorkspaceFolder}/docs/node_modules,type=volume"
],
"runArgs": [
"--shm-size=1g"
],
"remoteUser": "vscode",
"customizations": {
"vscode": {
Expand Down
44 changes: 22 additions & 22 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,37 +132,37 @@ jobs:
using namespace monoprop;

auto main() -> int {
constexpr size_t kModes = 2;
OperatorDict ham;
ham[VecZ{0, 1}] = std::complex<double>{0.0, 1.0};

// Graph-building / Schrodinger path: detail/graph_encoding/MPGraphEncodingStorage.h.
MonomialPropagator<kModes> graph_sim(ham,
2 * kModes,
VecZ{0, 1},
std::optional<unsigned int>{4U},
MPI_COMM_SELF,
std::nullopt,
std::nullopt,
CutoffType::Length,
std::nullopt);
MonomialPropagator graph_sim(ham,
6,
VecZ{0, 1},
4,
std::optional{4U},
MPI_COMM_SELF,
std::nullopt,
std::nullopt,
CutoffType::Length,
std::nullopt);
const std::vector<VecZ> monos{{0}, {1}, {2}};
graph_sim.build_graph(monos, VecZ{0, 1, 2}, VecD{1.0, 1.0, 1.0});
graph_sim.graph_memory_usage();
graph_sim.expectation_value_and_gradient(VecD{0.1, 0.2, 0.3});

MonomialPropagator<kModes> partition_sim(ham,
2 * kModes,
VecZ{0, 1},
std::nullopt,
MPI_COMM_SELF,
std::nullopt,
std::nullopt,
CutoffType::Length,
std::nullopt,
kModes,
Basis::Majorana,
2);
MonomialPropagator partition_sim(ham,
6,
VecZ{0, 1},
4,
std::nullopt,
MPI_COMM_SELF,
std::nullopt,
std::nullopt,
CutoffType::Length,
std::nullopt,
Basis::Majorana,
2);
partition_sim.size();

return 0;
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ Thumbs.db

external/upstream/_srcs/
tests/cpp/_srcs/
_dispatch*.py
_constants.py
build*/
Testing/
Expand All @@ -178,6 +177,8 @@ benches/results/**
# devcontainer files
.devcontainer/devcontainer-lock.json

notes/**

# `just capture-baseline` / `just diff-baseline` output (tools/capture-baseline.py)
.baseline-capture/**

Expand Down
222 changes: 155 additions & 67 deletions AGENTS.md

Large diffs are not rendered by default.

41 changes: 21 additions & 20 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release")
endif()

set(
monoprop_MAX_NUM_MODES
"250"
CACHE STRING
"Maximum number of simulable Fermionic modes with Python bindings"
)
option(monoprop_ENABLE_MPI "Enable MPI parallelization" OFF)
option(
monoprop_WIDE_TERM_INDEX
Expand Down Expand Up @@ -77,14 +71,17 @@ endif()
include(${PROJECT_SOURCE_DIR}/cmake/compiler_flags/Sanitizers.cmake)
include(${PROJECT_SOURCE_DIR}/cmake/compiler_flags/CXXFlags.cmake)

# The storage width at or above which a propagator picks the support-form row store over the dense one.
# Derived from whether ARCH_FLAG is actually emitted rather than from the option that asks for it, and
# deliberately not a cache entry: what moves the crossover is the target ISA, so a stale cached value
# would silently pick the wrong backend after a flag change.
# Sparse/dense crossover depends on vector popcount support.
# Dense scales with storage-word passes, while sparse is mostly width-flat.
# Without vector popcount, dense degrades earlier.
#
# This is intentionally not a cache variable: it should track the flags actually used
# for compilation. monoprop_ROW_STORE=dense|sparse already lets users force a backend
# at runtime without rebuilding.
#
# Thresholds are the first full 32-mode block where sparse is clearly faster than dense beyond
# run-to-run noise. Expect about +/-1 block variation across machines due to cache and popcount
# throughput.
# Thresholds are the first full 32-mode block where sparse is clearly faster than dense
# beyond run-to-run noise, measured end-to-end in the propagator. Expect about +/-1 block
# variation across machines due to cache and popcount throughput.
if(ARCH_FLAG)
set(monoprop_SPARSE_ROW_MIN_MODES 768)
else()
Expand All @@ -106,10 +103,6 @@ message(
" Build-type-specific : ${_cmake_build_type_specific_flags}"
)
message(STATUS " Vectorization flag : ${ARCH_FLAG}")
message(
STATUS
" Sparse rows from : ${monoprop_SPARSE_ROW_MIN_MODES} modes"
)
message(
STATUS
" Project defaults : ${CMAKE_CXX${CMAKE_CXX_STANDARD}_STANDARD_COMPILE_OPTION} ${monoprop_CXX_FLAGS}"
Expand All @@ -119,8 +112,11 @@ message(STATUS " Sanitizer profile : ${monoprop_SANITIZER}")

message(STATUS " MPI parallelization : ${monoprop_ENABLE_MPI}")
message(STATUS " Wide term index : ${monoprop_WIDE_TERM_INDEX}")
message(STATUS " Max simulable modes : ${monoprop_MAX_NUM_MODES}")
message(STATUS " C++ unit tests : ${monoprop_ENABLE_CXX_UNIT_TESTS}")
message(
STATUS
" Sparse rows from : ${monoprop_SPARSE_ROW_MIN_MODES} modes"
)

include(GNUInstallDirs)

Expand All @@ -130,8 +126,13 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
add_library(monoprop-objs OBJECT "")
add_library(monoprop SHARED $<TARGET_OBJECTS:monoprop-objs>)

# must run before add_subdirectory(cpp): CTest's enabled-ness does not propagate
# back up to a parent directory that has already been added as a subdirectory.
# Testing is enabled from the *top-level* list file, and must run before add_subdirectory(cpp), on
# purpose: CTest's root is wherever enable_testing() was called, so called from cpp/ it wrote no
# top-level CTestTestfile.cmake and every documented entry point (the CMakePresets test presets,
# `just test-mpi`, `just test-wide`) pointed ctest at a directory with no tests -- ctest reports "No
# tests were found" and exits 0 for that, so those commands were silently running nothing. And
# CTest's enabled-ness does not propagate back up to a parent directory that has already been added
# as a subdirectory, so this must precede add_subdirectory(cpp) below rather than follow it.
if(monoprop_ENABLE_CXX_UNIT_TESTS)
enable_testing()
include(CTest)
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,10 @@ just test-wide # Python + C++ unit tests with a 64-bit T
just test-sparse-rows # Python tests with the support-form row backend forced
```

The C++ suite runs against both row backends: `ctest` registers every case a second
time with `monoprop_ROW_STORE=sparse`, labelled `sparse-rows`.

See the [testing guide](https://docs.monoprop.algorithmiq.tech/testing)
for the with/without-MPI details and the rank matrix.
`ctest` runs every C++ case twice, once per row backend — the second pass carries
the `sparse-rows` label. See the
[testing guide](https://docs.algorithmiq.fi/monoprop/docs/testing) for that, the
with/without-MPI details, and the rank matrix.

## Repository layout

Expand Down
14 changes: 12 additions & 2 deletions benches/bench_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def test_model(
benchmark,
bench_comm,
model_configs,
model_rounds,
model,
record_model_config,
record_model_stats,
Expand All @@ -64,7 +65,12 @@ def test_model(
state: dict[str, Any] = {}

def setup():
state["baseline_rss"] = resting_rss_bytes()
# First round only: only then does setup() run before any model is built, matching
# `Baseline RSS` (resting memory before construction). In later rounds,
# pytest-benchmark still holds the previous round's args during setup(), so the
# old propagator is still live and cannot be reclaimed. That would make the reading
# baseline + one full model and misstate the model's memory cost versus `Peak RSS`.
state.setdefault("baseline_rss", resting_rss_bytes())
state["built"] = build_fn(config, comm=bench_comm)
return (state["built"], steps), {}

Expand All @@ -74,10 +80,14 @@ def run(built, n_steps):
propagator.propagate(circuit)
return propagator.expectation_value()

# setup() runs before every round, so each round rebuilds the model and evolves a fresh
# propagator -- these simulations are in place, and replaying a mutated one would time the wrong
# thing. record_model_stats below then describes the last round, which is what any round would
# produce: the term counts and memory are deterministic.
result = benchmark.pedantic(
barriered(run, bench_comm),
setup=barrier_setup(bench_comm, setup),
rounds=1,
rounds=model_rounds,
iterations=1,
)
assert isinstance(result, float)
Expand Down
27 changes: 21 additions & 6 deletions benches/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ def pytest_addoption(parser: pytest.Parser) -> None:
group.addoption(f"--{name}", type=int, default=default, help=help_text)

models = parser.getgroup("monoprop-models", "monoprop fixed-model overrides")
# One round is enough for the memory and term-count stats, which are deterministic, but it yields
# no spread for the timing -- and these models are expensive enough that a single sample can sit
# well off the median. Raise this when a timing difference is the point of the run.
models.addoption(
"--model-rounds",
type=int,
default=1,
help="Rounds per fixed model; each rebuilds the model first. >1 gives a median and stddev "
"(default: 1).",
)
for model, (config_cls, _builder, _steps) in MODELS.items():
for field in fields(config_cls):
models.addoption(
Expand Down Expand Up @@ -225,7 +235,6 @@ def _meta(nodes: int, ranks_per_node: int) -> dict[str, Any]:
"python_version": platform.python_version(),
"nanobind_version": monoprop.__nanobind_version__,
"nanobind_backend_version": nanobind_backend_version,
"monoprop_max_num_modes": monoprop.MAX_NUM_MODES,
"malloc_arena_max": os.environ.get("MALLOC_ARENA_MAX", "default"),
"omp_num_threads": os.environ.get("OMP_NUM_THREADS", "default"),
# Filled by _record_placement: the threads exist only once a propagator does.
Expand All @@ -242,11 +251,11 @@ def _meta(nodes: int, ranks_per_node: int) -> dict[str, Any]:
def _record_row_store(propagator: Any) -> None:
"""Fold one propagator's resolved row backend into this run's metadata.

``monoprop_ROW_STORE`` says what was asked for, not what ran: unset lets the mode width pick, and
the crossover it picks against is a build-time constant. The two backends accumulate a term sum in
different orders and have different footprints, so a report has to name the one that ran. Widths
differ within a run, hence so can the backend: a disagreement records as ``"mixed"`` rather than
letting the last propagator speak for the others.
``monoprop_ROW_STORE`` says what was asked for, not what ran: unset lets the storage width pick,
and the crossover it picks against is a build-time constant. The two backends accumulate a term
sum in different orders and have different footprints, so a report has to name the one that ran.
Widths differ within a run, hence so can the backend: a disagreement records as ``"mixed"``
rather than letting the last propagator speak for the others.
"""
if _rank() != 0:
return
Expand Down Expand Up @@ -317,6 +326,12 @@ def bench_rounds(request: pytest.FixtureRequest) -> int:
return int(request.config.getoption("--bench-rounds"))


@pytest.fixture(scope="session")
def model_rounds(request: pytest.FixtureRequest) -> int:
"""Return the round count for the fixed-model benchmarks."""
return int(request.config.getoption("--model-rounds"))


@pytest.fixture(scope="session")
def model_configs(request: pytest.FixtureRequest) -> dict[str, Any]:
"""Return each fixed model's config, every field resolved from the CLI.
Expand Down
44 changes: 18 additions & 26 deletions cmake/compiler_flags/CXXFlags.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -81,41 +81,33 @@ if(monoprop_ENABLE_ARCH_FLAGS)
endif()
endif()

# Query the machine-dependent flags for a given -march value and store the
# cleaned, space-separated string in the variable named by OUTPUT_VARIABLE. A
# MARCH of "default" queries the default target (no -march flag).
# Query the machine-dependent flags the compiler applies under a given architecture selection and
# store the cleaned, space-separated string in the variable named by OUTPUT_VARIABLE.
#
# Usage:
# _monoprop_query_machine_flags(MARCH <arch> OUTPUT_VARIABLE <var>)
# _monoprop_query_machine_flags(ARCH_FLAGS <flags...> OUTPUT_VARIABLE <var>)
function(_monoprop_query_machine_flags)
set(
_one_value_args
MARCH
OUTPUT_VARIABLE
)
cmake_parse_arguments(PARSE_ARGV 0 _arg "" "${_one_value_args}" "")
cmake_parse_arguments(PARSE_ARGV 0 _arg "" "OUTPUT_VARIABLE" "ARCH_FLAGS")

if(NOT _arg_OUTPUT_VARIABLE)
message(
FATAL_ERROR
"_monoprop_query_machine_flags: OUTPUT_VARIABLE is required"
)
endif()
if(NOT _arg_MARCH)
message(FATAL_ERROR "_monoprop_query_machine_flags: MARCH is required")
endif()

if(_arg_MARCH STREQUAL "default")
set(_march_args "")
set(_arch_args ${_arg_ARCH_FLAGS})
if(_arch_args)
string(JOIN " " _arch_label ${_arch_args})
else()
set(_march_args "-march=${_arg_MARCH}")
set(_arch_label "the default target")
endif()

if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
execute_process(
COMMAND
# gersemi: off
${CMAKE_CXX_COMPILER} ${_march_args} -\#\#\# -x c++ -c /dev/null
${CMAKE_CXX_COMPILER} ${_arch_args} -\#\#\# -x c++ -c /dev/null
# gersemi: on
ERROR_VARIABLE _query_output
ERROR_STRIP_TRAILING_WHITESPACE
Expand All @@ -124,7 +116,7 @@ function(_monoprop_query_machine_flags)
if(NOT _query_result EQUAL 0)
message(
WARNING
"Failed to query machine-dependent flags for '${_arg_MARCH}' with AppleClang (exit code ${_query_result}). Continuing with empty machine flags."
"Failed to query machine-dependent flags for ${_arch_label} with AppleClang (exit code ${_query_result}). Continuing with empty machine flags."
)
set(_flags "")
else()
Expand All @@ -141,15 +133,15 @@ function(_monoprop_query_machine_flags)
if(NOT _parse_result EQUAL 0)
message(
WARNING
"Failed to parse AppleClang machine-dependent flags for '${_arg_MARCH}' (exit code ${_parse_result}). Continuing with empty machine flags."
"Failed to parse AppleClang machine-dependent flags for ${_arch_label} (exit code ${_parse_result}). Continuing with empty machine flags."
)
set(_flags "")
endif()
endif()
else()
execute_process(
COMMAND
${CMAKE_CXX_COMPILER} ${_march_args} -Q --help=target
${CMAKE_CXX_COMPILER} ${_arch_args} -Q --help=target
COMMAND
${Python_EXECUTABLE} "${PROJECT_SOURCE_DIR}/tools/target-help-clean.py"
--mode gcc
Expand All @@ -160,19 +152,19 @@ function(_monoprop_query_machine_flags)
if(NOT _result EQUAL 0)
message(
FATAL_ERROR
"Failed to query machine-dependent flags for '${_arg_MARCH}' (exit code ${_result})"
"Failed to query machine-dependent flags for ${_arch_label} (exit code ${_result})"
)
endif()
endif()
set(${_arg_OUTPUT_VARIABLE} "${_flags}" PARENT_SCOPE)
endfunction()

# Empty is the no-arch-flag build and queries the default target.
set(monoprop_DEFAULT_VARIANT_FLAGS "")
if(monoprop_ENABLE_ARCH_FLAGS)
_monoprop_query_machine_flags(MARCH native OUTPUT_VARIABLE monoprop_DEFAULT_VARIANT_FLAGS)
else()
_monoprop_query_machine_flags(MARCH default OUTPUT_VARIABLE monoprop_DEFAULT_VARIANT_FLAGS)
endif()
_monoprop_query_machine_flags(
ARCH_FLAGS ${ARCH_FLAG}
OUTPUT_VARIABLE monoprop_DEFAULT_VARIANT_FLAGS
)

set(monoprop_VARIANTS "")
set(monoprop_VARIANT_FLAGS "")
Expand Down
Loading
Loading