Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m16s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m41s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m24s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 14m56s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 22m55s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m7s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 13m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 19m6s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 19m0s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m33s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 17m43s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m21s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m25s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 19m20s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 24m18s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 23m34s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 24m21s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 20m7s
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / Validation Battery (push) Successful in 35m52s
BraggIntegrationEngineMetal is BraggIntegrationEngineCPU with its integer pixel work run as Metal kernels, one thread per reflection: the neighbour mask, the owner map, pass A's walk over every box and the background clip. Everything in double, the profile learning and pass B stay on the CPU, in the CPU engine's own code - Apple GPUs have no double, and no GPU exp matches macOS expf bit for bit. The sums are integers, so the GPU's order does not matter; the stencil arithmetic is the same BraggStencil.h, compiled at run time (MSL 3.0, no offline Metal toolchain needed) with fast math off (sqrt and division correctly rounded, measured) and fp contraction off, as the host already builds (-ffp-contract=off, now extended to Objective-C++). The image is read in place (unified memory). The CPU engine is split so both engines share the finishing code: pass A counts into BraggBoxSums (integer; the double ring sums were exact integers already), the clip threshold is its integer floor, FinishPassA and ProfilePasses are shared. Verified byte-identical p.hkl and MTZ data before/after the split and with the Metal engine on lysozyme, cytochrome C, myoglobin, 4nwv and a small-molecule set. Measured on an Apple M6 (wall, CPU engine -> Metal): 4nwv 180.5 -> 151.2 s, cytochrome C 72.7 -> 59.8 s, lysozyme 29.2 -> 22.5 s, myoglobin 43.3 -> 41.2 s; the sparse small-molecule set +1.5 s (few reflections per frame leave the GPU idle). CI: a macOS step runs tests/BraggIntegrationEngineMetalTest.cpp, which requires every float of every reflection and every count to match the CPU engine over 19 setting combinations on random crowded frames (negative pixels, exact-pixel ties, hot, masked and overloaded pixels), three frames per engine; it fails without a Metal device and if the GPU path fell back to the CPU. Ten planted kernel bugs were each caught. rugnux --no-gpu hides the Metal device too (gpus_are_hidden). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
3.7 KiB
C++
142 lines
3.7 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;
|
|
|
|
// 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() {
|
|
std::unique_lock lock(gpu_work_m);
|
|
if (take_turns())
|
|
gpu_work_changed.wait(lock, [] { return gpu_image_passes_n == 0 && gpu_work_beside_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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void set_gpu(int32_t dev_id) {}
|
|
|
|
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
|