Files
Jungfraujoch/image_analysis/bragg_integration/BraggIntegrationEngineMetal.h
T
Filip LeonarskiandClaude Opus 5.5 229a432a66
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m54s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m49s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m58s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 18m10s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m35s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m13s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 25m6s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 26m15s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m34s
Build Packages / Generate python client (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m1s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m5s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m36s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 24m54s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 22m7s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 25m53s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 28m8s
Build Packages / Unit tests (push) Failing after 3h11m38s
Build Packages / Validation Battery (push) Successful in 33m42s
RESULT-CHANGING (owner approved, ulp level): Metal profile fit, CPU Gaussian from a shared exp
Pass B of BraggIntegrationEngineCPU is split where a device can take over: per reflection, what the
fit needs is worked out in double (BraggFitSetup), the profile, its readable mass and the reweighted
fit run in single precision (FitProfile -> BraggFitOut), and the error, the summation guard and the
result are finished in double. BraggIntegrationEngineMetal runs the middle as a Metal kernel, one
thread per reflection, summing in the CPU's order (FitOnDevice); the CUDA engine is untouched.

The Gaussian's exp is now BraggExp (BraggExp.h), on the CPU and on the GPU: no GPU exp reproduces the
host's expf, and BraggExp uses only +, -, * and exact power-of-two scaling, so both compile it to the
same bits. Checked on the Apple M6 over every float in [-110, 89] (2.2e9 arguments), with subnormal
products and sums matching too. It is closer to correctly rounded than macOS expf (max 0.53 ulp;
0.2% of arguments below -1 not correctly rounded vs 0.62%). This is the one result change: per
observation |dI|/sigma p99.9 <= 1.7e-6, max 1e-4, 10-30x below what ba3f75475 moved. Merged
statistics (resolution, completeness, CC1/2, R_meas, I/sigma, ISa, space group) are identical on
lysozyme, cytochrome C, myoglobin, a small-molecule set and 4nwv; only tNCS fields that flip on any
last-digit change move (cytochrome C's pseudo-translation flag, whose modulation ratio sits at the
2.5 threshold, turns off).

The Metal engine stays bit-identical to the CPU engine: BraggIntegrationEngineMetalTest passes
unchanged, and catches planted kernel bugs (another exp, 3 iterations, a dropped lost-peak test, <=
for the disk, inverted exclude logic).

Measured wall time, Apple M6 (CPU pass B -> GPU pass B): lysozyme 24.7 -> 18.6 s, myoglobin 39.8 ->
35.7 s, cytochrome C 59.3 -> 49.0 s, 4nwv 150.2 -> 131.3 s; the small-molecule set, sparse frames,
92.5 -> 95.6 s (noise of +-3 s). Metal's fast::exp in the kernel instead measured the same on every
set (18.6, 35.8, 49.1, 131.2 s), so it would buy nothing for the bitwise check it would cost. On the
CPU alone BraggExp costs nothing measurable (cytochrome C --no-gpu 72.8 -> 73.1 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:18:43 +02:00

51 lines
2.7 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <memory>
#include <string>
#include "BraggIntegrationEngineCPU.h"
// Apple GPU engine (macOS only): BraggIntegrationEngineCPU with its pixel work - the neighbour mask, the
// owner map, pass A's walk over every box, the background clip and pass B's single-precision profile
// fit - run as Metal kernels, one thread per reflection. Everything computed in double and the profile
// learning stay on the CPU, in the CPU engine's own code: Apple GPUs have no double precision.
//
// The result is the CPU engine's, bit for bit: the integer sums do not depend on the order the GPU
// visits pixels in, the fit's float sums are taken in the CPU's order, one reflection per thread, and
// the arithmetic deciding which pixel is which is the same source (BraggStencil.h, BraggPassB.h, and
// BraggExp.h for the Gaussian, as no GPU exp matches the host's), compiled for the GPU with fast math
// off - sqrt and division then round correctly - and, as on the host, with no a*b+c contracted into a
// fused multiply-add. The CPU and the GPU read the same memory, so the preprocessed image is not
// copied. tests/BraggIntegrationEngineMetalTest.cpp holds the engine to all of this.
class BraggIntegrationEngineMetal : public BraggIntegrationEngineCPU {
struct Device;
std::unique_ptr<Device> dev;
uint64_t frames_on_gpu = 0;
bool FitOnDevice(const std::vector<BraggFitSetup> &setups, const std::vector<float> &shell_profiles,
std::vector<BraggFitOut> &fits) override;
public:
// Whether this engine can integrate this experiment: a Metal device the process may use (rugnux
// --no-gpu hides it, see hide_gpus) and settings the kernels cover - not the trimmed-mean background,
// which only the CPU engine sorts the ring for.
static bool Available(const DiffractionExperiment &experiment);
// The Metal device's name, for the log; empty where there is none or the process may not use it.
static std::string DeviceName();
BraggIntegrationEngineMetal(const DiffractionExperiment &experiment, const PixelMask &mask);
~BraggIntegrationEngineMetal() override;
using BraggIntegrationEngineCPU::Run; // the CompressedImage overload stays the CPU engine's
std::vector<Reflection> Run(const ImagePreprocessorBuffer &image,
const std::vector<Reflection> &predicted, size_t npredicted,
int64_t image_number) override;
// The frames whose pixels the GPU walked, rather than the CPU engine this one falls back to.
[[nodiscard]] uint64_t FramesOnGPU() const { return frames_on_gpu; }
};