Spaces:
Running on Zero
Running on Zero
Download worker_probe.py from owenisas/workflow-studio: direct link, hf CLI and curl.
- Browser
- Download file 3.69 kB
-
https://huggingface.co/spaces/owenisas/workflow-studio/resolve/main/worker_probe.py
- Command line
-
hf download hf://spaces/owenisas/workflow-studio/worker_probe.py
-
curl -L -o worker_probe.py https://huggingface.co/spaces/owenisas/workflow-studio/resolve/main/worker_probe.py
3.69 kB
| """Watch forked ZeroGPU workers from the parent process (Linux /proc only). | |
| A worker that stalls before its first output leaves nothing in the logs, and a | |
| frozen process cannot report on itself. While a GPU call is pending, the parent | |
| logs each python child's state, per-thread kernel wait channels, CPU time and | |
| I/O. When a worker makes no progress between two looks, the parent sends it | |
| SIGUSR1 once; faulthandler, registered before the fork, then dumps every | |
| Python thread's stack from inside the worker. | |
| """ | |
| from __future__ import annotations | |
| from collections import Counter | |
| import os | |
| from pathlib import Path | |
| import signal | |
| PROC = Path("/proc") | |
| # No progress: under 5% of one CPU and under 1 MiB of reads/writes per interval. | |
| STALL_CPU_FRACTION = 0.05 | |
| STALL_IO_BYTES = 1 << 20 | |
| def _stat(text): | |
| """(comm, fields after comm); comm may itself contain spaces or parentheses.""" | |
| end = text.rindex(")") | |
| return text[text.index("(") + 1:end], text[end + 2:].split() | |
| def child_pids(parent=None, proc=PROC): | |
| parent = os.getpid() if parent is None else parent | |
| pids = [] | |
| for entry in proc.iterdir(): | |
| if not entry.name.isdigit(): | |
| continue | |
| try: | |
| comm, fields = _stat((entry / "stat").read_text()) | |
| except (OSError, ValueError): | |
| continue | |
| if int(fields[1]) == parent and "python" in comm: | |
| pids.append(int(entry.name)) | |
| return sorted(pids) | |
| def snapshot(pid, proc=PROC): | |
| base = proc / str(pid) | |
| _, fields = _stat((base / "stat").read_text()) | |
| try: | |
| io = dict(line.split(": ", 1) for line in (base / "io").read_text().splitlines()) | |
| io_bytes = int(io["rchar"]) + int(io["wchar"]) | |
| except (OSError, KeyError, ValueError): | |
| io_bytes = None | |
| threads = Counter() | |
| for task in (base / "task").iterdir(): | |
| try: | |
| state = _stat((task / "stat").read_text())[1][0] | |
| wchan = (task / "wchan").read_text().strip() or "-" | |
| except (OSError, ValueError, IndexError): | |
| continue | |
| threads[f"{state}:{wchan}"] += 1 | |
| return {"state": fields[0], "cpu_ticks": int(fields[11]) + int(fields[12]), | |
| "io_bytes": io_bytes, "threads": dict(threads)} | |
| def stalled(before, now, interval, ticks_per_s): | |
| if before is None: | |
| return False | |
| cpu_s = (now["cpu_ticks"] - before["cpu_ticks"]) / ticks_per_s | |
| io = None if None in (now["io_bytes"], before["io_bytes"]) else now["io_bytes"] - before["io_bytes"] | |
| return cpu_s < STALL_CPU_FRACTION * interval and (io is None or io < STALL_IO_BYTES) | |
| def watch(stop, interval, proc=PROC, kill=os.kill): | |
| """Until stop is set, log every python child each interval; dump a stalled one once.""" | |
| ticks_per_s = os.sysconf("SC_CLK_TCK") | |
| last, dumped = {}, set() | |
| while not stop.wait(interval): | |
| for pid in child_pids(proc=proc): | |
| try: | |
| now = snapshot(pid, proc) | |
| except (OSError, ValueError, IndexError): | |
| continue | |
| stuck = stalled(last.get(pid), now, interval, ticks_per_s) | |
| last[pid] = now | |
| threads = ",".join(f"{key}x{count}" for key, count in sorted(now["threads"].items())) | |
| io_mib = "?" if now["io_bytes"] is None else now["io_bytes"] >> 20 | |
| print(f"worker_probe pid={pid} state={now['state']} cpu_s={now['cpu_ticks'] / ticks_per_s:.0f} " | |
| f"io_mib={io_mib} stalled={stuck} threads={threads}", flush=True) | |
| if stuck and pid not in dumped: | |
| dumped.add(pid) | |
| try: | |
| kill(pid, signal.SIGUSR1) | |
| except OSError: | |
| pass | |