Thread count from the CPUs the process may use, not the machine's

std::thread::hardware_concurrency() counts every CPU of the node, so a job
given a CPU set (taskset, a cpuset, a Slurm allocation) or a container CPU
quota started one thread per node CPU: the default -N, the shared worker
pool and every "0 = all threads" default. AvailableCpus() (ThreadAffinity)
counts the start affinity mask, capped by the cgroup CPU quota (v2 cpu.max,
v1 cfs_quota/period, the process's own cgroup and then the mounted root),
read once. Outside Linux it is hardware_concurrency(), so the viewer tree
stays portable. It replaces every hardware_concurrency() call outside the
tests and the vendored pocketfft.

Only how many threads run changes; the passes split their work by n alone,
so the results do not depend on it.

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-08 11:15:29 +02:00
co-authored by Claude Opus 5.5
parent af3ea9ccad
commit 50006eff88
14 changed files with 88 additions and 23 deletions
+55
View File
@@ -5,6 +5,8 @@
#ifdef __linux__
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
@@ -26,6 +28,45 @@ namespace {
}
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;
@@ -76,6 +117,16 @@ int NumaNodeOfPciDevice(const std::string &pci_bus_id) {
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;
@@ -97,6 +148,10 @@ void RestoreThreadAffinity() {
#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() {}