Files
Jungfraujoch/common/ThreadAffinity.cpp
T
leonarski_fandClaude Opus 5.5 b43844ae06 rugnux: the merging engines sit on their card's NUMA node while they drive it
The image-loop workers are kept on the CPUs of their card's NUMA node (pin_gpu), but the two
threads that drive the merging tail were not: the quality guard took its card with set_gpu(1),
and the main engine runs on the pipeline's own thread, which goes on to everything else. On a
multi-socket server their pageable copies to the card went through a remote socket.

RotationScaleMerge now holds a ScopedGpuPin (new, CUDAWrapper) for each IngestHost,
IngestDevice and Run: pin_gpu(gpu_device) for the call, and afterwards the thread's previous
device and CPU mask back (GetThreadCpus/SetThreadCpus, new in ThreadAffinity). So the binding
covers whichever thread drives an engine, and does not stay on the pipeline thread. The threads
the engine starts itself (ComputeAsuGroups, Combine, FitErrorModel, ApplyFrenchWilson use
std::async) call RestoreThreadAffinity first, as the pool's workers do, so they are not
squeezed onto one node. The guard thread, which does nothing but the guard's merge, uses
pin_gpu(1) instead of set_gpu(1). Pool workers are untouched: they restore the start mask
themselves; the new test checks both that and that a pinned caller keeps its own mask.

No flag, no environment variable. On one NUMA node (this workstation) only the device setting
happens, so nothing can move. Measured on the 5080 workstation (one card, one node), base =
rc175-next 41fab6156, --model dropped, under the GPU lock: p.mtz md5 identical on myob_x10sa,
cytc_x10sa, thau_x10sa_16keV, 8a1a, 11if, 7qis, 6h5t, kdp_x10sa_20keV at default -N (2 runs),
-N 8 and CUDA_VISIBLE_DEVICES=0. Tail ("scale, merge and symmetry") unchanged within noise,
e.g. 8a1a 30.06 -> 29.37 s (base runs 29.75/30.36), cytc 5.33 -> 5.35 s. The gain is for
multi-socket boxes (T4 server: guard copies 1.7-2.1 vs 3.5-4.3 GB/s) and is not measured here.

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

182 lines
6.1 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ThreadAffinity.h"
#ifdef __linux__
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <pthread.h>
#include <sched.h>
namespace {
// The process's CPUs, taken before any thread has been pinned: static initialisation runs on the
// main thread before main().
cpu_set_t StartMask() {
cpu_set_t mask;
CPU_ZERO(&mask);
if (sched_getaffinity(0, sizeof(mask), &mask) != 0)
for (int c = 0; c < CPU_SETSIZE; c++)
CPU_SET(c, &mask);
return mask;
}
const cpu_set_t start_mask = StartMask();
// The CPUs the cgroup's quota allows, rounded up; 0 where none is set. The process's own cgroup
// first, then the root of the hierarchy as mounted, which is the container's own cgroup where the
// container does not see the host's paths. cgroup v2 (cpu.max: "max 100000" or "200000 100000")
// and v1 (cpu.cfs_quota_us = -1 or the quota, cpu.cfs_period_us).
unsigned CgroupCpuQuota() {
std::ifstream f("/proc/self/cgroup");
std::string line;
while (std::getline(f, line)) {
const size_t c1 = line.find(':'), c2 = line.find(':', c1 + 1);
if (c1 == std::string::npos || c2 == std::string::npos)
continue;
const std::string controllers = "," + line.substr(c1 + 1, c2 - c1 - 1) + ",";
const std::string path = line.substr(c2 + 1);
const bool v2 = line.compare(0, c2 + 1, "0::") == 0;
if (!v2 && controllers.find(",cpu,") == std::string::npos)
continue;
for (const std::string &dir : {"/sys/fs/cgroup" + std::string(v2 ? "" : "/cpu") + path,
"/sys/fs/cgroup" + std::string(v2 ? "" : "/cpu")}) {
double quota = -1, period = 0;
if (v2) {
std::ifstream q(dir + "/cpu.max");
std::string max;
if (!(q >> max >> period))
continue;
if (max != "max")
quota = std::atof(max.c_str());
} else {
std::ifstream q(dir + "/cpu.cfs_quota_us"), p(dir + "/cpu.cfs_period_us");
if (!(q >> quota) || !(p >> period))
continue;
}
if (quota > 0 && period > 0)
return static_cast<unsigned>(std::ceil(quota / period));
break; // found, and no quota set
}
}
return 0;
}
int NumaNodeCount() {
int n = 0;
std::error_code ec;
for (const auto &e : std::filesystem::directory_iterator("/sys/devices/system/node", ec)) {
const std::string name = e.path().filename().string();
if (name.rfind("node", 0) == 0 && name.size() > 4
&& name.find_first_not_of("0123456789", 4) == std::string::npos)
n++;
}
return n;
}
// "0-23,48-71" -> the CPUs it names.
bool ParseCpuList(const std::string &list, cpu_set_t &out) {
CPU_ZERO(&out);
std::stringstream ss(list);
std::string range;
bool any = false;
while (std::getline(ss, range, ',')) {
int lo, hi;
if (std::sscanf(range.c_str(), "%d-%d", &lo, &hi) == 2) {
} else if (std::sscanf(range.c_str(), "%d", &lo) == 1) {
hi = lo;
} else
continue;
for (int c = lo; c <= hi && c < CPU_SETSIZE; c++) {
CPU_SET(c, &out);
any = true;
}
}
return any;
}
}
int NumaNodeOfPciDevice(const std::string &pci_bus_id) {
if (NumaNodeCount() < 2)
return -1;
unsigned domain, bus, device, function;
if (std::sscanf(pci_bus_id.c_str(), "%x:%x:%x.%x", &domain, &bus, &device, &function) != 4)
return -1;
char path[128];
std::snprintf(path, sizeof(path), "/sys/bus/pci/devices/%04x:%02x:%02x.%x/numa_node",
domain, bus, device, function);
std::ifstream f(path);
int node = -1;
if (!(f >> node))
return -1;
return node;
}
unsigned AvailableCpus() {
static const unsigned n = [] {
unsigned cpus = static_cast<unsigned>(std::max(1, CPU_COUNT(&start_mask)));
if (const unsigned quota = CgroupCpuQuota(); quota > 0)
cpus = std::min(cpus, quota);
return cpus;
}();
return n;
}
void PinThreadToNumaNode(int node) {
if (node < 0)
return;
std::ifstream f("/sys/devices/system/node/node" + std::to_string(node) + "/cpulist");
std::string list;
cpu_set_t node_cpus;
if (!std::getline(f, list) || !ParseCpuList(list, node_cpus))
return;
cpu_set_t mask;
CPU_AND(&mask, &node_cpus, &start_mask);
if (CPU_COUNT(&mask) == 0)
return;
pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask);
}
void RestoreThreadAffinity() {
pthread_setaffinity_np(pthread_self(), sizeof(start_mask), &start_mask);
}
std::vector<int> GetThreadCpus() {
cpu_set_t mask;
std::vector<int> cpus;
if (pthread_getaffinity_np(pthread_self(), sizeof(mask), &mask) == 0)
for (int c = 0; c < CPU_SETSIZE; c++)
if (CPU_ISSET(c, &mask))
cpus.push_back(c);
return cpus;
}
void SetThreadCpus(const std::vector<int> &cpus) {
if (cpus.empty())
return;
cpu_set_t mask;
CPU_ZERO(&mask);
for (const int c: cpus)
CPU_SET(c, &mask);
pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask);
}
#else
#include <algorithm>
#include <thread>
unsigned AvailableCpus() { return std::max(1u, std::thread::hardware_concurrency()); }
int NumaNodeOfPciDevice(const std::string &) { return -1; }
void PinThreadToNumaNode(int) {}
void RestoreThreadAffinity() {}
std::vector<int> GetThreadCpus() { return {}; }
void SetThreadCpus(const std::vector<int> &) {}
#endif