PowerMachine commited on
Commit
e4e6b61
·
verified ·
1 Parent(s): 8433089

HAKO upload: hako/memory_manager.py

Browse files
Files changed (1) hide show
  1. hako/memory_manager.py +121 -0
hako/memory_manager.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Aggressive memory & storage management for HAKO.
2
+
3
+ Proposition P-MEM (bounded-resource operation). If every phase satisfies
4
+ (i) RSS(t) <= ram_soft_cap for all t (enforced by watchdog degrade),
5
+ (ii) disk blobs not referenced by decomposed artifacts are deleted,
6
+ (iii) telemetry/checkpoints are size-capped and compressed,
7
+ then the pipeline operates indefinitely within a fixed resource envelope.
8
+ Proof sketch: (i) bounds the state space of the resident set by construction
9
+ (degrade-or-abort policy with monotone batch reduction); (ii) makes total disk
10
+ usage a sum of compressed artifacts (monotone, bounded by artifact budget);
11
+ (iii) bounds every log by its cap. Induction over phases completes the bound.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import gc
16
+ import logging
17
+ import os
18
+ import shutil
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Iterable
22
+
23
+ import psutil
24
+
25
+ log = logging.getLogger("hako.mem")
26
+
27
+
28
+ class MemoryManager:
29
+ def __init__(self, cfg) -> None:
30
+ self.cfg = cfg
31
+ vm = psutil.virtual_memory()
32
+ self.ram_soft_cap = int(cfg.ram_soft_cap_ratio * vm.total)
33
+ self._last_report = 0.0
34
+
35
+ # ------------------------------------------------------------------ ram
36
+ def rss(self) -> int:
37
+ return psutil.Process(os.getpid()).memory_info().rss
38
+
39
+ def pressure(self) -> float:
40
+ """RSS pressure in [0, 1+] relative to the soft cap."""
41
+ return self.rss() / max(1, self.ram_soft_cap)
42
+
43
+ def check(self, degrade_hooks: Iterable | None = None) -> str:
44
+ """Watchdog: returns 'ok' | 'degraded'. Calls hooks to shrink state."""
45
+ p = self.pressure()
46
+ if p < 0.85:
47
+ return "ok"
48
+ gc.collect()
49
+ if p < 1.0:
50
+ return "ok"
51
+ action = "degraded"
52
+ for hook in degrade_hooks or []:
53
+ try:
54
+ hook()
55
+ except Exception as exc: # pragma: no cover - defensive
56
+ log.warning("degrade hook failed: %s", exc)
57
+ gc.collect()
58
+ if self.pressure() > 1.05:
59
+ raise MemoryError(
60
+ f"RSS {self.rss()/2**30:.2f}GiB exceeded soft cap "
61
+ f"{self.ram_soft_cap/2**30:.2f}GiB after degradation")
62
+ return action
63
+
64
+ def report(self, tag: str = "") -> None:
65
+ now = time.time()
66
+ if now - self._last_report < 15:
67
+ return
68
+ self._last_report = now
69
+ vm = psutil.virtual_memory()
70
+ log.info("mem[%s] rss=%.2fGiB cap=%.2fGiB sys_avail=%.2fGiB",
71
+ tag, self.rss() / 2**30, self.ram_soft_cap / 2**30,
72
+ vm.available / 2**30)
73
+
74
+ # ----------------------------------------------------------------- disk
75
+ def disk_free(self) -> int:
76
+ return psutil.disk_usage("/").free
77
+
78
+ def ensure_disk(self, need_bytes: int) -> None:
79
+ if self.disk_free() >= max(need_bytes, self.cfg.disk_min_free_bytes):
80
+ return
81
+ freed = self.sweep_caches()
82
+ log.info("disk sweep freed %.2f MiB", freed / 2**20)
83
+ if self.disk_free() < max(need_bytes, self.cfg.disk_min_free_bytes):
84
+ raise OSError("disk exhausted even after cache sweep")
85
+
86
+ def sweep_caches(self) -> int:
87
+ """Delete HF hub blobs and work leftovers that have decomposed twins."""
88
+ freed = 0
89
+ hf_blob_dirs = [
90
+ Path(self.cfg.hf_cache) / "hub",
91
+ ]
92
+ for base in hf_blob_dirs:
93
+ if not base.exists():
94
+ continue
95
+ for model_dir in base.iterdir():
96
+ marker = Path(self.cfg.artifacts_dir) / (
97
+ model_dir.name.replace("models--", "") + ".decomposed.json")
98
+ if marker.exists():
99
+ # decomposed copy exists -> raw weights are expendable
100
+ size = sum(f.stat().st_size for f in model_dir.rglob("*")
101
+ if f.is_file())
102
+ shutil.rmtree(model_dir, ignore_errors=True)
103
+ freed += size
104
+ gc.collect()
105
+ return freed
106
+
107
+ @staticmethod
108
+ def cap_file_size(path: Path, max_bytes: int, trim_head: bool = True) -> None:
109
+ """Keep a log under max_bytes by trimming the oldest half (JSONL-safe)."""
110
+ p = Path(path)
111
+ if not p.exists() or p.stat().st_size <= max_bytes:
112
+ return
113
+ lines = p.read_text(encoding="utf-8", errors="ignore").splitlines(keepends=True)
114
+ keep = lines[len(lines) // 2:] if trim_head else lines[:len(lines) // 2]
115
+ tmp = p.with_suffix(p.suffix + ".tmp")
116
+ tmp.write_text("".join(keep), encoding="utf-8")
117
+ tmp.replace(p)
118
+
119
+ @staticmethod
120
+ def rm_tree_quiet(path: Path) -> None:
121
+ shutil.rmtree(path, ignore_errors=True)