From the twin-multi prototype (af742b889, d197507c7), rebased onto twin-gate rule B and cleaned up. The twin arm takes a list of laws: RefinementReflections::mates per law and mates_from (the converse, which the GPU gathers the gradient by, as a three-fold is not an involution), one fraction per law, the twinned amplitude k sqrt(a0 |U(h)|^2 + sum_d a_d |U(T_d h)|^2) on the CPU and the GPU, and the fraction fit as the same intercept regression with one column per law (a law whose coefficient comes out negative taken out). RefinementTwinDomains: every coset of find_twin_laws, of any order. DecideTwin keeps the single-law rung as it was (null at 3 SE, or 2 with the L-test). Where that law is taken and the lattice has more domains, all of them are refined at once and taken where the added domains beat their own null (their mates unrelated, the adopted law's kept) by 3 SE, summed per twin group, each at a fraction of 0.05 or more (domains under it dropped and the rest refined again). TwinNull generalised from pairs to twin groups: kept adjacent, partners (Laws+1)(k+1) apart; for one law it is the null it was. CloseFreeSet, in RefineModel before both arms: every orbit under the domain operators in one set. An unsplit orbit stays; a split one goes free with the probability of its own free share (a salted hash of the orbit), so the free fraction is kept - the prototype's majority took a C-centred pseudo-hexagonal merge's free set from 4020 to 891, an unsalted hash inflated free sets by 60%. Open arm, against twin-grid on 23 twin/pseudo-symmetric sets (runs twinfix-grid2 / twinfix-multi / twinfix-multi2): the second rung ran on 9 sets and was taken on one, 9hnc (P2 on a tetragonal metric, deposited four domains at 0.06 each): domains 0.058/0.058/0.055, z 13.1, R-free 0.2174 -> 0.2063. On the other 8 the added domains refine to 0.000-0.016 and the single law stands. The closure moves reflections on 5ebi and the two C222 pseudo-hexagonal sets 6fvz/6fwc (their law, FREE_SET_SPLIT before, is now tested and refused); 5ebi's R-free 0.347 -> 0.377 on the closed free set. Single-law sets move by up to 0.003 (the regression and GPU gather round differently), all others <= 0.0002. Wall time +10-50% on the sets that reach the second rung. 5ebi deposited data (--mode refine): the closure moves 1 reflection and its strongest law is tested: R-free 0.390 -> 0.364. Report: REFINE_TWIN_MULTI*, REFINE_TWIN_FREE_CLOSED. Tests: RefinementTwin_domains_gradient, RefinementTwin_free_set_closed. Docs: §15.12, RUGNUX_REPORT.md, ACKNOWLEDGEMENT.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqXYdL4qBGYRiSXSGXfCLu
114 lines
6.7 KiB
C++
114 lines
6.7 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
// The device half of restrained refinement on a GPU (RefinementGPU.h, CUDA builds only). The host half works
|
|
// out with gemmi everything that depends on the cell, the group and the reflections; this half grids the atoms,
|
|
// transforms, composes the symmetry, evaluates the likelihood target, takes its derivative back to every atom
|
|
// by the FFT method, and evaluates the geometry restraints. Every kernel is deterministic - gathers and fixed
|
|
// reduction trees, no floating-point atomics - so the same input gives the same bits on every run.
|
|
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#include "ModelDensityGPU.h"
|
|
#include "ModelMaskGPU.h"
|
|
#include "RefinementMath.h"
|
|
|
|
// Everything an engine needs that does not depend on the atoms.
|
|
struct RefinementGPUSetup {
|
|
ModelDensityGrid grid; // the structure-factor grid, u fastest
|
|
ModelDensityGrid mask_grid; // the bulk-solvent mask's (BulkSolventMask::MaskGrid), u fastest
|
|
double volume = 0;
|
|
std::vector<std::array<int, 12>> sym_ops; // gemmi's Op: rot row-major and tran, in units of 1 / den
|
|
int den = 24;
|
|
double centring = 1; // the number of centring vectors
|
|
std::vector<ModelMaskOp> mask_ops; // every operator with every centring vector
|
|
std::vector<std::array<int, 3>> rows; // the reflections, in the order their numbers come back
|
|
std::vector<double> inv_d2;
|
|
std::vector<float> fo;
|
|
std::vector<char> work, centric;
|
|
// With twin laws: per law and row (law * rows + row) the row of its twin mate under the law, and the observed
|
|
// row whose mate it is (-1: none) - RefinementReflections::mates and mates_from; empty without a law.
|
|
int laws = 0;
|
|
std::vector<int> mate, mate_from;
|
|
// The adjoint of the composition, as a gather: the half-u spectrum points the gradient's coefficients
|
|
// land on (ascending), and for each the terms (row * ops + op) * 2 + conj adding to it, in order.
|
|
std::vector<int> scatter_point, scatter_start, scatter_term;
|
|
size_t max_atoms = 0, max_pairs = 0;
|
|
};
|
|
|
|
struct RefinementGPUEngineImpl;
|
|
// What the engines of one setup on one card share (RefinementGPU.cu): the reflections' tables, the bulk-solvent mask's
|
|
// buffers, and lanes of the transforms' working buffers, which the engines of a lane use one at a time.
|
|
struct RefinementGPUCard;
|
|
|
|
class RefinementGPUEngine {
|
|
public:
|
|
// What a card holds for the engines on it together (MakeCard), and each lane of working buffers beside.
|
|
static size_t CardBytes(const RefinementGPUSetup &setup);
|
|
static size_t LaneBytes(const RefinementGPUSetup &setup, size_t fft_work_bytes);
|
|
// What each engine holds beside, with restraint sets of up to `records` records and plane atoms and `slots`
|
|
// gradient slots together.
|
|
static size_t DeviceBytes(const RefinementGPUSetup &setup, size_t records, size_t slots);
|
|
static size_t FFTWorkBytes(int device, const RefinementGPUSetup &setup);
|
|
static std::shared_ptr<RefinementGPUCard> MakeCard(int device, const RefinementGPUSetup &setup);
|
|
|
|
// `setup` is read for as long as the engine lives; `card` is MakeCard(device, setup)'s. `own_lane`: on a lane of
|
|
// working buffers of its own, else on the card's lane with the fewest engines.
|
|
RefinementGPUEngine(int device, const RefinementGPUSetup &setup, std::shared_ptr<RefinementGPUCard> card,
|
|
size_t fft_work_bytes, bool own_lane);
|
|
~RefinementGPUEngine();
|
|
|
|
// n_cen exp(blur s^2 / 4) per reflection: prepare_asu_data()'s unblur.
|
|
void SetBlur(double blur);
|
|
// Per macro-cycle: F_model = k_total (F + fmask), the likelihood's D and variance, and with twin laws their
|
|
// fractions (XrayRowModel).
|
|
void SetRowModel(const std::vector<float> &k_total, const std::vector<std::array<float, 2>> &fmask,
|
|
const std::vector<float> &d, const std::vector<float> &var,
|
|
const std::vector<float> &twin_fractions = {});
|
|
|
|
// The atoms' densities, as ModelStructureFactorsGPU takes them (the boxes of ModelDensityGPU::Boxes() made
|
|
// from them): called again only where some atom's density changed. Read until the call returns; from pinned
|
|
// memory they go up by DMA.
|
|
void SetDensities(const ModelDensityAtom *atoms, size_t n);
|
|
// The atoms' grid positions (fractional times the grid size, not wrapped), in the order of SetDensities():
|
|
// read until the next evaluation returns, so from a pinned buffer the copy goes up by DMA.
|
|
void SetPositions(const std::array<float, 4> *pos, size_t n);
|
|
// F of the atoms at the rows.
|
|
void Fcalc(std::vector<std::array<float, 2>> &f);
|
|
// The bulk-solvent mask's F at the rows (the mask atoms fractional, wrapped), on the mask's own grid.
|
|
void Fmask(const std::vector<ModelMaskAtom> &mask_atoms, std::vector<std::array<float, 2>> &f);
|
|
// -ln L over the working rows for the atoms last set and, with `grad`, dT/d(x, y, z, B) per atom into
|
|
// HostGradient().
|
|
double Evaluate(bool grad);
|
|
// The gradients of the last evaluation, in the engine's pinned buffers: valid until the next one.
|
|
const std::array<float, 4> *HostGradient() const;
|
|
const std::array<float, 4> *HostRestraintGradient() const;
|
|
// dT/d(U11 U22 U33 U12 U13 U23) of the anisotropic atoms (six per atom, zero for the isotropic ones) of the last
|
|
// evaluation with a gradient where some atom was anisotropic.
|
|
const float *HostUGradient() const;
|
|
|
|
// Restraint set `which` (0 or 1), as RestraintSet holds it.
|
|
void SetRestraints(int which, const std::vector<RestraintRecord> &records, const std::vector<int> &plane_atoms,
|
|
const std::vector<int> &slot_start, const std::vector<int> &slot_index, int n_slots);
|
|
// The sum of E of set `which` at `xyzb` (x, y, z, B per atom) and dE/d(x, y, z, B) per atom.
|
|
double EvaluateRestraints(int which, const std::vector<std::array<float, 4>> &xyzb,
|
|
std::vector<std::array<float, 4>> *grad);
|
|
// Evaluate() and EvaluateRestraints() in one submission with one wait: the X-ray target is queued first, and
|
|
// `xyzb` writes the restraints' x, y, z, B of every atom while the device works on it. The gradients into
|
|
// HostGradient() and HostRestraintGradient().
|
|
double EvaluateWithRestraints(bool grad, int which, const std::function<void(std::array<float, 4> *)> &xyzb,
|
|
bool restraint_grad, double &restraints);
|
|
|
|
// The most atoms of a plane restraint the kernel takes.
|
|
static constexpr int MAX_PLANE_ATOMS = 32;
|
|
|
|
private:
|
|
std::unique_ptr<RefinementGPUEngineImpl> impl_;
|
|
};
|