Files
Jungfraujoch/image_analysis/azint/AzIntEngineGPU.cu
T
leonarski_fandClaude Opus 5 0b1fb6c870
Build Packages / build:viewer-tgz:cpu (push) Successful in 8m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 10m13s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 12m56s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 13m51s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m2s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m25s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 13m2s
Build Packages / build:rpm (rocky8) (push) Successful in 12m55s
Build Packages / XDS test (durin plugin) (push) Successful in 9m41s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / Build documentation (push) Successful in 47s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m13s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m35s
Build Packages / build:rpm (rocky9) (push) Successful in 13m38s
Build Packages / DIALS test (push) Successful in 13m57s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m21s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m49s
Build Packages / Unit tests (push) Successful in 1h1m3s
Build Packages / build:windows:nocuda (push) Canceled after 0s
Build Packages / build:windows:cuda (push) Canceled after 0s
image_analysis: share the read-only GPU lookup tables per device
One analysis engine is built per worker thread, and each uploaded its own copy of
tables that are pure functions of the detector geometry: the pixel -> azimuthal bin
map and the per-pixel corrections (both in AzIntEngineGPU AND again in
AdaptiveSpotFinderGPU, from the same mapping), plus the pixel mask. On an 18 Mpx
detector that is ~224 MB per worker; with 32 workers ~7 GB of device memory held 32
identical copies.

Upload each table once per GPU instead and hand every engine on that device a shared
pointer to it. The cache is keyed by (device, source-vector address) because workers
are pinned round-robin across GPUs, so on a multi-GPU node each device keeps its own
copy - a kernel may only read memory resident on the device it runs on - and the
table is freed on the device that allocated it. Entries are held weakly, so a table
goes away with the last engine using it.

Measured on an 18 Mpx detector, 32 worker threads, 16 GB card: the stills path went
from exhausting the card (OOM in de-novo indexing) to 8.6 GB peak, and a normal
rotation run from 14.6 GB to 7.4 GB - it had been running within 1.6 GB of the limit,
so any larger detector or second GPU consumer would have tipped it over. Per-worker
footprint drops 403 -> 173 MB. Merge statistics are unchanged on a six-crystal
regression subset, including two-pass runs where the second pass rebuilds the mapping
on refined geometry, and wall time is unchanged (13.5-13.8 s vs 13.8-14.1 s).

Also take the launch configuration from the current device rather than device 0 in
AzIntEngineGPU and ImagePreprocessorGPU: with round-robin pinning, device 0's SM count
and shared-memory size can belong to a different card than the one the kernels use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 18:41:27 +02:00

146 lines
5.4 KiB
Plaintext

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "AzIntEngineGPU.h"
inline void cuda_err(cudaError_t val) {
if (val != cudaSuccess)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, cudaGetErrorString(val));
}
__global__
void gpu_azim_shared(
const uint16_t *__restrict__ pixel_to_bin,
const float *__restrict__ corrections,
const int32_t *__restrict__ input_buffer,
float *__restrict__ azint_sum,
float *__restrict__ azint_sum2,
uint32_t *__restrict__ azint_count,
size_t num_pixels,
int azint_bins) {
extern __shared__ float shared[];
float *s_sum = shared;
float *s_sum2 = &s_sum[azint_bins];
uint32_t *s_count = (uint32_t *) &s_sum2[azint_bins];
// Initialize shared memory
for (int i = threadIdx.x; i < azint_bins; i += blockDim.x) {
s_sum[i] = 0.0f;
s_sum2[i] = 0.0f;
s_count[i] = 0;
}
__syncthreads();
for (size_t idx = blockIdx.x * blockDim.x + threadIdx.x;
idx < num_pixels;
idx += blockDim.x * gridDim.x) {
uint16_t bin = pixel_to_bin[idx];
int32_t v = input_buffer[idx];
bool valid = (v != INT32_MIN) & (v != INT32_MAX);
if (bin < azint_bins && valid) {
const float val = static_cast<float>(v) * corrections[idx];
const float val2 = val * val;
atomicAdd(&s_sum[bin], val);
atomicAdd(&s_sum2[bin], val2);
atomicAdd(&s_count[bin], 1);
}
}
__syncthreads();
// Merge to global memory
for (unsigned int i = threadIdx.x; i < azint_bins; i += blockDim.x) {
atomicAdd(&azint_sum[i], s_sum[i]);
atomicAdd(&azint_sum2[i], s_sum2[i]);
atomicAdd(&azint_count[i], s_count[i]);
}
}
__global__
void gpu_azim(
const uint16_t *__restrict__ pixel_to_bin,
const float *__restrict__ corrections,
const int32_t *__restrict__ input_buffer,
float *__restrict__ azint_sum,
float *__restrict__ azint_sum2,
uint32_t *__restrict__ azint_count,
size_t num_pixels,
int azint_bins) {
for (size_t idx = blockIdx.x * blockDim.x + threadIdx.x;
idx < num_pixels;
idx += blockDim.x * gridDim.x) {
uint16_t bin = pixel_to_bin[idx];
int32_t v = input_buffer[idx];
bool valid = (v != INT32_MIN) & (v != INT32_MAX);
if (bin < azint_bins && valid) {
const float val = static_cast<float>(v) * corrections[idx];
const float val2 = val * val;
atomicAdd(&azint_sum[bin], val);
atomicAdd(&azint_sum2[bin], val2);
atomicAdd(&azint_count[bin], 1);
}
}
}
AzIntEngineGPU::AzIntEngineGPU(const AzimuthalIntegrationMapping &integration, std::shared_ptr<CudaStream> stream)
: AzIntEngine(integration),
stream(stream),
gpu_sum(azint_bins),
gpu_sum2(azint_bins),
gpu_count(azint_bins),
cpu_sum_reg(azint_sum),
cpu_sum2_reg(azint_sum2),
cpu_count_reg(azint_count) {
int device = 0;
cuda_err(cudaGetDevice(&device)); // this worker's GPU, not necessarily 0
cudaDeviceProp prop{};
cuda_err(cudaGetDeviceProperties(&prop, device));
threads = 128;
blocks = 4 * prop.multiProcessorCount;
shared_size = prop.sharedMemPerBlock;
shared_needed = azint_bins * (2 * sizeof(float) + sizeof(uint32_t));
// Geometry-only, so shared per GPU: the first engine on this device uploads them, the rest reuse
// them. Keyed by the mapping's own vectors, which outlive every engine built from it.
gpu_azint_correction = SharedDeviceTable(integration.Corrections().data(), npixel,
integration.Corrections().data(), *stream);
gpu_pixel_to_bin = SharedDeviceTable(integration.GetPixelToBin().data(), npixel,
integration.GetPixelToBin().data(), *stream);
}
void AzIntEngineGPU::Run(const ImagePreprocessorBuffer &image, AzimuthalIntegrationProfile &profile) {
if (image.size() != integration.GetPixelToBin().size())
throw std::runtime_error("ImageSpotFinder::AzimIntegration: Mismatch in size");
cuda_err(cudaMemsetAsync(gpu_sum, 0, sizeof(float) * azint_bins, *stream));
cuda_err(cudaMemsetAsync(gpu_sum2, 0, sizeof(float) * azint_bins, *stream));
cuda_err(cudaMemsetAsync(gpu_count, 0, sizeof(uint32_t) * azint_bins, *stream));
if (shared_needed < shared_size) {
gpu_azim_shared<<<blocks, threads, shared_needed, *stream>>>(
gpu_pixel_to_bin->get(),gpu_azint_correction->get(),image.getGPUBuffer(), gpu_sum, gpu_sum2,
gpu_count, npixel, azint_bins
);
} else {
gpu_azim<<<blocks, threads, 0, *stream>>>(
gpu_pixel_to_bin->get(),gpu_azint_correction->get(),image.getGPUBuffer(), gpu_sum, gpu_sum2,
gpu_count, npixel, azint_bins
);
}
cudaMemcpyAsync(azint_sum.data(), gpu_sum, sizeof(float) * azint_bins, cudaMemcpyDeviceToHost, *stream);
cudaMemcpyAsync(azint_sum2.data(), gpu_sum2, sizeof(float) * azint_bins, cudaMemcpyDeviceToHost, *stream);
cudaMemcpyAsync(azint_count.data(), gpu_count, sizeof(uint32_t) * azint_bins, cudaMemcpyDeviceToHost, *stream);
cuda_err(cudaStreamSynchronize(*stream));
profile.Clear(integration);
profile.Add(azint_sum, azint_sum2, azint_count);
}