testzerogpu1 / memguard.py
HelloSun's picture
Copy from HelloSun/testzerogpu
1949064 verified
Raw History Blame Contribute Delete
5.79 kB
#!/usr/bin/env python3
"""Kill only the processes opencode spawns, never opencode itself.
Logic:
roots = processes whose comm starts with "opencode"
workers = every descendant of those roots (opencode itself excluded)
total = summed RSS of workers
while total > LIMIT: kill the direct child of opencode holding the most RSS
(whole subtree, so the runaway program A dies with its kids)
"""
import json
import os
import re
import signal
import sys
import time
LIMIT = float(sys.argv[1] if len(sys.argv) > 1 else 80) * 1024 ** 3
INTERVAL = float(sys.argv[2] if len(sys.argv) > 2 else 2)
PER_PROC = float(sys.argv[3] if len(sys.argv) > 3 else 64) * 1024 ** 3
MARK = "/tmp/memkill.last.json"
HIST = "/tmp/memkill.jsonl"
PAGE = os.sysconf("SC_PAGE_SIZE")
def read_cmd(p):
try:
with open("/proc/%d/cmdline" % p, "rb") as f:
return f.read().replace(b"\0", b" ").decode(errors="replace").strip()
except Exception:
return ""
def snapshot():
ppid = {}
rss = {}
comm = {}
for d in os.listdir("/proc"):
if not d.isdigit():
continue
p = int(d)
try:
with open("/proc/%d/stat" % p) as f:
s = f.read()
ppid[p] = int(s[s.rindex(")") + 2:].split()[1])
with open("/proc/%d/statm" % p) as f:
rss[p] = int(f.read().split()[1]) * PAGE
with open("/proc/%d/comm" % p) as f:
comm[p] = f.read().strip()
except Exception:
continue
return ppid, rss, comm
def protect():
"""never let the guard (or its own helpers) become a victim"""
out = {os.getpid()}
for d in os.listdir("/proc"):
if d.isdigit():
try:
with open("/proc/%s/cmdline" % d, "rb") as f:
if b"memkill.py" in f.read():
out.add(int(d))
except Exception:
pass
return out
def main():
try:
with open("/tmp/.memguard.pid", "w") as f:
f.write(str(os.getpid()))
except Exception:
pass
print("memkill: guard armed total_limit=%.0fGiB per_proc_limit=%.0fGiB"
% (LIMIT / 1024 ** 3, PER_PROC / 1024 ** 3), flush=True)
while True:
ppid, rss, comm = snapshot()
keep = protect()
roots = {p for p, c in comm.items() if c.startswith("opencode")} - keep
kids = {}
for child, parent in ppid.items():
kids.setdefault(parent, []).append(child)
# subtree walk helpers
def sub(p):
out = {p}
stack = [p]
while stack:
for k in kids.get(stack.pop(), ()):
if k not in out:
out.add(k)
stack.append(k)
return out
workers = set()
top = [] # direct children of opencode
for r in roots:
for k in kids.get(r, ()):
if k not in workers:
workers |= sub(k)
top.append(k)
workers -= roots | keep
# threshold counts opencode itself too, so "opencode + everything it
# started" stays under LIMIT -- but victims are only ever children
total = sum(rss.get(p, 0) for p in workers | roots)
print("%s opencode=%.2fGiB workers=%d total=%.2fGiB"
% (time.strftime("%H:%M:%S"),
sum(rss.get(p, 0) for p in roots) / 1024 ** 3,
len(workers), total / 1024 ** 3), flush=True)
victims = set()
top_set = set(top)
if total > LIMIT:
while total > LIMIT and top:
c = max(top, key=lambda p: sum(rss.get(x, 0) for x in sub(p)))
if c in victims:
break
victims |= sub(c)
top.remove(c)
total -= sum(rss.get(x, 0) for x in victims)
if PER_PROC:
for p in list(workers):
if rss.get(p, 0) > PER_PROC:
victims |= sub(p)
doomed = victims - roots - keep
cmds = {v: read_cmd(v) for v in doomed}
for v in sorted(doomed):
try:
os.kill(v, signal.SIGKILL)
print(" KILL pid=%d rss=%.2fGiB (opencode untouched) %s"
% (v, rss.get(v, 0) / 1024 ** 3, cmds[v][:90]), flush=True)
except Exception as e:
print(" skip pid=%d: %s" % (v, e), flush=True)
if doomed:
# the direct child of opencode == the command the assistant ran
heads = [v for v in doomed if v in top_set] or sorted(doomed)
rec = {
"ts": round(time.time(), 3),
"iso": time.strftime("%Y-%m-%d %H:%M:%S"),
"head_pid": heads[0],
"head_cmd": cmds.get(heads[0], ""),
"cmds": [cmds[v] for v in sorted(doomed)][:8],
"rss_gib": round(max(rss.get(v, 0) for v in doomed) / 1024 ** 3, 2),
"total_gib": round(sum(rss.get(v, 0) for v in doomed) / 1024 ** 3, 2),
"limit_gib": round(LIMIT / 1024 ** 3, 2),
"per_proc_gib": round(PER_PROC / 1024 ** 3, 2),
"reason": "over-limit",
}
try:
with open(MARK + ".tmp", "w") as f:
json.dump(rec, f)
os.replace(MARK + ".tmp", MARK)
with open(HIST, "a") as f:
f.write(json.dumps(rec) + "\n")
except Exception as e:
print(" marker failed: %s" % e, flush=True)
time.sleep(3)
time.sleep(INTERVAL)
if __name__ == "__main__":
main()