The image loop gives every worker its own analysis engine, so a run builds ninety-six of them. Each one derived, from scratch, tables that are the same in all of them: the byte-per-pixel mask, the resolution mask, the radial kernel, and the checksum that names the shared device tables. The checksum was the worst of it, because it is part of the cache KEY and so is computed before the lookup - a hit still hashed the whole table. On a 16 Mpx detector that is the bin table, the corrections and the mask, 126 MB an engine, about twelve gigabytes over a run, to answer a question whose answer had not changed. The header said it cost nothing measurable; a profile says otherwise, and says it is worst exactly during the ramp when the machine has nothing else to do. It cannot simply be remembered against the address, which is what it exists to catch: a buffer can be freed and another allocated where it was, and the cache would then hand back a device copy of something else. So the owner of the bytes computes it instead. The azimuthal mapping writes its two tables in its constructor and never again. The pixel mask re-derives its binary form and its checksum on every path that changes the mask, and all of those paths are now private to the class. The key therefore still describes the bytes as they are at the moment of the lookup. The resolution mask was two passes over every pixel - a float comparison into a vector<bool>, then a bit-by-bit repack - in each of the ninety-six. It is one pass now, writing the packed form directly, built once for the limits asked for and handed out as a shared pointer so a worker keeps the mask it was given. The radial kernel is cached on the six numbers it is derived from. Nothing computes a different value; only who computes it changes. Byte-identical merged output on a 16 Mpx set and on a small one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011n8riB6X59oRjkrSHzNPAU
100 lines
5.0 KiB
C++
100 lines
5.0 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);
|
|
|
|
// Pixels outside the resolution limits, bit-packed. Built by the integration mapping, which is
|
|
// shared by every worker's engine and hands out the same mask to all of them.
|
|
std::shared_ptr<const std::vector<uint32_t>> 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);
|
|
};
|
|
|
|
|
|
|