stream-limits¶
Cap the threads and memory of numeric Python libraries at a job’s allocation, with one interface, and measure the memory a block of code used.
On a cluster (PBS, Slurm, a container) a job gets a fixed number of CPUs and a fixed
amount of memory, and is killed if it exceeds either. Polars, numba, BLAS and pyarrow
each size their thread pools separately and default to every core of the node; code
that asks how much memory there is sees the node’s too. stream-limits finds the job’s
allocation, applies it to each library as far as the library allows, and says so when it
cannot. It depends on nothing outside the standard library and imports none of the
libraries it limits, so it can run before they are imported, which is when some of them
(Polars) need to be told.
It is what hdfe-stream, logit-stream and dpd-stream use, and it works on its own.
import stream_limits
stream_limits.set_limits() # the job's allocation; or set_limits(threads=8, memory_gb=16)
import polars as pl # now capped
import numpy as np
set_limits is limit_threads() and limit_memory()
in one call. Values you give are applied; values you leave out are the job’s allocation if one is
found, and nothing otherwise, so the same line is right on a cluster and on a laptop. The two
functions it calls remain, for the options the one call leaves out.
$ python -m stream_limits # what was found, from inside a job
{
"cpus": 8,
"cpus_source": "NCPUS",
"memory_gb": 17.18,
"memory_source": "qstat Resource_List.mem",
...
}
Where to start¶
On a cluster, before anything else: run
python -m stream_limitsinside a job and check that the CPUs and memory it reports are the ones you asked for. If they are not, see Schedulers and containers, which says where each number comes from and how to tell the library about a site that does it differently.To cap threads: Thread limits.
For a library that is not covered (DuckDB, Dask, Ray, JAX, Spark, …): Other libraries.
To bound memory: Memory limits, which covers the memory a job has, Polars’ spill-to-disk budget, and
MemoryBudget, for code that chooses between a fast path that needs memory and a slower one that needs less.If you use hdfe-stream, logit-stream or dpd-stream: they call
apply_scheduler_defaults()when imported, so a job is capped without a line of your code, and theirmax_memory_gbandn_threadsoptions use what is described here. Theirprofile=Trueoption, which estimates a fit’s memory before you run it, is documented in hdfe-stream.
Automatically, or explicitly¶
apply_scheduler_defaults() (which is set_limits() with no arguments) does limit_threads and
limit_memory, only for limits it finds (a variable, a scheduler’s environment, a cgroup, a restricted CPU affinity), and sets
only variables you have not set yourself; with no limit it does nothing, so a workstation is
untouched. Libraries built on stream-limits call it when they are imported. Importing
stream_limits itself changes nothing.
Install¶
pip install stream-limits # nothing else is installed
pip install "stream-limits[blas]" # threadpoolctl, to limit BLAS after NumPy is loaded
Running a program under a memory limit¶
The optional launcher runs a program in a child process, stops it if it goes over a memory or time limit, and reports its peak memory. It also gives other packages’ test suites fixtures for memory tests.