Make the long-running steps narrate themselves: memory, progress, and validate's start line - #9
Open
gaurav wants to merge 7 commits into
Open
Make the long-running steps narrate themselves: memory, progress, and validate's start line#9gaurav wants to merge 7 commits into
gaurav wants to merge 7 commits into
Conversation
…ext job The reported "memory leak" -- peak RSS climbing to 42.1 GiB by file 1201 -- is two things, neither of them a leak in our code. First, peak_rss_gib reads ru_maxrss, a high-water mark that only ever rises. Logged once per file it is the maximum over the whole run so far, not that file's footprint, so it climbs by construction and says nothing about what the process is currently holding. It now logs current RSS beside it (read from /proc, n/a on macOS), which can fall and is the number that shows real growth. Second, and the likely cause of the real growth: DuckDB sets its buffer-pool limit to ~80% of the *machine's* physical RAM, not the Slurm allocation -- the same mistake --threads makes with core count, but more expensive, because exceeding a cgroup is an OOM kill rather than contention. On a big node that is a limit of hundreds of GB inside --mem=64G, so DuckDB caches ever more of a growing database until the job dies. Adds a group-level --memory-limit (PUBMED2DB_DUCKDB_MEMORY_LIMIT) alongside --threads and --temp-dir. Progress lines now carry what you need to size the next srun rather than only an ETA: elapsed and s/file for the load, elapsed, docs/s and current RSS for the export. slurm/README.md is updated with the real command (--mem=64G --time=24:00:00), how to read the two memory figures, and why the memory limit is the knob to lower rather than raising --mem. Also closes a coverage gap flagged earlier: --limit's newest-N slice and the --verify gating both had no test because _sync_kind needed three seams stubbed. Both are covered now, and both fail if the behaviour is reverted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The JSON export's progress branch only fires after 10 s of real work, so it had never executed under test -- a mismatched %-format arg there would first surface partway through a 20-minute production run. Forcing the interval to zero exercises it. Also pins the load's progress line (rate and elapsed present, last line ending in a bare "done") and that each file logs current RSS beside the high-water peak. Mutation-checked: restoring the "~done to go" wording fails the test. CLAUDE.md gains the one thing this session found that neither the code nor the existing docs said: DuckDB cannot see a Slurm cgroup, so threads and memory_limit both default from the node rather than the allocation, and ru_maxrss only ever rises so a climbing per-file peak is not evidence of a leak. Points at slurm/README.md rather than repeating it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 5, 2026
A full-corpus export runs ~15 minutes, so the 10 s gate produced ~90 lines of near-identical progress. A minute still leaves a dozen-odd points to read the RSS ramp and the rate off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sizes it from what it actually does: one pass over the NDJSON shards plus a Python set of every exported PMID (~3 GiB at 40.9M records), not the export's whole-corpus queries — so 16 GB, not 256 GB. Both figures are estimates; the report's own duration/peak_rss_gib are what to size the next run from. Flags the parts that are Slurm-specific rather than validate-specific: the online checks need outbound HTTPS from the compute node, the exit status is what gates a batch script, and the manifest has to be kept between runs to catch a same-count export that lost records. Also refreshes the export progress-line references, now that they are logged once a minute rather than every ten seconds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A full-corpus validate is ~11 minutes of silence, and the two things it is silently misconfigured on — no database, no API key — only surfaced in the report at the end, by which time an anonymous run has already crawled at 3 req/s. It now logs one start line naming the shards and confirming both. The key itself is never logged, only that one was found. Progress is measured in bytes of shard consumed rather than records: the record total is what that pass is computing, and counting shards alone would report nothing at all for the default single-shard export. Reading through a raw binary handle keeps the position in the units st_size is in, for gzipped shards as well as plain. Each phase after the read is announced too, which is what distinguishes "still reading shards" (local, CPU-bound) from "hung on an NCBI call". Peak RSS was already in the report; it is now logged at the end as well, and current RSS rides along on each progress line, so a run reveals its own --mem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The section added a commit ago called both figures unmeasured; the README's own full-corpus example had them all along: 40,901,984 records in 10m 51s at peak RSS 5.2 GiB. So --mem=16G is ~3x headroom, which is the margin to keep for --previous-manifest's second PMID set. Also documents the new log lines and why progress is counted in bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
40,923,261 records across 16 shards (52.0 GiB) in 7m 57s, peak RSS 5.182 GiB, with an API key. The sizing advice is unchanged — 16 GB is still ~3x headroom — but the run was submitted with --mem=256G --time=06:00:00 copied from the export, and using 2% of both is worth saying out loud. Also records where the time actually goes: 7m 38s of the 7m 57s is the shard read, and every Entrez check together is 19 seconds. That is the one place worth optimizing, and it now has an issue (#13) rather than a paragraph of speculation here. The per-shard meaning of --sample-size is spelled out, since a 16-shard export at the default samples 240 records, not 15. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Diagnoses the
load's apparently-leaking memory, and makes every long-running step say enough about itself to size the nextsrunfrom its own log:loadandexportgained rate/elapsed/current-RSS on their progress lines (a minute apart, not ten seconds), andvalidate— previously ~11 minutes of silence — gained a start line, byte-based progress with an ETA, and per-phase lines.The "memory leak" is two things, neither a leak in our code
A long load was reaching
peak RSS 42.1 GiBby file 1201, well above the ~8 GiBslurm/README.mdpredicted.1.
peak_rss_gibreadsru_maxrss, a high-water mark that only ever rises. Logged once per file it is the maximum over the whole run so far, not that file's footprint — so it climbs by construction and says nothing about what the process currently holds. (Verified: it does not drop after freeing 400 MB.) The docs presented it as a per-file figure, which is only true for the first file.Each file now logs current RSS beside it, which can fall and is the number that shows real growth:
2. DuckDB sizes its buffer pool from the machine, not the allocation. It sets
memory_limitto ~80% of physical RAM — 12.7 GiB of a 16 GiB laptop, and hundreds of GB on a cluster node, regardless of--mem=64G. This is the same mistake--threadsmakes with core count, but more expensive: exceeding a cgroup is an OOM kill, not contention. As the database grows DuckDB caches more of it, RSS climbs run-long, and the job eventually dies with nothing to show for it.Adds a group-level
--memory-limit(PUBMED2DB_DUCKDB_MEMORY_LIMIT) alongside--threadsand--temp-dir.Progress lines size the next job
Previously an ETA and nothing else. Now the rate (what scales to the remaining work) and elapsed (what you compare against the limit you asked for):
Export gained current RSS too — it is the job that gets OOM-killed, so watching the ramp during the run is what tells you the next
--mem.They also come a tenth as often: the export's gate was 10 s, which over a ~15-minute full-corpus run is ~90 near-identical lines. It is now 60 s, still a dozen-odd points to read the RSS ramp and the rate off.
tqdmwas considered and rejected: its\rupdates are noise in a Slurm log, and logging already carries the timestamps you'd correlate againstsacct.validatenow narrates itself tooIt was ~11 minutes of silence, and the two things a run is silently misconfigured on — no database, no API key — only surfaced in the report at the end, by which time an anonymous run has already crawled at 3 req/s:
The key itself is never logged, only that one was found. Progress is counted in bytes of shard consumed, not records — the record total is what that pass is computing, and counting shards alone would print nothing at all for the default single-shard export; reading through a raw binary handle keeps the position in the units
st_sizeis in, for gzipped shards as well as plain. Each phase after the read is announced, which is what distinguishes "still reading shards" (local, CPU-bound) from "hung on an NCBI call".Docs
slurm/README.mdgains a Runningvalidatesection — sized from what it actually does (one pass over the shards plus a PMID set: measured at 7m 57s and peak RSS 5.2 GiB on the full corpus, so--mem=16G, not the export's 256 GB — and 7m 38s of that 7m 57s is the shard read, which is why issue #13 exists), plus the Slurm-specific parts: the online checks need outbound HTTPS from the compute node, the exit status is what gates a batch script, and the manifest has to be kept between runs. It also gets the real command (--mem=64G --time=24:00:00), a section on reading the two memory figures, and why the memory limit is the knob to lower rather than raising--mem. RootREADME.mdandCLAUDE.mdlist the third knob.Also closes a known coverage gap
--limit's newest-N slice and the--verifygating had no tests, because_sync_kindneeds_ensure_urls,requests.getandensure_module.ensureall stubbed. Both are covered now, and both were mutation-tested: reverting the slice tourls[:limit]or un-gatingverifyfails the respective test.Verification
/proccurrent-RSS check, which is expected to skip on macOS and always runs on the cluster).tests/test_util.pypins the peak-vs-current distinction directly: thatpeak_rss_gibdoes not fall after memory is freed, and thatcurrent_rss_gibis eitherNoneor a plausible value at or below the high-water mark.--memory-limitis asserted to actually shrink DuckDB's default rather than sit alongside it, comparing parsed byte values (DuckDB reports1GBback as953.6 MiB).Not verified
The DuckDB explanation is a strong inference from its documented default plus the 12.7-of-16 GiB reading locally — it has not been confirmed on the cluster. Running
SELECT current_setting('memory_limit')inside a job would settle it: if it comes back sized to the node rather than to--mem, the diagnosis holds. The suggested48GBunder--mem=64Gis a starting point, not a measured optimum — too low trades OOM risk for spilling, which shows up as a collapsed s/file rate.TODO
memory_limiton the cluster. The "Not verified" section above rests on this. Inside a real job:uv run python -c "import duckdb; print(duckdb.connect().execute(\"SELECT current_setting('memory_limit')\").fetchone())". If it comes back sized to the node rather than to--mem, the diagnosis holds and the--memory-limitdefault guidance is right. If it already reflects the cgroup, the memory story here needs rethinking before merge.PUBMED2DB_DUCKDB_MEMORY_LIMITagainst a real load.48GBunder--mem=64Gis a starting point, not a measurement. Watch the newRSSfigure: if it plateaus below the cap and the s/file rate holds, it is about right; if the rate collapses, DuckDB is spilling and the cap is too low.🤖 Generated with Claude Code