Files
Jungfraujoch/common/CUDAWrapper.h
T
leonarski_fandClaude Opus 5.5 943d3fc655 rugnux: no serial CUDA context creation before the run, parallel teardown
set_gpu_blocking_sync() called cudaSetDevice() on every card, and since
CUDA 12 that creates the card's primary context: ~0.18 s per card on the
workstation (measured, RTX 5080), one after another, before WALL_TIME
starts. On the 4-card server that is most of the 1.73 s per run spent
outside WALL_TIME (vs 0.37 s on one card).

- The blocking-sync flag is now set with the driver's
  cuDevicePrimaryCtxSetFlags, taken through cudaGetDriverEntryPointByVersion
  (no link against libcuda, so a CUDA build still starts without a
  driver). It creates no context; contexts created later carry the flag
  (cudaGetDeviceFlags 0xc, as before; user CPU time unchanged, so no
  spinning).
- Card 0's context is created by the existing cuFFT warm-up thread, beside
  the file open and pre-scan; cards 1..N-1 get theirs on a new background
  thread per card (create_gpu_contexts), as the pre-scan uses card 0 only.
- At exit the contexts are destroyed by one thread per card
  (release_gpu_contexts, cudaDeviceReset) after the run has destroyed
  everything that used them, instead of serially by the runtime's exit
  handler. Microbenchmark: an explicit reset costs the same as the
  implicit teardown on one card (175 ms), so no change on one card.

Which card does what is unchanged; only when contexts are made/destroyed.

Measured on the 1-card workstation (load 7-16, 2 repeats x 2 sets plus
2 earlier pairs on myob): process start -> first log line median 0.39 ->
0.18 s; last log line -> exit 0.18 -> 0.20 s (one context either way);
elapsed - WALL_TIME median 0.53 -> 0.37 s; WALL_TIME unchanged (context
creation now overlaps the input read). The multi-card gain (3 serial
context creations at start, 4 serial destroys at exit) cannot be measured
here. p.hkl md5 identical on myob_x10sa, cytc_x10sa, 2xfw, 6zqy -N 8;
all runs exit 0.

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

93 lines
4.5 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>
int32_t get_gpu_count();
// Names of the visible GPUs, in device order and one entry per device, so repeated cards repeat.
// Empty without CUDA and on a machine with no device, which is also what get_gpu_count() == 0 says.
std::vector<std::string> get_gpu_names();
// The same list collapsed for a person: "4x NVIDIA A100-SXM4-80GB", or several such groups separated
// by ", " on a mixed machine. Empty when no GPU is visible.
std::string get_gpu_description();
void set_gpu(int32_t dev_id);
// Pin the calling thread to the next GPU in round-robin order, using a process-wide counter
// (counter++ % get_gpu_count()). Call once per thread; no thread id needed. No-op when no GPU
// is visible. Honours CUDA_VISIBLE_DEVICES via get_gpu_count().
void pin_gpu();
// From here on, pin_gpu() also keeps the calling thread on the CPUs of the NUMA node its GPU is
// attached to (ThreadAffinity.h) - on a machine with one node, or without CUDA, nothing changes.
// Off unless a program asks for it.
void enable_gpu_numa_binding();
// pin_gpu() onto a given device: set_gpu(dev_id), and the NUMA binding above where it is enabled.
// For a worker that takes its card by index rather than round-robin.
void pin_gpu(int32_t dev_id);
// pin_gpu(dev_id) for as long as the object lives; then the calling thread gets back the device and
// the CPUs it had before. For a thread that drives a card only for a while - the merging engine, whose
// calls come from a thread that goes on to other work - where a pin for the rest of its life would
// carry the card's node onto everything that thread does, and starts, afterwards.
class ScopedGpuPin {
public:
explicit ScopedGpuPin(int32_t dev_id);
~ScopedGpuPin();
ScopedGpuPin(const ScopedGpuPin &) = delete;
ScopedGpuPin &operator=(const ScopedGpuPin &) = delete;
private:
int32_t prev_device = -1; // -1: nothing to give back
std::vector<int> prev_cpus; // empty: the CPUs were not changed
};
// 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; 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
// cuda_err(cudaGetLastError()) after some later kernel launch reports it, over work that went fine.
// A sticky error (an illegal access, say) is not cleared by this, and nothing here pretends it is:
// the context is gone in that case and every later call fails on its own. No-op without CUDA.
void cuda_clear_error();
// Throw if the CUDA context is lost - a sticky error (an illegal access, say) that every later call
// on this device will fail with. For a device route about to fall back to the host: there is no
// fallback from that, and falling back only moves the failure somewhere less clear. A handled,
// non-sticky failure passes. No-op without CUDA.
void cuda_throw_if_context_lost();
// GPU work that runs beside the main line of a process and gives all of its device memory back when it
// ends: a probe pass run on a copy of the run while the run itself scales and merges. Hold one for as
// long as that work runs. Build-independent - it only counts.
class GPUWorkBeside {
public:
GPUWorkBeside();
~GPUWorkBeside();
GPUWorkBeside(const GPUWorkBeside &) = delete;
GPUWorkBeside &operator=(const GPUWorkBeside &) = delete;
};
// Wait until no GPUWorkBeside is held, for at most `timeout`. True once none is - at once if none was.
bool wait_for_gpu_work_beside(std::chrono::seconds timeout);