Files
Jungfraujoch/common/CUDAWrapper.cu
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

176 lines
5.7 KiB
Plaintext

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// 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"
inline void cuda_err(cudaError_t val) {
if (val != cudaSuccess)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, cudaGetErrorString(val));
}
int32_t get_gpu_count() {
int device_count;
cudaError_t val = cudaGetDeviceCount(&device_count);
switch (val) {
case cudaSuccess:
return device_count;
case cudaErrorNoDevice:
case cudaErrorInsufficientDriver:
return 0;
default:
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, cudaGetErrorString(val));
}
}
std::vector<std::string> get_gpu_names() {
std::vector<std::string> names;
const int32_t count = get_gpu_count();
names.reserve(count);
for (int32_t i = 0; i < count; i++) {
cudaDeviceProp prop{};
// A device that cannot be queried still exists and still gets work, so it is listed - just
// without a name. Losing the whole list over one unreadable device would be worse.
if (cudaGetDeviceProperties(&prop, i) == cudaSuccess)
names.emplace_back(prop.name);
else
names.emplace_back("unknown GPU");
}
return names;
}
void set_gpu(int32_t dev_id) {
auto dev_count = get_gpu_count();
// Ignore if no GPU present
if (dev_count > 0) {
if ((dev_id < 0) || (dev_id >= dev_count))
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Device ID cannot be negative");
cuda_err(cudaSetDevice(dev_id));
}
}
namespace {
std::atomic<bool> gpu_numa_binding{false};
// The NUMA node of each device, read once.
int GpuNumaNode(int32_t dev_id) {
static std::mutex m;
static std::vector<int> node;
std::lock_guard<std::mutex> lock(m);
if (node.empty()) {
const int32_t count = get_gpu_count();
node.assign(count, -1);
for (int32_t i = 0; i < count; i++) {
char bus_id[32] = {};
if (cudaDeviceGetPCIBusId(bus_id, sizeof(bus_id), i) == cudaSuccess)
node[i] = NumaNodeOfPciDevice(bus_id);
}
}
return dev_id < static_cast<int32_t>(node.size()) ? node[dev_id] : -1;
}
}
void enable_gpu_numa_binding() {
gpu_numa_binding = true;
}
void pin_gpu(int32_t dev_id) {
if (get_gpu_count() == 0)
return;
set_gpu(dev_id);
if (gpu_numa_binding)
PinThreadToNumaNode(GpuNumaNode(dev_id));
}
ScopedGpuPin::ScopedGpuPin(int32_t dev_id) {
if (get_gpu_count() == 0)
return;
cuda_err(cudaGetDevice(&prev_device));
const int node = gpu_numa_binding ? GpuNumaNode(dev_id) : -1;
if (node >= 0)
prev_cpus = GetThreadCpus();
set_gpu(dev_id);
PinThreadToNumaNode(node);
}
ScopedGpuPin::~ScopedGpuPin() {
SetThreadCpus(prev_cpus);
if (prev_device >= 0)
cudaSetDevice(prev_device); // not set_gpu: a destructor must not throw
}
void pin_gpu() {
static std::atomic<uint32_t> counter{0};
auto dev_count = get_gpu_count();
if (dev_count > 0)
pin_gpu(static_cast<int32_t>(counter.fetch_add(1) % dev_count));
}
void set_gpu_blocking_sync() {
// 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;
}
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() {
cudaGetLastError();
}
void cuda_throw_if_context_lost() {
// cudaFree(nullptr) frees nothing, but a lost context fails it. The last error cannot say the same:
// once cudaGetLastError() has returned a sticky error, it and cudaPeekAtLastError() report success.
const cudaError_t err = cudaFree(nullptr);
if (err != cudaSuccess)
throw JFJochException(JFJochExceptionCategory::GPUCUDAError,
std::string("CUDA device unusable after an unrecoverable error: ")
+ cudaGetErrorString(err));
}