Files
Jungfraujoch/image_analysis/structure_refinement/ModelDensityGPU.h
T
leonarski_fandClaude Opus 5.5 b6392a3f58 RESULT-CHANGING (ulp level, CUDA builds): the CUDA model density is the CPU walk's, bit for bit
ModelDensityGPU's gather had arithmetic of its own: per-thread offsets in Cartesian, the atom's
nearest image to the brick, r2 = x x + y y + z z, the radius squared in float. It now does what
UseDensityBoxesByPlane + AtomDensity (ModelGrid.cpp) and ModelDensityMetal.metal do, in their order:
- each brick's atoms carry their box (c - d, wrapped start, 2 d); a point is in the box by a
  subtraction, and f = centre - (u, v, w) in the box's own unwrapped index;
- x = m00 fx + (m01 fy + m02 fz), y = m11 fy + m12 fz, z = m22 fz with gemmi's orth_n
  (orth * (1/n), as Grid sets it) in float; r2 = (y y + z z) + x x against max_r2 = float(r^2)
  rounded from the double radius (new ModelDensityAtom field);
- gemmi's expapprox, the five Gaussians in order, atoms in model order per point. Device code is
  already compiled with --fmad=false (top-level CMakeLists), so nothing is contracted.
- The centres are no longer wrapped into the cell (ModelStructureFactorsGPU, RigidBodyGPU's
  place_kernel): float(fractional * n), as MakeDensityBox holds them; the mask keeps wrapped
  fractional coordinates. The 'narrow' nearest-image branch is gone: Supports() already leaves
  only boxes narrower than the cell.
The target is PutModelDensityOnGrid() with no orbits to symmetrize (one copy of the content, which
is what the gather grids; symmetry stays composed in reciprocal space). A rounding tie in the box
centre c (float on the GPU, double on the CPU) changes no point: either box covers the sphere with
half a spacing to spare. New ModelDensityGPU_BitIdenticalToCPU ([ModelValidation][gpu]) memcmps the
grids: triclinic P1 and P 21 21 21 cells, 3000 atoms over -0.5..1.5 of the cell, a quarter
anisotropic - 0 points differ.

RTX 5080, alternating base (8d199cceb) / this, under the GPU lock:
- gather kernel (nsys): 4nwv --model median 2.06 -> 2.55 ms, total 0.79 -> 0.97 s;
  9s02 --mr median 0.30 -> 0.37 ms, total 0.94 -> 1.17 s (+~25%, the box test and per-point
  index arithmetic).
- wall, two each: 4nwv --model 76.0/68.9 -> 74.4/71.0 s, --mr 71.3/71.1 -> 70.9/71.6 s;
  9s02 --model 25.1/22.6 -> 23.3/23.5 s, --mr 23.3/23.6 -> 23.3/23.2 s; 5reo --model
  6.2/5.5 -> 5.5/5.7 s. Model validation 4nwv 12.4/12.3 -> 12.3/11.7 s; MR 4nwv 15.4/15.2 ->
  15.4/15.1 s, 9s02 2.3/2.4 -> 2.4/2.4 s: no change beyond noise.
- outputs (each build run-to-run identical): p.hkl identical on all; R-work/R-free and every
  decision unchanged (4nwv validation 0.2982/0.2969, 9s02 MR AGREES P 21 21 2 0.2503/0.2517,
  4nwv MR UNDECIDED I 2 2 2 / I 21 21 21). Moved: MODEL_FIT_SIGMA 9s02 99.21 -> 101.68, 5reo
  103.96 -> 104.98; MR placement Z 9s02 142.1 -> 145.6, 4nwv 46.7 -> 46.2; 4nwv anomalous site
  4.01 -> 4.02 sigma; 4nwv placed model 1322/125462 lines in the last digit (2fo-fc max |d| 0.04
  rms, CC 0.99999994); 9s02/5reo models identical, maps to 2e-5 rms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 18:10:23 +02:00

89 lines
4.3 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
// The density of a model's atoms on a grid, on the GPU (CUDA builds only): gemmi's
// DensityCalculator::put_model_density_on_grid() without the symmetrize - ONE copy of the content. The
// rigid body (RigidBodyGPU.cu) and the model's structure factors (ModelStructureFactorsGPU.cu) both grid
// with it, and compose the crystal's symmetry in reciprocal space (SymmetryComposition, RigidBodyRefine.h).
//
// A gather, not a scatter: the grid is cut into bricks of BRICK^3 points, each brick is one block, and
// every point adds, in model order, each atom whose box holds it and whose radius reaches it. Nothing is
// summed with a floating-point atomic. The arithmetic is the CPU walk's (UseDensityBoxesByPlane and
// AtomDensity, ModelGrid.cpp; ModelDensityMetal.metal spells out the same), so the grid is
// PutModelDensityOnGrid()'s before its symmetrize, bit for bit, for the same atoms and centres -
// tests/ModelDensityGPUTest.cpp holds it to that.
#include <cstddef>
#include <vector>
#include <cuda_runtime.h>
#include "../indexing/CUDAMemHelpers.h"
// One atom's density on a grid, as PutModelDensityOnGrid() (ModelGrid.cpp) sets it up: gemmi's
// precalculated five-Gaussian sum and the radius it cuts the sum at.
struct ModelDensityAtom {
float a[5];
float b[5][6]; // isotropic: b[k][0] multiplies r^2; anisotropic: the matrix, u11 u22 u33 u12 u13 u23
float occ;
float radius;
float max_r2; // the radius squared, as PutModelDensityOnGrid() rounds it: float(radius^2) in double
int aniso;
};
// The grid the density is put on, u fastest: index = u + nu * (v + nv * w).
struct ModelDensityGrid {
int nu = 0, nv = 0, nw = 0;
double orth[9] = {}, frac[9] = {}; // the cell's, row-major
};
class ModelDensityGPU {
public:
// The bytes an instance of this capacity reserves on the device.
static size_t DeviceBytes(size_t max_atoms, size_t max_pairs, size_t max_bricks);
static size_t Bricks(int nu, int nv, int nw);
// The most gather bricks the 2 d + 1 points of an atom's box can fall in along an axis of n points.
static size_t AxisBrickBound(int d, int n);
// gemmi's box around each atom (MakeAtomBox, ModelGrid.cpp): the points within ceil(radius / spacing)
// of the nearest one along each axis, three per atom.
static std::vector<int> Boxes(const ModelDensityGrid &grid, const std::vector<ModelDensityAtom> &atoms);
// (brick, atom) pairs of the gather over at most.
static size_t PairBound(const ModelDensityGrid &grid, const std::vector<ModelDensityAtom> &atoms);
// Whether the gather reproduces gemmi's box walk on this grid: every atom's box narrower than the cell,
// so that no point is reached by two images of one atom.
static bool Supports(const ModelDensityGrid &grid, const std::vector<ModelDensityAtom> &atoms);
ModelDensityGPU(cudaStream_t stream, size_t max_atoms, size_t max_pairs, size_t max_bricks);
// The grid and the atoms' densities. Throws if they are over the capacity or if !Supports().
void SetAtoms(const ModelDensityGrid &grid, const std::vector<ModelDensityAtom> &atoms);
// The density into d_grid (nu * nv * nw floats, every point written). d_pos: each atom's centre in
// grid units - its fractional position times the grid size, as the CPU's DensityBox holds it, not
// wrapped into the cell - in the order SetAtoms() got them; w is not read. Waits on the stream once,
// for the number of (brick, atom) pairs.
void Compute(const float4 *d_pos, float *d_grid);
// What the gather kernel needs of the grid.
struct Geometry {
int nu, nv, nw;
// gemmi's orth_n, orth * diag(1/nu, 1/nv, 1/nw), upper triangular, in single precision
float m00, m01, m02, m11, m12, m22;
};
private:
cudaStream_t stream;
size_t max_atoms, max_pairs, max_bricks;
Geometry geom{};
int n_atoms = 0;
CudaDevicePtr<ModelDensityAtom> atoms;
CudaDevicePtr<int> box;
CudaDevicePtr<int> overflow; // an atom's box over MAX_BRICKS_PER_AXIS
CudaDevicePtr<int> count, offset, key, value, key_sorted, value_sorted, brick_start, brick_end;
CudaDevicePtr<char> cub_temp;
size_t cub_bytes = 0;
};