import os import ctypes from modules.logger import log class LinuxUtils(): @staticmethod def get_status() -> dict[str, float] | None: lines = [] status = {} try: with open("/proc/self/status", encoding="utf-8") as handle: lines = handle.readlines() except OSError: return status for line in lines: key, _sep, value = line.partition(":") parts = value.strip().split() if not parts: continue try: status[key] = parts[0] except ValueError: continue log.debug(f'Linux status: {status}') return status @staticmethod def get_smaps(limit: int = 8) -> list[dict[str, float | str]] | None: try: with open("/proc/self/smaps", encoding="utf-8") as handle: lines = handle.readlines() except OSError: return None entries = [] current = None for raw_line in lines: line = raw_line.rstrip() if not line: continue if "-" in line and line[:1].isalnum() and line.split(maxsplit=1)[0].count("-") == 1: if current is not None: entries.append(current) parts = line.split(maxsplit=5) current = { "path": parts[5] if len(parts) > 5 else "[anonymous]", "rss": 0, "pss": 0, "private": 0, "shared": 0, } continue if current is None or ":" not in line: continue key, value = line.split(":", maxsplit=1) value = value.strip().split() if not value: continue try: amount = int(value[0]) except ValueError: continue if key == "Rss": current["rss"] += amount elif key == "Pss": current["pss"] += amount elif key in {"Private_Clean", "Private_Dirty"}: current["private"] += amount elif key in {"Shared_Clean", "Shared_Dirty"}: current["shared"] += amount if current is not None: entries.append(current) merged = {} for entry in entries: path = entry["path"] if path not in merged: merged[path] = entry.copy() else: merged[path]["rss"] += entry["rss"] merged[path]["pss"] += entry["pss"] merged[path]["private"] += entry["private"] merged[path]["shared"] += entry["shared"] top = sorted(merged.values(), key=lambda item: item["rss"], reverse=True)[:limit] for entry in top: entry["rss"] = round(entry["rss"] / 1024 / 1024, 3) entry["pss"] = round(entry["pss"] / 1024 / 1024, 3) entry["private"] = round(entry["private"] / 1024 / 1024, 3) entry["shared"] = round(entry["shared"] / 1024 / 1024, 3) log.debug(f'Linux smaps: top={top}') return top @staticmethod def malloc_trim() -> bool | None: try: libc = ctypes.CDLL("libc.so.6") libc.malloc_trim.argtypes = [ctypes.c_size_t] libc.malloc_trim.restype = ctypes.c_int status = bool(libc.malloc_trim(0)) log.debug(f"Linux trim: status={status}") except (AttributeError, OSError): log.debug("Linux trim: not supported") @staticmethod def advise_mmap(): """Mark mmaps as temporary so OS prioritizes dropping them.""" MADV_COLD = 5 # Linux 5.4+, mark as unlikely to be used libc = ctypes.CDLL('libc.so.6') advised = 0 with open('/proc/self/maps', 'r', encoding='utf-8') as f: for line in f: if 'blobs' in line or '/dev/zero' in line: try: addr, size = line.split()[0].split('-') addr = int(addr, 16) size = int(size, 16) - addr libc.madvise(ctypes.c_void_p(addr), size, MADV_COLD) advised += 1 except Exception: log.error(f"Linux mmap advise: {line.strip()}") log.debug(f"Linux mmap advise: num={advised}") @staticmethod def release_mmap(): """Use madvise to drop safetensors blob mmaps from page cache.""" try: libc = ctypes.CDLL('libc.so.6') # Get all memory mappings for this process dropped = [] with open('/proc/self/maps', 'r', encoding='utf-8') as f: for line in f: parts = line.split() if len(parts) >= 6: path = parts[5] if 'blobs' in path or '/dev/zero' in path: if path in dropped: continue try: addr, size = line.split()[0].split('-') addr = int(addr, 16) size = int(size, 16) - addr if libc.madvise(ctypes.c_void_p(addr), size, 4) == 0: dropped.append(path) except Exception: log.error(f"Linux mmap release: {line.strip()}") log.debug(f"Linux mmap release: {dropped}") except Exception as e: log.error(f"Linux mmap release: {e}") @staticmethod def advise_cache(): """Advise OS to drop cache for safetensors blobs.""" from modules.shared import opts try: if hasattr(os, 'posix_fadvise') and hasattr(os, 'POSIX_FADV_DONTNEED'): for root, _dirs, files in os.walk(opts.hfcache_dir, topdown=False): for f in files: if f.startswith(('blobs', 'snapshots')): try: path = os.path.join(root, f) fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) os.close(fd) except Exception: log.error(f"Linux cache: {path}") log.debug("Linux cache: advised") except Exception as e: log.error(f"Linux cache: {e}")