Files
Jungfraujoch/image_analysis/structure_refinement/RigidBodyGPU.cu
T
leonarski_fandClaude Opus 5.5 d5fcf2f05b rugnux: model validation's structure factors and maps on the GPU
ModelStructureFactorsGPU computes what compute_model_factors() and
map_from_coefficients() compute on the CPU - F_calc from the model's
density (IT92, Refmac-compatible blur, unblurred as prepare_asu_data()
does) and F_mask from the Refmac bulk-solvent mask, both on the
reflections prepare_asu_data(d_min) lists, in its order; and a map from
ASU coefficients on the grid get_size_for_hkl(coef, 0, 3.0) sizes - on a
device. Made once per cell, group, resolution and model, then evaluated
as often as the coordinates change, so refinement or MR can call it in a
loop. The device is an explicit parameter; every call leaves the calling
thread's current device as it found it.

Pieces:
- ModelDensityGPU: the rigid body's deterministic brick gather, moved
  out of RigidBodyGPU.cu into a component of its own (ModelMaskGPU's
  pattern); the rigid body uses it unchanged. MAX_BRICKS_PER_AXIS 8 ->
  16, so fine grids with high-B atoms (lysozyme at 1.2 A, a 0.9 A P1
  cell) are no longer refused; existing zones are gridded identically.
- One copy of the content is gridded and the symmetry composed in
  reciprocal space (SymmetryComposition), operators applied on the fly;
  the mask is ModelMaskGPU (every image of every atom, islands, shrink).
- Maps: gemmi's get_f_phi_on_grid() in ZYX order on the host (the
  coefficients written are the same), in-place cuFFT c2r, transposed back
  to XYZ on the device. One map at a time, in the engine's buffers.

Decided once, up front, per card, from its TOTAL memory: the engine's
bytes (16 N + cuFFT work + reflections, N the larger of the structure-
factor and map grids) must be at most half the card - the rigid body's
engines take at most a quarter beside it. Otherwise, or where the gather
cannot grid the cell, the CPU path runs, logged with needed vs total.
Anything to a resolution other than d_min (the null's 3.5 A fits) stays
on the CPU, so all replicates and the real model's side of the null are
computed the same way. A CUDA failure takes the existing path: the
validation restarts on the CPU.

Measured, model validation total per run (CPU path -> GPU), 16 GB card:
  F432 215 A cubic, 1.30 A, 500^3 grid: 47.7 -> 15.6 s (two validations;
     14.3 -> 2.4 and 33.4 -> 13.2, the rest of the second is writing the
     three 0.5 GB maps); F_calc + F_mask 5.7 s -> 0.05 s
  C2 1.11 A: 23.9 -> 13.6 s; P3_2 1.55 A: 18.2 -> 10.2 s;
  P2_1 1.25 A: 13.4 -> 6.5 s; F4_132 328 A: 13.0 -> 5.5 s;
  P6_5: 8.8 -> 4.2 s; P4_3 0.97 A: 4.6 -> 2.5 s; P1 0.92 A: 4.2 -> 2.2 s;
  small P1: 3.2 -> 1.3 s; P6_1: 8.8 -> 6.2 s; lysozyme: 1.8 -> 1.4 s.
p.mtz md5-identical on all 13 sets. Against the CPU path: FC within
1e-4 of mean |F|, phases of the strong half within 0.003 deg, maps within
1e-4 (2mFo-DFc) and 7e-4 (mFo-DFc) of their rms; every logged R, CC,
FOM, k_sol and anomalous site list identical at the printed precision,
except where a rigid-body commit sat on an exact R-free tie (0.2155 ->
0.2155) and fell the other way (R-work 0.2127 vs 0.2129). The GPU result
is bit-identical run to run and with -N 8 (maps, map MTZ, placed model).
Peak device memory of the engine: 2.5 GB at 500^3 (process total peaked
at 14.4 GB with what the merge still holds).

Tests: ModelStructureFactorsGPU_MatchesCPU (five groups, 3.5 and 1.5 A:
same reflections, F_calc <= 1e-5 of mean |F|, F_mask 2e-7 rms, repeat
bit-identical), ModelStructureFactorsGPU_MapMatchesCPU (<= 5e-6 of rms).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
2026-10-09 12:38:37 +02:00

698 lines
30 KiB
Plaintext

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "RigidBodyGPUEngine.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <string>
#include <cufft.h>
#include "ModelDensityGPU.h"
#include "ModelMaskGPU.h"
#include "ModelScaleGPU.h"
#include "../../common/JFJochException.h"
#include "../indexing/CUDAMemHelpers.h"
// Every kernel below is deterministic: nothing is summed with a floating-point atomic, each grid point
// adds its atoms in model order, and every reduction runs in a fixed order. The same placement gives the
// same residuals, bit for bit, on every run and whichever engine runs it.
namespace {
void cuda_err(cudaError_t val) {
if (val != cudaSuccess)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, cudaGetErrorString(val));
}
void cufft_err(cufftResult val) {
if (val != CUFFT_SUCCESS)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError,
"cuFFT error " + std::to_string(static_cast<int>(val)));
}
constexpr int THREADS = 256;
constexpr int MAX_SCALE_PARAMS = 7; // k_overall + up to six B* constraints
// The grid of a zone.
struct GridGeom {
int nu, nv, nw;
};
// Where the body is: x -> R x_rel + t, and the fractionalization.
struct Placement {
double r[9];
double t[3];
double frac[9];
};
// The scale a residual or a Jacobian row is taken at.
struct Scale {
double k_overall;
double b_star[6]; // u11 u22 u33 u12 u13 u23
double k_sol, b_sol;
int n_constraints;
double constraints[6][6];
};
__device__ __forceinline__ int imod(int a, int n) {
const int r = a % n;
return r < 0 ? r + n : r;
}
// b_star.r_u_r(h), as gemmi's SMat33.
__device__ __forceinline__ double r_u_r(const double *u, const int *h) {
const double x = h[0], y = h[1], z = h[2];
return x * x * u[0] + y * y * u[1] + z * z * u[2] + 2 * (x * y * u[3] + x * z * u[4] + y * z * u[5]);
}
// Each atom placed: its grid coordinates (fractional times the grid size, wrapped into the cell) for
// the gather and, for a mask atom, its wrapped fractional coordinates and mask radius for the mask.
__global__ void place_kernel(const double *rel, int n, Placement p, int nu, int nv, int nw, const int *mask_slot,
const float *mask_radius, float4 *pos, double *mask_atoms) {
const int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n)
return;
const double x = rel[3 * i], y = rel[3 * i + 1], z = rel[3 * i + 2];
const double c[3] = {p.r[0] * x + p.r[1] * y + p.r[2] * z + p.t[0],
p.r[3] * x + p.r[4] * y + p.r[5] * z + p.t[1],
p.r[6] * x + p.r[7] * y + p.r[8] * z + p.t[2]};
double f[3];
for (int k = 0; k < 3; k++) {
f[k] = p.frac[3 * k] * c[0] + p.frac[3 * k + 1] * c[1] + p.frac[3 * k + 2] * c[2];
f[k] -= floor(f[k]);
if (f[k] >= 1.0) // a tiny negative coordinate wraps to 1 - epsilon, which rounds to 1
f[k] = 0.0;
}
const int n3[3] = {nu, nv, nw};
float g[3];
for (int k = 0; k < 3; k++) {
g[k] = static_cast<float>(f[k] * n3[k]);
if (g[k] >= n3[k])
g[k] -= n3[k];
}
pos[i] = make_float4(g[0], g[1], g[2], 0.0f);
const int slot = mask_slot[i];
if (slot >= 0) {
for (int k = 0; k < 3; k++)
mask_atoms[4 * slot + k] = f[k];
mask_atoms[4 * slot + 3] = mask_radius[i];
}
}
// F at the zone's rows composed from the transform of one copy (SymmetryComposition::Compose), with
// dF/dt where `df_dt` is not null. `f1` is cuFFT's forward transform, which carries exp(-2 pi i h.x):
// conjugated and scaled by V/N it is gemmi's F1.
__global__ void compose_kernel(const float2 *f1, float norm, const RigidBodyGPUTerm *terms, const double *row_scale,
int rows, int ops, double2 *f, double2 *df_dt) {
const int m = blockIdx.x * blockDim.x + threadIdx.x;
if (m >= rows)
return;
double sr = 0, si = 0, d[3][2] = {};
for (int o = 0; o < ops; o++) {
const RigidBodyGPUTerm t = terms[static_cast<size_t>(m) * ops + o];
const float2 out = f1[t.index];
double vr = out.x * norm, vi = -out.y * norm; // F1 at the index read
if (t.conj)
vi = -vi;
const double tr = t.phase[0] * vr - t.phase[1] * vi, ti = t.phase[0] * vi + t.phase[1] * vr;
sr += tr;
si += ti;
for (int k = 0; k < 3; k++) {
d[k][0] += tr * t.s[k];
d[k][1] += ti * t.s[k];
}
}
const double scale = row_scale[m];
f[m] = make_double2(scale * sr, scale * si);
if (df_dt != nullptr) {
// times 2 pi i
const double two_pi = 2 * 3.14159265358979323846;
for (int k = 0; k < 3; k++)
df_dt[3 * static_cast<size_t>(m) + k] = make_double2(-scale * two_pi * d[k][1], scale * two_pi * d[k][0]);
}
}
// Fmask at each row, read at h directly (the mask is symmetric): gemmi's get_value_by_hkl() on the
// transform of the mask, taking the Friedel mate where h is outside the stored half.
__global__ void fmask_kernel(const float2 *fm, float norm, const int *row_hkl, int rows, int nu, int nv, int nw,
float2 *fmask) {
const int m = blockIdx.x * blockDim.x + threadIdx.x;
if (m >= rows)
return;
int h = row_hkl[3 * m], k = row_hkl[3 * m + 1], l = row_hkl[3 * m + 2];
const bool conj = h < 0;
if (conj) {
h = -h;
k = -k;
l = -l;
}
const float2 out = fm[h + static_cast<size_t>(nu / 2 + 1) * (imod(k, nv) + static_cast<size_t>(nv) * imod(l, nw))];
const float re = out.x * norm, im = -out.y * norm;
fmask[m] = make_float2(re, conj ? -im : im);
}
// Scaling<float>::Point's fcmol and fmask of each point.
__global__ void pack_points_kernel(const int *point_obs, const int *obs_row, int n, const double2 *f,
const float2 *fmask, float2 *fcmol_p, float2 *fmask_p) {
const int p = blockIdx.x * blockDim.x + threadIdx.x;
if (p >= n)
return;
const int m = obs_row[point_obs[p]];
fcmol_p[p] = make_float2(static_cast<float>(f[m].x), static_cast<float>(f[m].y));
fmask_p[p] = fmask[m];
}
// gemmi's Scaling::get_fcalc() of a row: fcmol + (float) solvent scale * fmask, in float.
__device__ float2 solvent_fcalc(const double2 &f, const float2 &fm, double stol2, const Scale &sc) {
const float solvent = static_cast<float>(sc.k_sol * exp(-sc.b_sol * stol2));
return make_float2(static_cast<float>(f.x) + solvent * fm.x, static_cast<float>(f.y) + solvent * fm.y);
}
// The residual of each observation: scale_data() applied to the row, then (Fobs - |F|) / <Fobs>.
__global__ void residual_kernel(const int *obs_row, const float *obs_fobs, int n, const double2 *f,
const float2 *fmask, const int *row_hkl, const double *row_stol2, Scale sc,
double f_mean, double *residual) {
const int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n)
return;
const int m = obs_row[i];
if (m < 0) {
residual[i] = 0.0;
return;
}
float2 value = solvent_fcalc(f[m], fmask[m], row_stol2[m], sc);
const float k = static_cast<float>(sc.k_overall * exp(-0.25 * r_u_r(sc.b_star, row_hkl + 3 * m)));
value.x *= k;
value.y *= k;
residual[i] = (obs_fobs[i] - hypotf(value.x, value.y)) / f_mean;
}
// One row of the fixed-scale Jacobian and of the scale parameters' own columns (RigidBodyTarget::Jacobian).
__global__ void jacobian_kernel(const int *obs_row, int n, const double2 *f, const double2 *df_dt,
const double2 *shifted, size_t rows, const float2 *fmask, const int *row_hkl,
const double *row_stol2, Scale sc, double f_mean, double step, double *jacobian,
double *jk) {
const int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n)
return;
const int p = 1 + sc.n_constraints;
const int m = obs_row[i];
if (m < 0) {
for (int j = 0; j < 6; j++)
jacobian[6 * static_cast<size_t>(i) + j] = 0.0;
for (int a = 0; a < p; a++)
jk[static_cast<size_t>(i) * p + a] = 0.0;
return;
}
const int *h = row_hkl + 3 * m;
// compute_value_and_derivatives() with k_sol and b_sol fixed
const float2 fcalc = solvent_fcalc(f[m], fmask[m], row_stol2[m], sc);
const double fcalc_abs = hypotf(fcalc.x, fcalc.y);
const double kaniso = exp(-0.25 * r_u_r(sc.b_star, h));
const double fe = fcalc_abs * kaniso;
const double y = sc.k_overall * fe;
const double x = h[0], yy = h[1], z = h[2];
const double du[6] = {-0.25 * y * (x * x), -0.25 * y * (yy * yy), -0.25 * y * (z * z),
-0.5 * y * (x * yy), -0.5 * y * (x * z), -0.5 * y * (yy * z)};
jk[static_cast<size_t>(i) * p] = -fe / f_mean;
for (int c = 0; c < sc.n_constraints; c++) {
double dot = 0;
for (int q = 0; q < 6; q++)
dot += sc.constraints[c][q] * du[q];
jk[static_cast<size_t>(i) * p + 1 + c] = -dot / f_mean;
}
// d|F_scaled|/dq = K Re(conj(F_total) dFcalc/dq) / |F_total|
const double k = sc.k_overall * kaniso / (fcalc_abs * f_mean);
for (int j = 0; j < 6; j++) {
double2 d;
if (j < 3) {
const double2 s = shifted[j * rows + m];
d = make_double2((s.x - f[m].x) / step, (s.y - f[m].y) / step);
} else {
d = df_dt[3 * static_cast<size_t>(m) + j - 3];
}
jacobian[6 * static_cast<size_t>(i) + j] = -k * (fcalc.x * d.x + fcalc.y * d.y);
}
}
// out[b] = sum over i of a[i * lda + ia] * c[i * ldc + ic] for the pair b = (ia, ic): one block per pair,
// a fixed stride per thread and a fixed shared-memory tree, so the sum is the same on every run.
__global__ void products_kernel(const double *a, int lda, const double *c, int ldc, int n, int nc, double *out) {
const int ia = blockIdx.x / nc, ic = blockIdx.x % nc;
double sum = 0;
for (int i = threadIdx.x; i < n; i += THREADS)
sum += a[static_cast<size_t>(i) * lda + ia] * c[static_cast<size_t>(i) * ldc + ic];
__shared__ double sh[THREADS];
sh[threadIdx.x] = sum;
__syncthreads();
for (int s = THREADS / 2; s > 0; s >>= 1) {
if (threadIdx.x < s)
sh[threadIdx.x] += sh[threadIdx.x + s];
__syncthreads();
}
if (threadIdx.x == 0)
out[blockIdx.x] = sh[0];
}
// J - J_k x, the scale re-fit projected out.
__global__ void project_kernel(double *jacobian, const double *jk, int p, const double *x, int n) {
const int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= n)
return;
for (int j = 0; j < 6; j++) {
double v = jacobian[6 * static_cast<size_t>(i) + j];
for (int a = 0; a < p; a++)
v -= jk[static_cast<size_t>(i) * p + a] * x[a * 6 + j];
jacobian[6 * static_cast<size_t>(i) + j] = v;
}
}
int blocks(size_t n) {
return static_cast<int>((n + THREADS - 1) / THREADS);
}
// A plan for an (nu, nv, nw) R2C transform, u fastest, `batch` grids back to back, whose work area the
// caller provides; its size in `work`.
cufftHandle MakePlan(int nu, int nv, int nw, int batch, size_t &work) {
cufftHandle plan;
cufft_err(cufftCreate(&plan));
cufft_err(cufftSetAutoAllocation(plan, 0));
int n[3] = {nw, nv, nu};
const int idist = nu * nv * nw, odist = (nu / 2 + 1) * nv * nw;
const cufftResult r = cufftMakePlanMany(plan, 3, n, nullptr, 1, idist, nullptr, 1, odist, CUFFT_R2C, batch, &work);
if (r != CUFFT_SUCCESS)
cufftDestroy(plan);
cufft_err(r);
return plan;
}
int GreatestStreamPriority() {
int least = 0, greatest = 0;
cuda_err(cudaDeviceGetStreamPriorityRange(&least, &greatest));
return greatest;
}
} // namespace
// The zone's grid as the density gather takes it.
static ModelDensityGrid DensityGrid(const RigidBodyGPUZone &zone) {
ModelDensityGrid g;
g.nu = zone.nu;
g.nv = zone.nv;
g.nw = zone.nw;
std::copy(zone.orth, zone.orth + 9, g.orth);
std::copy(zone.frac, zone.frac + 9, g.frac);
return g;
}
size_t RigidBodyGPUEngine::AxisBrickBound(int d, int n) {
return ModelDensityGPU::AxisBrickBound(d, n);
}
struct RigidBodyGPUEngineImpl {
int device = 0;
RigidBodyGPUCapacity cap;
// At the device's highest priority: a validation can run beside a merge, whose kernels would
// otherwise fill the card ahead of a fit's short ones, one evaluation at a time.
CudaStream stream{cudaStreamNonBlocking, GreatestStreamPriority()};
CudaDevicePtr<double> rel;
CudaDevicePtr<float4> pos;
CudaDevicePtr<int> mask_slot;
CudaDevicePtr<float> mask_radius;
CudaDevicePtr<double> mask_atoms; // 4 per mask atom: fractional x, y, z in [0,1), radius
CudaDevicePtr<float> grid; // 3 grids back to back
CudaDevicePtr<float2> spectrum; // their 3 transforms
CudaDevicePtr<char> fft_work;
CudaDevicePtr<RigidBodyGPUTerm> terms;
CudaDevicePtr<int> row_hkl;
CudaDevicePtr<double> row_scale, row_stol2;
CudaDevicePtr<double2> f, df_dt, shifted;
CudaDevicePtr<float2> fmask;
CudaDevicePtr<int> obs_row, point_obs;
CudaDevicePtr<float> obs_fobs;
CudaDevicePtr<double> residual, jacobian, jk;
CudaDevicePtr<float2> point_fcmol, point_fmask;
CudaDevicePtr<double> products, x;
std::map<std::array<int, 4>, cufftHandle> plans;
std::unique_ptr<ModelDensityGPU> density;
std::unique_ptr<ModelMaskGPU> mask;
std::unique_ptr<ModelScaleGPU> scale;
// The zone.
int n_atoms = 0;
GridGeom geom{};
double frac[9] = {};
double volume = 0;
size_t rows = 0, ops = 1, n_obs = 0, n_points = 0, n_constraints = 0, n_mask_atoms = 0;
std::vector<std::array<double, 6>> constraints;
double f_mean = 1;
explicit RigidBodyGPUEngineImpl(const RigidBodyGPUCapacity &c, int dev)
: device(dev), cap(c) {
const auto sync = CudaAlloc::Synchronous;
const size_t na = std::max<size_t>(cap.atoms, 1);
rel = CudaDevicePtr<double>(3 * na, sync);
pos = CudaDevicePtr<float4>(na, sync);
mask_slot = CudaDevicePtr<int>(na, sync);
mask_radius = CudaDevicePtr<float>(na, sync);
mask_atoms = CudaDevicePtr<double>(4 * na, sync);
grid = CudaDevicePtr<float>(3 * cap.grid_points, sync);
spectrum = CudaDevicePtr<float2>(3 * cap.complex_points, sync);
fft_work = CudaDevicePtr<char>(std::max<size_t>(cap.fft_work_bytes, 1), sync);
const size_t nr = std::max<size_t>(cap.rows, 1), no = std::max<size_t>(cap.observations, 1);
terms = CudaDevicePtr<RigidBodyGPUTerm>(std::max<size_t>(cap.terms, 1), sync);
row_hkl = CudaDevicePtr<int>(3 * nr, sync);
row_scale = CudaDevicePtr<double>(nr, sync);
row_stol2 = CudaDevicePtr<double>(nr, sync);
f = CudaDevicePtr<double2>(nr, sync);
df_dt = CudaDevicePtr<double2>(3 * nr, sync);
shifted = CudaDevicePtr<double2>(3 * nr, sync);
fmask = CudaDevicePtr<float2>(nr, sync);
obs_row = CudaDevicePtr<int>(no, sync);
point_obs = CudaDevicePtr<int>(no, sync);
obs_fobs = CudaDevicePtr<float>(no, sync);
residual = CudaDevicePtr<double>(no, sync);
jacobian = CudaDevicePtr<double>(6 * no, sync);
jk = CudaDevicePtr<double>(MAX_SCALE_PARAMS * no, sync);
point_fcmol = CudaDevicePtr<float2>(no, sync);
point_fmask = CudaDevicePtr<float2>(no, sync);
products = CudaDevicePtr<double>(MAX_SCALE_PARAMS * (MAX_SCALE_PARAMS + 6), sync);
x = CudaDevicePtr<double>(MAX_SCALE_PARAMS * 6, sync);
density = std::make_unique<ModelDensityGPU>(stream, cap.atoms, cap.pairs, cap.bricks);
mask = std::make_unique<ModelMaskGPU>(stream, std::max<size_t>(cap.grid_points, 1));
scale = std::make_unique<ModelScaleGPU>(stream, no);
}
// The points' fcmol and fmask at this evaluation.
void PackPoints() {
if (n_points == 0)
return;
pack_points_kernel<<<blocks(n_points), THREADS, 0, stream>>>(point_obs, obs_row, static_cast<int>(n_points),
f, fmask, point_fcmol, point_fmask);
cuda_err(cudaGetLastError());
}
~RigidBodyGPUEngineImpl() {
cudaSetDevice(device);
cudaStreamSynchronize(stream);
for (auto &[k, plan] : plans)
cufftDestroy(plan);
}
cufftHandle Plan(int batch) {
const std::array<int, 4> k{geom.nu, geom.nv, geom.nw, batch};
auto it = plans.find(k);
if (it != plans.end())
return it->second;
size_t work = 0;
const cufftHandle plan = MakePlan(geom.nu, geom.nv, geom.nw, batch, work);
if (work > cap.fft_work_bytes) {
cufftDestroy(plan);
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "rigid body: cuFFT work area over the reserve");
}
cufft_err(cufftSetWorkArea(plan, fft_work));
cufft_err(cufftSetStream(plan, stream));
plans.emplace(k, plan);
return plan;
}
size_t GridPoints() const { return static_cast<size_t>(geom.nu) * geom.nv * geom.nw; }
size_t ComplexPoints() const { return static_cast<size_t>(geom.nu / 2 + 1) * geom.nv * geom.nw; }
float Norm() const { return static_cast<float>(volume / static_cast<double>(GridPoints())); }
// The body placed at (R, t) and its density gathered into grid `slot`.
void Density(const double r[9], const double t[3], int slot) {
Placement p{};
std::copy(r, r + 9, p.r);
std::copy(t, t + 3, p.t);
std::copy(frac, frac + 9, p.frac);
place_kernel<<<blocks(n_atoms), THREADS, 0, stream>>>(rel, n_atoms, p, geom.nu, geom.nv, geom.nw, mask_slot,
mask_radius, pos, mask_atoms);
cuda_err(cudaGetLastError());
density->Compute(pos, grid.get() + slot * GridPoints());
}
void Compose(int slot, double2 *out, double2 *derivative) {
compose_kernel<<<blocks(rows), THREADS, 0, stream>>>(spectrum.get() + slot * ComplexPoints(), Norm(), terms,
row_scale, static_cast<int>(rows), static_cast<int>(ops),
out, derivative);
cuda_err(cudaGetLastError());
}
Scale MakeScale(double k_overall, const double b_star[6], double k_sol, double b_sol) const {
Scale s{};
s.k_overall = k_overall;
std::copy(b_star, b_star + 6, s.b_star);
s.k_sol = k_sol;
s.b_sol = b_sol;
s.n_constraints = static_cast<int>(n_constraints);
for (size_t c = 0; c < n_constraints; c++)
for (int q = 0; q < 6; q++)
s.constraints[c][q] = constraints[c][q];
return s;
}
};
size_t RigidBodyGPUEngine::Bricks(int nu, int nv, int nw) {
return ModelDensityGPU::Bricks(nu, nv, nw);
}
size_t RigidBodyGPUEngine::PairBound(const RigidBodyGPUZone &zone) {
return ModelDensityGPU::PairBound(DensityGrid(zone), zone.atoms);
}
bool RigidBodyGPUEngine::Supports(const RigidBodyGPUZone &zone) {
return ModelDensityGPU::Supports(DensityGrid(zone), zone.atoms);
}
void RigidBodyGPUEngine::MemoryInfo(size_t &free, size_t &total) {
cuda_err(cudaMemGetInfo(&free, &total));
}
int RigidBodyGPUEngine::CurrentDevice() {
int device = 0;
cuda_err(cudaGetDevice(&device));
return device;
}
size_t RigidBodyGPUEngine::FFTWorkBytes(int nu, int nv, int nw) {
size_t largest = 0;
for (int batch : {1, 3}) {
size_t work = 0;
const cufftHandle plan = MakePlan(nu, nv, nw, batch, work);
cufftDestroy(plan);
largest = std::max(largest, work);
}
return largest;
}
size_t RigidBodyGPUEngine::DeviceBytes(const RigidBodyGPUCapacity &c) {
const size_t na = c.atoms + 1, no = c.observations + 1, nr = c.rows + 1;
size_t bytes = na * (7 * sizeof(double) + sizeof(float4) + sizeof(int) + sizeof(float));
bytes += 3 * c.grid_points * sizeof(float) + 3 * c.complex_points * sizeof(float2) + c.fft_work_bytes;
bytes += ModelDensityGPU::DeviceBytes(c.atoms, c.pairs, c.bricks);
bytes += c.terms * sizeof(RigidBodyGPUTerm);
bytes += nr * (3 * sizeof(int) + 2 * sizeof(double) + 7 * sizeof(double2) + sizeof(float2));
bytes += no * (2 * sizeof(int) + sizeof(float) + (1 + 6 + MAX_SCALE_PARAMS) * sizeof(double) + 2 * sizeof(float2));
bytes += ModelMaskGPU::DeviceBytes(c.grid_points) + ModelScaleGPU::DeviceBytes(no);
return bytes;
}
RigidBodyGPUEngine::RigidBodyGPUEngine(const RigidBodyGPUCapacity &capacity, int device) {
cuda_err(cudaSetDevice(device));
impl_ = std::make_unique<RigidBodyGPUEngineImpl>(capacity, device);
}
RigidBodyGPUEngine::~RigidBodyGPUEngine() = default;
void RigidBodyGPUEngine::SetBody(const std::vector<std::array<double, 3>> &relative) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
if (relative.size() > e.cap.atoms)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "rigid body: more atoms than the reserve");
e.n_atoms = static_cast<int>(relative.size());
cuda_err(cudaMemcpyAsync(e.rel, relative.data(), relative.size() * 3 * sizeof(double), cudaMemcpyHostToDevice,
e.stream));
cuda_err(cudaStreamSynchronize(e.stream));
}
void RigidBodyGPUEngine::SetZone(const RigidBodyGPUZone &zone) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
const size_t np = static_cast<size_t>(zone.nu) * zone.nv * zone.nw;
const size_t nc = static_cast<size_t>(zone.nu / 2 + 1) * zone.nv * zone.nw;
if (static_cast<int>(zone.atoms.size()) != e.n_atoms || np > e.cap.grid_points || nc > e.cap.complex_points ||
Bricks(zone.nu, zone.nv, zone.nw) > e.cap.bricks || zone.row_hkl.size() > e.cap.rows ||
zone.terms.size() > e.cap.terms || zone.obs_row.size() > e.cap.observations ||
zone.constraints.size() + 1 > MAX_SCALE_PARAMS)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "rigid body: zone over the engine's reserve");
e.geom.nu = zone.nu;
e.geom.nv = zone.nv;
e.geom.nw = zone.nw;
std::copy(zone.frac, zone.frac + 9, e.frac);
e.volume = zone.volume;
e.rows = zone.row_hkl.size();
e.ops = zone.ops;
e.n_obs = zone.obs_row.size();
e.n_points = zone.point_obs.size();
e.n_mask_atoms = zone.mask_atom.size();
e.n_constraints = zone.constraints.size();
e.constraints = zone.constraints;
e.f_mean = zone.f_mean;
std::vector<int> slot(zone.atoms.size(), -1);
std::vector<float> radius(zone.atoms.size(), 0.0f);
for (size_t j = 0; j < zone.mask_atom.size(); j++) {
slot[zone.mask_atom[j]] = static_cast<int>(j);
radius[zone.mask_atom[j]] = zone.mask_radius[j];
}
auto up = [&](void *dst, const void *src, size_t bytes) {
if (bytes > 0)
cuda_err(cudaMemcpyAsync(dst, src, bytes, cudaMemcpyHostToDevice, e.stream));
};
up(e.mask_slot, slot.data(), slot.size() * sizeof(int));
up(e.mask_radius, radius.data(), radius.size() * sizeof(float));
up(e.terms, zone.terms.data(), zone.terms.size() * sizeof(RigidBodyGPUTerm));
up(e.row_hkl, zone.row_hkl.data(), zone.row_hkl.size() * 3 * sizeof(int));
up(e.row_scale, zone.row_scale.data(), zone.row_scale.size() * sizeof(double));
up(e.row_stol2, zone.row_stol2.data(), zone.row_stol2.size() * sizeof(double));
up(e.obs_row, zone.obs_row.data(), zone.obs_row.size() * sizeof(int));
up(e.obs_fobs, zone.obs_fobs.data(), zone.obs_fobs.size() * sizeof(float));
up(e.point_obs, zone.point_obs.data(), zone.point_obs.size() * sizeof(int));
ModelMaskGrid mask_grid;
mask_grid.nu = zone.nu;
mask_grid.nv = zone.nv;
mask_grid.nw = zone.nw;
std::copy(zone.orth, zone.orth + 9, mask_grid.orth);
mask_grid.volume = zone.volume;
std::vector<ModelMaskOp> images(zone.images.size());
for (size_t i = 0; i < images.size(); i++) {
std::copy(zone.images[i].begin(), zone.images[i].begin() + 9, images[i].rot);
std::copy(zone.images[i].begin() + 9, zone.images[i].end(), images[i].tran);
}
e.mask->SetGrid(mask_grid, images);
e.density->SetAtoms(DensityGrid(zone), zone.atoms);
e.scale->SetPoints(zone.point_hkl, zone.point_stol2, zone.point_fobs, zone.point_sigma, zone.constraints, zone.frac);
e.Plan(1);
e.Plan(3);
cuda_err(cudaStreamSynchronize(e.stream));
}
void RigidBodyGPUEngine::Fcalc(const double rotation[9], const double translation[3]) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
e.Density(rotation, translation, 0);
cufft_err(cufftExecR2C(e.Plan(1), e.grid, reinterpret_cast<cufftComplex *>(e.spectrum.get())));
e.Compose(0, e.f, e.df_dt);
}
void RigidBodyGPUEngine::Fmask() {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
float *mask_grid = e.grid.get() + e.GridPoints();
cufftComplex *mask_spectrum = reinterpret_cast<cufftComplex *>(e.spectrum.get() + e.ComplexPoints());
// The mask atoms Fcalc() placed, four doubles each, as ModelMaskAtom lays them out.
e.mask->Compute(reinterpret_cast<const ModelMaskAtom *>(e.mask_atoms.get()), static_cast<int>(e.n_mask_atoms),
mask_grid);
cufft_err(cufftExecR2C(e.Plan(1), mask_grid, mask_spectrum));
fmask_kernel<<<blocks(e.rows), THREADS, 0, e.stream>>>(reinterpret_cast<float2 *>(mask_spectrum), e.Norm(),
e.row_hkl, static_cast<int>(e.rows), e.geom.nu, e.geom.nv,
e.geom.nw, e.fmask);
cuda_err(cudaGetLastError());
}
void RigidBodyGPUEngine::DownloadPoints(std::vector<std::array<float, 2>> &fcmol,
std::vector<std::array<float, 2>> &fmask) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
fcmol.resize(e.n_points);
fmask.resize(e.n_points);
if (e.n_points == 0)
return;
e.PackPoints();
cuda_err(cudaMemcpyAsync(fcmol.data(), e.point_fcmol, e.n_points * sizeof(float2), cudaMemcpyDeviceToHost, e.stream));
cuda_err(cudaMemcpyAsync(fmask.data(), e.point_fmask, e.n_points * sizeof(float2), cudaMemcpyDeviceToHost, e.stream));
cuda_err(cudaStreamSynchronize(e.stream));
}
void RigidBodyGPUEngine::FitSolvent(double &k_sol, double &b_sol) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
e.PackPoints();
const ModelSolventFit fit = e.scale->FitSolvent(e.point_fcmol, e.point_fmask);
k_sol = fit.k_sol;
b_sol = fit.b_sol;
}
void RigidBodyGPUEngine::FitScale(double k_sol, double b_sol, double &k_overall, double b_star[6]) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
e.PackPoints();
const ModelScaleParams p = e.scale->Fit(e.point_fcmol, e.point_fmask, k_sol, b_sol);
k_overall = p.k_overall;
std::copy(p.b_star, p.b_star + 6, b_star);
}
void RigidBodyGPUEngine::Residuals(double k_overall, const double b_star[6], double k_sol, double b_sol,
double *residuals) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
const Scale sc = e.MakeScale(k_overall, b_star, k_sol, b_sol);
residual_kernel<<<blocks(e.n_obs), THREADS, 0, e.stream>>>(e.obs_row, e.obs_fobs, static_cast<int>(e.n_obs), e.f,
e.fmask, e.row_hkl, e.row_stol2, sc, e.f_mean,
e.residual);
cuda_err(cudaGetLastError());
cuda_err(cudaMemcpyAsync(residuals, e.residual, e.n_obs * sizeof(double), cudaMemcpyDeviceToHost, e.stream));
cuda_err(cudaStreamSynchronize(e.stream));
}
void RigidBodyGPUEngine::Jacobian(const double rotation[3][9], const double translation[3][3], double step,
double k_overall, const double b_star[6], double k_sol, double b_sol,
std::vector<double> &jtj, std::vector<double> &jtq) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
for (int j = 0; j < 3; j++)
e.Density(rotation[j], translation[j], j);
cufft_err(cufftExecR2C(e.Plan(3), e.grid, reinterpret_cast<cufftComplex *>(e.spectrum.get())));
for (int j = 0; j < 3; j++)
e.Compose(j, e.shifted.get() + j * e.rows, nullptr);
const Scale sc = e.MakeScale(k_overall, b_star, k_sol, b_sol);
const int p = 1 + static_cast<int>(e.n_constraints);
const int n = static_cast<int>(e.n_obs);
jacobian_kernel<<<blocks(e.n_obs), THREADS, 0, e.stream>>>(e.obs_row, n, e.f, e.df_dt, e.shifted, e.rows, e.fmask,
e.row_hkl, e.row_stol2, sc, e.f_mean, step, e.jacobian,
e.jk);
cuda_err(cudaGetLastError());
products_kernel<<<p * p, THREADS, 0, e.stream>>>(e.jk, p, e.jk, p, n, p, e.products);
cuda_err(cudaGetLastError());
products_kernel<<<p * 6, THREADS, 0, e.stream>>>(e.jk, p, e.jacobian, 6, n, 6, e.products.get() + p * p);
cuda_err(cudaGetLastError());
std::vector<double> out(p * p + p * 6);
cuda_err(cudaMemcpyAsync(out.data(), e.products, out.size() * sizeof(double), cudaMemcpyDeviceToHost, e.stream));
cuda_err(cudaStreamSynchronize(e.stream));
jtj.assign(out.begin(), out.begin() + p * p);
jtq.assign(out.begin() + p * p, out.end());
}
void RigidBodyGPUEngine::ProjectJacobian(const std::vector<double> &x, double *jacobian) {
RigidBodyGPUEngineImpl &e = *impl_;
cuda_err(cudaSetDevice(e.device));
const int p = 1 + static_cast<int>(e.n_constraints);
cuda_err(cudaMemcpyAsync(e.x, x.data(), x.size() * sizeof(double), cudaMemcpyHostToDevice, e.stream));
project_kernel<<<blocks(e.n_obs), THREADS, 0, e.stream>>>(e.jacobian, e.jk, p, e.x, static_cast<int>(e.n_obs));
cuda_err(cudaGetLastError());
cuda_err(cudaMemcpyAsync(jacobian, e.jacobian, e.n_obs * 6 * sizeof(double), cudaMemcpyDeviceToHost, e.stream));
cuda_err(cudaStreamSynchronize(e.stream));
}