Every worker thread built a full set of analysis engines. Two of them are never asked for on the offline path: the fixed-threshold spot finder, because detection is adaptive by default, and the azimuthal integrator, because the fused adaptive finder produces the profile as a by-product. They are still needed elsewhere - the broker defaults to non-adaptive detection, and --no-adaptive-spots asks for the finder - so they are built on first use rather than removed. A lazily built finder takes the current resolution mask on construction; without that it would find spots outside the limits it was never told about. The bitshuffle decoder sized its output buffer for the widest pixel type there is rather than the one the images actually have, holding a second full frame per worker on 16-bit data. It is sized from the image now and grows if a later frame needs more. The shared-table checksum runs over eight interleaved lanes. FNV's multiply is a loop-carried dependency, so one chain retires a byte every few cycles whatever memory bandwidth is spare, and every worker hashes tens of megabytes of geometry tables as it builds its engines - about 5% of all CPU samples on a 16M-pixel detector. Measured on a 16M-pixel rotation dataset: cudaMalloc 11314 -> 9474 calls and, with cudaFree, 117 s -> 78 s of aggregate thread time; both synchronise the whole device, so that time is spent blocking every other worker. Whole battery 15m32s -> 12m30s. Data quality against main, over 24 crystals and eight statistics each: the same space group on all 24, and every difference smaller than what two runs of an IDENTICAL binary produce (measured: 13 of 24 crystals reproduce exactly run to run, worst R_meas swing 5.5 points, against 4.6 points for main vs this branch). The float atomics in the reductions have always made this so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
4.8 KiB
C++
98 lines
4.8 KiB
C++
// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <mutex>
|
|
|
|
#include "../common/JFJochMessages.h"
|
|
#include "../common/DiffractionExperiment.h"
|
|
#include "../common/AzimuthalIntegrationMapping.h"
|
|
#include "../common/PixelMask.h"
|
|
#include "../common/AzimuthalIntegrationProfile.h"
|
|
#include "bragg_prediction/BraggPrediction.h"
|
|
#include "bragg_integration/BraggIntegrationEngine.h"
|
|
#include "spot_finding/ImageSpotFinder.h"
|
|
#include "spot_finding/AdaptiveSpotFinderCPU.h"
|
|
#include "indexing/IndexerThreadPool.h"
|
|
#include "azint/AzIntEngine.h"
|
|
#include "roi/ROIIntegration.h"
|
|
#include "IndexAndRefine.h"
|
|
#include "image_preprocessing/ImagePreprocessor.h"
|
|
#include "image_preprocessing/ImagePreprocessorBuffer.h"
|
|
|
|
class CudaStream;
|
|
class AdaptiveSpotFinderGPU;
|
|
|
|
// MXAnalysisWithoutFPGA is not thread safe - it has to owned by a single thread
|
|
class MXAnalysisWithoutFPGA {
|
|
const DiffractionExperiment &experiment;
|
|
const AzimuthalIntegrationMapping &integration;
|
|
|
|
std::vector<uint8_t> decompression_buffer;
|
|
|
|
std::unique_ptr<ImagePreprocessor> preprocessor;
|
|
|
|
size_t npixels;
|
|
size_t xpixels;
|
|
|
|
// Built on first use: the fused adaptive finder produces the azimuthal profile as a by-product,
|
|
// so on the rugnux path this engine is constructed and then never run.
|
|
std::unique_ptr<AzIntEngine> azint;
|
|
AzIntEngine &AzInt();
|
|
std::unique_ptr<ROIIntegration> roi;
|
|
// Built on first use. Which finder an image takes arrives with its SpotFindingSettings, and
|
|
// with adaptive detection on - the default everywhere but the broker - this one is never asked
|
|
// for; on the GPU it is ~14 MB and 15 device allocations per worker.
|
|
std::unique_ptr<ImageSpotFinder> spotFinder;
|
|
ImageSpotFinder &FixedThresholdFinder();
|
|
// Self-calibrating finder, used when spot settings request adaptive detection. Kept alongside the
|
|
// default finder because the choice arrives with the per-image settings, not at construction. It is
|
|
// an AdaptiveSpotFinderCPU by default; on the GPU path, when the fused engine is enabled (rugnux
|
|
// offline only), it is instead an AdaptiveSpotFinderGPU that also computes the azimuthal profile,
|
|
// aliased through fused_adaptive so Analyze() can take that profile and skip the separate azint pass.
|
|
std::unique_ptr<ImageSpotFinder> adaptiveSpotFinder;
|
|
AdaptiveSpotFinderGPU *fused_adaptive = nullptr;
|
|
const bool enable_fused_adaptive_gpu;
|
|
IndexAndRefine &indexer;
|
|
std::unique_ptr<BraggPrediction> prediction;
|
|
std::unique_ptr<BraggIntegrationEngine> bragg_engine;
|
|
std::unique_ptr<ImagePreprocessorBuffer> preprocessor_buffer;
|
|
const PixelMask &mask;
|
|
|
|
// Decompress the image into decompression_buffer (or read it straight from the message, when it is
|
|
// not compressed) and return where it landed.
|
|
const uint8_t *Decompress(const CompressedImage &image);
|
|
|
|
std::vector<bool> mask_resolution;
|
|
// The limits mask_resolution was built for. Kept as the OPTIONAL the caller passed, so an unset
|
|
// high-resolution limit compares equal to itself and the mask is not rebuilt on every image.
|
|
std::optional<float> mask_high_res;
|
|
std::optional<float> mask_low_res;
|
|
void UpdateMaskResolution(const SpotFindingSettings& settings);
|
|
#ifdef JFJOCH_USE_CUDA
|
|
std::shared_ptr<CudaStream> stream; // kept so RebuildROI() can recreate the GPU ROI engine
|
|
#endif
|
|
public:
|
|
// enable_fused_adaptive_gpu turns on the fused GPU azint+adaptive spot finder (only takes effect on
|
|
// the GPU path with adaptive detection). The rugnux offline path and the interactive viewer enable
|
|
// it by default, as does the online receiver. It only changes performance - the fused engine
|
|
// reproduces the CPU finder's spots. Note it also decides whether the preprocessed image is copied
|
|
// back to the host each frame: that copy exists only for a CPU engine to read, and with the flag on
|
|
// no CPU engine is built, so the copy is skipped.
|
|
MXAnalysisWithoutFPGA(const DiffractionExperiment &experiment, const AzimuthalIntegrationMapping &integration,
|
|
const PixelMask &mask, IndexAndRefine &indexer, bool enable_fused_adaptive_gpu = false);
|
|
void Analyze(DataMessage &output, AzimuthalIntegrationProfile &profile, const SpotFindingSettings &spot_finding_settings);
|
|
|
|
// Surgical ROI-only paths used when a full re-analysis is not wanted: rebuild the
|
|
// ROI engine after the ROI set changes, recompute ROIs after preprocessing a new
|
|
// image (reanalyze off), or just rerun ROIs on the current preprocessed image (an
|
|
// interactive ROI move). A full Analyze() already computes ROIs, so needs nothing.
|
|
void RebuildROI();
|
|
void AnalyzeROIOnly(DataMessage &output);
|
|
void RunROIOnly(DataMessage &output);
|
|
};
|
|
|
|
|
|
|