From 3ebf085e4c82cca8de19bc509054c7ce9b1db1a8 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 17 Oct 2026 09:31:04 +0200 Subject: [PATCH] Host memory: the machine's total, a budget decided from it, and a pool for large per-worker buffers get_host_total_memory (sysconf / hw.memsize / GlobalMemoryStatusEx), DefaultHostMemoryBudget at 75% of the total - decided from the total, never from what is free, as the GPU plans are - and HostWorkerPool, a process-wide pool (a third of the budget) that workers reserve their large buffers against before making them, so how many run at once is decided by bytes rather than threads. Only scheduling reads any of it. [HostMemory] tests. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4 --- common/CMakeLists.txt | 1 + common/HostMemory.cpp | 77 ++++++++++++++++++++++++++++++++++++++++ common/HostMemory.h | 60 +++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/HostMemoryTest.cpp | 48 +++++++++++++++++++++++++ 5 files changed, 187 insertions(+) create mode 100644 common/HostMemory.cpp create mode 100644 common/HostMemory.h create mode 100644 tests/HostMemoryTest.cpp diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index f8a8ff5c9..b4711ecf3 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -77,6 +77,7 @@ ADD_LIBRARY(JFJochCommon STATIC DetectorModuleGeometry.cpp DetectorModuleGeometry.h DetectorSetup.h DetectorSetup.cpp ZeroCopyReturnValue.h Histogram.h DiffractionGeometry.h CUDAWrapper.cpp CUDAWrapper.h + HostMemory.cpp HostMemory.h ThreadAffinity.cpp ThreadAffinity.h ADUHistogram.cpp ADUHistogram.h RawToConvertedGeometryCore.h diff --git a/common/HostMemory.cpp b/common/HostMemory.cpp new file mode 100644 index 000000000..54c4a8b66 --- /dev/null +++ b/common/HostMemory.cpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "HostMemory.h" + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif defined(__APPLE__) +#include +#include +#include +#else +#include +#endif + +size_t get_host_total_memory() { +#if defined(_WIN32) + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + if (!GlobalMemoryStatusEx(&status)) + return 0; + return static_cast(status.ullTotalPhys); +#elif defined(__APPLE__) + uint64_t bytes = 0; + size_t len = sizeof(bytes); + if (sysctlbyname("hw.memsize", &bytes, &len, nullptr, 0) != 0) + return 0; + return static_cast(bytes); +#else + const long pages = sysconf(_SC_PHYS_PAGES), page = sysconf(_SC_PAGE_SIZE); + if (pages <= 0 || page <= 0) + return 0; + return static_cast(pages) * static_cast(page); +#endif +} + +size_t DefaultHostMemoryBudget() { + return static_cast(HOST_MEMORY_BUDGET_FRACTION * static_cast(get_host_total_memory())); +} + +HostWorkerPool &HostWorkerPool::Instance() { + static HostWorkerPool pool; + return pool; +} + +HostWorkerPool::HostWorkerPool() { + SetSize(0); +} + +void HostWorkerPool::SetSize(size_t bytes) { + std::lock_guard lock(m); + size = bytes > 0 ? bytes + : static_cast(HOST_WORKER_POOL_FRACTION * static_cast(DefaultHostMemoryBudget())); + cv.notify_all(); +} + +size_t HostWorkerPool::Size() { + std::lock_guard lock(m); + return size; +} + +HostWorkerPool::Reservation::Reservation(HostWorkerPool &pool, size_t bytes) : pool(pool), bytes(bytes) { + std::unique_lock lock(pool.m); + pool.cv.wait(lock, [&] { return pool.reserved == 0 || pool.reserved + bytes <= pool.size; }); + pool.reserved += bytes; +} + +HostWorkerPool::Reservation::~Reservation() { + { + std::lock_guard lock(pool.m); + pool.reserved -= bytes; + } + pool.cv.notify_all(); +} diff --git a/common/HostMemory.h b/common/HostMemory.h new file mode 100644 index 000000000..d016ddb58 --- /dev/null +++ b/common/HostMemory.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include +#include + +// The machine's physical memory in bytes - its total, not what is free at the moment. 0 where it +// cannot be read. +size_t get_host_total_memory(); + +// The host memory a run plans against: this fraction of the machine's physical memory, decided from +// the total, never from what happens to be free (as what runs on a GPU is decided on the card's total). +// The plan decides only what runs at the same time - two merges side by side or one after the other, +// how many large per-worker buffers are held at once - and never what is computed, so no result +// depends on it. +constexpr double HOST_MEMORY_BUDGET_FRACTION = 0.75; +size_t DefaultHostMemoryBudget(); + +// The share of the budget that the large per-worker buffers of parallel steps - a translation function's +// grids, a null replicate's model maps - may hold at once. The rest is what the run holds beside them: +// its reflections, its merge, the detector-sized arrays. +constexpr double HOST_WORKER_POOL_FRACTION = 1.0 / 3.0; + +// One pool for those buffers, process-wide. A worker reserves its bytes before it makes them and hands +// them back when it is done; one that would take the pool past its size waits until others have handed +// theirs back - except that a reservation made while nothing is reserved is always let through, however +// large. So how many such workers run at once is decided by their bytes, not by the thread count, and +// however the steps holding them nest or run side by side; what each worker computes, and where its +// result goes, does not change. A worker must not reserve again while it holds a reservation, and none +// may reserve on ParallelFor's pool: a holder's own parallel passes need those threads. Work spread over +// that pool is bounded by its count instead (Size() / bytes each). +class HostWorkerPool { +public: + static HostWorkerPool &Instance(); + + // The pool's size in bytes; 0 sets it back to HOST_WORKER_POOL_FRACTION of DefaultHostMemoryBudget(). + void SetSize(size_t bytes); + [[nodiscard]] size_t Size(); + + class Reservation { + public: + Reservation(HostWorkerPool &pool, size_t bytes); + ~Reservation(); + Reservation(const Reservation &) = delete; + Reservation &operator=(const Reservation &) = delete; + private: + HostWorkerPool &pool; + size_t bytes; + }; + +private: + HostWorkerPool(); + std::mutex m; + std::condition_variable cv; + size_t size = 0; + size_t reserved = 0; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c6dd46391..e5fb8f6bc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -102,6 +102,7 @@ ADD_EXECUTABLE(jfjoch_test HotPixelFinderTest.cpp BandwidthEstimateTest.cpp ParallelForTest.cpp + HostMemoryTest.cpp RugnuxTest.cpp ResultReportTest.cpp DiagnosticOutputTest.cpp diff --git a/tests/HostMemoryTest.cpp b/tests/HostMemoryTest.cpp new file mode 100644 index 000000000..e58d17ebd --- /dev/null +++ b/tests/HostMemoryTest.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include + +#include +#include +#include +#include +#include +#include + +#include "../common/HostMemory.h" + +TEST_CASE("HostMemory_BudgetFromTheTotal", "[HostMemory]") { + const size_t total = get_host_total_memory(); + REQUIRE(total > 0); + CHECK(DefaultHostMemoryBudget() == static_cast(HOST_MEMORY_BUDGET_FRACTION * static_cast(total))); +} + +// As many reservations at once as the pool holds, and one at a time where a single one is larger than it. +TEST_CASE("HostWorkerPool_BoundsWorkersByBytes", "[HostMemory]") { + HostWorkerPool &pool = HostWorkerPool::Instance(); + for (const auto &[size, bytes, at_most] : std::vector>{ + {100, 40, 2}, {100, 100, 1}, {100, 1000, 1}, {1000, 10, 8}}) { + CAPTURE(size, bytes); + pool.SetSize(size); + std::atomic running{0}, most{0}, done{0}; + std::vector workers; + for (int i = 0; i < 8; i++) + workers.emplace_back([&] { + const HostWorkerPool::Reservation reserved(pool, bytes); + const int now = ++running; + int seen = most.load(); + while (now > seen && !most.compare_exchange_weak(seen, now)) {} + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + --running; + ++done; + }); + for (auto &w : workers) + w.join(); + CHECK(done == 8); + CHECK(most >= 1); + CHECK(most <= at_most); + } + pool.SetSize(0); + CHECK(pool.Size() == static_cast(HOST_WORKER_POOL_FRACTION * static_cast(DefaultHostMemoryBudget()))); +}