Files
Jungfraujoch/image_analysis/MXAnalysisWithoutFPGA.h
T
leonarski_fandClaude Opus 5 e7be5447d3
Build Packages / Unit tests (push) Successful in 1h1m55s
Build Packages / build:viewer-tgz:cpu (push) Successful in 8m10s
Build Packages / build:viewer-tgz:cuda (push) Successful in 9m20s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m9s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m13s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m43s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m22s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 13m21s
Build Packages / build:rpm (rocky8) (push) Successful in 12m0s
Build Packages / build:rpm (rocky9) (push) Successful in 13m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m18s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m6s
Build Packages / DIALS test (push) Successful in 13m59s
Build Packages / XDS test (durin plugin) (push) Successful in 8m4s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 8m40s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m1s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / Build documentation (push) Successful in 1m9s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:nocuda (push) Failing after 13m23s
Build Packages / build:windows:cuda (push) Failing after 12m24s
receiver: stop copying every frame back from the device on the Lite path
The Lite workflow built its analysis with the fused GPU engine disabled, which is
also what decides whether the preprocessed image is copied device-to-host after
every frame. So on a machine with a GPU the online path was moving the whole image
back - 72 MB on a large detector, every frame, per worker - for a host reader that
does not exist on that path.

It was left off deliberately when the fused engine was added, to keep the online
path unchanged in that commit, and never revisited. Nothing depends on it: the FPGA
workflow uses a different analysis class, and strong-pixel values are read through a
device gather rather than from the host image.

Turning it on changes no result, and cannot: adaptive detection is unreachable
online, because the REST schema exposes no way to enable it, so the classic GPU
finder runs either way. Measured anyway, both engines on the same frames across five
datasets including very weak ones: 2400 frames, 638260 spots, not one difference -
identical lists, identical indexing rate, identical merge statistics to every
printed digit.

On a large detector with eight workers the median per-image cost falls from 94 to
59 ms and preprocessing from 21 to 6 ms; throughput rises from about 48 to 55 Hz. No
percentile regresses, which is what matters for a service - the ninetieth improves
from 128 to 74 ms and the tail with it. Spot finding gets faster too, because the
large copy no longer contends with the device gather.

Correct two statements while here. The flag's comment and the data-analysis
document both said the online receiver uses the CPU adaptive finder; online never
runs an adaptive finder at all, and the copy the flag really controls was not
mentioned. That copy would be better expressed as what it is - whether a host engine
will read the image, which the constructor already knows - rather than inferred from
which spot finder is wanted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 15:16:18 +02:00

91 lines
4.3 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;
std::unique_ptr<AzIntEngine> azint;
std::unique_ptr<ROIIntegration> roi;
std::unique_ptr<ImageSpotFinder> spotFinder;
// 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;
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 while adaptive
// detection is not reachable through the REST API the copy is the flag's only effect online.
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);
};