Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5f1cc4e
perf(mpi): 🧭 route through one Router, GF(2)-linear across ranks
diagonal-hamiltonian Aug 26, 2026
72db27d
perf(mpi): ⏭️ skip the exchange for identity generators
diagonal-hamiltonian Aug 26, 2026
1927fe6
perf(mpi): πŸ•ΈοΈ exchange point-to-point over the peers routing can reach
diagonal-hamiltonian Aug 26, 2026
40041c1
perf(mpi): 🧭 route GF(2)-linear by default where the geometry allows it
diagonal-hamiltonian Aug 26, 2026
0204d9c
test(mpi): 🧭 check scan/find_rank agreement under both routers
diagonal-hamiltonian Aug 26, 2026
27da5fa
style(tests): 🎨 clang-format mpi_utils_tests
diagonal-hamiltonian Aug 27, 2026
ac5dcdb
docs(parallelism): πŸ“ document GF(2)-linear rank routing and its knobs
diagonal-hamiltonian Aug 27, 2026
c255455
fix(mpi): πŸ› mask known recv counts through the peer plan
diagonal-hamiltonian Aug 27, 2026
948eae0
refactor(mpi): ♻️ drive every sparse exchange from one pairwise helper
diagonal-hamiltonian Aug 27, 2026
f104b59
feat(mpi): πŸ“ˆ report a shift set that cannot reach every rank
diagonal-hamiltonian Aug 27, 2026
5c9f15f
perf(evolution): ⏭️ skip the scan too for an identity generator
diagonal-hamiltonian Aug 27, 2026
effc76e
perf(routing): ⚑ take the rank bits from a transposed basis
diagonal-hamiltonian Aug 27, 2026
f5428a4
perf(mpi): ⚑ complete the sparse exchange through the handle
diagonal-hamiltonian Aug 27, 2026
22ad704
test(mpi): βœ… reach the sparse paths a single peer cannot
diagonal-hamiltonian Aug 27, 2026
a610f04
refactor(routing)!: ♻️ collapse the linear-bit dial to a boolean
diagonal-hamiltonian Aug 29, 2026
b1ee72c
perf(routing): ⚑ route the emit path from the generator's shift
diagonal-hamiltonian Aug 29, 2026
9a14631
perf(mpi): ⚑ pack the send side under the count round
diagonal-hamiltonian Aug 29, 2026
e3f30bb
perf(mpi): ⚑ replay the layer exchange point-to-point
diagonal-hamiltonian Aug 29, 2026
25c00ce
fix(mpi): πŸ› gate the pairwise replay on routing, not on a rank's row
diagonal-hamiltonian Aug 29, 2026
3e6ab80
fix(mpi): πŸ”€ reconcile the boolean peer plan across the stack
diagonal-hamiltonian Aug 29, 2026
68a1907
feat(mpi): ✨ name the slot window a peer plan can reach
diagonal-hamiltonian Aug 29, 2026
18549e6
perf(evolution): ⚑ size the per-generator query path to the peer window
diagonal-hamiltonian Aug 29, 2026
a38ad17
perf(mpi): ⚑ narrow HybridComm's serial staging sweeps to the peer ranks
diagonal-hamiltonian Aug 29, 2026
2f8d3dc
test(mpi): βœ… cover derived_wire_plan_ with an empty partition-0 row
diagonal-hamiltonian Aug 29, 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
6 changes: 6 additions & 0 deletions cpp/include/monoprop/MonomialPropagator.h
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@ class MonomialPropagator {
// it captures this and rejects a later call once it moves, as it does for a rebuilt graph.
size_t initial_operator_epoch_{0};

bool routing_coverage_reported_{false}; // report_routing_coverage_ speaks once per propagator

size_t logical_num_modes_{NumModes};

CutoffType cutoff_type_;
Expand Down Expand Up @@ -449,6 +451,10 @@ class MonomialPropagator {
const VecD &parameters,
std::optional<size_t> only_rotate_len_k) -> void;

// Do this call's generator shifts span log2(R)? If not, ranks receive nothing (routing::gf2_rank).
// Not beside check_routing_agreement: at construction the gate list does not exist yet.
auto report_routing_coverage_(const std::vector<VecZ> &majoranas) -> void;

template <typename EvolutionFunc>
auto run_gate_loop_(const std::vector<VecZ> &majoranas,
std::optional<size_t> only_rotate_len_k,
Expand Down
17 changes: 16 additions & 1 deletion cpp/monoprop/Evolution.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "monoprop/detail/evolution/CosineRecomputeCallbacks.h"
#include "monoprop/detail/mpi/Exchange.h"
#include "monoprop/detail/mpi/MPICompat.h"
#include "monoprop/detail/mpi/Routing.h"

namespace monoprop {
namespace {
Expand Down Expand Up @@ -73,10 +74,23 @@ auto &acquire_flat_exchange_buffers() {
}

// A property of the communicator, not the layer: all ranks participate even at local total_count 0.
// Still true with the pairwise arm: `wire_bits` decides the transport for the whole communicator, so a
// rank that skipped the round strands the others whichever transport they are on.
auto layer_exchange_participates(const mpi::Comm &comm) -> bool {
return mpi::size(comm) != 1;
}

// The transport gate for post_flat_alltoallv, and the ONLY thing allowed to choose it: rank-uniform by
// construction, because linear_bits_for reads the environment alone and check_routing_agreement
// allreduces exactly this number at construction and throws rather than proceed on a mismatch. Fanout 1
// -- every generator's queries land on one destination rank -- is both why a layer's other legs are
// empty and why no rank can be on the other side of the branch. Any other geometry keeps the collective.
auto layer_exchange_wire_bits(const mpi::Comm &comm) -> int {
const auto ranks = static_cast<size_t>(mpi::geometry(comm).ranks);
const size_t bits = routing::linear_bits_for(ranks);
return (ranks >> bits) == 1 ? static_cast<int>(bits) : 0;
}

// Derives both sides at once: the count matrix is symmetric, so the recv layout is the send layout.
auto derive_layer_exchange(const LayerTraversal &layer, const mpi::Comm &comm, int scale, LayerExchangeLayout &layout)
-> void {
Expand Down Expand Up @@ -115,7 +129,8 @@ inline auto begin_flat_exchange(FlatExchangeBuffers &buffers, const mpi::Comm &c
.recv_counts = layout.counts.data(),
.recv_displs = layout.displs.data()},
mpi::size(comm),
comm);
comm,
layer_exchange_wire_bits(comm));
return handle;
}

Expand Down
58 changes: 58 additions & 0 deletions cpp/monoprop/detail/EnvConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,35 @@

#pragma once

#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>

// Single home for runtime environment configuration. Kept dependency-free by design, because it is
// pulled into hot-path headers.
//
// monoprop_NUM_THREADS positive int (1..1e6), else ignored β†’ num_threads
// monoprop_PARTITIONS int N | "auto" | "off"; parsed where it is used (resolve_partition_count_)
// monoprop_ROUTING "splitmix" | "linear" β†’ routing_mode
// monoprop_ROUTE_SEED decimal uint64 basis seed β†’ route_seed
//
// Both routing knobs THROW on a malformed value instead of falling back: each silently changes the
// transport, so a typo that defaulted would stay invisible until a performance postmortem.

namespace monoprop::config {

class EnvConfigError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};

enum class RoutingMode : std::uint8_t { Splitmix, Linear };

namespace detail {

inline auto parse_positive_int(const char *text) -> std::optional<int> {
Expand All @@ -43,17 +60,58 @@ inline auto parse_positive_int(const char *text) -> std::optional<int> {
return static_cast<int>(value);
}

[[noreturn]] inline auto reject_env(std::string_view name, const char *text, std::string_view expected) -> void {
throw EnvConfigError(std::string{name} + "=\"" + text + "\" is not " + std::string{expected}
+ "; correct it or leave the variable unset.");
}

// Unset and empty are both nullopt; a value that is present but unparseable throws.
inline auto parse_uint64(std::string_view name, const char *text) -> std::optional<std::uint64_t> {
if (text == nullptr || *text == '\0') {
return std::nullopt;
}
// strtoull WRAPS a negative literal to a huge unsigned rather than failing, so '-' is rejected here.
if (std::string_view{text}.find('-') != std::string_view::npos) {
reject_env(name, text, "a decimal uint64");
}
errno = 0;
char *end = nullptr;
const unsigned long long value = std::strtoull(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE) {
reject_env(name, text, "a decimal uint64");
}
return static_cast<std::uint64_t>(value);
}

inline auto parse_routing_mode(std::string_view name, const char *text) -> std::optional<RoutingMode> {
if (text == nullptr || *text == '\0') {
return std::nullopt;
}
const std::string_view value{text};
if (value == "splitmix") {
return RoutingMode::Splitmix;
}
if (value == "linear") {
return RoutingMode::Linear;
}
reject_env(name, text, "one of \"splitmix\" or \"linear\"");
}

} // namespace detail

struct Settings {
std::optional<int> num_threads;
std::optional<RoutingMode> routing_mode;
std::optional<std::uint64_t> route_seed;
};

// Parse the environment once; the Settings are cached and shared across TUs.
inline auto get() -> const Settings & {
static const Settings settings = [] {
Settings s;
s.num_threads = detail::parse_positive_int(std::getenv("monoprop_NUM_THREADS"));
s.routing_mode = detail::parse_routing_mode("monoprop_ROUTING", std::getenv("monoprop_ROUTING"));
s.route_seed = detail::parse_uint64("monoprop_ROUTE_SEED", std::getenv("monoprop_ROUTE_SEED"));
return s;
}();
return settings;
Expand Down
Loading
Loading