The challenges of working out how many CPUs your program can use on Linux

July 22, 2024

In yesterday's entry, I talked about our giant (Linux) login server and how we limit each person to only a small portion of that server's CPUs and RAM. These limits sometimes expose issues in how programs attempt to work out how many CPUs they have available so that they can automatically parallelize themselves, or parallelize their build process. This crops up even in areas where you might not expect it; for example, both the Go and Rust compilers attempt to parallelize various parts of compilation using multiple threads within a single compiler process.

In Linux, there are at least three different ways to count the number of 'CPUs' that you might be able to use. First, your program can read /proc/cpuinfo and count up how many online CPUs there are; if code does this in our giant login server, it will get 112 CPUs. Second, your program can call sched_getaffinity() and count how many bits are set in the result; this will detect if you've been limited to a subset of the CPUs by a tool such as taskset(1). Finally, you can read /proc/self/cgroup and then try to find your cgroup to see if you've been given cgroup-based resource limits. These limits won't be phrased in terms of the number of CPUs, but you can work backward from any CPU quota you've been assigned.

In a shell script, you can do the second with nproc, which will also give you the full CPU count if there are no particular limits. As far as I know, there's no straightforward API or program that will give you information on your cgroup CPU quota if there is one. The closest it looks you can come is to use cgget (if it's even installed), but you have to walk all the way back up the cgroup hierarchy to check for CPU limits; it's not necessarily visible in the cgroup (or cgroups) listed in /proc/self/cgroup.

Given the existence of nproc and sched_getaffinity() (and how using them is easier than reading /proc/cpuinfo), I think a lot of scripts and programs will notice CPU affinity restrictions and restrict their parallelism accordingly. My experience suggests that almost nothing is looking for cgroup-based restrictions. This occasionally creates amusing load average situations on our giant login server when a program will see 112 CPUs 'available' and promptly try to use all of them, resulting in their CPU quota being massively over-subscribed and the load average going quite high without actually really affecting anyone else.

(I once did this myself on the login server by absently firing up a heavily parallel build process without realizing I was on the wrong machine for it.)

PS: The corollary of this is that if you want to limit the multi-CPU load impact of something, such as building Firefox from source, it's probably better to use taskset(1) than to do it with systemd features, because it's much more likely that things will notice the taskset limits and not flood your process table and spike the load average. This will work best on single-user machines, such as your desktop, where you don't have to worry about coordinating taskset CPU ranges with anyone or anything else.


Comments on this page:

By Ivan at 2024-07-23 03:55:14:

The community of the R programming language has been struggling with this for a while. The people running the mandatory package checks obviously want to do that in parallel, but every now and then a package would run parallel::detectCores() or some OpenMP code with default settings or manually count the processors of the check server and try to use all 64 of them, very inefficiently due to Amdahl's law. So the policy is now to use no more than two parallel threads or processes unless the user explicitly asks for more. This naturally leaves the owners of powerful laptops or workstations unhappy because they do prefer the code to use all the CPU cores without additional settings.

I think that hwloc understands as many CPU limits on Linux as feasible, including the cgroups, but it's really overkill for something as small as a build tool. The end result is that a lot of limits must be checked in order to find out how many processes can be used.

By Russell at 2024-07-27 07:07:50:

Whether to interpret the cgroup CPU quota as a parallelism limit is not obvious.

On one side of the argument is that the throttling that results from exceeding the quota causes your threads to be prevented from running at all for long stretches of time. Dan Luu wrote about this back in 2019, although the situation may be somewhat improved on newer kernels: https://danluu.com/cgroup-throttling/ Also, more threads doing the same task waste more cycles on locking.

On the other side of it is that other software in the cgroup that doesn't interpret it as a parallelism limit could hog most of the quota. And the quota is not actually designed as a parallelism limit -- you can set non-integer multiples of 100% CPU and it works fine.

Finally, interaction with CPU frequency scaling is potentially hairy. I have no idea what schedutil does with it, and a hardware frequency governor might interpret the less-than-100% duty cycle of a throttled cgroup as a workload that doesn't require the full frequency of the CPU.

By cks at 2024-07-27 13:17:48:

My view is that if your program is trying to decide how much work to do at the same time, you definitely want to take the cgroup CPU quota into account because it certainly influences how much parallelism you can get in practice. If your cgroup CPU quota is '400%', using sixteen CPU-consuming threads is not going to be useful, no more than it would be if you had only four CPUs. That some other program could be claiming part of the quota is no different from that some other program could be using a bunch of CPU without cgroup CPU limits.

Written on 22 July 2024.
« Our giant login server: solving resource problems with brute force
Seeing and matching pf rules when using tcpdump on OpenBSD's pflog interface »

Page tools: View Source, View Normal.
Search:
Login: Password:

Last modified: Mon Jul 22 22:20:00 2024
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.