On macOS, where a Metal device may be used (not under --no-gpu), PutModelDensityOnGrid puts the density on the grid with a Metal gather (ModelDensityMetal), as ModelDensityGPU.cu does under CUDA: the grid in bricks of 4^3 points, one threadgroup each; every point adds, in model order, each atom whose box holds it and whose radius reaches it. No float atomics. The arithmetic is the CPU walk's of the previous commit, spelled out in ModelDensityMetal.metal - the point's place in single precision in the same order, gemmi's float expapprox ported as is, fast math and contraction off - so the grid is the CPU's bit for bit (ModelDensityMetal_BitIdenticalToCPU, [portable][Metal]: a triclinic cell and a P 21 21 21 one, a quarter of the atoms anisotropic, boxes wrapping at every face). The symmetrize stays on the CPU. - Each brick's atoms: (brick, atom) pairs counted and filled on the CPU in parallel over slabs of w, each worker taking the atoms in order, so the lists keep model order; at most the atoms' boxes' bricks. - The kernel writes straight into the grid FFTW reads where its pages can be handed to the GPU (large vectors are page-aligned on macOS), into its own buffer and copied otherwise. - 4^3 bricks rather than CUDA's 8^3: every point tests each atom reaching its brick, and the smaller brick took a half to a third of the kernel time. The box test is a subtraction against a start the host wraps. - Every grid goes to the GPU: giving it only grids of 256k points or more, or models of 8000 atoms or more, left 9s02's MR 0.8 s slower. - A box wider than the cell (an atom reaching a point through two images) stays on the CPU, whose walk adds both. A Metal failure puts that grid and every later one on the CPU, with one warning. - Each call runs in an autorelease pool of its own (see the rotation function's). Apple M6 (16 GB), alternating with the rotation-function build (the previous commit but one), two each: - 4nwv: wall 295.8/292.6 -> 275.2/277.8 s; model validation 48.9/49.0 -> 41.0/40.9 s; MR 130.2/130.3 -> 121.4/122.5 s (rescoring 40.9/40.3 -> 35.6/36.5 s, null 31.7/32.1 -> 26.8/26.6 s); user CPU 1998/2011 -> 1692/1681 s; peak footprint 17.8/18.5 -> 17.2/18.6 GB. The density's CPU time, sampled over a whole run, 316 CPU-s (16% of the busy samples) -> none, the pairs' sort 31 CPU-s. - 9s02: wall 76.7/76.5 -> 72.9/73.1 s; MR 16.9/16.8 -> 13.5/13.4 s. - 9zlo: wall 70.4/70.7 -> 69.8/69.6 s; MR 3.1/3.1 -> 2.2/2.2 s. p.hkl, the placed model and the maps are byte-identical to the previous commit's (density on the CPU) on all three, and so are the MR and validation logs but for their timings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
109 lines
4.8 KiB
Metal
109 lines
4.8 KiB
Metal
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
// The model density's gather (ModelDensityMetal.h), compiled when it is first used from this source, which
|
|
// CMakeLists.txt copies into the binary - with fast math off and no a*b+c contracted, as on the host, so that
|
|
// every operation below rounds as the CPU's does.
|
|
|
|
#include <metal_stdlib>
|
|
using namespace metal;
|
|
|
|
#pragma clang fp contract(off)
|
|
|
|
// A brick's edge in points (ModelDensityMetal.mm says the same): every point of a brick tests each atom whose box
|
|
// reaches the brick, and with 4 rather than CUDA's 8 far fewer of them test atoms that miss them - the kernel
|
|
// took a half to a third of the time on an Apple M6.
|
|
constant int BRICK = 4;
|
|
|
|
struct Atom { // ModelDensityMetalAtom
|
|
float nctr[3];
|
|
int c[3], d[3];
|
|
float max_r2;
|
|
float occ;
|
|
int aniso;
|
|
float a[5];
|
|
float b[5][6];
|
|
};
|
|
|
|
// Where an atom's box starts along each axis, wrapped into the grid, and its width less one (2 d): the test
|
|
// whether a point is in the box, without a division.
|
|
struct Box {
|
|
int first[3];
|
|
int span[3];
|
|
};
|
|
|
|
struct Grid { // ModelDensityMetalGrid
|
|
int nu, nv, nw;
|
|
float m00, m01, m02, m11, m12, m22;
|
|
};
|
|
|
|
// gemmi's unsafe_expapprox() (formfact.hpp, MPL-2.0; https://github.com/project-gemmi/gemmi), which is based on
|
|
// expapprox() from https://github.com/jhjourdan/SIMD-math-prims: the same operations, the bits moved with as_type
|
|
// where gemmi uses memcpy.
|
|
static float expapprox(float x) {
|
|
const float val = 12102203.1615614f * x + 1065353216.f;
|
|
const int vali = static_cast<int>(val);
|
|
const int xu1 = vali & 0x7F800000;
|
|
const int xu2 = (vali & 0x7FFFFF) | 0x3F800000;
|
|
const float a = as_type<float>(xu1);
|
|
const float b = as_type<float>(xu2);
|
|
return a * (0.509871020f + b * (0.312146713f + b * (0.166617139f + b *
|
|
(-2.190619930e-3f + b * 1.3555747234e-2f))));
|
|
}
|
|
|
|
// The point p's offset in the box along an axis of n points, 0 to span where it is in it.
|
|
static int box_offset(int p, int first, int n) {
|
|
const int m = p - first;
|
|
return m < 0 ? m + n : m;
|
|
}
|
|
|
|
// One threadgroup per brick of BRICK^3 points: each point adds the brick's atoms in model order, as
|
|
// UseDensityBoxesByPlane() and AtomDensity (ModelGrid.cpp) add them on the CPU.
|
|
kernel void model_density(device const Atom *atoms [[buffer(0)]], constant Grid &g [[buffer(1)]],
|
|
device const uint *pair_atom [[buffer(2)]], device const uint *brick_first [[buffer(3)]],
|
|
device float *out [[buffer(4)]], device const Box *boxes [[buffer(5)]],
|
|
uint3 group [[threadgroup_position_in_grid]], uint3 t [[thread_position_in_threadgroup]]) {
|
|
const uint brick = group.x;
|
|
const int nbu = (g.nu + BRICK - 1) / BRICK, nbv = (g.nv + BRICK - 1) / BRICK;
|
|
const int u = int(brick % uint(nbu)) * BRICK + int(t.x);
|
|
const int v = int(brick / uint(nbu) % uint(nbv)) * BRICK + int(t.y);
|
|
const int w = int(brick / uint(nbu * nbv)) * BRICK + int(t.z);
|
|
if (u >= g.nu || v >= g.nv || w >= g.nw)
|
|
return;
|
|
float acc = 0.0f;
|
|
for (uint k = brick_first[brick]; k < brick_first[brick + 1]; k++) {
|
|
const uint i = pair_atom[k];
|
|
device const Box &box = boxes[i];
|
|
const int mu = box_offset(u, box.first[0], g.nu), mv = box_offset(v, box.first[1], g.nv),
|
|
mw = box_offset(w, box.first[2], g.nw);
|
|
if (mu > box.span[0] || mv > box.span[1] || mw > box.span[2])
|
|
continue;
|
|
device const Atom &at = atoms[i];
|
|
// The point's index in the box's own (unwrapped) range, as the CPU walks it.
|
|
const int uu = at.c[0] - at.d[0] + mu, vv = at.c[1] - at.d[1] + mv, ww = at.c[2] - at.d[2] + mw;
|
|
const float fx = at.nctr[0] - float(uu), fy = at.nctr[1] - float(vv), fz = at.nctr[2] - float(ww);
|
|
const float z = g.m22 * fz;
|
|
const float y = g.m11 * fy + g.m12 * fz;
|
|
const float yz = y * y + z * z;
|
|
const float x = g.m00 * fx + (g.m01 * fy + g.m02 * fz);
|
|
const float r2 = yz + x * x;
|
|
if (r2 > at.max_r2)
|
|
continue;
|
|
float density = 0.0f;
|
|
if (at.aniso == 0) {
|
|
for (int q = 0; q < 5; q++)
|
|
density += at.a[q] * expapprox(max(at.b[q][0] * r2, -88.f));
|
|
} else {
|
|
for (int q = 0; q < 5; q++) {
|
|
device const float *b = at.b[q];
|
|
// gemmi's SMat33::r_u_r(), in its order
|
|
const float rur = x * x * b[0] + y * y * b[1] + z * z * b[2]
|
|
+ 2 * (x * y * b[3] + x * z * b[4] + y * z * b[5]);
|
|
density += at.a[q] * expapprox(max(rur, -88.f));
|
|
}
|
|
}
|
|
acc += at.occ * density;
|
|
}
|
|
out[uint(u) + uint(g.nu) * (uint(v) + uint(g.nv) * uint(w))] = acc;
|
|
}
|