Files
Jungfraujoch/image_analysis/indexing/IndexerThreadPool.h
T
leonarski_fandClaude Opus 5.5 1c3a2e202c GPU memory over the run: FFT indexers given back after the last indexing; speculative probe takes turns with the merge on a small card
- IndexerThreadPool::ReleaseIndexers(): each worker drops the indexers it has
  built (once its current frame is done) and the allocation pool is trimmed, so
  their memory goes back to the card, not only to the pool. A later frame builds
  them again, as on first use. A rotation pass calls it once its first pass is
  done and again when the leftover-lattice census - the last of its indexing -
  ends. Measured on the RTX 5080: the 1.24 GB (pre-pass) / 0.62 GB (later
  passes) the pass's pool held through the image pass and the scaling is gone;
  4nwv's pre-pass image pass peaks 5.97 instead of 7.18 GB of device memory.
- GPUMerge: the main line holds one through ScaleMergeAndSymmetry. The
  speculative geometry probe (GPUWorkBeside(after_merges=true)) waits for it on
  a card below the take-turns size (12 GB), where the probe's FFT indexers and
  image workers beside the merge's arrays would overfill the card - on a
  Windows (WDDM) card that does not fail an allocation but pages it and crawls.
  The merge waits for nothing, so nothing the merge waits on (the rotation-scale
  walk's probes) can wait in a circle with it. No change on a 16 GB card.

Output unchanged: p.hkl byte-identical on cytc_x10sa, myob_x10sa, 4nwv and
8qaw on this card; also identical with the card's total read as 8.55 GB
(10 / 9 image workers instead of 16) and with 4 image workers, so the worker
count does not change the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:39:19 +02:00

123 lines
5.3 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <future>
#include <vector>
#include <optional>
#include <memory>
#include <latch>
#include "../common/JFJochMessages.h"
#include "../common/DiffractionSpot.h"
#include "../common/DiffractionExperiment.h"
#include "Indexer.h"
// When a worker builds its indexers.
//
// Preconstruct (the default, and what the online service needs): every indexer the requested
// algorithm could resolve to is built while the pool is being constructed, so it is resident and
// planned before the first frame arrives. Building one means a cuFFT plan plus ~0.6 GB of device
// allocation, and the first cuFFT call in a process also pays the library's one-time init (0.3 s
// measured); jfjoch_broker cannot have any of that land on the live data path, so holding an indexer
// the resolved algorithm may never dispatch to is an accepted cost there.
//
// OnFirstUse (offline batch - rugnux, jfjoch_viewer): nothing is latency-critical, so the indexer is
// built on the first frame that needs it. The algorithm is only resolved from the frame's
// DiffractionExperiment (rotation -> FFT, stills with a known cell -> FFBIDX), so preconstructing
// leaves each worker holding a fully allocated FFTIndexerGPU - 0.6 GB of cuFFT plan and histograms -
// that can never be dispatched to.
enum class IndexerConstruction { Preconstruct, OnFirstUse };
// Force cuFFT's one-time library initialisation - 0.3 s measured, paid by whichever call happens to
// be first - so it can be got out of the way on a background thread at startup rather than landing in
// the middle of the first pass with nothing overlapping it. No-op without a GPU.
void WarmUpCuFFT();
// cuFFT plans are kept and reused for as long as one of these lives, and destroyed when the last one
// ends (CudaFFTPlanScope): one per processing run, so no plan outlives the run that made it. Nothing
// without CUDA.
class CudaFFTPlanScope;
class FFTPlanReuse {
std::unique_ptr<CudaFFTPlanScope> scope;
public:
FFTPlanReuse();
~FFTPlanReuse();
FFTPlanReuse(const FFTPlanReuse &) = delete;
FFTPlanReuse &operator=(const FFTPlanReuse &) = delete;
};
class IndexerThread {
struct TaskInput {
const DiffractionExperiment &experiment;
const std::vector<Coord> &recip;
const bool severity_only;
};
// Held by value: with IndexerConstruction::OnFirstUse the worker builds its indexer long after
// the constructor returned, and pools are routinely built from a temporary - for instance
// IndexerThreadPool(experiment.GetIndexingSettings()), which returns by value.
const IndexingSettings settings_;
const IndexerConstruction construction_;
const bool beside_;
bool stop = false;
bool drop_indexers = false; // see Release()
enum class TaskState {STARTING, IDLE, READY, RUNNING, COMPLETED, ERROR} state = TaskState::STARTING;
std::mutex m;
std::condition_variable c_running;
std::condition_variable c_start;
std::condition_variable c_done;
std::unique_ptr<IndexerResult> result = nullptr;
std::unique_ptr<TaskInput> task_input = nullptr;
std::thread worker_thread;
void Worker(int threadid);
public:
IndexerThread(const IndexingSettings& settings, int threadid, IndexerConstruction construction, bool beside);
~IndexerThread();
std::unique_ptr<IndexerResult> Run(const DiffractionExperiment &experiment, const std::vector<Coord> &recip,
bool severity_only = false);
// The thread drops the indexers it has built - once the frame it is on, if any, is done - and builds
// them again on the next frame that needs one. Returns when they are gone.
void Release();
void Finalize();
};
class IndexerThreadPool {
std::mutex m;
std::condition_variable c;
std::vector<uint8_t> worker_busy;
size_t worker_free_count;
std::vector<std::unique_ptr<IndexerThread>> tasks;
const int64_t viable_cell_min_spots;
const bool blocking;
const IndexingSettings settings_;
int GetFreeWorker();
public:
// beside: the pool serves a probe pass that holds GPUWorkBeside, so an FFT indexer that does not fit
// the card waits for the main line's image pass to end, not for the probes - it is one of them (see
// MakeFFTIndexerGPU).
IndexerThreadPool(const IndexingSettings& settings,
IndexerConstruction construction = IndexerConstruction::Preconstruct,
bool beside = false);
// The settings the pool's indexers were built with.
[[nodiscard]] const IndexingSettings &Settings() const { return settings_; }
// severity_only skips indexing and produces just the spindle severity - see Indexer::Run.
IndexerResult Run(const DiffractionExperiment& experiment, const std::vector<Coord>& recip,
bool severity_only = false);
// Give back to the card the device memory of the indexers built so far - an FFT indexer holds
// 0.6-1.2 GB of it - for a stretch with nothing to index. A frame that comes after all builds its
// indexer again, as on first use, and the indexer it gets computes what the dropped one would.
void ReleaseIndexers();
};