spot_finding: accumulate the adaptive ring statistics in integers
The per-ring sums were floats reduced by atomics, so the ring sigma - and with it the detection threshold - depended on the order the blocks happened to arrive in. Detection compares an INTEGER pixel value against that threshold, so a threshold that drifts across an integer flips every pixel of that value in the ring at once, which is how a last-bit difference turned into a different spot list. A preprocessed pixel is an exact int32 and the masked and saturated sentinels are skipped, so v and v*v are exact in 64 bits, and integer addition is associative: the sums no longer care about arrival order. Both engines now accumulate the same way, so they agree exactly rather than approximately, and the GPU spot list is bit-identical across runs. The corrected sums that feed the reported azimuthal profile stay float - a pixel value times a float correction has no exact integer form - but they do not enter the detection decision. Cost: the ring reduction needs 28 bytes per bin instead of 20 in the plain pass, which drops it from eight co-resident blocks per SM to seven and costs about 11% of that kernel (0.582 -> 0.650 ms/frame on a 4.5 Mpx frame). End to end it does not show: alternating runs on three rotation crystals came out the same or slightly faster, and the battery is unchanged in every number. The CPU engine got 30% faster (32.2 -> 22.6 ms/frame), integers being cheaper than doubles. Tests: exact CPU/GPU agreement on the spot list, and 50 repeats of bit-identical output where there were four. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,15 +187,51 @@ TEST_CASE("AdaptiveSpotFinderGPU_RunToRunReproducible", "[AdaptiveSpotFinderGPU]
|
||||
auto stream = std::make_shared<CudaStream>();
|
||||
AdaptiveSpotFinderGPU gpu(mapping, stream);
|
||||
|
||||
// The ring accumulators are exact integers, so the threshold does not depend on the order the
|
||||
// block atomics arrive in and the spot list has to be bit-identical every time - not merely
|
||||
// close. Repeat enough times to give a scheduling-dependent threshold a chance to show itself:
|
||||
// the effect it used to have was ~1 changed observation in a million, so a handful of repeats on
|
||||
// a quiet background would not have caught it.
|
||||
const auto first = gpu.Run(buffer, settings);
|
||||
REQUIRE(first.size() > 0);
|
||||
for (int repeat = 0; repeat < 4; repeat++) {
|
||||
for (int repeat = 0; repeat < 50; repeat++) {
|
||||
const auto again = gpu.Run(buffer, settings);
|
||||
REQUIRE(again.size() == first.size());
|
||||
REQUIRE(SortedCoords(again) == SortedCoords(first));
|
||||
}
|
||||
}
|
||||
|
||||
// The threshold is computed from sums of int32 pixel values, so the two engines can agree EXACTLY
|
||||
// rather than approximately - and that is the property worth locking, because it is what makes the
|
||||
// GPU path's spot list independent of how the reduction happened to be scheduled.
|
||||
TEST_CASE("AdaptiveSpotFinderGPU_RingStatsMatchCPUExactly", "[AdaptiveSpotFinderGPU]") {
|
||||
if (get_gpu_count() == 0) {
|
||||
WARN("No CUDA GPU present. Skipping AdaptiveSpotFinderGPU_RingStatsMatchCPUExactly");
|
||||
return;
|
||||
}
|
||||
|
||||
DiffractionExperiment x = MakeExperiment();
|
||||
PixelMask pixel_mask(x);
|
||||
AzimuthalIntegrationMapping mapping(x, pixel_mask);
|
||||
|
||||
ImagePreprocessorBufferGPU buffer(x.GetPixelsNum());
|
||||
FillTestImage(buffer, x);
|
||||
REQUIRE(cudaMemcpy(buffer.getGPUBuffer(), buffer.getBuffer().data(),
|
||||
x.GetPixelsNum() * sizeof(int32_t), cudaMemcpyHostToDevice) == cudaSuccess);
|
||||
REQUIRE(cudaDeviceSynchronize() == cudaSuccess);
|
||||
|
||||
const SpotFindingSettings settings = AdaptiveSettings();
|
||||
|
||||
auto stream = std::make_shared<CudaStream>();
|
||||
AdaptiveSpotFinderGPU gpu(mapping, stream);
|
||||
AdaptiveSpotFinderCPU cpu(mapping);
|
||||
|
||||
const auto gpu_spots = gpu.Run(buffer, settings);
|
||||
const auto cpu_spots = cpu.Run(buffer, settings);
|
||||
REQUIRE(gpu_spots.size() == cpu_spots.size());
|
||||
REQUIRE(SortedCoords(gpu_spots) == SortedCoords(cpu_spots));
|
||||
}
|
||||
|
||||
TEST_CASE("AdaptiveSpotFinderGPU_Speed", "[AdaptiveSpotFinderGPU][.benchmark]") {
|
||||
if (get_gpu_count() == 0) {
|
||||
WARN("No CUDA GPU present. Skipping AdaptiveSpotFinderGPU_Speed");
|
||||
|
||||
Reference in New Issue
Block a user