Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`). * Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection. * Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS). * Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead. * Rugnux: On macOS, Bragg integration runs on the Apple GPU. * Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand. * Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`. * Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit. * Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed. * Rugnux: The beam-stop shadow is found correctly on tilted detectors. * Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement. * jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps. * jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection. * jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images. * Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges. --------- Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home> Reviewed-on: #85 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
154 lines
6.9 KiB
C++
154 lines
6.9 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
// The rigid body's target evaluated on a GPU (CUDA builds only; the header itself needs no CUDA).
|
|
// It is the function RigidBodyTarget computes on the CPU - the same density, the same composition of
|
|
// Fcalc from one copy, the same bulk-solvent mask, scale fit, residuals and Jacobian - moved to the
|
|
// device so that a fit takes a fraction of a second instead of several seconds. The two agree to
|
|
// rounding, not bit for bit: float distances, cuFFT for FFTW. The GPU is deterministic on its own.
|
|
|
|
#include <condition_variable>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
#include "RigidBodyRefine.h"
|
|
|
|
class Logger;
|
|
class RigidBodyGPUEngine;
|
|
struct RigidBodyGPUZone;
|
|
struct RigidBodyGPUCapacity;
|
|
|
|
// A CUDA failure inside the rigid body, or in model validation's structure factors and maps
|
|
// (ModelStructureFactorsGPU). Model validation catches it and reports the validation as failed.
|
|
class RigidBodyGPUFailure : public std::runtime_error {
|
|
public:
|
|
explicit RigidBodyGPUFailure(const std::string &what) : std::runtime_error(what) {}
|
|
};
|
|
|
|
// A few engines, reserved once for a whole validation or search: each is a stream and the buffers for one
|
|
// fit at a time, sized for the finest zone of the ladder to d_min. Every fit takes one for its length, and
|
|
// waits for one when all are taken. Engines are interchangeable and every kernel deterministic, so which fit
|
|
// gets which engine does not change a number. A pool belongs to one cell and one d_min, and serves every
|
|
// model and group it was made for (Plan, Cover): what a zone needs of a model's atoms - their densities,
|
|
// radii and mask radii, which the placement does not change - is worked out once per group, atom count and
|
|
// zone and kept.
|
|
class RigidBodyGPUPool {
|
|
public:
|
|
// A pool without engines yet, sized for the finest zone of the ladder to d_min: EngineBytes() is
|
|
// what each engine will take, so a caller can decide how many first. Null where there is no GPU or
|
|
// where the cell is too small for the gather (an atom's box wider than the cell), logged.
|
|
// `max_observations`: the most working reflections any fit will be given in its finest zone.
|
|
static std::unique_ptr<RigidBodyGPUPool> Plan(const gemmi::Model &model, const gemmi::UnitCell &cell,
|
|
const gemmi::SpaceGroup &sg, double d_min,
|
|
size_t max_observations, Logger &logger);
|
|
~RigidBodyGPUPool();
|
|
|
|
// Sizes the engines, before Reserve(), for fits of `model` in `sg` as well - in the pool's cell, to its
|
|
// d_min, with at most its observations. False, logged, where the cell is too small for the gather.
|
|
bool Cover(const gemmi::Model &model, const gemmi::SpaceGroup &sg, Logger &logger);
|
|
|
|
size_t EngineBytes() const;
|
|
// Reserves `engines` engines, engine i on the i-th card from Device(), so a validation uses every
|
|
// card there is. Engines that cannot be set up throw RigidBodyGPUFailure.
|
|
void Reserve(size_t engines);
|
|
size_t Engines() const { return engines_.size(); }
|
|
// The card the pool was made from, which its first engine is on and the others count on from.
|
|
int Device() const { return device_; }
|
|
RigidBodyGPUEngine &Acquire();
|
|
void Release(RigidBodyGPUEngine &engine);
|
|
|
|
// The zone to d_min without its observations, for this pool's model.
|
|
const RigidBodyGPUZone &Zone(const gemmi::Model &model, const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg,
|
|
double d_min);
|
|
// The zone with these observations and the composition of Fcalc at them. The null's replicates are all
|
|
// fitted to the same reflections, so they share it.
|
|
std::shared_ptr<const RigidBodyGPUZone> ObservedZone(const gemmi::Model &model, const gemmi::UnitCell &cell,
|
|
const gemmi::SpaceGroup &sg,
|
|
const gemmi::AsuData<gemmi::ValueSigma<float>> &fobs,
|
|
double d_min);
|
|
|
|
private:
|
|
RigidBodyGPUPool() = default;
|
|
int device_ = 0;
|
|
gemmi::UnitCell cell_;
|
|
double d_min_ = 0;
|
|
size_t max_observations_ = 0;
|
|
std::unique_ptr<RigidBodyGPUCapacity> cap_;
|
|
std::vector<std::unique_ptr<RigidBodyGPUEngine>> engines_;
|
|
std::vector<RigidBodyGPUEngine *> idle_;
|
|
std::mutex m_;
|
|
std::condition_variable cv_;
|
|
// By group, atom count and zone.
|
|
std::map<std::tuple<std::string, size_t, double>, std::unique_ptr<RigidBodyGPUZone>> zones_;
|
|
std::mutex zones_m_;
|
|
struct ObservedEntry {
|
|
std::string group;
|
|
size_t atoms;
|
|
double d_min;
|
|
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
|
std::shared_ptr<const RigidBodyGPUZone> zone;
|
|
};
|
|
std::vector<ObservedEntry> observed_;
|
|
std::mutex observed_m_;
|
|
};
|
|
|
|
// An engine taken from a pool for as long as this lives.
|
|
class RigidBodyGPULease {
|
|
public:
|
|
explicit RigidBodyGPULease(RigidBodyGPUPool &pool) : pool_(pool), engine_(pool.Acquire()) {}
|
|
~RigidBodyGPULease() { pool_.Release(engine_); }
|
|
RigidBodyGPULease(const RigidBodyGPULease &) = delete;
|
|
RigidBodyGPULease &operator=(const RigidBodyGPULease &) = delete;
|
|
RigidBodyGPUEngine &Engine() { return engine_; }
|
|
|
|
private:
|
|
RigidBodyGPUPool &pool_;
|
|
RigidBodyGPUEngine &engine_;
|
|
};
|
|
|
|
class RigidBodyTargetGPU : public RigidBodyTargetBase {
|
|
public:
|
|
// Takes an engine from `pool` for its lifetime. q = 0 is the placement `model` has now; unlike the
|
|
// CPU target the model is never moved by an evaluation, only by Place().
|
|
RigidBodyTargetGPU(RigidBodyGPUPool &pool, gemmi::Model &model, const gemmi::UnitCell &cell,
|
|
const gemmi::SpaceGroup &sg, size_t nthreads);
|
|
~RigidBodyTargetGPU() override;
|
|
|
|
void SetZone(const gemmi::AsuData<gemmi::ValueSigma<float>> &fobs, double d_min) override;
|
|
size_t NumObservations() const override;
|
|
|
|
bool Residuals(const double q[6], double *residuals) override;
|
|
bool Jacobian(const double q[6], double *jacobian) override;
|
|
|
|
private:
|
|
// The placement at q as x -> R (x - centroid) + centroid + t.
|
|
void Placement(const double q[6], double rotation[9], double translation[3]) const;
|
|
void HostScale();
|
|
|
|
RigidBodyGPUPool &pool_;
|
|
RigidBodyGPULease lease_;
|
|
RigidBodyGPUEngine &engine_;
|
|
gemmi::Model &model_;
|
|
const gemmi::UnitCell &cell_;
|
|
const gemmi::SpaceGroup &sg_;
|
|
size_t nthreads_;
|
|
|
|
double d_min_ = 0;
|
|
std::shared_ptr<const RigidBodyGPUZone> zone_;
|
|
bool solvent_fitted_ = false;
|
|
bool host_scale_ = false; // the zone's scale is fitted on the host (HostScale)
|
|
|
|
bool have_point_ = false;
|
|
std::array<double, 6> q_{};
|
|
double k_overall_ = 1;
|
|
gemmi::SMat33<double> b_star_{0, 0, 0, 0, 0, 0};
|
|
};
|