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
89 lines
4.3 KiB
C++
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;
|
|
};
|