Files
Jungfraujoch/common/CUDAWrapper.cpp
T
leonarski_fandClaude Opus 5.5 1c3a2e202c GPU memory over the run: FFT indexers given back after the last indexing; speculative probe takes turns with the merge on a small card
- IndexerThreadPool::ReleaseIndexers(): each worker drops the indexers it has
  built (once its current frame is done) and the allocation pool is trimmed, so
  their memory goes back to the card, not only to the pool. A later frame builds
  them again, as on first use. A rotation pass calls it once its first pass is
  done and again when the leftover-lattice census - the last of its indexing -
  ends. Measured on the RTX 5080: the 1.24 GB (pre-pass) / 0.62 GB (later
  passes) the pass's pool held through the image pass and the scaling is gone;
  4nwv's pre-pass image pass peaks 5.97 instead of 7.18 GB of device memory.
- GPUMerge: the main line holds one through ScaleMergeAndSymmetry. The
  speculative geometry probe (GPUWorkBeside(after_merges=true)) waits for it on
  a card below the take-turns size (12 GB), where the probe's FFT indexers and
  image workers beside the merge's arrays would overfill the card - on a
  Windows (WDDM) card that does not fail an allocation but pages it and crawls.
  The merge waits for nothing, so nothing the merge waits on (the rotation-scale
  walk's probes) can wait in a circle with it. No change on a 16 GB card.

Output unchanged: p.hkl byte-identical on cytc_x10sa, myob_x10sa, 4nwv and
8qaw on this card; also identical with the card's total read as 8.55 GB
(10 / 9 image workers instead of 16) and with 4 image workers, so the worker
count does not change the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:39:19 +02:00

161 lines
4.1 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "CUDAWrapper.h"
#include <atomic>
#include <condition_variable>
#include <mutex>
// Build-independent: the CUDA build gets get_gpu_names() from CUDAWrapper.cu, the CPU-only build from
// the stub below, and this collapses whichever list came back. Four identical cards read better as
// "4x <name>" than as the same name four times, and a mixed machine keeps one group per model.
std::string get_gpu_description() {
const auto names = get_gpu_names();
std::string out;
for (size_t i = 0; i < names.size();) {
size_t n = 1;
while (i + n < names.size() && names[i + n] == names[i])
n++;
if (!out.empty())
out += ", ";
if (n > 1)
out += std::to_string(n) + "x ";
out += names[i];
i += n;
}
return out;
}
namespace {
// How many probe passes (GPUWorkBeside) and main-line image passes (GPUImagePass) are running, for a
// thread to wait until none of one kind is.
std::mutex gpu_work_m;
std::condition_variable gpu_work_changed;
int gpu_work_beside_n = 0, gpu_image_passes_n = 0, gpu_merges_n = 0;
// A card too small for a probe pass beside the main line's image pass, or beside another probe pass:
// measured on a 16 Mpx detector, two of them together held 8.6 GB. On such a card a probe pass waits,
// before it starts, for every other one and for the image passes to end, and an image pass for the
// probe passes - so they take turns, and only when each runs depends on the card, never what it
// computes. None waits once it is running, so nothing waits in a circle.
constexpr size_t TAKE_TURNS_BELOW_BYTES = 12'000'000'000ull;
bool take_turns() {
static const bool small = get_gpu_count() > 0 && get_gpu_total_memory(0) < TAKE_TURNS_BELOW_BYTES;
return small;
}
void remove(int &n) {
{
std::lock_guard lock(gpu_work_m);
--n;
}
gpu_work_changed.notify_all();
}
}
GPUWorkBeside::GPUWorkBeside(bool after_merges) {
std::unique_lock lock(gpu_work_m);
if (take_turns())
gpu_work_changed.wait(lock, [after_merges] {
return gpu_image_passes_n == 0 && gpu_work_beside_n == 0 && (!after_merges || gpu_merges_n == 0);
});
++gpu_work_beside_n;
}
GPUWorkBeside::~GPUWorkBeside() {
remove(gpu_work_beside_n);
}
bool wait_for_gpu_work_beside(std::chrono::seconds timeout) {
std::unique_lock lock(gpu_work_m);
return gpu_work_changed.wait_for(lock, timeout, [] { return gpu_work_beside_n == 0; });
}
GPUImagePass::GPUImagePass() {
std::unique_lock lock(gpu_work_m);
if (take_turns())
gpu_work_changed.wait(lock, [] { return gpu_work_beside_n == 0; });
++gpu_image_passes_n;
}
GPUImagePass::~GPUImagePass() {
remove(gpu_image_passes_n);
}
GPUMerge::GPUMerge() {
std::lock_guard lock(gpu_work_m);
++gpu_merges_n;
}
GPUMerge::~GPUMerge() {
remove(gpu_merges_n);
}
void wait_for_gpu_image_passes() {
std::unique_lock lock(gpu_work_m);
gpu_work_changed.wait(lock, [] { return gpu_image_passes_n == 0; });
}
#ifndef JFJOCH_USE_CUDA
int32_t get_gpu_count() {
return 0;
}
namespace {
std::atomic<bool> gpus_hidden{false};
}
void hide_gpus() {
gpus_hidden = true;
}
bool gpus_are_hidden() {
return gpus_hidden;
}
std::vector<std::string> get_gpu_names() {
return {};
}
size_t get_gpu_total_memory(int32_t) {
return 0;
}
bool get_gpu_free_memory(size_t &, size_t &) {
return false;
}
void set_gpu(int32_t dev_id) {}
bool gpu_direct_host_read() {
return false;
}
void pin_gpu() {}
void pin_gpu(int32_t dev_id) {}
ScopedGpuPin::ScopedGpuPin(int32_t) {}
ScopedGpuPin::~ScopedGpuPin() {}
void enable_gpu_numa_binding() {}
std::vector<int> get_gpu_numa_nodes() { return {}; }
void set_gpu_blocking_sync() {}
void create_gpu_contexts(int32_t) {}
void release_gpu_contexts() {}
void cuda_clear_error() {}
void cuda_throw_if_context_lost() {}
#endif