Two byte-identical cuts to the per-evaluation host chain, which on a large model is twice the GPU's work per evaluation (34 ms against 16 ms of kernels per weight-trial slot, measured in nsys) and so bounds the refinement on one card with three slots, and the whole of it on a card with one slot. - The gather's per-atom density records (38 MB on a 242k-atom model, sent on every B-factor evaluation) live in a PinnedVector and go up by DMA; the boxes go through a pinned staging buffer of the density object. ModelDensityGPU::SetAtoms and RefinementGPUEngine::SetDensities take a pointer and a count. - RefinementMinimiser::Direction takes each pair's dot product in the same sweep that applies the previous pair's update to q: the dot products are serial chains of n additions, and the update's memory traffic now hides under them (20 -> 13 ms per call at 365k parameters). The same operations per element in the same order; the loop-2 coefficient stays written as (a[i] - b) with b on its own, because a[i] - rho * dot contracts into a fused multiply-add and changes the bits (checked bitwise in a standalone harness). - A debug line with the GPU engine's bytes and the budget. 4nwv (open arm, 242k atoms), live flags, one lock hold, loaded box: refinement 42.35 -> 38.40 s; refine.gpu.upload 8.14 -> 3.94 s. All outputs identical but provenance lines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
99 lines
5.1 KiB
C++
99 lines
5.1 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);
|
|
static void Boxes(const ModelDensityGrid &grid, const ModelDensityAtom *atoms, size_t n, int *box);
|
|
// (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);
|
|
static bool Supports(const ModelDensityGrid &grid, const int *box, size_t n);
|
|
|
|
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(). The atoms are
|
|
// read until the call returns; from pinned memory (PinnedVector) they go up by DMA.
|
|
void SetAtoms(const ModelDensityGrid &grid, const std::vector<ModelDensityAtom> &atoms);
|
|
void SetAtoms(const ModelDensityGrid &grid, const ModelDensityAtom *atoms, size_t n);
|
|
|
|
// 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);
|
|
|
|
// The densities and the boxes of the atoms last set, on the device, for a kernel that walks the same boxes.
|
|
const ModelDensityAtom *DeviceAtoms() const { return atoms; }
|
|
const int *DeviceBoxes() const { return box; }
|
|
|
|
// 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;
|
|
CudaHostPtr<int> host_box; // the boxes' staging buffer, pinned
|
|
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;
|
|
CudaHostPtr<int> host_count; // the pair count and the overflow flag Compute() reads back, pinned
|
|
CudaDevicePtr<char> cub_temp;
|
|
size_t cub_bytes = 0;
|
|
};
|