From https://docs.docker.com/get-started - "Fundamentally, a container is nothing but a running process, with some added encapsulation features applied to it in order to keep it isolated from the host and from other containers."
Docker runs on a host machine. That host machine (or virtual machine) has a certain number of physical (or virtual) CPU's. The reason that multiprocessing.cpu_count()
displays 8 in your case is because that is the number of CPU's your system has. Using docker options like --cpus
or --cpuset-cpus
doesn't change your machine's hardware, which is what cpu_count()
is reporting.
On my current system:
# native
$ python -c 'import multiprocessing as mp; print(mp.cpu_count())'
12
# docker
$ docker run -it --rm --cpus 1 --cpuset-cpus 0 python python -c 'import multiprocessing as mp; print(mp.cpu_count())'
12
From https://docs.docker.com/config/containers/resource_constraints/#cpu - "By default, each container’s access to the host machine’s CPU cycles is unlimited."
But you can limit containers with options like --cpus
or --cpuset-cpus
.
--cpus
can be a floating point number up to the number of physical CPU's available. You can think of this number as a numerator in the fraction <--cpus arg>
/<physical CPU's>
. If you have 8 physical CPU's and you specify --cpus 4
, what you're telling docker is to use no more than 50% (4/8) of your total CPU's. --cpus 1.5
would use 18.75% (1.5/8).
--cpuset-cpus
actually does limit specifically which physical/virtual CPU's to use.
(And there are many other CPU-related options that are covered in docker's documentation.)
Here is a smaller code sample:
import logging
import multiprocessing
import sys
import psutil
from joblib.parallel import Parallel, delayed
def get_logger():
logger = logging.getLogger()
if not logger.hasHandlers():
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter("[%(process)d/%(processName)s] %(message)s")
handler.setFormatter(formatter)
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
return logger
def fn1(n):
get_logger().debug("fn1(%d); cpu# %d", n, psutil.Process().cpu_num())
if __name__ == "__main__":
get_logger().debug("main")
Parallel(n_jobs=multiprocessing.cpu_count())(delayed(fn1)(n) for n in range(1, 101))
Running this both natively and within docker will log lines such as:
[21/LokyProcess-2] fn1(81); cpu# 11
[28/LokyProcess-9] fn1(82); cpu# 6
[29/LokyProcess-10] fn1(83); cpu# 2
[31/LokyProcess-12] fn1(84); cpu# 0
[22/LokyProcess-3] fn1(85); cpu# 3
[23/LokyProcess-4] fn1(86); cpu# 1
[20/LokyProcess-1] fn1(87); cpu# 7
[25/LokyProcess-6] fn1(88); cpu# 3
[27/LokyProcess-8] fn1(89); cpu# 4
[21/LokyProcess-2] fn1(90); cpu# 9
[28/LokyProcess-9] fn1(91); cpu# 10
[26/LokyProcess-7] fn1(92); cpu# 11
[22/LokyProcess-3] fn1(95); cpu# 9
[29/LokyProcess-10] fn1(93); cpu# 2
[24/LokyProcess-5] fn1(94); cpu# 10
[23/LokyProcess-4] fn1(96); cpu# 1
[20/LokyProcess-1] fn1(97); cpu# 9
[23/LokyProcess-4] fn1(98); cpu# 1
[27/LokyProcess-8] fn1(99); cpu# 4
[21/LokyProcess-2] fn1(100); cpu# 5
Notice that all 12 CPU's are in use on my system.
Notice that
- the same physical CPU is used by multiple processes (cpu#3 by process #'s 22 & 25)
- one individual process can use multiple CPU's (process #21 uses CPU #'s 11 & 9)
Running the same program with docker run --cpus 1 ...
will still result in all 12 CPU's being used by all 12 processes started, just as if the --cpus argument wasn't present. It just limits percentage of total CPU time docker is allowed to use.
Running the same program with docker run --cpusets-cpus 0-1 ...
will result in only 2 physical CPU's being used by all 12 processes started:
[11/LokyProcess-2] fn1(35); cpu# 0
[11/LokyProcess-2] fn1(36); cpu# 0
[12/LokyProcess-3] fn1(37); cpu# 1
[11/LokyProcess-2] fn1(38); cpu# 0
[15/LokyProcess-6] fn1(39); cpu# 1
[17/LokyProcess-8] fn1(40); cpu# 0
[11/LokyProcess-2] fn1(41); cpu# 0
[10/LokyProcess-1] fn1(42); cpu# 1
[11/LokyProcess-2] fn1(43); cpu# 1
[13/LokyProcess-4] fn1(44); cpu# 1
[12/LokyProcess-3] fn1(45); cpu# 0
[12/LokyProcess-3] fn1(46); cpu# 1
To answer the statement "they always take only one physical CPU"-- this is only true if the --cpusets-cpus
arg is exactly/only 1 CPU.
(As a side note-- the reason for logging being set up the way it is in the example is becuase of an open bug in joblib.)