How to determine the number of constrained CPUs within a Docker container? For use with make -j
Asked Answered
F

1

6

Docker supports a --cpus option which constrains a container to using a certain proportion of the host's computing resource. For example, --cpus 2.5 will allocate computing resource equivalent to approximately two and a half of the host's CPU capability. This allocation seems to be somewhat "logical" rather than physical, in that a container running eight processes but with --cpus 1.0 tends to run each process on a separate CPU but limited to approximately 12.5%. This is fine, I suppose.

The problem is that I am not able to find a way for a process within a constrained container to determine how many equivalent CPUs it has available. This is useful specifically for build processes, for example make -j <n>, but what is n? Commonly used nproc outputs the number of CPUs on the host regardless of the --cpus option value, which makes it unsuitable as it is far too large. The value of /proc/cpuinfo is also the host's view (same as nproc).

Is there a way for a contained process to determine the number of CPUs available to it?

As an example, on my 16-core host, I'd like to use docker run ... --cpus 4.0 ... and then have make -j <something> run only four processes rather than, say, sixteen.

FWIW, I'm using the Docker Executor with GitLab-CI and gitlab-runner, which is why --cpuset-cpus= isn't really helpful. The Executor should be able to allocate a number of CPUs to each job across all of the host's CPUs, rather than a specific subset. Although I could create separate Executors for, say, each explicit set of four CPUs, there's no obvious way I'm aware of for gitlab-runner to allocate these in a balanced way to incoming jobs, and I want to avoid pushing the responsibility onto the jobs themselves, perhaps using tags to select an Executor, since that would require each job to know too much about the runner.

I did consider setting a corresponding environment variable like NUM_CPUS=4 in the gitlab-runner's config.toml so that the build job can find out how many to use with make -j $NUM_CPUS, however variables in config.toml can only be set for all runners, not individual ones:

concurrent = 4
check_interval = 0

[[runners]]
  name = "build"
  url = "https://gitlab.com/"
  executor = "docker"
  environment = ["NUM_CPUS=4"]
  [runners.docker]
    # ...
    cpus = "4"    # match NUM_CPUS!
    environment = ["FOO=456"]

The container's environment will see NUM_CPUS=4 but the statement to effect FOO=456 is invalid and of course no such value appears in the container's environment, which makes it impossible to vary the value between Executors.

I should add that in the near future I want to create Executors with different numbers of CPUs - for example, one with just one CPU for simple single-process jobs, and another with lots of CPUs for intensive multi-process jobs, and the GitLab-CI jobs will be able to pick these using GitLab-CI tags. I specifically want to avoid the situation where the pipeline jobs explicitly state that they are to use exactly, say, 4 CPUs, since that requires the jobs to know too much about the build environment and makes it brittle.

Fletcherfletcherism answered 13/12, 2021 at 22:5 Comment(1)
I also have this problem, with the added wrinkle of the runners being in a k8s cluster and a range of CPU units (request ... limit), so what is a good value is determined at runtime (i.e., the OP's workaround won't work for us).Congelation
F
0

You can determines the allocated CPU resources in a constrained container by inspecting the cgroup virtual file system.

With cgroups v2, the relevant information is located inside /sys/fs/cgroup/cpu.max. The file contains two numbers, formally called cfs_quota_us and cfs_period_us, respectively. The first integer specifies the time in microseconds the processes in your container can run on the CPU, while the second integer specifies the period over which this is measured, also in microseconds. For example, if your container is allowed to occupy two cores at the same time, i.e. --cpus 2, then your container is allowed to run for 200000 us every 100000 us.

With a bit of bash, you can integer-divide the two numbers and use the result as an argument to other commands.

make -j $[$(cat /sys/fs/cgroup/cpu.max|tr ' ' '/')]

The tr ' ' '/' part replaces the space between the integers by a / to create a division expression in bash. This is probably not the most robust way, but it worked for me on two different container runtimes.


Note, if the container is unconstrained, the above command fails as /sys/fs/cgroup/cpu.max contains something like max 100000. Bash returned 0 from the division in my case, which might break downstream code.

Flintlock answered 12/6, 2024 at 5:42 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.