CUDA: let worker streams run concurrently
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 12m51s
Build Packages / build:rpm (rocky8) (push) Successful in 12m23s
Build Packages / XDS test (durin plugin) (push) Successful in 8m38s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m7s
Build Packages / Unit tests (push) Successful in 1h19m8s
Build Packages / Create release (push) Skipped
Build Packages / build:viewer-tgz:cpu (push) Successful in 6m53s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m25s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m31s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m9s
Build Packages / build:rpm (rocky9) (push) Successful in 13m46s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m47s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m3s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m23s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m19s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m25s
Build Packages / DIALS test (push) Successful in 14m46s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m56s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m56s
Build Packages / build:windows:nocuda (push) Canceled after 0s
Build Packages / build:windows:cuda (push) Canceled after 0s

Every per-thread stream was created with cudaStreamDefault, and the 20 MB raw
image upload went to the legacy NULL stream. A NULL-stream operation implicitly
synchronises with every blocking stream in the process, so with one engine per
worker thread no two workers' GPU work could ever overlap - the whole GPU
pipeline ran serially however many threads were asked for.

Create the streams non-blocking and put the upload on the engine's own stream.
Measured on 2000 serial stills, interleaved, medians of three: 24.6 -> 19.2 s at
-N 32 (-22%), 32.7 -> 21.0 s at -N 16 (-36%), CPU utilisation 436-570% -> 723-859%.
Output bit-identical - same observations, uniques, completeness, R-meas, CC1/2,
error model and cell. The stream is synchronised at the end of the same function,
so the ordering the code relies on is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 23:05:11 +02:00
co-authored by Claude Opus 5
parent 111cb2f9a6
commit 2be8680422
2 changed files with 8 additions and 2 deletions
@@ -141,7 +141,10 @@ ImageStatistics ImagePreprocessorGPU::Analyze(ImagePreprocessorBuffer &processed
if (sat_value > saturation_limit)
sat_value = static_cast<T>(saturation_limit);
cudaMemcpy(gpu_decompressed_image, input, npixels * sizeof(T), cudaMemcpyHostToDevice);
// On this engine's own stream, not the NULL stream: a NULL-stream copy implicitly synchronises with
// every blocking stream in the process, which serialised all workers behind whichever one was
// uploading. The stream is synchronised at the end of this function, so the ordering is unchanged.
cudaMemcpyAsync(gpu_decompressed_image, input, npixels * sizeof(T), cudaMemcpyHostToDevice, *stream);
cpu_stats[0] = ImageStatistics{.max_value = INT64_MIN, .min_value = INT64_MAX};
cudaMemcpyAsync(gpu_stats, cpu_stats.data(), sizeof(ImageStatistics), cudaMemcpyHostToDevice, *stream);
+4 -1
View File
@@ -12,7 +12,10 @@
class CudaStream {
cudaStream_t stream_ = nullptr;
public:
CudaStream(unsigned int flags = cudaStreamDefault) {
// Non-blocking by default: a stream created with cudaStreamDefault synchronises against the legacy
// NULL stream, so any NULL-stream operation anywhere in the process serialises every worker's GPU
// work against every other's. With one engine per worker thread that costs most of the parallelism.
CudaStream(unsigned int flags = cudaStreamNonBlocking) {
if (cudaStreamCreateWithFlags(&stream_, flags) != cudaSuccess)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError,
"Failed to create CUDA stream");