[Engine] the sampler and the HIP properties probe never call close() - ResourceWarning lines on every Minimal serving runtime run
Maintainers usually reply within 3 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
Research direction
Start with the six open() sites named in serve/telemetry.py and serve/server.py, then run python -X dev -m unittest discover -s serve -p 'test_*.py' to see the warnings. Close each file after reading while preserving the existing results and exception handling. Done when the suite passes and the six readers no longer emit ResourceWarning lines.
Written by the indexing model from the issue text.
Description
Problem and result. The Minimal serving runtime job prints the same ResourceWarning lines on
every run. Six open() calls sit inside an expression, so nothing ever calls close() on them:
serve/telemetry.py:495: ResourceWarning: unclosed file <_io.TextIOWrapper name='/proc/stat' mode='r' encoding='UTF-8'>
serve/telemetry.py:478: ResourceWarning: unclosed file <_io.TextIOWrapper name='/proc/cpuinfo' ...>
serve/telemetry.py:458: ResourceWarning: unclosed file <... physical_package_id ...> (one per CPU)
serve/server.py:3364 / :3401: ResourceWarning: unclosed file <... /tmp/tmpXXXX/0/properties ...>
The patch closes all six and changes no answer.
Where they are (line numbers at v0.2.8):
serve/telemetry.py:458_cpu_topology()- one leaked handle percpu*/topology/physical_package_id
glob hit. A 4-CPU runner prints four lines; this 16-CPU box leaks sixteen per call. Read from the
code, not measured as a cost:_cpu_topology()is called twice in the samestaticdict
(socketsandnuma_nodes), so every CPU directory is read twice at each sampler start.serve/telemetry.py:476_cpu_name()- the/proc/cpuinfoscan. It reports at 478, thereturn
inside the loop, not at theopen(): the loop's iterator still holds the file when the frame is
released. Worth knowing, because the line number in a report points away from the line to fix.serve/telemetry.py:495_CpuRamFallback._times()-/proc/stat.sample()reaches the fallback
once a second when psutil is absent (serve/telemetry.py:633), and_CpuRamFallback.cpu()calls
_times()again, so a second of sampling is two/proc/statreads plus one/proc/meminfo.serve/telemetry.py:522_CpuRamFallback.ram()-/proc/meminfo. Same defect, and it never
printed in the log above: the log understates the set by one reader.serve/server.py:3364hip_speed_scores()andserve/server.py:3401hip_gpu_archs()- the
propertiesprobe, both copies. In the test job these read a faked sysfs root
(/tmp/tmpXXXX/0/propertiesin the log), which is why they print on a machine with no AMD card.
Measured on this box (Arch Linux, CPython 3.14.8, AMD Ryzen 7 9800X3D, 16 logical CPUs; both
versions loaded into one process so the reads are back to back against the same live /proc, and
the same probe re-run against the pure v0.2.8 tag in a throwaway worktree):
- 40 rounds of the five telemetry readers with
warningscaptured: 800 before, 0 after. The 800
is arithmetic rather than a guess: 40 rounds x 20 files, where 20 = 16 per-CPU topology files +
cpuinfo + stat + stat again throughcpu()+ meminfo. On the pure tag: 800 as released, 0 with the
patch applied, andgit apply --checklands clean on upstream's bytes. - Same answers before and after:
_cpu_name()returnsAMD Ryzen 7 9800X3D 8-Core Processor,
_cpu_topology()returns(1, 1),cpu()returns0.0,ram()returns
(119748005888, 134679015424). - It is not a descriptor leak on CPython, and I want to be precise about that. 1,000 rounds of
_times()+ram()with no collection at all hold +0 descriptors on/proc/self/fd, before
and after: CPython decrefs the temporary at the end of the statement and the descriptor goes with
it. The warnings do not need the collector either - 40 rounds withgc.collect()and 40 rounds
without it both give 800. SoResourceWarninghere is the interpreter's "nobody calledclose()"
complaint, i.e. hygiene and log noise, not fd pressure. On an interpreter without prompt
deallocation the same six would be a real leak; this box runs CPython only, so that is not tested. - The arms that reach these paths -
serve.test_parallel,test_replicas,test_responses,
test_security,test_server,test_session_save_retry,test_gpu_roles_server,test_vram:
Ran 504 tests ... OK (skipped=1)in 202.9 s. -
- The full serve suite on the patched tree, run the way the job runs it (
unittest discover -s serve -p 'test_*.py'):Ran 1589 tests ... OK (skipped=17)in 464.2 s, and its log carries 5unclosed filelines where the same command on the unpatched tree carried 121 - 108 of them on a/procor/syspath. A 4-CPU runner prints 4 of those, which is what the quoted log shows; a 16-CPU box prints 108. The 5 that remain are 4serve/lifeline.py:59pipe descriptors and one stdlib frame, none of them a site this patch touches.
- The full serve suite on the patched tree, run the way the job runs it (
The patch. Six with-statements, nothing else. Sites that had no encoding argument were left
without one on purpose, so this is a lifetime change only and not a locale change; both
try/except (OSError, ValueError) shapes still catch what they caught, and a missing properties
file behaves as before.
diff --git a/serve/server.py b/serve/server.py
index 43e973f8..08a720f0 100644
--- a/serve/server.py
+++ b/serve/server.py
@@ -4055,7 +4055,8 @@ def hip_speed_scores(indices: list[int], root: str = "/sys/class/kfd/kfd/topolog
cards = []
for n in nodes:
try:
- props = dict(line.split() for line in open(os.path.join(root, str(n), "properties")) if len(line.split()) == 2)
+ with open(os.path.join(root, str(n), "properties")) as fh:
+ props = dict(line.split() for line in fh if len(line.split()) == 2)
simd, clk = int(props.get("simd_count", 0)), int(props.get("max_engine_clk_fcompute", 0))
except (OSError, ValueError):
continue
@@ -4092,7 +4093,8 @@ def hip_gpu_archs(root: str = "/sys/class/kfd/kfd/topology/nodes") -> dict[int,
out: dict[int, str] = {}
for n in nodes:
try:
- props = dict(line.split() for line in open(os.path.join(root, str(n), "properties")) if len(line.split()) == 2)
+ with open(os.path.join(root, str(n), "properties")) as fh:
+ props = dict(line.split() for line in fh if len(line.split()) == 2)
ver, simd = int(props.get("gfx_target_version", 0)), int(props.get("simd_count", 0))
except (OSError, ValueError):
continue
diff --git a/serve/telemetry.py b/serve/telemetry.py
index f56da090..8722f62f 100644
--- a/serve/telemetry.py
+++ b/serve/telemetry.py
@@ -455,7 +455,8 @@ def _cpu_topology(root: str = "/sys/devices/system"):
packages = set()
for f in glob.glob(os.path.join(root, "cpu", "cpu[0-9]*", "topology", "physical_package_id")):
try:
- packages.add(open(f, encoding="utf-8").read().strip())
+ with open(f, encoding="utf-8") as fh:
+ packages.add(fh.read().strip())
except OSError:
continue
nodes = len(glob.glob(os.path.join(root, "node", "node[0-9]*")))
@@ -473,9 +474,10 @@ def _cpu_name():
except OSError:
pass
elif os.path.exists("/proc/cpuinfo"):
- for line in open("/proc/cpuinfo", encoding="utf-8", errors="replace"):
- if line.startswith("model name"):
- return line.split(":", 1)[1].strip()
+ with open("/proc/cpuinfo", encoding="utf-8", errors="replace") as fh:
+ for line in fh:
+ if line.startswith("model name"):
+ return line.split(":", 1)[1].strip()
return platform.processor() or None
@@ -492,7 +494,8 @@ class _CpuRamFallback:
return idle.value, kern.value + user.value # kernel time includes idle
return None
try:
- f = [int(x) for x in open("/proc/stat").readline().split()[1:]]
+ with open("/proc/stat") as fh:
+ f = [int(x) for x in fh.readline().split()[1:]]
return f[3] + f[4], sum(f)
except (OSError, ValueError):
return None
@@ -519,7 +522,8 @@ class _CpuRamFallback:
return m.ullTotalPhys - m.ullAvailPhys, m.ullTotalPhys
return None, None
try:
- info = dict(line.split(":", 1) for line in open("/proc/meminfo"))
+ with open("/proc/meminfo") as fh:
+ info = dict(line.split(":", 1) for line in fh)
total = int(info["MemTotal"].split()[0]) * 1024
avail = int(info["MemAvailable"].split()[0]) * 1024
return total - avail, total
Limits, and what this does not claim.
- Windows and macOS were not tested. These are the
/procand/sysbranches; the registry and
ctypesarms above them and the macOSsysctlarm are untouched. - The
serve/lifeline.py:59warning that prints in the same job names an unnamed descriptor
(fd 8, 9, 10 - pipes) and points at whatever frame released it. Different shape, not claimed here. - #3590 already fixed the watcher's
start_of()handle in this same family. This is the sampler and
the HIP probe, which that thread did not cover - its own body says "the sampler half has no
existing thread", andResourceWarningappears in no issue title (search: repo:Niko1221/Strata ResourceWarning in:titlereturns 0).
How to see it. python -X dev -m unittest discover -s serve -p 'test_*.py' prints them; or
40 rounds of the five readers with warnings.simplefilter("always") and count
ResourceWarning.
— posted from the Strata-Mnemosyne fork (memory-core, merged to v0.2.8 today). Numbers are ours,
produced on the hardware named above.
- Dominant language
- C++
- Stars
- 11.6k
- Forks
- 1k
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Niko1221/Strata
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Niko1221/Strata#4630 · 1 comment ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Niko1221/Strata#4589 · 1 comment ·
Maintainers usually reply within 3 days
-
[Engine] strata-sycl.sh drops every STRATA_* variable whose value is 0, so the documented STRATA_PREFILL_MMQ=0 (FP16 prompt path) never reaches the engine; "off" works by accidentPossibly taken @ctr-137 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 3 days
-
[Other] tools/ab_engine.py reuses ab_<arm>_r<n>.log in the temp dir across runs, so a later A/B appends its engine logs to an earlier onePossibly taken @ctr-137 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 3 days
-
[Other] setup.sh --update on an Intel (SYCL) install always ends with "the engine was NOT: it is still 0.1.12": engine_version() reads the launcher script the config names as exePossibly taken @ctr-137 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 3 days
Similar issues
-
good first issue invalid
Difficulty 1/5 Under an hour Newbie friendliness 72/100
DanielJerolm/gpcc#74 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
patrick-hudson/put.gd-desktop#808 ·
Maintainers usually reply within 1 day
-
DPI change is not fully handled in the cascading window on WindowsPossibly taken @yukawa claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 4 days
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
enhancement Ready for final check
Difficulty 2/5 1-3 hours Newbie friendliness 79/100
mpogue2/SquareDesk#1818 · 1 comment ·