|
4 | 4 |
|
5 | 5 | import logging |
6 | 6 | import os |
| 7 | +from pathlib import Path |
7 | 8 | from typing import Any, Sequence, Union |
8 | 9 |
|
9 | 10 | import numpy as np |
@@ -71,21 +72,100 @@ def auto_overlap( |
71 | 72 | _GPU_MEMORY_FALLBACK = 8 * 1024**3 |
72 | 73 |
|
73 | 74 |
|
| 75 | +def cpu_allocation() -> int: |
| 76 | + """Return the number of CPUs this process may actually use. |
| 77 | +
|
| 78 | + ``os.cpu_count()`` reports the machine's cores, which on a shared cluster |
| 79 | + node is wildly more than a job was granted -- a 4-core allocation on a |
| 80 | + 128-core node would size itself for 128. Prefer what the scheduler says, |
| 81 | + then the process' CPU affinity mask, and only then the machine. |
| 82 | +
|
| 83 | + Returns |
| 84 | + ------- |
| 85 | + int |
| 86 | + Usable CPU count (always >= 1). |
| 87 | + """ |
| 88 | + for var in ("SLURM_CPUS_PER_TASK", "SLURM_CPUS_ON_NODE"): |
| 89 | + try: |
| 90 | + value = int(os.environ[var]) |
| 91 | + except (KeyError, ValueError): |
| 92 | + continue |
| 93 | + if value > 0: |
| 94 | + return value |
| 95 | + try: |
| 96 | + return max(1, len(os.sched_getaffinity(0))) |
| 97 | + except AttributeError: # not POSIX |
| 98 | + return max(1, os.cpu_count() or 1) |
| 99 | + |
| 100 | + |
| 101 | +def _cgroup_memory_limit() -> "int | None": |
| 102 | + """Memory ceiling from the process' cgroup, if one applies. |
| 103 | +
|
| 104 | + SLURM confines jobs with cgroups, so this is the limit that actually gets |
| 105 | + the process OOM-killed -- unlike the node-wide figure ``psutil`` reports. |
| 106 | + """ |
| 107 | + for path in ( |
| 108 | + "/sys/fs/cgroup/memory.max", # cgroup v2 |
| 109 | + "/sys/fs/cgroup/memory/memory.limit_in_bytes", # cgroup v1 |
| 110 | + ): |
| 111 | + try: |
| 112 | + raw = Path(path).read_text().strip() |
| 113 | + except OSError: |
| 114 | + continue |
| 115 | + if raw == "max": |
| 116 | + continue |
| 117 | + try: |
| 118 | + value = int(raw) |
| 119 | + except ValueError: |
| 120 | + continue |
| 121 | + # v1 reports a sentinel near 2**63 when unlimited. |
| 122 | + if 0 < value < 2**62: |
| 123 | + return value |
| 124 | + return None |
| 125 | + |
| 126 | + |
74 | 127 | def _get_available_memory() -> int: |
75 | | - """Return available system RAM in bytes. |
| 128 | + """Return the memory this process may actually use, in bytes. |
| 129 | +
|
| 130 | + Takes the **smallest** of everything that can constrain it: the SLURM |
| 131 | + allocation, the cgroup limit, and the node's free RAM. A node with 512 GB |
| 132 | + free must never convince a 128 GB job that it has room -- that mismatch is |
| 133 | + exactly how a job sizes itself into an OOM kill. |
76 | 134 |
|
77 | 135 | Returns |
78 | 136 | ------- |
79 | 137 | int |
80 | | - Available memory via ``psutil``, or an 8 GiB fallback if it is not |
81 | | - installed. |
| 138 | + Usable memory in bytes, or an 8 GiB fallback if nothing is knowable. |
82 | 139 | """ |
| 140 | + limits = [] |
| 141 | + |
| 142 | + per_node = os.environ.get("SLURM_MEM_PER_NODE") |
| 143 | + if per_node: |
| 144 | + try: |
| 145 | + limits.append(int(per_node) * 1024**2) # SLURM reports MB |
| 146 | + except ValueError: |
| 147 | + pass |
| 148 | + per_cpu = os.environ.get("SLURM_MEM_PER_CPU") |
| 149 | + if per_cpu: |
| 150 | + try: |
| 151 | + limits.append(int(per_cpu) * 1024**2 * cpu_allocation()) |
| 152 | + except ValueError: |
| 153 | + pass |
| 154 | + |
| 155 | + cgroup = _cgroup_memory_limit() |
| 156 | + if cgroup is not None: |
| 157 | + limits.append(cgroup) |
| 158 | + |
83 | 159 | try: |
84 | 160 | import psutil |
85 | 161 |
|
86 | | - return int(psutil.virtual_memory().available) |
| 162 | + limits.append(int(psutil.virtual_memory().available)) |
87 | 163 | except Exception: |
| 164 | + pass |
| 165 | + |
| 166 | + if not limits: |
88 | 167 | return 8 * 1024**3 |
| 168 | + return max(1, min(limits)) |
89 | 169 |
|
90 | 170 |
|
91 | 171 | def safe_worker_count( |
@@ -124,7 +204,7 @@ def safe_worker_count( |
124 | 204 | int |
125 | 205 | Worker-thread count (always >= 1). |
126 | 206 | """ |
127 | | - cpu_cap = max(1, (os.cpu_count() or 1) - 1) |
| 207 | + cpu_cap = max(1, cpu_allocation() - 1) |
128 | 208 | if use_gpu: |
129 | 209 | return 1 |
130 | 210 | avail = _get_available_memory() |
@@ -204,7 +284,7 @@ def auto_tile_shape( |
204 | 284 | >>> tile |
205 | 285 | (128, 512, 512) |
206 | 286 | """ |
207 | | - n_workers = n_workers or os.cpu_count() or 1 |
| 287 | + n_workers = n_workers or cpu_allocation() |
208 | 288 | itemsize = np.dtype(dtype).itemsize |
209 | 289 | n_spatial = min(3, len(shape)) |
210 | 290 |
|
@@ -308,7 +388,7 @@ def auto_tile_shape_cellpose( |
308 | 388 | >>> tile |
309 | 389 | (1, 2048, 2048) |
310 | 390 | """ |
311 | | - n_workers = n_workers or os.cpu_count() or 1 |
| 391 | + n_workers = n_workers or cpu_allocation() |
312 | 392 | itemsize = np.dtype(dtype).itemsize |
313 | 393 |
|
314 | 394 | if use_gpu: |
|
0 commit comments