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
698 lines
30 KiB
Plaintext
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));
|
|
}
|