stream-limits 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stream_limits-0.2.1 → stream_limits-0.3.0}/CHANGELOG.md +23 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/CITATION.cff +2 -2
- {stream_limits-0.2.1/stream_limits.egg-info → stream_limits-0.3.0}/PKG-INFO +1 -1
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/reference.md +2 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/threads.md +20 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/pyproject.toml +1 -1
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/__init__.py +47 -1
- {stream_limits-0.2.1 → stream_limits-0.3.0/stream_limits.egg-info}/PKG-INFO +1 -1
- {stream_limits-0.2.1 → stream_limits-0.3.0}/tests/test_override_and_libraries.py +65 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/LICENSE.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/MANIFEST.in +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/README.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/_static/.gitkeep +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/changelog.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/conf.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/index.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/launcher.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/memory.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/other-libraries.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/docs/schedulers.md +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/setup.cfg +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/__main__.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/_exec_cgroup.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/enforcement.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/launcher.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/py.typed +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/pytest_plugin.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits/testing.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits.egg-info/SOURCES.txt +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits.egg-info/dependency_links.txt +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits.egg-info/requires.txt +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/stream_limits.egg-info/top_level.txt +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/tests/conftest.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/tests/test_launcher.py +0 -0
- {stream_limits-0.2.1 → stream_limits-0.3.0}/tests/test_limits.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **A new function, `set_openmp_wait_policy`, controls how idle OpenMP threads
|
|
6
|
+
wait.** Programs that make many short parallel calls, such as numba loops run
|
|
7
|
+
once per batch of data, can run noticeably slower when the idle threads spin
|
|
8
|
+
while they wait for the next call, especially on processors that mix fast and
|
|
9
|
+
efficient cores. Telling the threads to sleep instead (`OMP_WAIT_POLICY=passive`,
|
|
10
|
+
the default here) fixed this in diff-stream: fits ran 2% to 15% faster on a
|
|
11
|
+
cloud server, and on a laptop with mixed cores 8 threads went from slower than
|
|
12
|
+
one thread to faster. Programs with a few long parallel calls, such as
|
|
13
|
+
hdfe-stream and logit-stream, run the same either way. On Windows, where
|
|
14
|
+
numba's OpenMP runtime ignores the setting, the call changes nothing.
|
|
15
|
+
- **A package has to ask for it.** Importing stream-limits, and
|
|
16
|
+
`apply_scheduler_defaults`, still change nothing, because the setting reaches
|
|
17
|
+
every OpenMP library in the process (BLAS builds on OpenMP, scikit-learn,
|
|
18
|
+
LightGBM). A value you have set yourself is kept unless you pass
|
|
19
|
+
`override=True`.
|
|
20
|
+
- **It must run before OpenMP starts.** The setting is read once, when the
|
|
21
|
+
OpenMP runtime loads; numba loads its runtime at its first parallel call, so
|
|
22
|
+
calling the function after importing numba is still in time. The function
|
|
23
|
+
reports when a runtime was already loaded (Linux), in which case the setting
|
|
24
|
+
reaches only programs started afterwards.
|
|
25
|
+
|
|
3
26
|
## 0.2.1
|
|
4
27
|
|
|
5
28
|
- **The watchdog no longer counts the pages of memory-mapped files.** A program
|
|
@@ -7,8 +7,8 @@ authors:
|
|
|
7
7
|
- family-names: Tucker
|
|
8
8
|
given-names: Lee
|
|
9
9
|
email: lee.c.tucker@census.gov
|
|
10
|
-
version: 0.
|
|
11
|
-
date-released: 2026-11-
|
|
10
|
+
version: 0.3.0
|
|
11
|
+
date-released: 2026-11-08
|
|
12
12
|
license: MIT
|
|
13
13
|
repository-code: "https://github.com/leetucker/stream-limits"
|
|
14
14
|
url: "https://github.com/leetucker/stream-limits"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stream-limits
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Cap the threads and memory of numeric Python libraries (Polars, numba, BLAS, pyarrow) at a scheduler's allocation, and measure peak memory
|
|
5
5
|
Author: Lee Tucker
|
|
6
6
|
License-Expression: MIT
|
|
@@ -81,3 +81,23 @@ allocation, read from the environment, cgroups or the scheduler (see
|
|
|
81
81
|
[Schedulers and containers](schedulers.md)), and otherwise the machine's. A job that runs several
|
|
82
82
|
processes should give each its share, for example
|
|
83
83
|
`STREAM_LIMITS_MAX_THREADS=$(( NCPUS / 4 ))` for four.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## How OpenMP threads wait
|
|
87
|
+
|
|
88
|
+
OpenMP threads that finish a parallel region either spin, waiting for the next one, or
|
|
89
|
+
sleep. Spinning is quicker to resume, but a program that makes many short parallel calls
|
|
90
|
+
(numba's `prange` loops, one per batch of data) can run several times slower with it,
|
|
91
|
+
notably on CPUs that mix performance and efficiency cores, and a little slower on others.
|
|
92
|
+
Programs with few, long parallel regions are unaffected: hdfe-stream's solvers and
|
|
93
|
+
logit-stream's fits ran as fast either way.
|
|
94
|
+
|
|
95
|
+
{py:func}`stream_limits.set_openmp_wait_policy` sets `OMP_WAIT_POLICY` (to `"passive"` unless you
|
|
96
|
+
say `"active"`). A package opts in by calling it, because the setting reaches every
|
|
97
|
+
OpenMP library in the process (BLAS builds on OpenMP, scikit-learn, LightGBM); importing
|
|
98
|
+
stream-limits, and `apply_scheduler_defaults`, leave it alone. A value you have set is kept unless
|
|
99
|
+
you pass `override=True`. The runtime reads the variable when it loads, which for numba is
|
|
100
|
+
at its first parallel call, so call the function before that. The result says when a runtime
|
|
101
|
+
was already loaded (Linux), in which case the variable is set for child processes only.
|
|
102
|
+
On Windows, numba uses Microsoft's OpenMP runtime, which does not read the variable, so the
|
|
103
|
+
call changes nothing there.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "stream-limits"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Cap the threads and memory of numeric Python libraries (Polars, numba, BLAS, pyarrow) at a scheduler's allocation, and measure peak memory"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -87,10 +87,11 @@ import warnings
|
|
|
87
87
|
from pathlib import Path
|
|
88
88
|
from typing import Iterator, Mapping
|
|
89
89
|
|
|
90
|
-
__version__ = "0.
|
|
90
|
+
__version__ = "0.3.0"
|
|
91
91
|
|
|
92
92
|
__all__ = [
|
|
93
93
|
"set_limits", "limit_threads", "limit_memory", "apply_scheduler_defaults", "thread_scope",
|
|
94
|
+
"set_openmp_wait_policy",
|
|
94
95
|
"detect_cpus", "detect_memory_gb", "describe", "MemoryBudget",
|
|
95
96
|
"peak_rss_gb", "reset_peak_rss", "current_rss_gb",
|
|
96
97
|
]
|
|
@@ -790,6 +791,51 @@ def thread_scope(n: int | None = None) -> Iterator[None]:
|
|
|
790
791
|
numba.set_num_threads(previous)
|
|
791
792
|
|
|
792
793
|
|
|
794
|
+
def _openmp_loaded() -> bool | None:
|
|
795
|
+
"""Whether an OpenMP runtime is already in this process (Linux only; None
|
|
796
|
+
elsewhere). It reads ``OMP_WAIT_POLICY`` once, when it loads."""
|
|
797
|
+
try:
|
|
798
|
+
maps = Path("/proc/self/maps").read_text()
|
|
799
|
+
except OSError:
|
|
800
|
+
return None
|
|
801
|
+
return any(name in maps for name in ("libgomp", "libiomp5", "libomp"))
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def set_openmp_wait_policy(policy: str = "passive", *, override: bool = False,
|
|
805
|
+
environ=None) -> dict:
|
|
806
|
+
"""Set how OpenMP threads wait between parallel regions, by ``OMP_WAIT_POLICY``.
|
|
807
|
+
|
|
808
|
+
With ``"passive"`` an idle thread sleeps until the next region; with
|
|
809
|
+
``"active"`` it spins. A package whose compiled code makes many short
|
|
810
|
+
parallel calls (numba's ``prange`` loops, one per batch of data) can run
|
|
811
|
+
several times slower with spinning threads, notably on CPUs with
|
|
812
|
+
performance and efficiency cores, and a little slower on others. Code with
|
|
813
|
+
few, long parallel regions (hdfe-stream's solvers, logit-stream's fits)
|
|
814
|
+
is unaffected, and this function is not called by importing the package:
|
|
815
|
+
a package opts in, since the setting reaches every OpenMP library in the
|
|
816
|
+
process (BLAS builds on OpenMP, scikit-learn, LightGBM). Microsoft's OpenMP
|
|
817
|
+
runtime, the one numba uses on Windows, does not read the variable, so there
|
|
818
|
+
the call changes nothing.
|
|
819
|
+
|
|
820
|
+
A value the user has set is left alone unless `override`. The runtime reads
|
|
821
|
+
the variable once, when it loads (numba loads its OpenMP layer at the first
|
|
822
|
+
parallel call), so call this before the first parallel call; the result's
|
|
823
|
+
``"late"`` is True when a runtime was already loaded (Linux), in which case
|
|
824
|
+
the variable is still set, for child processes, but this process is
|
|
825
|
+
unaffected. Returns ``{"policy": ..., "set": bool, "late": bool | None}``,
|
|
826
|
+
where ``"policy"`` is the value in effect."""
|
|
827
|
+
if policy not in ("passive", "active"):
|
|
828
|
+
raise ValueError(f"policy must be 'passive' or 'active', not {policy!r}")
|
|
829
|
+
environ = os.environ if environ is None else environ
|
|
830
|
+
current = environ.get("OMP_WAIT_POLICY")
|
|
831
|
+
if current and not override:
|
|
832
|
+
return {"policy": current.lower(), "set": False, "late": None}
|
|
833
|
+
late = _openmp_loaded() if environ is os.environ else None
|
|
834
|
+
environ["OMP_WAIT_POLICY"] = policy
|
|
835
|
+
return {"policy": policy, "set": True, "late": late}
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
|
|
793
839
|
# ----------------------------------------------------- peak memory used
|
|
794
840
|
def _windows_memory(handle=None) -> tuple[int, int] | None:
|
|
795
841
|
"""This process's ``(working set, peak working set)`` in bytes, from the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stream-limits
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Cap the threads and memory of numeric Python libraries (Polars, numba, BLAS, pyarrow) at a scheduler's allocation, and measure peak memory
|
|
5
5
|
Author: Lee Tucker
|
|
6
6
|
License-Expression: MIT
|
|
@@ -213,3 +213,68 @@ def test_a_preset_spill_dir_is_kept_unless_override(tmp_path):
|
|
|
213
213
|
a = _run(code.format(extra=""), POLARS_OOC_SPILL_DIR="/somewhere/else")
|
|
214
214
|
b = _run(code.format(extra=", override=True"), POLARS_OOC_SPILL_DIR="/somewhere/else")
|
|
215
215
|
assert a.stdout.strip() == "/somewhere/else" and b.stdout.strip() == str(tmp_path), (a.stderr, b.stderr)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# --- set_openmp_wait_policy ---
|
|
219
|
+
|
|
220
|
+
def test_wait_policy_sets_the_variable_and_respects_the_user():
|
|
221
|
+
env = {}
|
|
222
|
+
assert sl.set_openmp_wait_policy(environ=env) == {"policy": "passive", "set": True, "late": None}
|
|
223
|
+
assert env["OMP_WAIT_POLICY"] == "passive"
|
|
224
|
+
env = {"OMP_WAIT_POLICY": "ACTIVE"}
|
|
225
|
+
assert sl.set_openmp_wait_policy(environ=env) == {"policy": "active", "set": False, "late": None}
|
|
226
|
+
assert env["OMP_WAIT_POLICY"] == "ACTIVE"
|
|
227
|
+
assert sl.set_openmp_wait_policy(environ=env, override=True)["set"] is True
|
|
228
|
+
assert env["OMP_WAIT_POLICY"] == "passive"
|
|
229
|
+
with pytest.raises(ValueError):
|
|
230
|
+
sl.set_openmp_wait_policy("sleepy", environ={})
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_importing_the_module_leaves_the_policy_alone():
|
|
234
|
+
r = _run("import os, stream_limits; print(os.environ.get('OMP_WAIT_POLICY'))")
|
|
235
|
+
assert r.stdout.strip() == "None"
|
|
236
|
+
r = _run("import os, stream_limits; stream_limits.apply_scheduler_defaults(); "
|
|
237
|
+
"print(os.environ.get('OMP_WAIT_POLICY'))", NCPUS="2")
|
|
238
|
+
assert r.stdout.strip() == "None"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
NUMBA_PROBE = """
|
|
242
|
+
import numpy as np, sys
|
|
243
|
+
{before}
|
|
244
|
+
import numba
|
|
245
|
+
from numba import njit, prange
|
|
246
|
+
import stream_limits
|
|
247
|
+
print(stream_limits.set_openmp_wait_policy()["late"])
|
|
248
|
+
@njit(parallel=True)
|
|
249
|
+
def f(a):
|
|
250
|
+
s = 0.0
|
|
251
|
+
for i in prange(a.size):
|
|
252
|
+
s += a[i]
|
|
253
|
+
return s
|
|
254
|
+
f(np.ones(100))
|
|
255
|
+
{after}
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_wait_policy_takes_effect_when_numba_is_imported_first():
|
|
260
|
+
# numba loads its OpenMP runtime at the first parallel call, which is after
|
|
261
|
+
r = _run(NUMBA_PROBE.format(before="", after="print(numba.threading_layer())"),
|
|
262
|
+
OMP_DISPLAY_ENV="verbose")
|
|
263
|
+
if "omp" not in r.stdout.split():
|
|
264
|
+
pytest.skip("numba's OpenMP threading layer is not in use")
|
|
265
|
+
# "late" is only detectable on Linux (it reads /proc/self/maps)
|
|
266
|
+
assert r.stdout.split()[0] == ("False" if sys.platform.startswith("linux") else "None")
|
|
267
|
+
if sys.platform == "win32":
|
|
268
|
+
pytest.skip("Microsoft's OpenMP runtime does not read or display OMP_WAIT_POLICY")
|
|
269
|
+
assert "OMP_WAIT_POLICY = 'PASSIVE'" in r.stderr
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_wait_policy_reports_a_runtime_that_is_already_loaded():
|
|
273
|
+
r = _run(NUMBA_PROBE.format(before="", after="") +
|
|
274
|
+
"\nimport stream_limits as s\nprint(s.set_openmp_wait_policy(override=True)['late'])")
|
|
275
|
+
if not sys.platform.startswith("linux"):
|
|
276
|
+
pytest.skip("detection needs /proc/self/maps")
|
|
277
|
+
lines = r.stdout.split()
|
|
278
|
+
if lines[-1] == "False":
|
|
279
|
+
pytest.skip("numba's OpenMP layer is not in use")
|
|
280
|
+
assert lines[-1] == "True"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|