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
4 changes: 4 additions & 0 deletions cpp/monoprop/detail/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ target_sources(
TYPE HEADERS
FILES
"EnvConfig.h"
"MemoryBytes.h"
"ProcessMemory.h"
)

target_sources(monoprop-objs PRIVATE ProcessMemory.cpp)

add_subdirectory(evolution)
add_subdirectory(graph)
add_subdirectory(graph_encoding)
Expand Down
33 changes: 33 additions & 0 deletions cpp/monoprop/detail/MemoryBytes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2026 Algorithmiq
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <cstddef>

namespace monoprop::detail {

/// Bytes a container has taken from the allocator. Variadic so a roll-up over many members is one call.
template <typename... Vecs>
[[nodiscard]] inline auto capacity_bytes(const Vecs &...vecs) -> size_t {
return (0uz + ... + (vecs.capacity() * sizeof(typename Vecs::value_type)));
}

/// Of capacity_bytes(): reserved and never written. Unfaulted, but not free -- a growth holds old+new at once.
template <typename... Vecs>
[[nodiscard]] inline auto capacity_slack_bytes(const Vecs &...vecs) -> size_t {
return (0uz + ... + ((vecs.capacity() - vecs.size()) * sizeof(typename Vecs::value_type)));
}

} // namespace monoprop::detail
124 changes: 124 additions & 0 deletions cpp/monoprop/detail/ProcessMemory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2026 Algorithmiq
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "monoprop/detail/ProcessMemory.h"

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iterator>
#include <memory>
#include <string>
#include <string_view>

#if defined(__GLIBC__)

Check warning on line 26 in cpp/monoprop/detail/ProcessMemory.cpp

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

preprocessor condition can be written more concisely using '#ifdef' [readability-use-concise-preprocessor-directives]
#include <malloc.h>
#endif

namespace monoprop::detail {
namespace {

auto slurp(const char *path) -> std::string {
std::ifstream in(path);
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
}

auto digits_at(std::string_view text, size_t at) -> size_t {
size_t value = 0;
for (; at < text.size() && text[at] >= '0' && text[at] <= '9'; ++at) {
value = (value * 10) + static_cast<size_t>(text[at] - '0');
}
return value;
}

// /proc reports these in kB.
auto status_field(std::string_view text, std::string_view key) -> size_t {
const auto at = text.find(key);
if (at == std::string_view::npos) {
return 0uz;
}
return digits_at(text, text.find_first_of("0123456789", at + key.size())) * 1024;
}

constexpr std::string_view kSizeAttr{R"(size=")"};
constexpr std::string_view kHeapTag{R"(<heap nr=)"};

// The per-arena element names repeat once more in the process-wide roll-up, which is LAST.
auto last_size_attr(std::string_view xml, std::string_view tag) -> size_t {
const auto at = xml.rfind(tag);
if (at == std::string_view::npos) {
return 0uz;
}
const auto size_at = xml.find(kSizeAttr, at);
return size_at == std::string_view::npos ? 0uz : digits_at(xml, size_at + kSizeAttr.size());
}

auto malloc_info_xml() -> std::string {
#if defined(__GLIBC__)

Check warning on line 69 in cpp/monoprop/detail/ProcessMemory.cpp

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

preprocessor condition can be written more concisely using '#ifdef' [readability-use-concise-preprocessor-directives]
// open_memstream itself allocates, so this reads a few KiB above the state it describes.
char *buf = nullptr;
size_t len = 0;
FILE *stream = ::open_memstream(&buf, &len);
if (stream == nullptr) {
return {};
}
const int rc = ::malloc_info(0, stream);
(void)std::fclose(stream);
// Owns the buffer before the copy below: constructing the string can throw, and free() must still run.
// NOLINTNEXTLINE(cppcoreguidelines-no-malloc) -- open_memstream's buffer is malloc'd
const std::unique_ptr<char, decltype(&std::free)> owned(buf, &std::free);

Check failure on line 81 in cpp/monoprop/detail/ProcessMemory.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Don't take the address of 'free', call it from a lambda instead.

See more on https://sonarcloud.io/project/issues?id=Algorithmiq_monoprop&issues=AaBC4Biqme1aiht3P44M&open=AaBC4Biqme1aiht3P44M&pullRequest=293

Check failure on line 81 in cpp/monoprop/detail/ProcessMemory.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Don't take the address of 'free', call it from a lambda instead.

See more on https://sonarcloud.io/project/issues?id=Algorithmiq_monoprop&issues=AaBC4Biqme1aiht3P44L&open=AaBC4Biqme1aiht3P44L&pullRequest=293
return (rc == 0 && buf != nullptr) ? std::string(buf, len) : std::string{};
#else
return {};
#endif
}

auto read_process_memory() -> ProcessMemory {
ProcessMemory out;
#if defined(__linux__)

Check warning on line 90 in cpp/monoprop/detail/ProcessMemory.cpp

View workflow job for this annotation

GitHub Actions / clang-tidy analysis

preprocessor condition can be written more concisely using '#ifdef' [readability-use-concise-preprocessor-directives]
const std::string status = slurp("/proc/self/status");
out.rss_bytes = status_field(status, "VmRSS:");
out.peak_rss_bytes = status_field(status, "VmHWM:");
#endif
const std::string xml = malloc_info_xml();
if (xml.empty()) {
return out;
}
// `system current` is arenas only; mmap'd chunks are separate and never free (free() unmaps them).
const size_t mmapped = last_size_attr(xml, R"(<total type="mmap")");
out.alloc_system_bytes = last_size_attr(xml, R"(<system type="current")") + mmapped;
out.alloc_retained_bytes =
last_size_attr(xml, R"(<total type="rest")") + last_size_attr(xml, R"(<total type="fast")");
out.alloc_in_use_bytes = out.alloc_system_bytes - std::min(out.alloc_system_bytes, out.alloc_retained_bytes);
for (auto at = xml.find(kHeapTag); at != std::string::npos; at = xml.find(kHeapTag, at + kHeapTag.size())) {
++out.alloc_arenas;
}
return out;
}

} // namespace

// Allocates, so the header's "never throws" is held here rather than asserted: all-or-nothing, since a
// partially-filled result would break the identity its consumers check.
auto process_memory() noexcept -> ProcessMemory {
try {
return read_process_memory();
}
catch (...) {
return {};
}
}

} // namespace monoprop::detail
37 changes: 37 additions & 0 deletions cpp/monoprop/detail/ProcessMemory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2026 Algorithmiq
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <cstddef>

namespace monoprop::detail {

/// What the kernel and the allocator report, as opposed to what a byte ledger estimates.
///
/// Every field is PER PROCESS: report once, never summed over partitions; over RANKS it is a job total.
/// `alloc_retained_bytes` is what a ledger cannot reach -- freed chunks stay faulted and stay resident.
struct ProcessMemory {
size_t rss_bytes{0uz}; ///< /proc/self/status VmRSS: pages faulted in right now.
size_t peak_rss_bytes{0uz}; ///< VmHWM: the kernel's peak over the process's life.
size_t alloc_in_use_bytes{0uz}; ///< malloc(3) chunks handed out and not yet freed.
size_t alloc_retained_bytes{0uz}; ///< Freed chunks the allocator still holds.
size_t alloc_system_bytes{0uz}; ///< What the allocator has taken from the kernel.
size_t alloc_arenas{0uz}; ///< Arena count (MALLOC_ARENA_MAX bounds it).
};

/// Zero-filled where the platform cannot answer, and never throws: a diagnostic must not fail its caller.
auto process_memory() noexcept -> ProcessMemory;

} // namespace monoprop::detail
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,12 @@ auto MonomialPropagator<NumModes>::partitioned_core_term_() const -> double {
template <size_t NumModes>
auto MonomialPropagator<NumModes>::partitioned_operator_memory_usage_() const
-> detail::MPOperatorMemoryBreakdown<NumModes> {
return sum_partitions_([](const MonomialPropagator &s) { return s.operator_memory_usage(); });
auto out = sum_partitions_([](const MonomialPropagator &s) { return s.operator_memory_usage(); });
// The transport belongs to the group, not to a partition, so it is added ONCE after the sum.
const auto [transport, staging] = partition_group_->transport_memory_bytes();
out.transport_bytes = transport;
out.transport_staging_bytes = staging;
return out;
}

template <size_t NumModes>
Expand Down
23 changes: 23 additions & 0 deletions cpp/monoprop/detail/mpi/HybridComm.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include <mpi.h>

#include "monoprop/detail/MemoryBytes.h"
#include "monoprop/detail/mpi/CheckedCount.h"
#include "monoprop/detail/mpi/Comm.h"
#include "monoprop/detail/mpi/PartitionBarrier.h"
Expand All @@ -47,7 +48,7 @@
using std::runtime_error::runtime_error;
};

class HybridComm {

Check warning on line 51 in cpp/monoprop/detail/mpi/HybridComm.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Class has 37 methods, which is greater than the 35 authorized. Split it into smaller classes.

See more on https://sonarcloud.io/project/issues?id=Algorithmiq_monoprop&issues=AaBCb_PrznZbWxSilmas&open=AaBCb_PrznZbWxSilmas&pullRequest=293
public:
// n_local_partitions = S, identical on every rank (the facade ctor checks that before constructing).
HybridComm(MPI_Comm parent, int n_local_partitions)
Expand Down Expand Up @@ -82,9 +83,9 @@
counts_stride_ = round_up_(p, kIntsPerLine);
rows_stride_ = round_up_(static_cast<size_t>(r_), kLongsPerLine);
// One spare line each: the allocator guarantees alignof(T), not 64, so row 0 must be realigned.
counts_matrix_store_.assign(static_cast<size_t>(s_) * counts_stride_ + kIntsPerLine, 0);

Check warning on line 86 in cpp/monoprop/detail/mpi/HybridComm.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]
counts_matrix_ = align_to_line_(counts_matrix_store_.data());
rows_store_.assign(static_cast<size_t>(s_) * rows_stride_ + kLongsPerLine, 0LL);

Check warning on line 88 in cpp/monoprop/detail/mpi/HybridComm.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]
rows_ = align_to_line_(rows_store_.data());
assert(reinterpret_cast<uintptr_t>(counts_matrix_) % kLineBytes == 0);
assert(reinterpret_cast<uintptr_t>(rows_) % kLineBytes == 0);
Expand All @@ -97,7 +98,29 @@
auto operator=(const HybridComm &) -> HybridComm & = delete;

auto size() const -> int { return r_ * s_; }

// Per PROCESS, not per partition; capacities, so staging reads at the largest exchange's high-water mark.
[[nodiscard]] auto staging_bytes() const -> size_t {
return monoprop::detail::capacity_bytes(stage_send_, stage_recv_, red_vec_);
}
[[nodiscard]] auto memory_bytes() const -> size_t {
return sizeof(HybridComm) + staging_bytes()
+ monoprop::detail::capacity_bytes(slots_,
counts_send_,
counts_recv_,
mpi_send_counts_,
mpi_send_displs_,
mpi_recv_counts_,
mpi_recv_displs_,
pack_off_,
base_send_,
base_recv_,
col_sum_,
recv_col_,
counts_matrix_store_,
rows_store_);
}
auto global_rank(int local_partition) const -> int { return mpi_rank_ * s_ + local_partition; }

Check warning on line 123 in cpp/monoprop/detail/mpi/HybridComm.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]

auto alltoall_counts(int local_partition, const int *send_counts /*[P]*/, int *recv_counts /*[P]*/) -> void {
guard_partition0_(local_partition, "alltoall_counts", [this, local_partition, send_counts, recv_counts] {
Expand Down Expand Up @@ -183,7 +206,7 @@
const int t = local_partition;
for (int a = 0; a < r_; ++a) {
for (int su = 0; su < s_; ++su) {
recv_counts[a * s_ + su] = counts_recv_[counts_idx_(a, t, su)];

Check warning on line 209 in cpp/monoprop/detail/mpi/HybridComm.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]
}
}
// No trailing barrier: past the last sync only counts_recv_ is read, and partition 0 cannot rewrite
Expand Down Expand Up @@ -233,12 +256,12 @@
std::byte *dst = args.recv;
const int t = local_partition;
for (int a = 0; a < r_; ++a) {
size_t cur = base_recv_[static_cast<size_t>(a) * static_cast<size_t>(s_) + static_cast<size_t>(t)];

Check warning on line 259 in cpp/monoprop/detail/mpi/HybridComm.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]
for (int su = 0; su < s_; ++su) {
const int g = a * s_ + su;

Check warning on line 261 in cpp/monoprop/detail/mpi/HybridComm.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]
const int cnt = args.recv_counts[g];
if (cnt != 0) {
std::memcpy(dst + static_cast<size_t>(args.recv_displs[g]) * args.elem,

Check warning on line 264 in cpp/monoprop/detail/mpi/HybridComm.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]
stage_recv_.data() + cur * args.elem,
static_cast<size_t>(cnt) * args.elem);
}
Expand Down
7 changes: 7 additions & 0 deletions cpp/monoprop/detail/mpi/ShmComm.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <type_traits>
#include <vector>

#include "monoprop/detail/MemoryBytes.h"
#include "monoprop/detail/mpi/CheckedCount.h"
#include "monoprop/detail/mpi/Comm.h"
#include "monoprop/detail/mpi/PartitionBarrier.h"
Expand All @@ -45,6 +46,12 @@ class ShmComm {

auto size() const -> int { return n_; }

// Per PROCESS, not per partition. R == 1 has no funnel: alltoallv memcpys out of published pointers.
[[nodiscard]] auto memory_bytes() const -> size_t {
return sizeof(ShmComm) + monoprop::detail::capacity_bytes(slots_);
}
[[nodiscard]] auto staging_bytes() const -> size_t { return 0uz; }

// recv_counts[s] = what rank s sends to me (the transpose of the send-count matrix).
auto alltoall_counts(int rank, const int *send_counts, int *recv_counts) -> void {
slots_[static_cast<size_t>(rank)].counts = send_counts;
Expand Down
10 changes: 10 additions & 0 deletions cpp/monoprop/detail/operator/InvertedIndex.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <vector>

#include "monoprop/TypeAliases.h"
#include "monoprop/detail/MemoryBytes.h"
#include "monoprop/detail/operator/RowAccess.h"

namespace monoprop::detail {
Expand Down Expand Up @@ -203,6 +204,15 @@ struct InvertedIndex {
return total;
}

// Diagnostic: the part of memory_bytes() no resize ever wrote. Unfaulted, but see reserved_bytes.
auto slack_bytes() const -> size_t {
size_t total = capacity_slack_bytes(row_parity_);
for (const auto &col : cols) {
total += capacity_slack_bytes(col.words, col.set_rows);
}
return total;
}

// Diagnostic tier split of memory_bytes(): {dense_bytes, sparse_bytes, dense_columns}.
auto tier_memory_bytes() const -> std::array<size_t, 3> {
std::array<size_t, 3> out{0, 0, 0};
Expand Down
Loading
Loading