Files
Jungfraujoch/image_analysis/structure_refinement/RigidBodyGPU.h
T
leonarski_fandClaude Opus 5.5 0d25d765cf
Build Packages / Create release (push) Successful in 22s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Failing after 1m31s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 9m12s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m39s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 12m39s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m36s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m15s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 15m15s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m8s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 17m13s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 17m33s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 22m58s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 24m10s
Build Packages / Build documentation (push) Successful in 1m9s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m10s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m23s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m59s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 21m53s
Build Packages / Unit tests (push) Successful in 2h5m52s
Build Packages / Validation Battery (push) Successful in 36m32s
Model validation: one GPU budget for all its parts, 80% of the card's total
What a validation puts on its GPU was decided part by part on fixed fractions of the card: the
structure factors to d_min on a quarter, the null's structure factors on whatever of that quarter
the first left, the rigid-body engines on another quarter, the scale fits on an eighth. On an 8 GB
card that sent the null's structure factors of a 4.4 GB validation to the CPU (and held the rigid
body to one engine) although everything the validation holds fits the card with room to spare.

Now PlanModelValidationGPU decides all of it at once, before anything is reserved, from the card's
total memory: every engine is reserved up front and kept to the end, so their sum is held against
one budget, 80% of the card less what a merge engine still at work holds there (the finalist
ledger's P1 merge, RotationScaleMerge::DeviceBytes). Each part takes one engine where it still fits,
in the order structure factors to d_min, rigid body, the null's structure factors, scale fits; a part
that does not fit runs on the CPU. What is left goes to more rigid-body engines (up to one per null
replicate plus the real fit) and then more scale-fit engines, which only change how many fits run at
once. A second validation may run beside where twice this one's whole GPU plan fits the cards'
budgets; it gets this one's bytes as gpu_beside_bytes, which limit its engine counts and never its
placement, so its numbers are those of the validation it stands in for. One log line states the
placement, the bytes held and the budget.

- RigidBodyGPUPool::Plan sizes the pool without engines (EngineBytes), Reserve(n) makes them; Create
  (Plan + engines on a quarter of the card) stays for molecular replacement, whose own rules, and the
  --mr hand check's structure factors (ReadModelAnomalousMap, a quarter), are unchanged.
- The null's resolution is worked out before the plan, so its engine is decided and reserved with
  the others instead of after the indexing probe.

Placement changes only on smaller cards: with the logged footprints of the open-arm sets, 4nwv's
null structure factors move to the GPU below ~9 GB (and its rigid body at 6 GB), 8tyy's structure
factors to d_min below ~8.7 GB; on 16 GB and larger nothing moves. On the RTX 5080 (16 GB), 4nwv,
8tyy, 6oel and 3inp give byte-identical reflection files, maps and models to rc175 HEAD, with more
rigid-body engines where they fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:05:50 +02:00

147 lines
6.8 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 <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: each is a stream and the buffers for one fit at
// a time, sized for the finest zone of the ladder to d_min. The real fit and the null's replicates each
// take one for the length of their fit, and wait for one when all are taken. Engines are
// interchangeable and every kernel deterministic, so which replicate gets which engine does not change
// a number. A pool belongs to one model: what a zone needs of its atoms - their densities, radii and mask
// radii, which the placement does not change - is worked out once per 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);
// Plan(), then as many engines as fit a quarter of the card's total memory, at most `max_engines`
// (molecular replacement's rule). Null, logged, where not even one fits.
static std::unique_ptr<RigidBodyGPUPool> Create(const gemmi::Model &model, const gemmi::UnitCell &cell,
const gemmi::SpaceGroup &sg, double d_min,
size_t max_observations, size_t max_engines, Logger &logger);
~RigidBodyGPUPool();
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;
std::unique_ptr<RigidBodyGPUCapacity> cap_;
std::vector<std::unique_ptr<RigidBodyGPUEngine>> engines_;
std::vector<RigidBodyGPUEngine *> idle_;
std::mutex m_;
std::condition_variable cv_;
std::map<double, std::unique_ptr<RigidBodyGPUZone>> zones_;
std::mutex zones_m_;
struct ObservedEntry {
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};
};