// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "RigidBodyGPUEngine.h" #include #include #include #include #include #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)); } // The calling thread on the engine's card for one call, and back on its own card after: a caller's later work // - a function of the search, or memory it allocates - runs on the card the caller chose, not on whichever card // the last engine it borrowed is on. class DeviceGuard { public: explicit DeviceGuard(int device) { cuda_err(cudaGetDevice(&previous_)); cuda_err(cudaSetDevice(device)); } ~DeviceGuard() { cudaSetDevice(previous_); } DeviceGuard(const DeviceGuard &) = delete; DeviceGuard &operator=(const DeviceGuard &) = delete; private: int previous_ = 0; }; void cufft_err(cufftResult val) { if (val != CUFFT_SUCCESS) throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "cuFFT error " + std::to_string(static_cast(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, not wrapped, as the CPU's density // walk centres an atom) 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]}; const int n3[3] = {nu, nv, nw}; double f[3]; float g[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]; g[k] = static_cast(f[k] * n3[k]); 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; } 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, const double2 *fixed, 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(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] = fixed != nullptr ? make_double2(scale * sr + fixed[m].x, scale * si + fixed[m].y) : 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(m) + k] = make_double2(-scale * two_pi * d[k][1], scale * two_pi * d[k][0]); } } __global__ void add_kernel(const double2 *a, int n, double2 *sum) { const int m = blockIdx.x * blockDim.x + threadIdx.x; if (m < n) sum[m] = make_double2(sum[m].x + a[m].x, sum[m].y + a[m].y); } // 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(nu / 2 + 1) * (imod(k, nv) + static_cast(nv) * imod(l, nw))]; const float re = out.x * norm, im = -out.y * norm; fmask[m] = make_float2(re, conj ? -im : im); } // Scaling::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(f[m].x), static_cast(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(sc.k_sol * exp(-sc.b_sol * stol2)); return make_float2(static_cast(f.x) + solvent * fm.x, static_cast(f.y) + solvent * fm.y); } // The residual of each observation: scale_data() applied to the row, then (Fobs - |F|) / . __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(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(i) + j] = 0.0; for (int a = 0; a < p; a++) jk[static_cast(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(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(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(m) + j - 3]; } jacobian[6 * static_cast(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(i) * lda + ia] * c[static_cast(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(i) + j]; for (int a = 0; a < p; a++) v -= jk[static_cast(i) * p + a] * x[a * 6 + j]; jacobian[6 * static_cast(i) + j] = v; } } int blocks(size_t n) { return static_cast((n + THREADS - 1) / THREADS); } // An (nu, nv, nw) R2C transform, u fastest. CudaFFTShape GridShape(int nu, int nv, int nw) { return {.n = {nw, nv, nu}, .type = CUFFT_R2C}; } 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 rel; CudaDevicePtr pos; CudaDevicePtr mask_slot; CudaDevicePtr mask_radius; CudaDevicePtr mask_atoms; // 4 per mask atom: fractional x, y, z in [0,1), radius CudaDevicePtr copies_mask_atoms; // the same for the body and fixed copies of it, grown as needed size_t copies_mask_capacity = 0; CudaDevicePtr grid; // 3 grids back to back CudaDevicePtr spectrum; // their 3 transforms CudaDevicePtr fft_work; CudaDevicePtr terms; CudaDevicePtr row_hkl; CudaDevicePtr row_scale, row_stol2; CudaDevicePtr f, df_dt, shifted, fixed; bool has_fixed = false; // the zone's fixed Fcalc is in `fixed` CudaDevicePtr fmask; CudaDevicePtr obs_row, point_obs; CudaDevicePtr obs_fobs; CudaDevicePtr residual, jacobian, jk; CudaDevicePtr point_fcmol, point_fmask; CudaDevicePtr products, x; // The zone grid's transform, one grid at a time: a batch of three gives the same bits, but would be one more // plan per zone and card to make (CudaFFTPlan). CudaFFTPlan plan; std::unique_ptr density; std::unique_ptr mask; std::unique_ptr 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> constraints; double f_mean = 1; explicit RigidBodyGPUEngineImpl(const RigidBodyGPUCapacity &c, int dev) : device(dev), cap(c) { // A card busy with a probe pass beside the run is waited for, not given up on (RigidBodyGPUPool). const auto sync = CudaAlloc::SynchronousAfterWorkBeside; const size_t na = std::max(cap.atoms, 1); rel = CudaDevicePtr(3 * na, sync); pos = CudaDevicePtr(na, sync); mask_slot = CudaDevicePtr(na, sync); mask_radius = CudaDevicePtr(na, sync); mask_atoms = CudaDevicePtr(4 * na, sync); grid = CudaDevicePtr(3 * cap.grid_points, sync); spectrum = CudaDevicePtr(3 * cap.complex_points, sync); fft_work = CudaDevicePtr(std::max(cap.fft_work_bytes, 1), sync); const size_t nr = std::max(cap.rows, 1), no = std::max(cap.observations, 1); terms = CudaDevicePtr(std::max(cap.terms, 1), sync); row_hkl = CudaDevicePtr(3 * nr, sync); row_scale = CudaDevicePtr(nr, sync); row_stol2 = CudaDevicePtr(nr, sync); f = CudaDevicePtr(nr, sync); df_dt = CudaDevicePtr(3 * nr, sync); shifted = CudaDevicePtr(3 * nr, sync); fixed = CudaDevicePtr(nr, sync); fmask = CudaDevicePtr(nr, sync); obs_row = CudaDevicePtr(no, sync); point_obs = CudaDevicePtr(no, sync); obs_fobs = CudaDevicePtr(no, sync); residual = CudaDevicePtr(no, sync); jacobian = CudaDevicePtr(6 * no, sync); jk = CudaDevicePtr(MAX_SCALE_PARAMS * no, sync); point_fcmol = CudaDevicePtr(no, sync); point_fmask = CudaDevicePtr(no, sync); products = CudaDevicePtr(MAX_SCALE_PARAMS * (MAX_SCALE_PARAMS + 6), sync); x = CudaDevicePtr(MAX_SCALE_PARAMS * 6, sync); density = std::make_unique(stream, cap.atoms, cap.pairs, cap.bricks); mask = std::make_unique(stream, std::max(cap.grid_points, 1)); scale = std::make_unique(stream, no); } // The points' fcmol and fmask at this evaluation. void PackPoints() { if (n_points == 0) return; pack_points_kernel<<>>(point_obs, obs_row, static_cast(n_points), f, fmask, point_fcmol, point_fmask); cuda_err(cudaGetLastError()); } ~RigidBodyGPUEngineImpl() { cudaSetDevice(device); cudaStreamSynchronize(stream); } // Grid `slot`'s transform into its spectrum. void Transform(int slot) { cufft_err(plan.R2C(grid.get() + slot * GridPoints(), reinterpret_cast(spectrum.get()) + slot * ComplexPoints())); } size_t GridPoints() const { return static_cast(geom.nu) * geom.nv * geom.nw; } size_t ComplexPoints() const { return static_cast(geom.nu / 2 + 1) * geom.nv * geom.nw; } float Norm() const { return static_cast(volume / static_cast(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<<>>(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<<>>(spectrum.get() + slot * ComplexPoints(), Norm(), terms, row_scale, static_cast(rows), static_cast(ops), has_fixed ? fixed.get() : nullptr, 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(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); } int RigidBodyGPUEngine::CurrentDevice() { int device = 0; cuda_err(cudaGetDevice(&device)); return device; } size_t RigidBodyGPUEngine::FFTWorkBytes(int nu, int nv, int nw) { return CudaFFTPlan::WorkBytes(GridShape(nu, nv, nw)); } 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) + 8 * 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) { const DeviceGuard guard(device); impl_ = std::make_unique(capacity, device); } // Released on its own card, which need not be the one the destroying thread is on. RigidBodyGPUEngine::~RigidBodyGPUEngine() { int current = 0; cudaGetDevice(¤t); cudaSetDevice(impl_->device); impl_.reset(); cudaSetDevice(current); } int RigidBodyGPUEngine::Device() const { return impl_->device; } void RigidBodyGPUEngine::SetBody(const std::vector> &relative) { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(e.device); if (relative.size() > e.cap.atoms) throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "rigid body: more atoms than the reserve"); e.n_atoms = static_cast(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_; const DeviceGuard guard(e.device); const size_t np = static_cast(zone.nu) * zone.nv * zone.nw; const size_t nc = static_cast(zone.nu / 2 + 1) * zone.nv * zone.nw; if (static_cast(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; e.has_fixed = false; std::vector slot(zone.atoms.size(), -1); std::vector radius(zone.atoms.size(), 0.0f); for (size_t j = 0; j < zone.mask_atom.size(); j++) { slot[zone.mask_atom[j]] = static_cast(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 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 = CudaFFTPlan(GridShape(e.geom.nu, e.geom.nv, e.geom.nw), e.stream, e.fft_work); if (e.plan.WorkBytes() > e.cap.fft_work_bytes) throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "rigid body: cuFFT work area over the reserve"); cuda_err(cudaStreamSynchronize(e.stream)); } void RigidBodyGPUEngine::Fcalc(const double rotation[9], const double translation[3]) { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(e.device); e.Density(rotation, translation, 0); e.Transform(0); e.Compose(0, e.f, e.df_dt); } void RigidBodyGPUEngine::SetFixedCopies(const std::vector> &fixed, const std::array &moving) { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(e.device); const size_t copies = fixed.size() + 1; if (e.copies_mask_capacity < copies * e.n_mask_atoms) { e.copies_mask_capacity = copies * e.n_mask_atoms; e.copies_mask_atoms = CudaDevicePtr(4 * std::max(e.copies_mask_capacity, 1), CudaAlloc::Synchronous); } cuda_err(cudaMemsetAsync(e.fixed, 0, e.rows * sizeof(double2), e.stream)); for (size_t c = 0; c < copies; c++) { const std::array &x = c < fixed.size() ? fixed[c] : moving; Placement p{}; std::copy(x.begin(), x.begin() + 9, p.r); std::copy(x.begin() + 9, x.end(), p.t); std::copy(e.frac, e.frac + 9, p.frac); place_kernel<<>>(e.rel, e.n_atoms, p, e.geom.nu, e.geom.nv, e.geom.nw, e.mask_slot, e.mask_radius, e.pos, e.copies_mask_atoms.get() + 4 * c * e.n_mask_atoms); cuda_err(cudaGetLastError()); if (c < fixed.size()) { e.density->Compute(e.pos, e.grid.get()); cufft_err(e.plan.R2C(e.grid, reinterpret_cast(e.spectrum.get()))); e.has_fixed = false; e.Compose(0, e.shifted, nullptr); add_kernel<<>>(e.shifted, static_cast(e.rows), e.fixed); cuda_err(cudaGetLastError()); } } float *mask_grid = e.grid.get() + e.GridPoints(); cufftComplex *mask_spectrum = reinterpret_cast(e.spectrum.get() + e.ComplexPoints()); e.mask->Compute(reinterpret_cast(e.copies_mask_atoms.get()), static_cast(copies * e.n_mask_atoms), mask_grid); cufft_err(e.plan.R2C(mask_grid, mask_spectrum)); fmask_kernel<<>>(reinterpret_cast(mask_spectrum), e.Norm(), e.row_hkl, static_cast(e.rows), e.geom.nu, e.geom.nv, e.geom.nw, e.fmask); cuda_err(cudaGetLastError()); cuda_err(cudaStreamSynchronize(e.stream)); e.has_fixed = true; } void RigidBodyGPUEngine::Fmask() { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(e.device); float *mask_grid = e.grid.get() + e.GridPoints(); cufftComplex *mask_spectrum = reinterpret_cast(e.spectrum.get() + e.ComplexPoints()); // The mask atoms Fcalc() placed, four doubles each, as ModelMaskAtom lays them out. e.mask->Compute(reinterpret_cast(e.mask_atoms.get()), static_cast(e.n_mask_atoms), mask_grid); cufft_err(e.plan.R2C(mask_grid, mask_spectrum)); fmask_kernel<<>>(reinterpret_cast(mask_spectrum), e.Norm(), e.row_hkl, static_cast(e.rows), e.geom.nu, e.geom.nv, e.geom.nw, e.fmask); cuda_err(cudaGetLastError()); } void RigidBodyGPUEngine::DownloadPoints(std::vector> &fcmol, std::vector> &fmask) { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(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 b_hi, double &k_sol, double &b_sol) { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(e.device); e.PackPoints(); const ModelSolventFit fit = e.scale->FitSolvent(e.point_fcmol, e.point_fmask, b_hi); 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_; const DeviceGuard guard(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_; const DeviceGuard guard(e.device); const Scale sc = e.MakeScale(k_overall, b_star, k_sol, b_sol); residual_kernel<<>>(e.obs_row, e.obs_fobs, static_cast(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 &jtj, std::vector &jtq) { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(e.device); for (int j = 0; j < 3; j++) e.Density(rotation[j], translation[j], j); for (int j = 0; j < 3; j++) e.Transform(j); 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(e.n_constraints); const int n = static_cast(e.n_obs); jacobian_kernel<<>>(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<<

>>(e.jk, p, e.jk, p, n, p, e.products); cuda_err(cudaGetLastError()); products_kernel<<

>>(e.jk, p, e.jacobian, 6, n, 6, e.products.get() + p * p); cuda_err(cudaGetLastError()); std::vector 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 &x, double *jacobian) { RigidBodyGPUEngineImpl &e = *impl_; const DeviceGuard guard(e.device); const int p = 1 + static_cast(e.n_constraints); cuda_err(cudaMemcpyAsync(e.x, x.data(), x.size() * sizeof(double), cudaMemcpyHostToDevice, e.stream)); project_kernel<<>>(e.jacobian, e.jk, p, e.x, static_cast(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)); }