Found while chasing a 12% run-to-run spread in the merged reflection count of one crystal. The GPU kernels claim output slots with an atomicAdd and, on overflow, undid the increment with an atomicSub - so the counter saturated at the capacity and the host could not tell a full buffer from an overflowing one. Which reflections survived was then decided by CUDA block scheduling and changed every run. Measured on that dataset: every frame predicts 23000-44000 against a 20000 buffer, and the spread reached the merged output (161591 / 165193 / 166110 / 166479 unique across four runs of the same command). Single-threaded runs diverge too - this is entirely GPU-side. Stop clamping the counter, so the true number predicted reaches the host, and warn once per predictor when it exceeds the buffer. Which reflections are kept is unchanged: making that reproducible means deciding what to keep when a frame predicts more than the pipeline carries, and the obvious answers are worse - the capacity is not the real limit, kPredictionOutput (10000, selected by smallest excitation error) is, and on this crystal both a bigger buffer and a strided selection collapse the merge, because the rotation combine rebuilds fulls from exactly the partials that a smallest-excitation-error cut throws away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
64 lines
2.8 KiB
C++
64 lines
2.8 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||
// SPDX-License-Identifier: GPL-3.0-only
|
||
|
||
#pragma once
|
||
|
||
#include <vector>
|
||
|
||
#include "../../common/CrystalLattice.h"
|
||
#include "../../common/DiffractionExperiment.h"
|
||
#include "../../common/Reflection.h"
|
||
|
||
struct BraggPredictionSettings {
|
||
float high_res_A = 1.5;
|
||
float ewald_dist_cutoff = 0.0005;
|
||
int max_hkl = 100;
|
||
char centering = 'P';
|
||
float wedge_deg = 0.1f;
|
||
float mosaicity_deg = 0.2f;
|
||
float min_zeta = 0.05;
|
||
float mosaicity_multiplier = 4.0;
|
||
// Relative X-ray bandwidth Δλ/λ expressed as a Gaussian sigma (0 = monochromatic).
|
||
// When > 0 the Ewald-shell acceptance is thickened radially per reflection by
|
||
// σ_bw = |recip_z|·bandwidth_sigma (= bλ/2d²), so the 1/d² pink-beam smear no
|
||
// longer clips high-resolution reflections.
|
||
float bandwidth_sigma = 0.0f;
|
||
};
|
||
|
||
class BraggPrediction {
|
||
protected:
|
||
const int max_reflections;
|
||
std::vector<Reflection> reflections;
|
||
|
||
// A frame that predicts more than the buffer holds keeps an ARBITRARY subset of them: the GPU
|
||
// kernels claim slots with an atomicAdd, so which ones survive depends on block scheduling and
|
||
// changes from run to run. Say so, once per predictor, rather than let it pass silently - it is
|
||
// not a small effect (measured: a 3% run-to-run spread in the number of merged reflections, and
|
||
// every frame of that dataset overflowed).
|
||
void ReportOverflow(int predicted);
|
||
bool overflow_reported = false;
|
||
|
||
// Deterministically cap Calc's output at kPredictionOutput: if more were predicted, keep the ones
|
||
// closest to the Ewald sphere (smallest excitation error), ties broken by hkl. Returns the kept
|
||
// count. Below the cap it is a no-op. Call at the end of every Calc override.
|
||
int TruncateToOutput(int count);
|
||
public:
|
||
// The prediction buffer holds up to kPredictionCapacity reflections so a strong lattice does not
|
||
// overflow it (the GPU kernels then fill it in a non-deterministic atomic order - ReportOverflow
|
||
// warns when that happens; a large unit cell, ~2.8e6 A^3, reaches ~40000 per frame and overflows on
|
||
// every one). Calc returns at most kPredictionOutput, the number that flows downstream and is serialized - kept low so the
|
||
// per-image reflection list stays within the frame transport headroom.
|
||
static constexpr int kPredictionCapacity = 20000;
|
||
static constexpr int kPredictionOutput = 10000;
|
||
|
||
explicit BraggPrediction(int max_reflections = kPredictionCapacity);
|
||
|
||
virtual ~BraggPrediction() = default;
|
||
virtual int Calc(const DiffractionExperiment &experiment, const CrystalLattice &lattice,
|
||
const BraggPredictionSettings &settings);
|
||
const std::vector<Reflection> &GetReflections() const;
|
||
};
|
||
|
||
|
||
|