Without a GPU (macOS), rc176's worker pool - 1/3 of the 12.9 GB budget on a 16 GB Mac - let only two
molecular-replacement placements run at once, as each reserved 32 B per grid point (2.1 GB on 4nwv)
against a measured ~0.95 GB. MR rescoring went from 38.6 s to 69 s and 4nwv's wall time from 402 s
(budget off) to 444 s.
On the M6 Mac mini (16 GB), 4nwv, two runs each in alternating order on a quiet machine:
- old build 234774ff0: 418.4 s, 20.0-21.3 GiB peak footprint, 576-682k swapouts
- rc176 as shipped: 444.2 s, 14.5-15.2 GiB, 39-66k swapouts
- this change: 402.4 s, 15.5-15.8 GiB, 66-93k swapouts
- budget switched off: 401.7 s, 21.8-23.3 GiB, 268-433k swapouts
9zlo/9s02/9gjx: 71.9/79.0/82.8 s (old build 73.6/82.2/83.6), no swap. p.hkl byte-identical in every
run: the pool decides only what runs at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
61 lines
2.6 KiB
C++
61 lines
2.6 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <condition_variable>
|
|
#include <cstddef>
|
|
#include <mutex>
|
|
|
|
// 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 = 0.5;
|
|
|
|
// 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;
|
|
};
|