Ordering. A canonical pass with a model to validate now waits, before deciding whether it is superseded, for the question GeometryWalkTakesRound asks: its own post-refinement fit and, for a move of less than a step, the speculative probe (AwaitGeometryWalkAnswer). On a card too small for the probe beside the merge the probe waited for the merge to end, so the answer could never be in before the model stage, and validation/MR/refinement ran on a merge the walk then replaced (measured on a large-cell set: a model validation on a wrong P1 supercell merge, OOM-killed above the logged host budget). The merge first re-declares what it still holds of the card (GPUMerge::Holds), and the probe is waited for only where it can then start (gpu_work_fits_beside_merges); otherwise nothing waits, as before. The cost - the probe's remaining time - falls only on --model runs whose fit moved the detector; fast_live does not speculate, so the broker's live mode is unchanged. Final outputs do not change: a superseded pass's files were rewritten by the walk's pass anyway. Model-cell check (owner approved). Where no change of basis of CellMappingOperators carries the model's cell onto the data's (CellsCorrespond tolerance), the unplaced validation stops with cell_mismatch and a reason naming both cells, instead of refractionalising the model into another lattice's cell. The report says MODEL_VALIDATION=CELL_MISMATCH and warns MODEL_CELL_MISMATCH (in place of MODEL_NOT_VALIDATED). Molecular replacement is still tried afterwards, as for any model not placed as given; placed paths (MR, ExpandIntoSubgroup, RefinableIn) are untouched. --model stays processing-inert. ModelValidation_WrongModelInAGroupOffTheDataMetric moves to cells within the tolerance so that it keeps testing what it tested. One host ledger. HostWorkerPool now knows the host budget and the bytes the run holds itself (HostWorkerPool::Held): workers may hold the pool's size or what the budget leaves beside the held bytes, whichever is less. ScaleMergeAndSymmetry holds its reflections, then engine + merge (+ post-refinement), and at the model stage the reflections and the engine where the ledger merge keeps it; a validation holds its own CPU grids; the change-of-basis frames reserve their grids per frame instead of all starting at once. The reserved == 0 escape stays, so a worker larger than what is left still runs, alone. Only what runs at the same time changes. Tests: ModelValidation_StopsOnAModelOfAnotherLattice, HostWorkerPool_HeldBytesShareTheBudget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVeDQqN9qRD57vnShzTC37
108 lines
2.7 KiB
C++
108 lines
2.7 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "HostMemory.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#if defined(_WIN32)
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <windows.h>
|
|
#elif defined(__APPLE__)
|
|
#include <cstdint>
|
|
#include <sys/types.h>
|
|
#include <sys/sysctl.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
size_t get_host_total_memory() {
|
|
#if defined(_WIN32)
|
|
MEMORYSTATUSEX status{};
|
|
status.dwLength = sizeof(status);
|
|
if (!GlobalMemoryStatusEx(&status))
|
|
return 0;
|
|
return static_cast<size_t>(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<size_t>(bytes);
|
|
#else
|
|
const long pages = sysconf(_SC_PHYS_PAGES), page = sysconf(_SC_PAGE_SIZE);
|
|
if (pages <= 0 || page <= 0)
|
|
return 0;
|
|
return static_cast<size_t>(pages) * static_cast<size_t>(page);
|
|
#endif
|
|
}
|
|
|
|
size_t DefaultHostMemoryBudget() {
|
|
return static_cast<size_t>(HOST_MEMORY_BUDGET_FRACTION * static_cast<double>(get_host_total_memory()));
|
|
}
|
|
|
|
HostWorkerPool &HostWorkerPool::Instance() {
|
|
static HostWorkerPool pool;
|
|
return pool;
|
|
}
|
|
|
|
HostWorkerPool::HostWorkerPool() {
|
|
SetBudget(0);
|
|
SetSize(0);
|
|
}
|
|
|
|
void HostWorkerPool::SetSize(size_t bytes) {
|
|
std::lock_guard lock(m);
|
|
size = bytes > 0 ? bytes
|
|
: static_cast<size_t>(HOST_WORKER_POOL_FRACTION * static_cast<double>(DefaultHostMemoryBudget()));
|
|
cv.notify_all();
|
|
}
|
|
|
|
void HostWorkerPool::SetBudget(size_t bytes) {
|
|
std::lock_guard lock(m);
|
|
budget = bytes > 0 ? bytes : DefaultHostMemoryBudget();
|
|
cv.notify_all();
|
|
}
|
|
|
|
size_t HostWorkerPool::available() const {
|
|
return std::min(size, budget > held ? budget - held : 0);
|
|
}
|
|
|
|
size_t HostWorkerPool::Size() {
|
|
std::lock_guard lock(m);
|
|
return available();
|
|
}
|
|
|
|
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.available(); });
|
|
pool.reserved += bytes;
|
|
}
|
|
|
|
HostWorkerPool::Reservation::~Reservation() {
|
|
{
|
|
std::lock_guard lock(pool.m);
|
|
pool.reserved -= bytes;
|
|
}
|
|
pool.cv.notify_all();
|
|
}
|
|
|
|
HostWorkerPool::Held::Held(HostWorkerPool &pool, size_t bytes) : pool(pool), bytes(0) {
|
|
Set(bytes);
|
|
}
|
|
|
|
HostWorkerPool::Held::~Held() {
|
|
Set(0);
|
|
}
|
|
|
|
void HostWorkerPool::Held::Set(size_t new_bytes) {
|
|
{
|
|
std::lock_guard lock(pool.m);
|
|
pool.held = pool.held - bytes + new_bytes;
|
|
bytes = new_bytes;
|
|
}
|
|
pool.cv.notify_all();
|
|
}
|