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()))); +}