Memory limits¶
The memory a job has¶
stream_limits.detect_memory_gb() is the memory the process may use. It is read from,
first match winning:
STREAM_LIMITS_MEMORY_GB;the variables named in
STREAM_LIMITS_MEMORY_VARS(a size such as16gb);the cgroup memory limit, the lowest one on the path to the root, so a scheduler’s limit on the job is found even though the process sits in a child group;
PBS’s own record of the job,
qstat -f $PBS_JOBID(Resource_List.mem, elsevmem), for sites that set no memory variable and enforce no cgroup;SLURM_MEM_PER_NODE, orSLURM_MEM_PER_CPUtimes the CPUs;the machine’s physical memory.
Use it instead of asking the operating system how much memory there is: on a shared node that number is the node’s, and a guard of “a quarter of the memory” then allows many times what the job has. Schedulers and containers describes each source.
Polars’ memory budget¶
Polars 2.x has an out-of-core memory budget past which its streaming engine spills to disk
instead of growing. stream_limits.limit_memory() sets it from the memory limit.
A streaming sort of 1.9 GB of Parquet peaked at 6.2 GB with Polars’ defaults, a multiple of the
data. With a 400 MB budget it peaked at 1.8 GB, in twice the time. The budget bounds what
Polars’ operators hold, not the whole process: its Parquet readers hold about 1.5 GB more on
8 threads (less on fewer), and your own arrays are on top. The default budget is
0.5 x (limit - 1.5 GB), at least 0.25 GB; change the share with fraction= or
STREAM_LIMITS_POLARS_FRACTION.
Some things to know:
It is read once, at import, like the thread pool, so
limit_memorymust run before Polars is imported (a warning says so otherwise).It is undocumented in Polars 2.x. The names are those of Polars’ binary (
POLARS_OOC_MEMORY_BUDGET_MB,POLARS_OOC_SPILL_DIR, …). So it is set only for Polars 2.0 or later (read from the package metadata, without importing Polars), never over a budget you set yourself, andSTREAM_LIMITS_POLARS_SPILL=0turns it off.Where it spills is Polars’ temporary directory. On a cluster, make sure
$TMPDIRis the job’s scratch space, or givespill_dir=orSTREAM_LIMITS_SPILL_DIR.How much it helps depends on the code. Code that sorts or joins a large Parquet scan through Polars’ streaming engine gains the most. Code that already works in bounded pieces gains little: in hdfe-stream the peak of a 3-million-row fit did not change, and in dpd-stream on 4.8 million rows it fell from 2.0 to 1.6 GB, with identical results and no slowdown.
Choosing between a fast path and a smaller one¶
stream_limits.MemoryBudget is for code that has a fast path that needs a lot of
memory and a slower one that needs less, or an allocation that cannot shrink and should be
refused up front instead of at the operating system’s kill.
headroom = limit x (1 - reserve) - memory the process holds now
from stream_limits import MemoryBudget
budget = MemoryBudget(limit_gb=None, reserve=None) # defaults: the job's allocation, 0.15
need_gb = nnz * 12 / 1e9 # the exact size of the allocation
if budget.fits(need_gb):
build_matrix()
else:
stream_instead()
The limit defaults to
stream_limits.detect_memory_gb().The reserve is the share of the limit held back for what the code does not model: allocator slack, transient copies, pages a cgroup charges for files. The default is 0.15 (
STREAM_LIMITS_MEMORY_RESERVE). Lower it to run closer to the limit; raise it for a job that must not be killed.headroom_gb(upcoming_gb)subtracts allocations you know are coming later in the same phase, andfits(need, upcoming, share)asks about a share of the headroom (half, for per-thread copies that must leave room for others).It reads the process’s resident memory (one file read on Linux; 0.5 GB is assumed where it cannot be read), so the answer reflects what the process holds at that moment.
Ask whether your exact allocation fits, not whether an estimate’s upper bound does: estimates are pessimistic by design, and refusing a job for an estimate’s pessimism helps no one.
Measuring what was used¶
stream_limits.peak_rss_gb() and stream_limits.reset_peak_rss() read the
kernel’s own high-water mark of the process’s resident memory. Nothing is polled, so it costs
nothing.
stream_limits.reset_peak_rss()
fit_the_model()
print(stream_limits.peak_rss_gb()) # GB, the peak during the call
On Linux the counter is reset by writing to /proc/self/clear_refs, so a second measurement in
the same process reports its own peak; on macOS and Windows the system’s counter cannot be
reset, so it is the process’s lifetime peak (a second measurement is the largest of both). Child
processes are not included. stream_limits.current_rss_gb() is the memory held now.