core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
cpu_budget.hpp
Go to the documentation of this file.
1// core — how many CPUs this process may actually USE, as opposed to how many it can SEE.
2//
3// THE PROBLEM. Kokkos sizes its host backend from the CPUs it can see. A container — Colab,
4// Binder, Docker, a Slurm cgroup — normally shows the whole host while granting a fraction of it
5// through a cgroup CPU *quota*, and a quota is invisible to OpenMP: omp_get_max_threads() returns
6// the host count, the pool spin-waits at every barrier, and the run collapses. Measured on the
7// 1.0.0 CPU wheel (suite docs/SCALING_ISSUES.md issue 7), a 42-step quick start that takes 25.7 s
8// on two CPUs did NOT finish in fifteen minutes with 48 CPUs visible and two CPUs of quota — and
9// took 26.0 s with the pool bounded. The trap is silent and it is the first thing a Colab user
10// hits.
11//
12// An affinity MASK is not the same thing and is not the trap: OpenMP honours a mask and sizes
13// itself correctly. What follows therefore reads the quota, and takes the affinity count only as
14// the ceiling.
15//
16// WHERE THE QUOTA LIVES. Not, in general, at /sys/fs/cgroup/cpu.max — that is the cgroup ROOT, and
17// on a systemd host it says "max" while the quota sits on the process's own scope further down.
18// A container usually has its own cgroup namespace, so there the root IS the container's cgroup
19// and the short path happens to work; outside one it does not. So: resolve the process's cgroup
20// from /proc/self/cgroup and walk UP, taking the tightest quota on the chain, because a limit set
21// on an ancestor binds just as hard as one set on the leaf.
22#ifndef PECLET_CORE_COMMON_CPU_BUDGET_HPP
23#define PECLET_CORE_COMMON_CPU_BUDGET_HPP
24
25#include <algorithm>
26#include <cstdlib>
27#include <fstream>
28#include <sstream>
29#include <string>
30
31#if defined(__linux__)
32#include <sched.h>
33#endif
34
35namespace peclet::core {
36
37namespace detail {
38
40inline int affinityCpus() {
41#if defined(__linux__)
42 cpu_set_t set;
43 CPU_ZERO(&set);
44 if (sched_getaffinity(0, sizeof(set), &set) == 0)
45 return CPU_COUNT(&set);
46#endif
47 return 0;
48}
49
51inline std::string readFirstLine(const std::string& path) {
52 std::ifstream in(path);
53 std::string line;
54 if (in && std::getline(in, line))
55 return line;
56 return {};
57}
58
61inline std::string cgroupPath(const std::string& procSelfCgroup, const std::string& controller) {
62 std::ifstream in(procSelfCgroup);
63 std::string line;
64 while (std::getline(in, line)) {
65 // "hierarchy-ID:controller-list:path"
66 const auto c1 = line.find(':');
67 if (c1 == std::string::npos)
68 continue;
69 const auto c2 = line.find(':', c1 + 1);
70 if (c2 == std::string::npos)
71 continue;
72 const std::string ctrl = line.substr(c1 + 1, c2 - c1 - 1);
73 const std::string path = line.substr(c2 + 1);
74 if (controller.empty()) {
75 if (ctrl.empty()) // "0::/..." — the v2 unified hierarchy
76 return path;
77 } else if (ctrl.find(controller) != std::string::npos) {
78 return path; // "N:cpu,cpuacct:/..."
79 }
80 }
81 return {};
82}
83
86inline int quotaFromCpuMax(const std::string& file) {
87 std::istringstream in(readFirstLine(file));
88 std::string quota;
89 long period = 0;
90 if (!(in >> quota >> period) || quota == "max" || period <= 0)
91 return 0;
92 const long q = std::strtol(quota.c_str(), nullptr, 10);
93 return q > 0 ? static_cast<int>((q + period - 1) / period) : 0;
94}
95
97inline int quotaFromCfs(const std::string& dir) {
98 const long q = std::strtol(readFirstLine(dir + "/cpu.cfs_quota_us").c_str(), nullptr, 10);
99 const long p = std::strtol(readFirstLine(dir + "/cpu.cfs_period_us").c_str(), nullptr, 10);
100 if (q <= 0 || p <= 0)
101 return 0; // -1 is "no quota"
102 return static_cast<int>((q + p - 1) / p);
103}
104
107inline int cgroupQuota(const std::string& cgroupRoot, const std::string& procSelfCgroup) {
108 int best = 0;
109 const auto tighten = [&best](int n) {
110 if (n > 0 && (best == 0 || n < best))
111 best = n;
112 };
113
114 // v2: <root>/<path>/cpu.max, then every ancestor up to <root>/cpu.max.
115 std::string path = cgroupPath(procSelfCgroup, "");
116 for (;;) {
117 tighten(quotaFromCpuMax(cgroupRoot + path + "/cpu.max"));
118 if (path.empty())
119 break;
120 const auto slash = path.rfind('/');
121 path = (slash == std::string::npos) ? std::string{} : path.substr(0, slash);
122 }
123
124 // v1: <root>/cpu/<path>/cpu.cfs_quota_us, likewise up the chain.
125 path = cgroupPath(procSelfCgroup, "cpu");
126 for (;;) {
127 tighten(quotaFromCfs(cgroupRoot + "/cpu" + path));
128 if (path.empty())
129 break;
130 const auto slash = path.rfind('/');
131 path = (slash == std::string::npos) ? std::string{} : path.substr(0, slash);
132 }
133 return best;
134}
135
136} // namespace detail
137
140inline int usableCpus() {
141 const int affinity = detail::affinityCpus();
142 const int quota = detail::cgroupQuota("/sys/fs/cgroup", "/proc/self/cgroup");
143 int n = affinity > 0 ? affinity : quota;
144 if (quota > 0 && (n == 0 || quota < n))
145 n = quota;
146 return std::max(1, n);
147}
148
172inline int defaultHostThreads(bool hostBackendSizesItself) {
173 if (const char* v = std::getenv("KOKKOS_NUM_THREADS"); v && *v)
174 return 0; // Kokkos reads this one itself, whatever the backend
175 if (const char* v = std::getenv("OMP_NUM_THREADS"); v && *v) {
176 if (hostBackendSizesItself)
177 return 0; // the OpenMP runtime will act on it; do not second-guess the user
178 if (const int n = std::atoi(v); n > 0)
179 return n; // e.g. "4"; OpenMP's list form ("4,2") takes the outermost level
180 // Unparseable. Fall through to the budget rather than returning 0: on a backend that does not
181 // size itself, 0 means one thread, and answering garbage with a 7x slowdown helps nobody.
182 }
183 const int usable = usableCpus();
184 if (!hostBackendSizesItself)
185 return usable; // nothing else will; its own default is 1
186 const int affinity = detail::affinityCpus();
187 return (affinity > 0 && usable < affinity) ? usable : 0;
188}
189
190} // namespace peclet::core
191
192#endif // PECLET_CORE_COMMON_CPU_BUDGET_HPP
std::string cgroupPath(const std::string &procSelfCgroup, const std::string &controller)
The cgroup path of this process for controller ("" = the v2 unified hierarchy), e....
int quotaFromCfs(const std::string &dir)
The same for cgroup v1's pair of files in dir. 0 = no limit here.
int cgroupQuota(const std::string &cgroupRoot, const std::string &procSelfCgroup)
The tightest CPU quota binding this process, walking from its own cgroup up to the root.
int quotaFromCpuMax(const std::string &file)
CPUs allowed by one cgroup-v2 cpu.max file ("max 100000" or "200000 100000"), rounded UP so a fractio...
int affinityCpus()
CPUs in this process's affinity mask, or 0 if that cannot be determined.
std::string readFirstLine(const std::string &path)
First whitespace-separated fields of a file, or an empty string if it cannot be read.
int defaultHostThreads(bool hostBackendSizesItself)
The host thread count to hand a backend that has not been told one, or 0 for "say nothing".
int usableCpus()
How many CPUs this process may actually use: the affinity count, lowered by any cgroup quota binding ...