Merge branch 'startup-walk' into rc175: SMV frames read through mmap; card contexts created and released in parallel

Exact: p.hkl md5 identical to the base on 2xfw, 6zqy, 2ygz, 8 small SMV sets, myob_x10sa, cytc_x10sa and -N 8.
SMV walkers 2xfw 48 -> 41 s, 6zqy 26 -> 23 s, 2ygz 32 -> 26 s; start-up to first log line 0.39 -> 0.18 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
2026-10-10 00:38:06 +02:00
co-authored by Claude Opus 5.5
6 changed files with 108 additions and 16 deletions
+4
View File
@@ -75,6 +75,10 @@ void enable_gpu_numa_binding() {}
void set_gpu_blocking_sync() {}
void create_gpu_contexts(int32_t) {}
void release_gpu_contexts() {}
void cuda_clear_error() {}
void cuda_throw_if_context_lost() {}
+41 -9
View File
@@ -2,10 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only
#include <fstream>
#include <thread>
#include <atomic>
#include <mutex>
#include <vector>
#include <cuda.h>
#include "CUDAWrapper.h"
#include "ThreadAffinity.h"
#include "JFJochException.h"
@@ -116,16 +119,45 @@ void pin_gpu() {
}
void set_gpu_blocking_sync() {
const int32_t count = get_gpu_count();
for (int32_t i = 0; i < count; i++) {
if (cudaSetDevice(i) != cudaSuccess)
continue;
// cudaErrorSetOnActiveProcess where a context exists already: it keeps its flags.
if (cudaSetDeviceFlags(cudaDeviceScheduleBlockingSync) != cudaSuccess)
cudaGetLastError();
// Through the driver's primary-context flags, which a context created later takes: the runtime's
// cudaSetDevice creates the device's context, serially, and on four cards that was most of a
// second before the run had started. The function is taken from the driver at run time, so the
// program does not link against it and still starts on a machine without one.
void *fn = nullptr;
cudaDriverEntryPointQueryResult found{};
if (get_gpu_count() == 0
|| cudaGetDriverEntryPointByVersion("cuDevicePrimaryCtxSetFlags", &fn, 12000, cudaEnableDefault,
&found) != cudaSuccess
|| found != cudaDriverEntryPointSuccess) {
cudaGetLastError();
return;
}
if (count > 0)
cudaSetDevice(0);
const auto set_flags = reinterpret_cast<CUresult (*)(CUdevice, unsigned int)>(fn);
for (int32_t i = 0; i < get_gpu_count(); i++)
set_flags(i, CU_CTX_SCHED_BLOCKING_SYNC);
}
void create_gpu_contexts(int32_t first_device) {
std::vector<std::thread> threads;
for (int32_t i = first_device; i < get_gpu_count(); i++)
threads.emplace_back([i] {
// A device that fails here fails again, with its error, where it is used.
if (cudaSetDevice(i) != cudaSuccess || cudaFree(nullptr) != cudaSuccess)
cudaGetLastError();
});
for (auto &t : threads)
t.join();
}
void release_gpu_contexts() {
std::vector<std::thread> threads;
for (int32_t i = 0; i < get_gpu_count(); i++)
threads.emplace_back([i] {
if (cudaSetDevice(i) != cudaSuccess || cudaDeviceReset() != cudaSuccess)
cudaGetLastError();
});
for (auto &t : threads)
t.join();
}
void cuda_clear_error() {
+11 -1
View File
@@ -50,9 +50,19 @@ private:
// Have every GPU's host threads BLOCK in a synchronisation (cudaDeviceScheduleBlockingSync) instead
// of spinning on a core until the device finishes. Must be called before anything creates a CUDA
// context; a device that already has one keeps the flags it was created with. No-op without CUDA.
// context; it creates none itself. No-op without CUDA.
void set_gpu_blocking_sync();
// Create the CUDA context of every device from first_device on, one thread per device, and return
// when they all exist - for a caller that runs it beside other work, so that the first use of a card
// does not wait for its context. No-op without CUDA.
void create_gpu_contexts(int32_t first_device);
// Destroy every device's CUDA context, one thread per device, for a program about to exit with no
// CUDA work left: at exit the runtime destroys them one after another, a fifth of a second per card.
// No-op without CUDA.
void release_gpu_contexts();
// Drop the error CUDA has recorded for the calling thread. Call it where a CUDA failure has been
// HANDLED - a device route that fell back to the host, an indexing attempt whose failure was turned
// into a result - because the error otherwise stays as the thread's last error and the next
+42 -4
View File
@@ -15,6 +15,13 @@
#include "../common/JFJochException.h"
#ifndef _WIN32
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
namespace smv {
namespace {
@@ -369,18 +376,49 @@ Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vec
throw JFJochException(JFJochExceptionCategory::ArrayOutOfBounds,
path + ": image is larger than the buffer given for it");
const size_t image_bytes = npixel * sizeof(uint16_t);
const auto too_short = [&] {
return JFJochException(JFJochExceptionCategory::InputParameterInvalid,
path + ": file ends before the image does");
};
#ifdef _WIN32
std::ifstream f(path, std::ios::binary);
if (!f)
throw JFJochException(JFJochExceptionCategory::MockFileOpenError, "Cannot open " + path);
f.seekg(static_cast<std::streamoff>(h.data_offset), std::ios::beg);
scratch.resize(npixel * sizeof(uint16_t));
scratch.resize(image_bytes);
f.read(reinterpret_cast<char *>(scratch.data()), static_cast<std::streamsize>(scratch.size()));
if (static_cast<size_t>(f.gcount()) != scratch.size())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
path + ": file ends before the image does");
throw too_short();
const uint8_t *src = scratch.data();
#else
// Mapped rather than read: the pixels are converted straight out of the page cache, where a read
// first copies the whole frame into a buffer. The image loop that reads a sweep - again in every
// pass, and in every round of the geometry walk - is bound by memory traffic, and the copy adds the
// whole file to it: measured on a 9 Mpx sweep of 360 frames, mapping took a seventh off the run.
(void) scratch;
const int fd = open(path.c_str(), O_RDONLY);
if (fd < 0)
throw JFJochException(JFJochExceptionCategory::MockFileOpenError, "Cannot open " + path);
struct stat st{};
if (fstat(fd, &st) != 0 || static_cast<size_t>(st.st_size) < h.data_offset + image_bytes) {
close(fd);
throw too_short();
}
const size_t mapped_bytes = h.data_offset + image_bytes;
void *mapped = mmap(nullptr, mapped_bytes, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
if (mapped == MAP_FAILED)
throw JFJochException(JFJochExceptionCategory::MockFileOpenError, "Cannot map " + path);
struct Unmap {
void *p;
size_t bytes;
~Unmap() { munmap(p, bytes); }
} unmap{mapped, mapped_bytes};
const uint8_t *src = static_cast<const uint8_t *>(mapped) + h.data_offset;
#endif
// Stored unsigned 16-bit, handed out as the signed 32-bit the rest of the code reads.
const uint8_t *src = scratch.data();
if (h.little_endian) {
for (size_t i = 0; i < npixel; i++)
out[i] = static_cast<int32_t>(static_cast<uint16_t>(src[2 * i] | (src[2 * i + 1] << 8)));
+2 -1
View File
@@ -85,7 +85,8 @@ Header ReadHeader(const std::string &path);
Header Read(const std::string &path, std::vector<int32_t> &out);
// The same, widening the stored pixels into memory the caller already has. scratch carries the
// raw bytes between frames so a worker walking a sweep allocates once.
// raw bytes between frames so a worker walking a sweep allocates once - on Windows only; elsewhere the
// file is mapped and scratch is not used.
Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vector<uint8_t> &scratch);
} // namespace smv
+8 -1
View File
@@ -1589,6 +1589,10 @@ static int RunRugnux(int argc, char **argv) {
// Off the critical path, so cuFFT's one-time initialisation overlaps the file open, the mask read
// and the beam-stop pre-scan instead of landing inside the first pass.
auto cufft_warmup = std::async(std::launch::async, WarmUpCuFFT);
// The same for the CUDA contexts of the other cards (the warm-up above creates card 0's), which
// the pre-scan does not use: created one after another at start-up, they cost a fifth of a second
// per card before the run began.
auto context_warmup = std::async(std::launch::async, create_gpu_contexts, 1);
// 1. Read Input File. An HDF5 master, one PILATUS miniCBF frame, or one marCCD frame - naming
// any frame of a sweep reads the whole sweep, which is how these are archived and how a user
@@ -3221,7 +3225,10 @@ int main(int argc, char **argv) {
// run, a fifth of all CPU time went to that spinning, with no change in wall time either way.
set_gpu_blocking_sync();
enable_gpu_numa_binding();
return RunRugnux(argc, argv);
const int ret = RunRugnux(argc, argv);
// Everything that used a card has been destroyed with the run.
release_gpu_contexts();
return ret;
} catch (const std::exception &e) {
Logger("rugnux").Error("{}", FailureText(e));
return EXIT_FAILURE;