Files
Jungfraujoch/image_analysis/image_preprocessing/ImagePreprocessorGPU.h
T
leonarski_fandClaude Opus 5.5 09c41d067d Pinned host memory from a process-wide pool; the compressed chunk uploaded from pinned staging
The GPU engines are built per worker per step, and each took its page-locked host buffers with
cudaHostAlloc / cudaHostRegister and gave them back with cudaFreeHost / cudaHostUnregister. Those
calls map and lock pages under the process's memory-map lock, so all workers queue behind each
other: on the 5080 workstation a myoglobin run made ~3400 of them (6-22 thread-s, depending on the
load), 9gdj ~10400 (124 thread-s); on a four-socket host they cost 20-30 ms each.

- CudaHostPtr takes its memory from a pool: a freed buffer is kept in a power-of-two size class for
  the next engine, up to 512 MiB in all; a buffer above 64 MiB is never kept. Rugnux::Run hands the
  pool back at its end.
- PinnedVector (std::vector with the pool's allocator) replaces the registered vectors of
  AdaptiveSpotFinderGPU and ImagePreprocessorGPU; the Bragg predictors download into a pooled
  pinned buffer and copy into their output, instead of registering it.
- BSLZ4DecoderGPU copies the compressed chunk into a pooled pinned buffer before the upload, so it
  is one DMA instead of the driver's pageable staging (9gdj: 163 GB pageable H2D, 52 s of copy
  time, -> 147 GB pinned, 13 s).

Measured with nsys on the workstation: myob 3398 -> 1396 calls (674 of them the release at the
end), cytc 2986 -> 1270, 9gdj 10428 -> 1748 (124 -> 3.7 thread-s). Output identical (p.hkl md5 on
myob, cytc, 9gdj; CI tier battery).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 10:37:23 +02:00

60 lines
3.3 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <memory>
#include "ImagePreprocessor.h"
#include "BSLZ4DecoderGPU.h"
#include "../indexing/CUDAMemHelpers.h"
#include "../indexing/CudaSharedTables.h"
class ImagePreprocessorGPU : public ImagePreprocessor {
std::shared_ptr<CudaStream> stream;
const bool copy_image_to_host;
int threads;
int blocks;
// Geometry-only, so one copy per GPU shared with every other engine on it (CudaSharedTables.h).
std::shared_ptr<CudaDevicePtr<uint8_t>> gpu_mask;
// Landing buffer for the HOST-upload path only. The device-decode path un-transposes straight
// into the preprocessed image, so it never needs this - and at 4 bytes per pixel it is worth a
// frame per worker, so it is allocated on first use rather than always.
CudaDevicePtr<uint8_t> gpu_decompressed_image;
CudaDevicePtr<ImageStatistics> gpu_stats;
PinnedVector<ImageStatistics> cpu_stats;
CudaRegisteredVector<uint8_t> input_reg; // page-locks the caller's decompression buffer
// The uncompressed-image counterpart of input_reg: the region PinInputRegion last page-locked.
const void *pinned_input = nullptr;
size_t pinned_input_bytes = 0;
std::vector<int32_t> cpu_image;
// Built on first use: a decoder that can serve this engine's images, sized to the frame.
std::unique_ptr<BSLZ4DecoderGPU> bslz4_decoder;
template <class T> ImageStatistics Analyze(ImagePreprocessorBuffer &processed_image, const uint8_t *input, T err_value, T sat_value);
// Preprocess an image already sitting in gpu_decompressed_image (the host-upload path).
template <class T> ImageStatistics AnalyzeOnDevice(ImagePreprocessorBuffer &processed_image, T err_value, T sat_value);
// Preprocess straight out of the bitshuffled bytes (the device-decode path). Same per-pixel
// decision and same statistics as AnalyzeOnDevice, with the un-transpose folded in.
template <class T, int ES> ImageStatistics UntransposeAndAnalyze(ImagePreprocessorBuffer &processed_image,
const BSLZ4ShuffledImage &shuffled,
T err_value, T sat_value);
public:
// copy_image_to_host copies the preprocessed image back after every frame. It is only needed when
// something on the CPU reads it - the GPU engines all work off the device buffer - and at 4 bytes
// per pixel it is the single largest transfer in the pipeline, so the caller says whether it wants it.
ImagePreprocessorGPU(const DiffractionExperiment &experiment, const PixelMask &mask, std::shared_ptr<CudaStream> stream,
bool copy_image_to_host = true);
~ImagePreprocessorGPU() override;
ImageStatistics Analyze(ImagePreprocessorBuffer &processed_image, const uint8_t *decompressed_image, CompressedImageMode image_mode) override;
bool AnalyzeCompressed(ImagePreprocessorBuffer &processed_image, const CompressedImage &image,
ImageStatistics &stats) override;
[[nodiscard]] float GetLastDecompressionTime_s() const override;
void PinInputBuffer(std::vector<uint8_t> &buffer, size_t size) override;
void PinInputRegion(const void *ptr, size_t bytes) override;
};