Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-109595: Add -Xcpu_count=<n> cmdline for container users #109667

Merged
merged 71 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
41be170
gh-109595: Add -Xcpu_count=<n> cmdline for container users
corona10 Sep 21, 2023
f7a7428
Check style
corona10 Sep 21, 2023
7009bbe
Add help
corona10 Sep 21, 2023
d1f91d8
Apply suggestions from code review
corona10 Sep 21, 2023
8c92ed6
Apply suggestions from code review
corona10 Sep 21, 2023
c27bdfc
Check style
corona10 Sep 21, 2023
b13e5ee
Address code review
corona10 Sep 21, 2023
45fce16
Address code review
corona10 Sep 21, 2023
49a48e4
Address code review
corona10 Sep 21, 2023
829a8e8
Address code review
corona10 Sep 21, 2023
cfb33a4
Fix test
corona10 Sep 21, 2023
95a2173
Update NEWS.d
corona10 Sep 21, 2023
0394d1d
Address code review
corona10 Sep 21, 2023
f4a3f01
Update
corona10 Sep 21, 2023
60a28fe
Update
corona10 Sep 21, 2023
7e5595c
nit
corona10 Sep 21, 2023
8678012
Update
corona10 Sep 21, 2023
cbc5484
Add PYTHONCPUCOUNT
corona10 Sep 21, 2023
4622b60
Add PYTHONCPUCOUNT
corona10 Sep 21, 2023
50f0178
Apply suggestions from code review
corona10 Sep 21, 2023
5c3ac68
Update Python/initconfig.c
corona10 Sep 21, 2023
a276bfd
Fix
corona10 Sep 21, 2023
9c582be
Update whatsnew
corona10 Sep 21, 2023
35b952f
nit
corona10 Sep 21, 2023
f04ea58
Check style
corona10 Sep 21, 2023
7ecf705
Fix docs
corona10 Sep 21, 2023
18dcd44
Update doc
corona10 Sep 21, 2023
b344f4f
nit
corona10 Sep 21, 2023
8069d21
Update
corona10 Sep 21, 2023
c70bc82
Update
corona10 Sep 21, 2023
c8abc29
Update
corona10 Sep 21, 2023
2ac6901
Address code review
corona10 Sep 21, 2023
f0a3ebf
Address code review
corona10 Sep 21, 2023
89d8bb2
Address Victor's suggestion
corona10 Sep 24, 2023
66c617f
nit
corona10 Sep 24, 2023
4a72ed4
Address Erlend's review
corona10 Sep 26, 2023
0426e3e
fix
corona10 Sep 26, 2023
ac5329b
Merge remote-tracking branch 'upstream/main' into gh-109595
corona10 Sep 30, 2023
e50e678
fix
corona10 Sep 30, 2023
24fe0e4
Add space
corona10 Sep 30, 2023
32843ed
Update os.py
corona10 Sep 30, 2023
a954f1c
nit
corona10 Sep 30, 2023
3ab2bc4
Add test code
corona10 Sep 30, 2023
7231697
Update docs
corona10 Sep 30, 2023
b18da0d
fix
corona10 Oct 1, 2023
3579fc4
Address code reivew
corona10 Oct 1, 2023
134ed9e
Address code review
corona10 Oct 1, 2023
2bec7f4
Add test
corona10 Oct 1, 2023
9f7cb5e
fix
corona10 Oct 1, 2023
1217ab5
fix
corona10 Oct 1, 2023
64da2f9
fix
corona10 Oct 1, 2023
64c7329
fix
corona10 Oct 1, 2023
cc54afb
Update
corona10 Oct 1, 2023
ba421c7
Address code review
corona10 Oct 1, 2023
5f20bf6
Update NEWS.d
corona10 Oct 1, 2023
c11789b
Update
corona10 Oct 1, 2023
936c182
Update
corona10 Oct 1, 2023
2f0dc1c
Address code review
corona10 Oct 1, 2023
a7b2c88
Rename to PYTHON_CPU_COUNT
corona10 Oct 1, 2023
551c76d
Hidden overrided cpu count
corona10 Oct 2, 2023
75021be
Use overridden
corona10 Oct 2, 2023
57dd53b
Minor refactoring
corona10 Oct 2, 2023
3f9da50
Revert to PYTHONCPUCOUNT
corona10 Oct 2, 2023
57e82c5
fix
corona10 Oct 2, 2023
c37c8d0
Use PYTHON_CPU_COUNT
corona10 Oct 3, 2023
c7726e5
Address Greg's review
corona10 Oct 3, 2023
de8bf53
Address Greg's code review
corona10 Oct 3, 2023
633914b
nit
corona10 Oct 3, 2023
37fbdfe
Include multiprocessing in docs, reword.
gpshead Oct 3, 2023
a0cfb21
Merge remote-tracking branch 'upstream/main' into gh-109595
corona10 Oct 3, 2023
8daa3b7
Merge remote-tracking branch 'upstream/main' into gh-109595
corona10 Oct 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,19 @@ PyConfig

.. versionadded:: 3.12

.. c:member:: int cpu_count

If the value of :c:member:`~PyConfig.cpu_count` is not ``-1`` then it will override
the return value of :func:`os.cpu_count` and :func:`os.process_cpu_count` functions
into *cpu_count*.

Configured by the :samp:`-X cpu_count={n|default}` command line
flag or the :envvar:`PYTHON_CPU_COUNT` environment variable.

Default: ``-1``.

.. versionadded:: 3.13

.. c:member:: int isolated

If greater than ``0``, enable isolated mode:
Expand Down
7 changes: 7 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5195,6 +5195,10 @@ Miscellaneous System Information

.. versionadded:: 3.4

.. versionchanged:: 3.13
If :option:`-X cpu_count <-X>` is given or :envvar:`PYTHON_CPU_COUNT` is set,
:func:`cpu_count` returns the overridden value *n*.


.. function:: getloadavg()

Expand All @@ -5214,6 +5218,9 @@ Miscellaneous System Information
The :func:`cpu_count` function can be used to get the number of logical CPUs
in the **system**.

If :option:`-X cpu_count <-X>` is given or :envvar:`PYTHON_CPU_COUNT` is set,
:func:`process_cpu_count` returns the overridden value *n*.

See also the :func:`sched_getaffinity` functions.

.. versionadded:: 3.13
Expand Down
17 changes: 17 additions & 0 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,11 @@ Miscellaneous options
report Python calls. This option is only available on some platforms and
will do nothing if is not supported on the current system. The default value
is "off". See also :envvar:`PYTHONPERFSUPPORT` and :ref:`perf_profiling`.
* :samp:`-X cpu_count={n}` overrides :func:`os.cpu_count` and :func:`os.process_cpu_count`.
*n* must be greater than or equal to 1.
This option is useful for users who need to limit CPU resources of a container system.
See also :envvar:`PYTHON_CPU_COUNT`.
If *n* is ``default``, :func:`os.cpu_count` and :func:`os.process_cpu_count` are not overridden.

It also allows passing arbitrary values and retrieving them through the
:data:`sys._xoptions` dictionary.
Expand Down Expand Up @@ -593,6 +598,9 @@ Miscellaneous options
.. versionadded:: 3.12
The ``-X perf`` option.

.. versionadded:: 3.13
The ``-X cpu_count`` option.


Options you shouldn't use
~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -1063,6 +1071,15 @@ conflict.

.. versionadded:: 3.12

.. envvar:: PYTHON_CPU_COUNT

If this variable is set to a positive integer, it overrides
:func:`os.cpu_count` and and :func:`os.process_cpu_count` return result.

See also the :option:`-X cpu_count <-X>` command-line option.

.. versionadded:: 3.13


Debug-mode variables
~~~~~~~~~~~~~~~~~~~~
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ os
usable by the calling thread of the current process.
(Contributed by Victor Stinner in :gh:`109649`.)

* :func:`os.cpu_count` and :func:`os.process_cpu_count` can be overridden through
the new environment variable :envvar:`PYTHON_CPU_COUNT` or the new command-line option
:option:`-X cpu_count <-X>`. This option is useful for users who need to limit
CPU resources of a container system without having to modify the container (application code).
(Contributed by Donghee Na in :gh:`109595`)

pathlib
-------

Expand Down
2 changes: 2 additions & 0 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ typedef struct PyConfig {
int safe_path;
int int_max_str_digits;

int cpu_count;
vstinner marked this conversation as resolved.
Show resolved Hide resolved

/* --- Path configuration inputs ------------ */
int pathconfig_warnings;
wchar_t *program_name;
Expand Down
3 changes: 2 additions & 1 deletion Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def _get_exports_list(module):
from posix import *
try:
from posix import _exit
from posix import _get_cpu_count_config
gpshead marked this conversation as resolved.
Show resolved Hide resolved
__all__.append('_exit')
except ImportError:
pass
Expand Down Expand Up @@ -1138,7 +1139,7 @@ def add_dll_directory(path):
)


if _exists('sched_getaffinity'):
if _exists('sched_getaffinity') and _get_cpu_count_config() != 'overridden':
def process_cpu_count():
"""
Get the number of CPUs of the current process.
Expand Down
25 changes: 21 additions & 4 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,11 +878,8 @@ def test_int_max_str_digits(self):
assert_python_failure('-c', code, PYTHONINTMAXSTRDIGITS='foo')
assert_python_failure('-c', code, PYTHONINTMAXSTRDIGITS='100')

def res2int(res):
out = res.out.strip().decode("utf-8")
return tuple(int(i) for i in out.split())

res = assert_python_ok('-c', code)
corona10 marked this conversation as resolved.
Show resolved Hide resolved
res2int = self.res2int
current_max = sys.get_int_max_str_digits()
self.assertEqual(res2int(res), (current_max, current_max))
res = assert_python_ok('-X', 'int_max_str_digits=0', '-c', code)
Expand All @@ -902,6 +899,26 @@ def res2int(res):
)
self.assertEqual(res2int(res), (6000, 6000))

def test_cpu_count(self):
code = "import os; print(os.cpu_count(), os.process_cpu_count())"
res = assert_python_ok('-X', 'cpu_count=4321', '-c', code)
self.assertEqual(self.res2int(res), (4321, 4321))
res = assert_python_ok('-c', code, PYTHON_CPU_COUNT='1234')
self.assertEqual(self.res2int(res), (1234, 1234))

def test_cpu_count_default(self):
code = "import os; print(os.cpu_count(), os.process_cpu_count())"
res = assert_python_ok('-X', 'cpu_count=default', '-c', code)
self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count()))
res = assert_python_ok('-X', 'cpu_count=default', '-c', code, PYTHON_CPU_COUNT='1234')
self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count()))
es = assert_python_ok('-c', code, PYTHON_CPU_COUNT='default')
self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count()))

def res2int(self, res):
out = res.out.strip().decode("utf-8")
return tuple(int(i) for i in out.split())


@unittest.skipIf(interpreter_requires_environment(),
'Cannot run -I tests when PYTHON env vars are required.')
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
'use_hash_seed': 0,
'hash_seed': 0,
'int_max_str_digits': sys.int_info.default_max_str_digits,
'cpu_count': -1,
'faulthandler': 0,
'tracemalloc': 0,
'perf_profiling': 0,
Expand Down Expand Up @@ -895,6 +896,7 @@ def test_init_from_config(self):
'module_search_paths': self.IGNORE_CONFIG,
'safe_path': 1,
'int_max_str_digits': 31337,
'cpu_count': 4321,

'check_hash_pycs_mode': 'always',
'pathconfig_warnings': 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Add :option:`-X cpu_count <-X>` command line option to override return results of
:func:`os.cpu_count` and :func:`os.process_cpu_count`.
This option is useful for users who need to limit CPU resources of a container system
without having to modify the container (application code).
Patch by Donghee Na.
20 changes: 19 additions & 1 deletion Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -14335,6 +14335,22 @@ os_get_terminal_size_impl(PyObject *module, int fd)
}
#endif /* defined(TERMSIZE_USE_CONIO) || defined(TERMSIZE_USE_IOCTL) */

/*[clinic input]
os._get_cpu_count_config

Private function for get PyConfig.cpu_count
[clinic start generated code]*/

static PyObject *
os__get_cpu_count_config_impl(PyObject *module)
gpshead marked this conversation as resolved.
Show resolved Hide resolved
/*[clinic end generated code: output=3b0ba7e445c9c6b2 input=c52e5bc863732c21]*/
{
const PyConfig *config = _Py_GetConfig();
if (config->cpu_count > 0) {
return PyUnicode_FromString("overridden");
}
return PyUnicode_FromString("default");
}

/*[clinic input]
os.cpu_count
Expand All @@ -14348,7 +14364,12 @@ static PyObject *
os_cpu_count_impl(PyObject *module)
/*[clinic end generated code: output=5fc29463c3936a9c input=ba2f6f8980a0e2eb]*/
{
int ncpu;
const PyConfig *config = _Py_GetConfig();
if (config->cpu_count > 0) {
return PyLong_FromLong(config->cpu_count);
}

int ncpu = 0;
#ifdef MS_WINDOWS
# ifdef MS_WINDOWS_DESKTOP
ncpu = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
Expand Down Expand Up @@ -15977,6 +15998,7 @@ static PyMethodDef posix_methods[] = {
OS_LISTXATTR_METHODDEF

OS_GET_TERMINAL_SIZE_METHODDEF
OS__GET_CPU_COUNT_CONFIG_METHODDEF
OS_CPU_COUNT_METHODDEF
OS_GET_INHERITABLE_METHODDEF
OS_SET_INHERITABLE_METHODDEF
Expand Down
1 change: 1 addition & 0 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,7 @@ static int test_init_from_config(void)

putenv("PYTHONINTMAXSTRDIGITS=6666");
config.int_max_str_digits = 31337;
config.cpu_count = 4321;

init_from_config_clear(&config);

Expand Down
55 changes: 54 additions & 1 deletion Python/initconfig.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
SPEC(use_frozen_modules, UINT),
SPEC(safe_path, UINT),
SPEC(int_max_str_digits, INT),
SPEC(cpu_count, INT),
SPEC(pathconfig_warnings, UINT),
SPEC(program_name, WSTR),
SPEC(pythonpath_env, WSTR_OPT),
Expand Down Expand Up @@ -228,7 +229,10 @@ The following implementation-specific options are available:\n\
\n\
-X int_max_str_digits=number: limit the size of int<->str conversions.\n\
This helps avoid denial of service attacks when parsing untrusted data.\n\
The default is sys.int_info.default_max_str_digits. 0 disables."
The default is sys.int_info.default_max_str_digits. 0 disables.\n\
\n\
-X cpu_count=[n|default]: override CPU count of os.cpu_count() and os.process_cpu_count().\n\
This helps for users who need to limit CPU resources of a container system."

#ifdef Py_STATS
"\n\
Expand Down Expand Up @@ -731,6 +735,8 @@ config_check_consistency(const PyConfig *config)
assert(config->_is_python_build >= 0);
assert(config->safe_path >= 0);
assert(config->int_max_str_digits >= 0);
// cpu_count can be -1 if the user doesn't override it.
assert(config->cpu_count != 0);
corona10 marked this conversation as resolved.
Show resolved Hide resolved
// config->use_frozen_modules is initialized later
// by _PyConfig_InitImportConfig().
#ifdef Py_STATS
Expand Down Expand Up @@ -830,6 +836,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
config->int_max_str_digits = -1;
config->_is_python_build = 0;
config->code_debug_ranges = 1;
config->cpu_count = -1;
}


Expand Down Expand Up @@ -1615,6 +1622,45 @@ config_read_env_vars(PyConfig *config)
return _PyStatus_OK();
}

static PyStatus
config_init_cpu_count(PyConfig *config)
{
const char *env = config_get_env(config, "PYTHON_CPU_COUNT");
if (env) {
int cpu_count = -1;
if (strcmp(env, "default") == 0) {
cpu_count = -1;
}
else if (_Py_str_to_int(env, &cpu_count) < 0 || cpu_count < 1) {
goto error;
}
config->cpu_count = cpu_count;
}

const wchar_t *xoption = config_get_xoption(config, L"cpu_count");
if (xoption) {
int cpu_count = -1;
const wchar_t *sep = wcschr(xoption, L'=');
if (sep) {
if (wcscmp(sep + 1, L"default") == 0) {
cpu_count = -1;
}
else if (config_wstr_to_int(sep + 1, &cpu_count) < 0 || cpu_count < 1) {
goto error;
}
}
else {
goto error;
}
config->cpu_count = cpu_count;
}
return _PyStatus_OK();

error:
return _PyStatus_ERR("-X cpu_count=n option: n is missing or an invalid number, "
"n must be greater than 0");
}

static PyStatus
config_init_perf_profiling(PyConfig *config)
{
Expand Down Expand Up @@ -1797,6 +1843,13 @@ config_read_complex_options(PyConfig *config)
}
}

if (config->cpu_count < 0) {
status = config_init_cpu_count(config);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
}

if (config->pycache_prefix == NULL) {
status = config_init_pycache_prefix(config);
if (_PyStatus_EXCEPTION(status)) {
Expand Down