Multiprocessing python program inside Docker

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.)

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)