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
60 lines
3.3 KiB
C++
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;
|
|
};
|
|
|