EugeneBoondock's picture
Sponge online learners and the compression-scored search that found them
b999890 verified
Raw History Blame Contribute Delete
6.19 kB
"""Resource guards: keep the search inside the machine's RAM and disk.
Nothing here touches the GPU; the whole project is CPU and numpy only.
"""
from __future__ import annotations
import ctypes
import os
import shutil
import sys
from pathlib import Path
MB = 1024 * 1024
GB = 1024 * MB
_JOB_HANDLE = None # kept alive so the Windows job object is not released
def free_disk_gb(path: str | Path) -> float:
return shutil.disk_usage(str(path)).free / GB
def dir_size_mb(path: str | Path) -> float:
total = 0
for root, _dirs, files in os.walk(str(path)):
for name in files:
try:
total += os.path.getsize(os.path.join(root, name))
except OSError:
continue
return total / MB
def free_ram_mb() -> float | None:
"""Physical RAM currently available, or None if it cannot be read."""
if sys.platform == "win32":
return _free_ram_mb_windows()
try:
with open("/proc/meminfo", encoding="ascii") as handle:
for line in handle:
if line.startswith("MemAvailable:"):
return int(line.split()[1]) / 1024
except OSError:
return None
return None
def _free_ram_mb_windows() -> float | None:
class MemoryStatus(ctypes.Structure):
_fields_ = [
("dwLength", ctypes.c_uint32),
("dwMemoryLoad", ctypes.c_uint32),
("ullTotalPhys", ctypes.c_uint64),
("ullAvailPhys", ctypes.c_uint64),
("ullTotalPageFile", ctypes.c_uint64),
("ullAvailPageFile", ctypes.c_uint64),
("ullTotalVirtual", ctypes.c_uint64),
("ullAvailVirtual", ctypes.c_uint64),
("ullAvailExtendedVirtual", ctypes.c_uint64),
]
status = MemoryStatus()
status.dwLength = ctypes.sizeof(MemoryStatus)
if not ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)):
return None
return status.ullAvailPhys / MB
def confine_own_process(limit_mb: int) -> bool:
"""Hard-cap this process's memory; on Windows also forbid it from starting processes.
Allocations past the cap fail with MemoryError. Returns True when the cap is in force.
"""
if sys.platform == "win32":
return _cap_memory_windows(limit_mb)
try:
import resource
limit = limit_mb * MB
resource.setrlimit(resource.RLIMIT_AS, (limit, limit))
return True
except (ImportError, ValueError, OSError):
return False
def _cap_memory_windows(limit_mb: int) -> bool:
global _JOB_HANDLE
from ctypes import wintypes
class BasicLimits(ctypes.Structure):
_fields_ = [
("PerProcessUserTimeLimit", ctypes.c_int64),
("PerJobUserTimeLimit", ctypes.c_int64),
("LimitFlags", wintypes.DWORD),
("MinimumWorkingSetSize", ctypes.c_size_t),
("MaximumWorkingSetSize", ctypes.c_size_t),
("ActiveProcessLimit", wintypes.DWORD),
("Affinity", ctypes.c_size_t),
("PriorityClass", wintypes.DWORD),
("SchedulingClass", wintypes.DWORD),
]
class IoCounters(ctypes.Structure):
_fields_ = [(name, ctypes.c_uint64) for name in (
"ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
"ReadTransferCount", "WriteTransferCount", "OtherTransferCount")]
class ExtendedLimits(ctypes.Structure):
_fields_ = [
("BasicLimitInformation", BasicLimits),
("IoInfo", IoCounters),
("ProcessMemoryLimit", ctypes.c_size_t),
("JobMemoryLimit", ctypes.c_size_t),
("PeakProcessMemoryUsed", ctypes.c_size_t),
("PeakJobMemoryUsed", ctypes.c_size_t),
]
job_object_extended_limit_information = 9
job_object_limit_process_memory = 0x00000100
job_object_limit_active_process = 0x00000008
job_object_limit_kill_on_job_close = 0x00002000
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
k32.CreateJobObjectW.restype = wintypes.HANDLE
k32.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR]
k32.GetCurrentProcess.restype = wintypes.HANDLE
k32.SetInformationJobObject.restype = wintypes.BOOL
k32.SetInformationJobObject.argtypes = [
wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
k32.AssignProcessToJobObject.restype = wintypes.BOOL
k32.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
job = k32.CreateJobObjectW(None, None)
if not job:
return False
info = ExtendedLimits()
info.BasicLimitInformation.LimitFlags = (
job_object_limit_process_memory | job_object_limit_active_process
| job_object_limit_kill_on_job_close)
info.BasicLimitInformation.ActiveProcessLimit = 1
info.ProcessMemoryLimit = limit_mb * MB
if not k32.SetInformationJobObject(
job, job_object_extended_limit_information,
ctypes.byref(info), ctypes.sizeof(info)):
return False
if not k32.AssignProcessToJobObject(job, k32.GetCurrentProcess()):
return False
_JOB_HANDLE = job
return True
def check_budget(run_dir: str | Path, parallel: int, mem_mb: int,
min_free_disk_gb: float, max_run_mb: float) -> str | None:
"""Return a reason to stop, or None when the run is inside its budget."""
free_gb = free_disk_gb(run_dir)
if free_gb < min_free_disk_gb:
return f"only {free_gb:.1f} GB disk free (floor is {min_free_disk_gb} GB)"
used_mb = dir_size_mb(run_dir)
if used_mb > max_run_mb:
return f"run folder is {used_mb:.0f} MB (cap is {max_run_mb:.0f} MB)"
free_mb = free_ram_mb()
needed = parallel * mem_mb
if free_mb is not None and needed > 0.5 * free_mb:
return (f"{parallel} workers x {mem_mb} MB = {needed} MB, more than half of the "
f"{free_mb:.0f} MB RAM currently free; lower --parallel or --mem-mb")
return None