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
6 changes: 5 additions & 1 deletion cpp/include/monoprop/MonomialPropagator.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ class MonomialPropagator {
if (partition_group_) {
return partitioned_operator_memory_usage_();
}
return detail::estimate_memory_usage(mp_op_);
// The stamp array is a member of THIS class, not of the operator, so the operator-side estimate
// leaves matched_scratch_bytes at 0 and only this level can fill it in.
auto breakdown = detail::estimate_memory_usage(mp_op_);
breakdown.matched_scratch_bytes = matched_scratch_.memory_bytes();
return breakdown;
}

auto graph_layers() const -> size_t { return partition_group_ ? partitioned_graph_layers_() : graph_.layers(); }
Expand Down
16 changes: 10 additions & 6 deletions cpp/monoprop/detail/evolution/layer_build/Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,26 @@ namespace monoprop::detail {
// Marks matched followers without a per-gate O(n) memset: one counter bump clears every mark. Reused
// across gates.
struct MatchedEpochSet {
std::vector<uint32_t> epoch_;
uint32_t cur_ = 0;
// One 2-byte stamp per term: the counter is never serialised, never exchanged, only compared to cur_.
using Stamp = uint16_t;

// u32 wrap resets the array — once per 2^32-1 gates.
std::vector<Stamp> epoch_;
Stamp cur_ = 0;

// Wraps once per 65535 gates; without the fill a stale stamp on a row reused after a truncation aliases.
auto begin_gate(size_t n) -> void {
if (cur_ == std::numeric_limits<uint32_t>::max()) {
std::fill(epoch_.begin(), epoch_.end(), 0);
if (cur_ == std::numeric_limits<Stamp>::max()) {
std::fill(epoch_.begin(), epoch_.end(), Stamp{0});
cur_ = 0;
}
++cur_;
if (epoch_.size() < n) {
epoch_.resize(n, 0);
epoch_.resize(n, Stamp{0});
}
}
auto mark(size_t i) -> void { epoch_[i] = cur_; }
[[nodiscard]] auto is_marked(size_t i) const -> bool { return epoch_[i] == cur_; }
[[nodiscard]] auto memory_bytes() const -> size_t { return epoch_.capacity() * sizeof(Stamp); }
};

// A trivial aggregate on purpose — not std::pair — so DefaultInitVector can skip the zero-fill and lower
Expand Down
5 changes: 4 additions & 1 deletion cpp/monoprop/detail/operator/MPOperator.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
#include <format>
#include <print>

#include "monoprop/TypeAliases.h"

Check warning on line 29 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

circular header file dependency detected while including 'TypeAliases.h', please check the include path [misc-header-include-cycle]

Check warning on line 29 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

circular header file dependency detected while including 'TypeAliases.h', please check the include path [misc-header-include-cycle]
#include "monoprop/Utilities.h"
#include "monoprop/detail/operator/InvertedIndex.h"
#include "monoprop/detail/operator/OperatorIndex.h"
Expand Down Expand Up @@ -275,7 +275,7 @@

template <typename FlatMap>
inline auto unordered_flat_map_storage_bytes(const FlatMap &map) -> size_t {
return sizeof(FlatMap) + map.bucket_count() * (sizeof(typename FlatMap::value_type) + sizeof(unsigned char));

Check warning on line 278 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]

Check warning on line 278 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]
}

template <size_t NumModes>
Expand All @@ -287,6 +287,8 @@
size_t init_operator_bytes{0uz};
size_t initial_state_bytes{0uz};
size_t inverted_index_bytes{0uz};
// The MatchedEpochSet stamp array. Propagator-owned, so 0 unless MonomialPropagator fills it in.
size_t matched_scratch_bytes{0uz};

// Diagnostics: breakdowns of the fields above, deliberately excluded from total_bytes() so they can
// never double-count.
Expand All @@ -301,7 +303,7 @@

auto total_bytes() const -> size_t {
return operator_terms_bytes + op_coeffs_bytes + state_coeffs_bytes + indexing_bytes + init_operator_bytes
+ initial_state_bytes + inverted_index_bytes;
+ initial_state_bytes + inverted_index_bytes + matched_scratch_bytes;
}

auto operator+=(const MPOperatorMemoryBreakdown &o) -> MPOperatorMemoryBreakdown & {
Expand All @@ -312,6 +314,7 @@
init_operator_bytes += o.init_operator_bytes;
initial_state_bytes += o.initial_state_bytes;
inverted_index_bytes += o.inverted_index_bytes;
matched_scratch_bytes += o.matched_scratch_bytes;
inverted_index_dense_bytes += o.inverted_index_dense_bytes;
inverted_index_sparse_bytes += o.inverted_index_sparse_bytes;
inverted_index_dense_columns += o.inverted_index_dense_columns;
Expand All @@ -328,9 +331,9 @@
breakdown.operator_terms_bytes = op.store->memory_bytes();
breakdown.op_coeffs_bytes = op.op_coeffs.capacity() * sizeof(double);
// Every representation of the state at once: the sparse scored set plus the dense vector.
breakdown.state_coeffs_bytes = op.state_coeffs.capacity() * sizeof(double)

Check warning on line 334 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]

Check warning on line 334 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]
+ op.state_rows_.capacity() * sizeof(TermIndex)

Check warning on line 335 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]

Check warning on line 335 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]
+ op.state_vals_.capacity() * sizeof(double);

Check warning on line 336 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]

Check warning on line 336 in cpp/monoprop/detail/operator/MPOperator.h

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

'*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations [readability-math-missing-parentheses]
breakdown.indexing_bytes = op.store->index_estimated_memory_bytes();
breakdown.init_operator_bytes = unordered_flat_map_storage_bytes(op.init_op_map);
breakdown.init_operator_entries = op.init_op_map.size();
Expand Down
35 changes: 32 additions & 3 deletions cpp/tests/evolution_detail_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ BOOST_AUTO_TEST_CASE(matched_epoch_tail_grow) {
BOOST_TEST(!set.is_marked(3));
}

// When the epoch counter saturates uint32_t, begin_gate zero-fills and restarts so marks stay correct.
BOOST_AUTO_TEST_CASE(matched_epoch_u32_wrap_resets) {
// Reaches the wrap by assigning cur_, which pins the branch and the counter restart but not the fill.
BOOST_AUTO_TEST_CASE(matched_epoch_stamp_wrap_resets) {
MatchedEpochSet set;
set.begin_gate(4); // allocate the backing array
// Force the counter to the wrap boundary; a stale slot still equals the pre-wrap counter.
set.cur_ = std::numeric_limits<uint32_t>::max();
set.cur_ = std::numeric_limits<MatchedEpochSet::Stamp>::max();
set.mark(1);
BOOST_TEST(set.is_marked(1));

Expand All @@ -102,6 +102,35 @@ BOOST_AUTO_TEST_CASE(matched_epoch_u32_wrap_resets) {
BOOST_TEST(set.is_marked(2));
}

// Reaches the wrap by counting gates, with the mark at epoch 1 so a missing fill would alias onto it.
BOOST_AUTO_TEST_CASE(matched_epoch_stamp_wrap_reached_by_gate_count) {
constexpr auto kMaxStamp = std::numeric_limits<MatchedEpochSet::Stamp>::max();
constexpr size_t kPeriod = static_cast<size_t>(kMaxStamp);

MatchedEpochSet set;
set.begin_gate(4);
BOOST_REQUIRE(set.cur_ == MatchedEpochSet::Stamp{1});
set.mark(1);
BOOST_TEST(set.is_marked(1));

// One increment per gate, folded into a single assertion rather than 65534 of them.
bool one_epoch_per_gate = true;
for (size_t k = 2; k <= kPeriod; ++k) {
set.begin_gate(4);
one_epoch_per_gate = one_epoch_per_gate && (static_cast<size_t>(set.cur_) == k);
}
BOOST_TEST(one_epoch_per_gate);
BOOST_TEST(set.cur_ == kMaxStamp); // boundary reached by counting, not by assignment

// The wrap: cur_ returns to 1, the surviving mark's own stamp, so a false is_marked(1) is the fill.
set.begin_gate(4);
BOOST_TEST(set.cur_ == MatchedEpochSet::Stamp{1});
BOOST_TEST(!set.is_marked(1));
set.mark(2);
BOOST_TEST(set.is_marked(2));
BOOST_TEST(!set.is_marked(1));
}

// A self-resolve hit whose index the store only grew into after construction is a real hit -- it must reach
// the sink -- but it is outside the matched set, whose array is sized to combined_size.
BOOST_AUTO_TEST_CASE(self_resolve_mark_bounded_by_combined_size) {
Expand Down
51 changes: 51 additions & 0 deletions cpp/tests/mp_operator_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@

#include <algorithm>
#include <complex>
#include <optional>
#include <utility>
#include <vector>

#include "monoprop/MonomialPropagator.h"
#include "monoprop/algebra/Algebra.h"
#include "monoprop/detail/mpi/MPICompat.h"
#include "monoprop/detail/operator/MPOperator.h"

using namespace monoprop;
Expand Down Expand Up @@ -332,6 +335,54 @@ BOOST_AUTO_TEST_CASE(mp_operator_estimate_memory_usage_tracks_inverted_index_pre
BOOST_CHECK_GT(after.inverted_index_bytes, 0U); // present arm
}

// matched_scratch_bytes is summed by total_bytes() and accumulated by operator+= for the facade's sum.
BOOST_AUTO_TEST_CASE(mp_operator_breakdown_counts_matched_scratch_in_total_and_sum) {
detail::MPOperatorMemoryBreakdown<8> acc;
acc.op_coeffs_bytes = 100;
acc.matched_scratch_bytes = 7;
BOOST_CHECK_EQUAL(acc.total_bytes(), 107U);

detail::MPOperatorMemoryBreakdown<8> other;
other.op_coeffs_bytes = 20;
other.matched_scratch_bytes = 3;

acc += other;
BOOST_CHECK_EQUAL(acc.matched_scratch_bytes, 10U);
BOOST_CHECK_EQUAL(acc.total_bytes(), 130U);

// An operator on its own has no stamp array to report.
auto bare = build_indexed_op({indices_to_bitset<8>({0, 1})});
BOOST_CHECK_EQUAL(detail::estimate_memory_usage<8>(bare).matched_scratch_bytes, 0U);
}

// epoch_ is empty until the first begin_gate, so this must apply a gate before the bytes can be nonzero.
BOOST_AUTO_TEST_CASE(mp_operator_breakdown_matched_scratch_nonzero_after_a_gate) {
constexpr size_t kModes = 2;
OperatorDict ham;
ham[VecZ{0, 1}] = cd{0.0, 1.0};
VecZ initial_state{0, 1};
auto sim = MonomialPropagator<kModes>(ham,
2 * kModes,
initial_state,
std::nullopt,
MPI_COMM_SELF,
std::nullopt,
std::nullopt,
CutoffType::Length,
std::nullopt);
BOOST_CHECK_EQUAL(sim.operator_memory_usage().matched_scratch_bytes, 0U); // no gate applied yet

const std::vector<VecZ> monos{{0}};
sim.build_graph(monos, VecZ{0}, VecD{1.0});

const auto live = sim.operator_memory_usage();
BOOST_CHECK_GT(live.matched_scratch_bytes, 0U);

auto without = live;
without.matched_scratch_bytes = 0;
BOOST_CHECK_EQUAL(live.total_bytes() - without.total_bytes(), live.matched_scratch_bytes);
}

// init_operator_entries is a count: accumulated by operator+= but never summed into total_bytes().
BOOST_AUTO_TEST_CASE(mp_operator_breakdown_keeps_init_operator_entries_out_of_total) {
detail::MPOperatorMemoryBreakdown<8> acc;
Expand Down
1 change: 1 addition & 0 deletions src/monoprop/bindings/binder.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ auto bind_monomial_propagator(nb::module_ &mod) -> void {
{"init_operator_bytes", b.init_operator_bytes},
{"initial_state_bytes", b.initial_state_bytes},
{"inverted_index_bytes", b.inverted_index_bytes},
{"matched_scratch_bytes", b.matched_scratch_bytes},
{"total_bytes", b.total_bytes()},
// Diagnostics (not part of total_bytes; see the struct).
{"d_invidx_dense_bytes", b.inverted_index_dense_bytes},
Expand Down
Loading