Files
Jungfraujoch/common/CUDAWrapper.cpp
T
leonarski_fandClaude Opus 5.5 7e5e4c54e4
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m41s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m26s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m57s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m13s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m50s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m52s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m23s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 26m53s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m4s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 31m16s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 22m48s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / Build documentation (push) Successful in 1m13s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m45s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 26m47s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m38s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m38s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m1s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m18s
Build Packages / Unit tests (push) Successful in 1h32m38s
Build Packages / Validation Battery (push) Successful in 42m36s
rugnux: the model's first validation starts beside the analyses instead of after them
Validation reads of the final merge only its French-Wilson amplitudes, which the analyses
made last, from the anisotropy tensor. That tensor is a fit to the merged intensities alone
(the unmerged observations only give its error bar), so AnisotropyPriorHKL computes it from
the merge without the rest of the analysis, bit for bit what AnisotropyTensorHKL of the full
analysis gives (test). The first ValidateAgainstModel now runs from the start of the analyses
on a copy of the merge given those same amplitudes, on the caller's card (get_gpu), with its
log held and replayed where it was printed before; nothing it decides is acted on earlier.
The model's preparation (census wait, indexer pool, GPU hold, merge engine release) moves
with it.

Measured, --simulate-live 10 --fast-live, 4axt model, RTX 5080, analyses + validation steps:
strong lysozyme 0.91 -> 0.63 s, reference lysozyme 1.02 -> 0.66 s (validation beside the
analyses 0.60-0.62 s vs 0.39-0.45 s alone, CPU shared; analyses 0.53 -> 0.62 s). The
analyses were 80% waiting for the anisotropy (0.43 of 0.53 s), so starting on the full
analysis' tensor would have gained ~0.1 s. p.hkl, p_maps.mtz identical, p.mtz data identical,
all validation numbers identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-19 13:53:36 +02:00

200 lines
5.3 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;
// What the merges hold of the card, as they said (GPUMerge::Holds), and how many have not said yet.
size_t gpu_merge_bytes = 0;
int gpu_merges_unsized_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;
}
// Whether work holding `bytes` fits beside the merges: within 80% of the first card's total, as the
// GPU budgets of the merge and the model validation count it. With no card there is nothing to share.
bool fits_beside_merges(size_t bytes) {
static const size_t card = get_gpu_count() > 0 ? get_gpu_total_memory(0) : 0;
return card == 0 || (gpu_merges_unsized_n == 0 && bytes + gpu_merge_bytes <= card / 5 * 4);
}
void remove(int &n) {
{
std::lock_guard lock(gpu_work_m);
--n;
}
gpu_work_changed.notify_all();
}
}
bool gpu_takes_turns() {
return take_turns();
}
GPUWorkBeside::GPUWorkBeside(bool after_merges, size_t bytes) {
std::unique_lock lock(gpu_work_m);
gpu_work_changed.wait(lock, [after_merges, bytes] {
return (!take_turns() || (gpu_image_passes_n == 0 && gpu_work_beside_n == 0))
&& (!after_merges || gpu_merges_n == 0 || fits_beside_merges(bytes));
});
++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;
++gpu_merges_unsized_n;
}
GPUMerge::~GPUMerge() {
{
std::lock_guard lock(gpu_work_m);
if (bytes_)
gpu_merge_bytes -= *bytes_;
else
--gpu_merges_unsized_n;
}
remove(gpu_merges_n);
}
void GPUMerge::Holds(size_t bytes) {
{
std::lock_guard lock(gpu_work_m);
if (bytes_)
gpu_merge_bytes -= *bytes_;
else
--gpu_merges_unsized_n;
gpu_merge_bytes += bytes;
bytes_ = bytes;
}
gpu_work_changed.notify_all();
}
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) {}
int32_t get_gpu() {
return 0;
}
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